| // Copyright 2020 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 "src/devices/usb/drivers/xhci/xhci-transfer-ring.h" |
| |
| #include <lib/fpromise/bridge.h> |
| #include <lib/fpromise/promise.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/types.h> |
| |
| #include <atomic> |
| #include <memory> |
| #include <thread> |
| |
| #include <fake-dma-buffer/fake-dma-buffer.h> |
| #include <fake-mmio-reg/fake-mmio-reg.h> |
| #include <fbl/algorithm.h> |
| #include <zxtest/zxtest.h> |
| |
| #include "src/devices/testing/mock-ddk/mock-device.h" |
| #include "src/devices/usb/drivers/xhci/usb-xhci.h" |
| #include "src/devices/usb/drivers/xhci/xhci-event-ring.h" |
| |
| namespace usb_xhci { |
| |
| const zx::bti kFakeBti(42); |
| |
| class TransferRingHarness : public zxtest::Test { |
| public: |
| TransferRingHarness() |
| : trb_context_allocator_(-1, true), |
| hci_(root_.get(), ddk_fake::CreateBufferFactory(), loop_.dispatcher()) {} |
| void SetUp() override { |
| constexpr auto kOffset = 6 * sizeof(uint32_t); |
| constexpr auto kErdp = 2062 * sizeof(uint32_t); |
| |
| region_.emplace(sizeof(uint32_t), 4096); |
| buffer_.emplace(region_->GetMmioBuffer()); |
| (*region_)[kOffset].SetReadCallback([=]() { return 0x2000; }); |
| (*region_)[kErdp].SetReadCallback([=]() { return erdp_; }); |
| (*region_)[kErdp].SetWriteCallback([=](uint64_t value) { |
| ERDP reg; |
| reg.set_reg_value(value); |
| erdp_ = reg.Pointer(); |
| }); |
| hci_.SetTestHarness(this); |
| ASSERT_OK(hci_.InitThread()); |
| } |
| |
| void TearDown() override {} |
| |
| using TestRequest = usb::CallbackRequest<sizeof(max_align_t)>; |
| template <typename Callback> |
| zx_status_t AllocateRequest(std::optional<TestRequest>* request, uint32_t device_id, |
| uint64_t data_size, uint8_t endpoint, Callback callback) { |
| return TestRequest::Alloc(request, data_size, endpoint, hci_.UsbHciGetRequestSize(), |
| std::move(callback)); |
| } |
| |
| void RequestQueue(usb_request_t* usb_request, |
| const usb_request_complete_callback_t* complete_cb) { |
| pending_req_ = Request(usb_request, *complete_cb, sizeof(usb_request_t)); |
| } |
| |
| Request Borrow(TestRequest request) { |
| request.Queue(*this); |
| return std::move(*pending_req_); |
| } |
| |
| TransferRing* ring() { return ring_; } |
| |
| void SetRing(TransferRing* ring) { ring_ = ring; } |
| |
| std::unique_ptr<TRBContext> AllocateContext() { return trb_context_allocator_.New(); } |
| |
| EventRing& event_ring() { return event_ring_; } |
| |
| private: |
| async::Loop loop_{&kAsyncLoopConfigNeverAttachToThread}; |
| |
| using AllocatorTraits = fbl::InstancedSlabAllocatorTraits<std::unique_ptr<TRBContext>, 4096U>; |
| using AllocatorType = fbl::SlabAllocator<AllocatorTraits>; |
| AllocatorType trb_context_allocator_; |
| |
| std::shared_ptr<MockDevice> root_ = MockDevice::FakeRootParent(); |
| std::optional<Request> pending_req_; |
| fbl::DoublyLinkedList<std::unique_ptr<TRBContext>> pending_contexts_; |
| std::optional<fdf::MmioBuffer> buffer_; |
| UsbXhci hci_; |
| TransferRing* ring_; |
| EventRing event_ring_; |
| CommandRing command_ring_; |
| uint64_t erdp_; |
| std::optional<ddk_fake::FakeMmioRegRegion> region_; |
| }; |
| |
| void UsbXhci::ConnectToEndpoint(ConnectToEndpointRequest& request, |
| ConnectToEndpointCompleter::Sync& completer) { |
| completer.Reply(fit::as_error(ZX_ERR_NOT_SUPPORTED)); |
| } |
| |
| void UsbXhci::UsbHciSetBusInterface(const usb_bus_interface_protocol_t* bus_intf) {} |
| |
| size_t UsbXhci::UsbHciGetMaxDeviceCount() { return 0; } |
| |
| zx_status_t UsbXhci::UsbHciEnableEndpoint(uint32_t device_id, |
| const usb_endpoint_descriptor_t* ep_desc, |
| const usb_ss_ep_comp_descriptor_t* ss_com_desc, |
| bool enable) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t UsbXhci::InitThread() { |
| fbl::AllocChecker ac; |
| interrupters_ = fbl::MakeArray<Interrupter>(&ac, 1); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| max_slots_ = 32; |
| device_state_ = fbl::MakeArray<fbl::RefPtr<DeviceState>>(&ac, max_slots_); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| for (size_t i = 0; i < max_slots_; i++) { |
| device_state_[i] = fbl::MakeRefCounted<DeviceState>(static_cast<uint32_t>(i), this); |
| fbl::AutoLock l(&device_state_[i]->transaction_lock()); |
| for (size_t c = 0; c < max_slots_; c++) { |
| zx_status_t status = device_state_[i]->InitEndpoint( |
| static_cast<uint8_t>(c), |
| &static_cast<TransferRingHarness*>(GetTestHarness())->event_ring(), nullptr); |
| if (status != ZX_OK) { |
| return status; |
| } |
| } |
| } |
| fbl::AutoLock l(&device_state_[0]->transaction_lock()); |
| static_cast<TransferRingHarness*>(GetTestHarness()) |
| ->SetRing(&device_state_[0]->GetEndpoint(0).transfer_ring()); |
| return ZX_OK; |
| } |
| |
| uint64_t UsbXhci::UsbHciGetCurrentFrame() { return 0; } |
| |
| zx_status_t UsbXhci::UsbHciConfigureHub(uint32_t device_id, usb_speed_t speed, |
| const usb_hub_descriptor_t* desc, bool multi_tt) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| zx_status_t UsbXhci::UsbHciHubDeviceAdded(uint32_t device_id, uint32_t port, usb_speed_t speed) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t UsbXhci::UsbHciHubDeviceRemoved(uint32_t hub_id, uint32_t port) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t UsbXhci::UsbHciHubDeviceReset(uint32_t device_id, uint32_t port) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t UsbXhci::UsbHciResetEndpoint(uint32_t device_id, uint8_t ep_address) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t UsbXhci::UsbHciResetDevice(uint32_t hub_address, uint32_t device_id) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| size_t UsbXhci::UsbHciGetMaxTransferSize(uint32_t device_id, uint8_t ep_address) { return 0; } |
| |
| zx_status_t UsbXhci::UsbHciCancelAll(uint32_t device_id, uint8_t ep_address) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| size_t UsbXhci::UsbHciGetRequestSize() { return Request::RequestSize(sizeof(usb_request_t)); } |
| |
| void UsbXhci::UsbHciRequestQueue(usb_request_t* usb_request, |
| const usb_request_complete_callback_t* complete_cb) {} |
| |
| zx_status_t EventRing::AddSegmentIfNone() { return ZX_OK; } |
| |
| void UsbXhci::Shutdown(zx_status_t status) {} |
| |
| void EventRing::RemovePressure() {} |
| |
| fpromise::promise<void, zx_status_t> UsbXhci::DeviceOffline(uint32_t slot) { |
| return fpromise::make_error_promise<zx_status_t>(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| fpromise::promise<void, zx_status_t> UsbXhci::UsbHciDisableEndpoint(uint32_t device_id, |
| uint8_t ep_addr) { |
| return fpromise::make_error_promise<zx_status_t>(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| fpromise::promise<void, zx_status_t> EnumerateDevice(UsbXhci* hci, uint8_t port, |
| std::optional<HubInfo> hub_info) { |
| return fpromise::make_error_promise<zx_status_t>(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| Endpoint::Endpoint(UsbXhci* hci, uint32_t device_id, uint8_t address) |
| : usb_endpoint::UsbEndpoint(hci->bti(), address), hci_(hci) {} |
| zx_status_t Endpoint::Init(EventRing* event_ring, fdf::MmioBuffer* mmio) { |
| return transfer_ring_.Init(zx_system_get_page_size(), kFakeBti, event_ring, false, mmio, hci_); |
| } |
| void Endpoint::QueueRequests(QueueRequestsRequest& request, |
| QueueRequestsCompleter::Sync& completer) {} |
| void Endpoint::CancelAll(CancelAllCompleter::Sync& completer) { |
| completer.Reply(fit::as_error(ZX_ERR_NOT_SUPPORTED)); |
| } |
| void Endpoint::OnUnbound(fidl::UnbindInfo info, |
| fidl::ServerEnd<fuchsia_hardware_usb_endpoint::Endpoint> server_end) {} |
| DeviceState::~DeviceState() = default; |
| zx_status_t DeviceState::InitEndpoint(uint8_t ep_addr, EventRing* event_ring, fdf::MmioBuffer* mmio) |
| __TA_REQUIRES(transaction_lock_) { |
| rings_[ep_addr].emplace(hci_, device_id_, ep_addr); |
| return rings_[ep_addr]->Init(event_ring, mmio); |
| } |
| |
| TEST_F(TransferRingHarness, EmptyShortTransferTest) { |
| auto ring = this->ring(); |
| ASSERT_EQ(ring->HandleShortPacket(nullptr, 0, nullptr), ZX_ERR_IO); |
| } |
| |
| TEST_F(TransferRingHarness, CorruptedTransferRingShortTransferTest) { |
| auto ring = this->ring(); |
| Normal trb; |
| { |
| auto context = ring->AllocateContext(); |
| ASSERT_OK(ring->AddTRB(trb, std::move(context))); |
| } |
| ASSERT_EQ(ring->HandleShortPacket(nullptr, 0, nullptr), ZX_ERR_IO); |
| ring->TakePendingTRBs(); |
| } |
| |
| TEST_F(TransferRingHarness, MultiPageShortTransferTest) { |
| constexpr size_t kNumTrbs = 510; |
| constexpr size_t kTrbLength = 20; |
| constexpr size_t kShortLength = 4; |
| |
| auto ring = this->ring(); |
| TRB* first = nullptr; |
| TRB* last; |
| for (size_t i = 0; i < kNumTrbs; i++) { |
| TRB* ptr; |
| ASSERT_OK(ring->AllocateTRB(&ptr, nullptr)); |
| if (first == nullptr) { |
| first = ptr; |
| } |
| Control::FromTRB(ptr).set_Type(Control::Normal).ToTrb(ptr); |
| static_cast<Normal*>(ptr)->set_LENGTH(kTrbLength); |
| last = ptr; |
| } |
| |
| ASSERT_OK(ring->AssignContext(last, ring->AllocateContext(), first)); |
| |
| TRB* last_trb; |
| ASSERT_OK(ring->HandleShortPacket(last - 1, kShortLength, &last_trb)); |
| EXPECT_EQ(last_trb, last); |
| |
| std::unique_ptr<TRBContext> context; |
| ASSERT_OK(ring->CompleteTRB(last, &context)); |
| EXPECT_EQ(context->short_transfer_len.value(), ((kNumTrbs - 1) * kTrbLength) - kShortLength); |
| EXPECT_EQ(context->first_trb, first); |
| EXPECT_EQ(context->trb, last); |
| } |
| |
| TEST_F(TransferRingHarness, SetStall) { |
| auto ring = this->ring(); |
| ASSERT_FALSE(ring->stalled()); |
| ring->set_stall(true); |
| ASSERT_TRUE(ring->stalled()); |
| ring->set_stall(false); |
| ASSERT_FALSE(ring->stalled()); |
| } |
| |
| TEST_F(TransferRingHarness, AllocateContiguousFailsIfNotEnoughContiguousPhysicalMemoryExists) { |
| constexpr auto kOverAllocateAmount = 9001; |
| auto ring = this->ring(); |
| ASSERT_EQ(ring->AllocateContiguous(kOverAllocateAmount).error_value(), ZX_ERR_NO_MEMORY); |
| } |
| |
| TEST_F(TransferRingHarness, AllocateContiguousAllocatesContiguousBlocks) { |
| auto ring = this->ring(); |
| constexpr auto kContiguousCount = 42; |
| constexpr auto kIterationCount = 512; |
| for (size_t i = 0; i < kIterationCount; i++) { |
| auto result = ring->AllocateContiguous(kContiguousCount); |
| ASSERT_TRUE(result.is_ok()); |
| ASSERT_EQ(result->trbs.size(), kContiguousCount); |
| auto trb_start = result->trbs.data(); |
| for (size_t c = 0; c < kContiguousCount; c++) { |
| ASSERT_NE(Control::FromTRB(trb_start + c).Type(), Control::Link); |
| } |
| } |
| } |
| |
| TEST_F(TransferRingHarness, CanHandleConsecutiveLinks) { |
| auto ring = this->ring(); |
| const size_t trb_per_segment = zx_system_get_page_size() / sizeof(TRB); |
| // 1 TRB is the link TRB, and TransferRing::AllocInternal() will allocate if there's not 2 |
| // available TRBs. |
| const size_t trb_per_segment_no_alloc = trb_per_segment - 3; |
| std::deque<TRB*> pending_trbs; |
| // Fill up a segment. |
| for (size_t i = 0; i < trb_per_segment_no_alloc; i++) { |
| auto context = ring->AllocateContext(); |
| TRBContext* ref = context.get(); |
| ASSERT_OK(ring->AddTRB(TRB(), std::move(context))); |
| |
| pending_trbs.emplace_back(ref->trb); |
| } |
| |
| // Move the dequeue pointer forward two steps, to TRB 2. |
| for (size_t i = 0; i < 3; i++) { |
| std::unique_ptr<TRBContext> ctx; |
| ring->CompleteTRB(pending_trbs.front(), &ctx); |
| pending_trbs.pop_front(); |
| } |
| |
| // Finish filling up the segment. This will allocate a new link TRB (TRB 0) and we'll start |
| // filling up a new segment. |
| for (size_t i = 0; i < 4; i++) { |
| auto context = ring->AllocateContext(); |
| TRBContext* ref = context.get(); |
| ASSERT_OK(ring->AddTRB(TRB(), std::move(context))); |
| |
| pending_trbs.emplace_back(ref->trb); |
| } |
| |
| // Move the dequeue pointer forward again - to TRB 3. |
| for (size_t i = 0; i < 1; i++) { |
| std::unique_ptr<TRBContext> ctx; |
| ring->CompleteTRB(pending_trbs.front(), &ctx); |
| pending_trbs.pop_front(); |
| } |
| |
| // This will allocate a new link TRB at 1. |
| for (size_t i = 0; i < trb_per_segment_no_alloc; i++) { |
| auto context = ring->AllocateContext(); |
| TRBContext* ref = context.get(); |
| ASSERT_OK(ring->AddTRB(TRB(), std::move(context))); |
| |
| pending_trbs.emplace_back(ref->trb); |
| } |
| |
| // At this stage, we have 3 TRB segments. |
| // Segment 0 looks like this: |
| // 0 // link to seg#1,0 |
| // 1 // link to seg#2,0 |
| // ... |
| // 255 // link to seg#0,0 |
| // |
| // Segment 1 looks like this: |
| // 0 |
| // ... |
| // 255 // link to seg#0,1 |
| // |
| // Segment 2 looks like this: |
| // 0 |
| // ... |
| // 255 // link to seg#1,2 |
| // |
| // Notice that there are two consecutive links here - one between seg#1,255 -> seg#0,1 and then |
| // seg#0,1 -> seg#2,0. |
| |
| // Clean up all the pending TRBs. |
| while (!pending_trbs.empty()) { |
| std::unique_ptr<TRBContext> ctx; |
| ASSERT_OK(ring->CompleteTRB(pending_trbs.front(), &ctx)); |
| pending_trbs.pop_front(); |
| } |
| |
| // Move through, allocating and deallocating TRBs. |
| // This will eventually hit the consecutive links. |
| for (size_t i = 0; i < 3 * trb_per_segment; i++) { |
| auto context = ring->AllocateContext(); |
| TRBContext* ref = context.get(); |
| ASSERT_OK(ring->AddTRB(TRB(), std::move(context))); |
| std::unique_ptr<TRBContext> ctx; |
| ASSERT_OK(ring->CompleteTRB(ref->trb, &ctx)); |
| } |
| } |
| |
| TEST_F(TransferRingHarness, Peek) { |
| auto ring = this->ring(); |
| TRB* trb; |
| ring->AllocateTRB(&trb, nullptr); |
| auto result = ring->PeekCommandRingControlRegister(0); |
| ASSERT_EQ(trb + 1, ring->PhysToVirt(result->PTR())); |
| ASSERT_TRUE(result->RCS()); |
| } |
| |
| TEST_F(TransferRingHarness, First) { |
| ContiguousTRBInfo info; |
| TRB a; |
| TRB b; |
| info.trbs = cpp20::span(&a, 1); |
| ASSERT_EQ(info.first().data(), &a); |
| info.nop = cpp20::span(&b, 1); |
| ASSERT_EQ(info.first().data(), &b); |
| } |
| |
| } // namespace usb_xhci |