| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "intel-dsp-ipc.h" |
| |
| #include <lib/zx/time.h> |
| #include <threads.h> |
| #include <zircon/assert.h> |
| #include <zircon/errors.h> |
| |
| #include <cstdint> |
| #include <unordered_set> |
| #include <vector> |
| |
| #include <ddk/debug.h> |
| #include <mmio-ptr/fake.h> |
| #include <zxtest/zxtest.h> |
| |
| #include "debug-logging.h" |
| |
| namespace audio::intel_hda { |
| namespace { |
| |
| // A simple C11 thread wrapper. |
| class Thread { |
| public: |
| Thread(std::function<void()> start) : start_(std::move(start)) { |
| int result = thrd_create( |
| &thread_, |
| [](void* start_ptr) { |
| auto start = static_cast<std::function<void()>*>(start_ptr); |
| (*start)(); |
| return 0; |
| }, |
| &start_); |
| ZX_ASSERT(result == thrd_success); |
| } |
| |
| void Join() { |
| ZX_ASSERT(!joined_); |
| int result; |
| thrd_join(thread_, &result); |
| joined_ = true; |
| } |
| |
| ~Thread() { |
| if (!joined_) { |
| Join(); |
| } |
| } |
| |
| // Disallow copy/move. |
| Thread(const Thread&) = delete; |
| Thread(Thread&&) = delete; |
| Thread& operator=(Thread&&) = delete; |
| Thread& operator=(const Thread&) = delete; |
| |
| private: |
| bool joined_ = false; |
| std::function<void()> start_; |
| thrd_t thread_; |
| }; |
| |
| // Wait for a condition to be set, using exponential backoff. |
| template <typename F> |
| void WaitForCond(F cond) { |
| zx::duration delay = zx::usec(1); |
| while (!cond()) { |
| zx::nanosleep(zx::deadline_after(delay)); |
| delay *= 2; |
| } |
| } |
| |
| // Simulate the hardware sending an IPC reply, and firing an interrupt. |
| void SendReply(DspChannel* dsp, MMIO_PTR adsp_registers_t* regs, const IpcMessage& reply) { |
| // Send the reply. |
| REG_WR(®s->hipct, reply.primary | ADSP_REG_HIPCT_BUSY | (1 << IPC_PRI_RSP_SHIFT)); |
| REG_WR(®s->hipcte, reply.extension); |
| REG_SET_BITS(®s->adspis, ADSP_REG_ADSPIC_IPC); // Indicate IPC reply ready. |
| dsp->ProcessIrq(); |
| } |
| |
| // Simulate the hardware sending an IPC notification, and firing an interrupt. |
| void SendNotification(DspChannel* dsp, MMIO_PTR adsp_registers_t* regs, NotificationType type) { |
| REG_WR(®s->hipct, static_cast<uint8_t>(MsgTarget::FW_GEN_MSG) << IPC_PRI_MSG_TGT_SHIFT | |
| static_cast<uint8_t>(MsgDir::MSG_NOTIFICATION) << IPC_PRI_RSP_SHIFT | |
| static_cast<uint8_t>(GlobalType::NOTIFICATION) << IPC_PRI_TYPE_SHIFT | |
| (static_cast<uint16_t>(type) << IPC_PRI_NOTIF_TYPE_SHIFT) | |
| ADSP_REG_HIPCT_BUSY); |
| REG_WR(®s->hipcte, 0U); |
| REG_SET_BITS(®s->adspis, ADSP_REG_ADSPIC_IPC); // Indicate IPC ready. |
| dsp->ProcessIrq(); |
| } |
| |
| // Poll the IPC-related registers until a message is sent by the driver. |
| IpcMessage ReadMessage(MMIO_PTR adsp_registers_t* regs) { |
| // Wait for a message. |
| WaitForCond([&]() { return (REG_RD(®s->hipci) & ADSP_REG_HIPCI_BUSY) != 0; }); |
| |
| // Read it. |
| uint32_t primary = REG_RD(®s->hipci); |
| uint32_t extension = REG_RD(®s->hipcie); |
| |
| // Clear the busy bit. |
| REG_CLR_BITS(®s->hipci, ADSP_REG_HIPCI_BUSY); |
| |
| return IpcMessage(primary, extension); |
| } |
| |
| TEST(Ipc, ConstructDestruct) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), std::nullopt, zx::duration::infinite()); |
| } |
| |
| TEST(Ipc, SimpleSend) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), std::nullopt, zx::duration::infinite()); |
| |
| // Start a thread, give it a chance to run. |
| auto worker = Thread([&]() { |
| Status result = dsp->Send(0xaa, 0x55); |
| ZX_ASSERT_MSG(result.ok(), "Send failed: %s (code: %d)", result.ToString().c_str(), |
| result.code()); |
| }); |
| |
| // Simulate the DSP reading the message. |
| IpcMessage message = ReadMessage(FakeMmioPtr(®s)); |
| EXPECT_EQ(message.primary & IPC_PRI_MODULE_ID_MASK, 0xaa); |
| EXPECT_EQ(message.extension & IPC_EXT_DATA_OFF_SIZE_MASK, 0x55); |
| |
| // Ensure the thread remains blocked on the send, even if we wait a little. |
| zx::nanosleep(zx::deadline_after(zx::msec(10))); |
| EXPECT_TRUE(dsp->IsOperationPending()); |
| |
| // Simulate the DSP sending a successful reply. |
| SendReply(dsp.get(), FakeMmioPtr(®s), IpcMessage(0, 0)); |
| } |
| |
| TEST(Ipc, ErrorReply) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), std::nullopt, zx::duration::infinite()); |
| |
| // Start a thread. |
| auto worker = Thread([&]() { |
| Status result = dsp->Send(0xaa, 0x55); |
| ZX_ASSERT_MSG(result.ToString() == "DSP returned error 42 (ZX_ERR_INTERNAL)", |
| "Got incorrect error message: %s", result.ToString().c_str()); |
| }); |
| |
| // Read (and ignore) the message. |
| (void)ReadMessage(FakeMmioPtr(®s)); |
| |
| // Simulate the DSP sending an error reply, with an arbitrary error code (42). |
| // |
| // The test will abort if the child thread gets the wrong error code. |
| SendReply(dsp.get(), FakeMmioPtr(®s), IpcMessage(42, 0)); |
| } |
| |
| TEST(Ipc, HardwareTimeout) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = |
| CreateHardwareDspChannel("UnitTests", FakeMmioPtr(®s), std::nullopt, zx::usec(1)); |
| |
| // Start a thread. |
| auto worker = Thread([&]() { |
| Status result = dsp->Send(0xaa, 0x55); |
| ZX_ASSERT_MSG(result.code() == ZX_ERR_TIMED_OUT, "Got incorrect error: %s", |
| result.ToString().c_str()); |
| }); |
| |
| // Wait for it to time out. |
| worker.Join(); |
| } |
| |
| TEST(Ipc, UnsolicitedReply) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), std::nullopt, zx::duration::infinite()); |
| |
| // Ensuring sending a reply and an IRQ doesn't crash. |
| SendReply(dsp.get(), FakeMmioPtr(®s), IpcMessage(42, 0)); |
| } |
| |
| TEST(Ipc, QueuedMessages) { |
| constexpr int kNumThreads = 10; |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), std::nullopt, zx::duration::infinite()); |
| |
| // Start many threads, racing to send messages. |
| std::array<std::unique_ptr<Thread>, kNumThreads> workers; |
| for (int i = 0; i < kNumThreads; i++) { |
| workers[i] = std::make_unique<Thread>([i, &dsp]() { |
| Status result = dsp->Send(0, i); |
| ZX_ASSERT_MSG(result.ok(), "Send failed: %s (code: %d)", result.ToString().c_str(), |
| result.code()); |
| }); |
| } |
| |
| // Simulate the DSP reading off the messages one by one, and ensure that |
| // we got all the messages. |
| std::unordered_set<int> seen_messages{}; |
| for (int i = 0; i < kNumThreads; i++) { |
| IpcMessage message = ReadMessage(FakeMmioPtr(®s)); |
| EXPECT_TRUE(seen_messages.find(message.extension) == seen_messages.end()); |
| seen_messages.insert(message.extension); |
| SendReply(dsp.get(), FakeMmioPtr(®s), IpcMessage(0, 0)); |
| } |
| } |
| |
| TEST(Ipc, ShutdownWithQueuedSend) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), std::nullopt, zx::duration::infinite()); |
| |
| // Start a thread, and wait for it to send. |
| Thread thread([&dsp]() { |
| Status result = dsp->Send(0, 0); |
| ZX_ASSERT_MSG(!result.ok() && result.code() == ZX_ERR_CANCELED, |
| "Expected send to fail with 'ZX_ERR_CANCELED', but got: %s", |
| result.ToString().c_str()); |
| }); |
| WaitForCond([&]() { return dsp->IsOperationPending(); }); |
| |
| // Shut down the IPC object. |
| dsp->Shutdown(); |
| } |
| |
| TEST(Ipc, DestructWithQueuedThread) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), std::nullopt, zx::duration::infinite()); |
| |
| // Start a thread, and wait for it to send. |
| Thread thread([&dsp]() { |
| Status result = dsp->Send(0, 0); |
| ZX_ASSERT_MSG(!result.ok() && result.code() == ZX_ERR_CANCELED, |
| "Expected send to fail with 'ZX_ERR_CANCELED', but got: %s", |
| result.ToString().c_str()); |
| }); |
| WaitForCond([&]() { return dsp->IsOperationPending(); }); |
| |
| // Destruct the object. |
| dsp.reset(); |
| } |
| |
| TEST(Ipc, NotificationNoReceiver) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), std::nullopt, zx::duration::infinite()); |
| |
| // Ensure notifications without a receiver don't crash. |
| for (int i = 0; i < 10; i++) { |
| SendNotification(dsp.get(), FakeMmioPtr(®s), NotificationType::FW_READY); |
| } |
| } |
| |
| TEST(Ipc, NotificationReceived) { |
| std::optional<NotificationType> received_notification; |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel( |
| "UnitTests", FakeMmioPtr(®s), [&](NotificationType type) { received_notification = type; }, |
| zx::duration::infinite()); |
| |
| // Ensure a notification is sent and received. |
| SendNotification(dsp.get(), FakeMmioPtr(®s), NotificationType::FW_READY); |
| EXPECT_EQ(received_notification, NotificationType::FW_READY); |
| } |
| |
| TEST(Ipc, SendBigData) { |
| adsp_registers_t regs = {}; |
| std::unique_ptr<DspChannel> dsp = CreateHardwareDspChannel("UnitTests", FakeMmioPtr(®s)); |
| |
| // Create a large amount of data. |
| std::vector<uint8_t> data; |
| data.resize(1'000'000); |
| |
| // Ensure we get a valid error trying to send it. |
| EXPECT_EQ(ZX_ERR_INVALID_ARGS, dsp->SendWithData(0, 0, data, {}, nullptr).code()); |
| } |
| |
| } // namespace |
| } // namespace audio::intel_hda |