| // Copyright 2025 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| #include <assert.h> |
| #include <lib/unittest/unittest.h> |
| #include <platform.h> |
| #include <zircon/errors.h> |
| |
| #include <arch/interrupt.h> |
| #include <kernel/auto_preempt_disabler.h> |
| #include <kernel/mp.h> |
| #include <kernel/scheduler.h> |
| |
| namespace { |
| |
| bool pending_ipi() { |
| BEGIN_TEST; |
| |
| if (!platform_supports_suspend_cpu()) { |
| printf("platform_suspend_cpu is not supported, skipping test\n"); |
| END_TEST; |
| } |
| |
| { |
| AutoPreemptDisabler apd; |
| InterruptDisableGuard irqd; |
| const cpu_num_t curr_cpu = arch_curr_cpu_num(); |
| |
| // Pend an interrupt to ourselves to ensure that platform_suspend_cpu returns quickly. |
| mp_interrupt(mp_ipi_target::MASK, cpu_num_to_mask(curr_cpu)); |
| |
| const zx_status_t status = platform_suspend_cpu(PlatformAllowDomainPowerDown::No); |
| ASSERT_OK(status); |
| } |
| |
| END_TEST; |
| } |
| |
| // This test creates two threads, A and B. Each on their own CPU. A will suspend and wait to be |
| // woken by B. B will wait to observe that A has suspended before waking it via IPI. Because A |
| // might wake from suspend due to a stray interrupt, we retry the test until it succeeds, an error |
| // occurs or the timeout has elapsed. |
| bool one_wakes_another() { |
| BEGIN_TEST; |
| |
| constexpr zx_duration_boot_t kTimeout = ZX_SEC(10); |
| |
| if (!platform_supports_suspend_cpu()) { |
| printf("platform_suspend_cpu is not supported, skipping test\n"); |
| END_TEST; |
| } |
| |
| cpu_mask_t active_cpus = Scheduler::PeekActiveMask(); |
| if (ktl::popcount(active_cpus) <= 1) { |
| printf("not enough active CPUs, skipping test\n"); |
| END_TEST; |
| } |
| |
| enum class State : uint32_t { |
| READY = 0, |
| SUSPENDING, |
| DONE, |
| }; |
| struct Arg { |
| const cpu_num_t target; |
| ktl::atomic<State> target_state; |
| }; |
| |
| thread_start_routine a_entry = [](void* arg_) -> int { |
| auto* arg = reinterpret_cast<Arg*>(arg_); |
| |
| AutoPreemptDisabler apd; |
| InterruptDisableGuard irqd; |
| |
| arg->target_state.store(State::SUSPENDING, ktl::memory_order_release); |
| const zx_status_t status = platform_suspend_cpu(PlatformAllowDomainPowerDown::No); |
| arg->target_state.store(State::DONE, ktl::memory_order_release); |
| |
| return status; |
| }; |
| |
| thread_start_routine b_entry = [](void* arg_) -> int { |
| auto* arg = reinterpret_cast<Arg*>(arg_); |
| |
| // Wait until the target reports that it's about to suspend. |
| State reported; |
| while ((reported = arg->target_state.load(ktl::memory_order_acquire)) == State::READY) { |
| arch::Yield(); |
| } |
| |
| if (reported == State::SUSPENDING) { |
| // Wait a short amount of time, see that the thread is still suspended, then wake it. |
| Thread::Current::SleepRelative(ZX_MSEC(1)); |
| if (arg->target_state.load(ktl::memory_order_acquire) == State::SUSPENDING) { |
| mp_interrupt(mp_ipi_target::MASK, cpu_num_to_mask(arg->target)); |
| return ZX_OK; |
| } |
| } |
| |
| // Somehow the CPU must have returned from suspend. |
| return ZX_ERR_BAD_STATE; |
| }; |
| |
| // First, select the CPU that will wake the other. Choose the boot CPU because we know it's must |
| // be active and because we want the suspending CPU to be one of the secondaries since they |
| // receive fewer (if any) hardware interrupts (SPIs). |
| ASSERT_TRUE(active_cpus & cpu_num_to_mask(BOOT_CPU_ID)); |
| const cpu_num_t cpu_b = BOOT_CPU_ID; |
| active_cpus &= ~cpu_num_to_mask(BOOT_CPU_ID); |
| const cpu_num_t cpu_a = remove_cpu_from_mask(active_cpus); |
| |
| const auto deadline = Deadline::after_boot(kTimeout); |
| for (size_t attempt = 0; current_boot_time() < deadline.when(); ++attempt) { |
| printf("attempt %zu\n", attempt); |
| |
| Arg arg(cpu_a, State::READY); |
| |
| Thread* a = Thread::Create("one_wakes_another-a", a_entry, &arg, DEFAULT_PRIORITY); |
| ASSERT_NONNULL(a); |
| a->SetCpuAffinity(cpu_num_to_mask(cpu_a)); |
| a->Resume(); |
| |
| Thread* b = Thread::Create("one_wakes_another-b", b_entry, &arg, DEFAULT_PRIORITY); |
| ASSERT_NONNULL(a); |
| b->SetCpuAffinity(cpu_num_to_mask(cpu_b)); |
| b->Resume(); |
| |
| zx_status_t a_status; |
| a->Join(&a_status, ZX_TIME_INFINITE); |
| ASSERT_OK(a_status); |
| |
| zx_status_t b_status; |
| b->Join(&b_status, ZX_TIME_INFINITE); |
| |
| // Perhaps CPU-A woke from suspend early. Try again. |
| if (b_status == ZX_ERR_BAD_STATE) { |
| continue; |
| } |
| |
| ASSERT_OK(b_status); |
| END_TEST; |
| } |
| |
| printf("failed to complete successfully before timeout\n"); |
| ASSERT_TRUE(false); |
| |
| END_TEST; |
| } |
| |
| } // namespace |
| |
| UNITTEST_START_TESTCASE(platform_suspend_cpu_tests) |
| UNITTEST("pending_ipi", pending_ipi) |
| UNITTEST("one_wakes_another", one_wakes_another) |
| UNITTEST_END_TESTCASE(platform_suspend_cpu_tests, "platform_suspend_cpu_tests", |
| "platform_suspend_cpu tests") |