| // Copyright 2021 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. |
| #ifndef SRC_DEVICES_PCI_TESTING_PCI_PROTOCOL_FAKE_H_ |
| #define SRC_DEVICES_PCI_TESTING_PCI_PROTOCOL_FAKE_H_ |
| #include <fuchsia/hardware/pci/c/banjo.h> |
| #include <fuchsia/hardware/pci/cpp/banjo.h> |
| #include <lib/ddk/device.h> |
| #include <lib/fake-bti/bti.h> |
| #include <lib/zx/bti.h> |
| #include <lib/zx/interrupt.h> |
| #include <zircon/errors.h> |
| #include <zircon/hw/pci.h> |
| #include <zircon/status.h> |
| |
| #include <array> |
| #include <optional> |
| #include <vector> |
| |
| #include <fbl/algorithm.h> |
| |
| // These are here to prevent a large dependency chain from including the userspace |
| // PCI driver's headers. |
| #define PCI_DEVICE_BAR_COUNT 6 |
| #define MSI_MAX_VECTORS 32 |
| #define PCI_CFG_HEADER_SIZE 64 |
| #define kFakePciInternalError "Internal FakePciProtocol Error" |
| |
| namespace pci { |
| class FakePciProtocol : public ddk::PciProtocol<FakePciProtocol> { |
| public: |
| FakePciProtocol() { reset(); } |
| |
| struct FakeBar { |
| size_t size; |
| zx::vmo vmo; |
| }; |
| |
| struct FakeCapability { |
| bool operator<(const FakeCapability& r) const { return this->position < r.position; } |
| |
| uint8_t id; |
| uint8_t position; |
| uint8_t size; |
| }; |
| |
| zx_status_t PciGetBar(uint32_t bar_id, pci_bar_t* out_res) { |
| if (!out_res) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| if (bar_id >= PCI_DEVICE_BAR_COUNT) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| if (bars_[bar_id].size == 0) { |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| auto& bar = bars_[bar_id]; |
| zx::vmo bar_vmo{}; |
| zx_status_t status = bar.vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &bar_vmo); |
| ZX_ASSERT_MSG(status == ZX_OK, kFakePciInternalError); |
| out_res->id = bar_id; |
| out_res->size = bar.size; |
| out_res->type = ZX_PCI_BAR_TYPE_MMIO; |
| out_res->u.handle = bar_vmo.release(); |
| return ZX_OK; |
| } |
| |
| zx_status_t PciAckInterrupt() { |
| return (irq_mode_ == PCI_IRQ_MODE_LEGACY) ? ZX_OK : ZX_ERR_BAD_STATE; |
| } |
| |
| zx_status_t PciMapInterrupt(uint32_t which_irq, zx::interrupt* out_handle) { |
| if (!out_handle) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| switch (irq_mode_) { |
| case PCI_IRQ_MODE_LEGACY: |
| case PCI_IRQ_MODE_LEGACY_NOACK: |
| if (which_irq > 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| return legacy_interrupt_->duplicate(ZX_RIGHT_SAME_RIGHTS, out_handle); |
| case PCI_IRQ_MODE_MSI: |
| if (which_irq >= msi_interrupts_.size()) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| return msi_interrupts_[which_irq].duplicate(ZX_RIGHT_SAME_RIGHTS, out_handle); |
| case PCI_IRQ_MODE_MSI_X: |
| if (which_irq >= msix_interrupts_.size()) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| return msix_interrupts_[which_irq].duplicate(ZX_RIGHT_SAME_RIGHTS, out_handle); |
| } |
| |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| zx_status_t PciConfigureIrqMode(uint32_t requested_irq_count, pci_irq_mode_t* out_irq_mode) { |
| ZX_ASSERT(requested_irq_count); |
| zx_status_t status; |
| if (msix_interrupts_.size() >= requested_irq_count) { |
| if ((status = PciSetIrqMode(PCI_IRQ_MODE_MSI_X, requested_irq_count)) == ZX_OK) { |
| if (out_irq_mode) { |
| *out_irq_mode = PCI_IRQ_MODE_MSI_X; |
| } |
| return ZX_OK; |
| } |
| } |
| |
| if (msi_interrupts_.size() >= requested_irq_count) { |
| if ((status = PciSetIrqMode(PCI_IRQ_MODE_MSI, requested_irq_count)) == ZX_OK) { |
| if (out_irq_mode) { |
| *out_irq_mode = PCI_IRQ_MODE_MSI; |
| } |
| return ZX_OK; |
| } |
| } |
| |
| if (legacy_interrupt_ && requested_irq_count == 1) { |
| if ((status = PciSetIrqMode(PCI_IRQ_MODE_LEGACY, requested_irq_count)) == ZX_OK) { |
| if (out_irq_mode) { |
| *out_irq_mode = PCI_IRQ_MODE_LEGACY; |
| } |
| return ZX_OK; |
| } |
| } |
| |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t PciQueryIrqMode(pci_irq_mode_t mode, uint32_t* out_max_irqs) { |
| ZX_ASSERT(out_max_irqs); |
| ZX_ASSERT(mode < PCI_IRQ_MODE_COUNT); |
| |
| switch (mode) { |
| case PCI_IRQ_MODE_LEGACY: |
| case PCI_IRQ_MODE_LEGACY_NOACK: |
| if (legacy_interrupt_) { |
| *out_max_irqs = 1; |
| return ZX_OK; |
| } |
| break; |
| case PCI_IRQ_MODE_MSI: |
| if (!msi_interrupts_.empty()) { |
| // MSI interrupts are only supported in powers of 2. |
| *out_max_irqs = (msi_interrupts_.size() <= 1) |
| ? msi_interrupts_.size() |
| : fbl::round_down(msi_interrupts_.size(), 2u); |
| return ZX_OK; |
| } |
| break; |
| case PCI_IRQ_MODE_MSI_X: |
| if (!msix_interrupts_.empty()) { |
| *out_max_irqs = msix_interrupts_.size(); |
| return ZX_OK; |
| break; |
| } |
| } |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| // This allows us to mimic the kernel's handling of outstanding MsiDispatchers per MsiAllocation |
| // objects. A device's legacy interrupt is still a valid object if the interrupt mode is |
| // switched, albeit not a useful one. |
| bool AllMappedInterruptsFreed() { |
| zx_info_handle_count_t info; |
| for (auto& interrupts : {&msix_interrupts_, &msi_interrupts_}) { |
| for (auto& interrupt : *interrupts) { |
| zx_status_t status = |
| interrupt.get_info(ZX_INFO_HANDLE_COUNT, &info, sizeof(info), nullptr, nullptr); |
| ZX_ASSERT_MSG(status == ZX_OK, "%s status %d", kFakePciInternalError, status); |
| |
| if (info.handle_count > 1) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| zx_status_t PciSetIrqMode(pci_irq_mode_t mode, uint32_t requested_irq_count) { |
| if (!AllMappedInterruptsFreed()) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| switch (mode) { |
| case PCI_IRQ_MODE_LEGACY: |
| case PCI_IRQ_MODE_LEGACY_NOACK: |
| if (requested_irq_count > 1) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| if (legacy_interrupt_) { |
| irq_mode_ = mode; |
| irq_cnt_ = 1; |
| } |
| return ZX_OK; |
| case PCI_IRQ_MODE_MSI: |
| if (msi_interrupts_.empty()) { |
| break; |
| } |
| if (!fbl::is_pow2(requested_irq_count) || requested_irq_count > MSI_MAX_VECTORS) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (msi_interrupts_.size() < requested_irq_count) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| irq_mode_ = PCI_IRQ_MODE_MSI; |
| irq_cnt_ = requested_irq_count; |
| return ZX_OK; |
| case PCI_IRQ_MODE_MSI_X: |
| if (msix_interrupts_.empty()) { |
| break; |
| } |
| |
| if (msix_interrupts_.size() < requested_irq_count) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| irq_mode_ = PCI_IRQ_MODE_MSI_X; |
| irq_cnt_ = requested_irq_count; |
| return ZX_OK; |
| } |
| |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t PciEnableBusMaster(bool enable) { |
| bus_master_en_ = enable; |
| return ZX_OK; |
| } |
| |
| zx_status_t PciResetDevice() { |
| reset_cnt_++; |
| return ZX_OK; |
| } |
| |
| zx_status_t PciGetDeviceInfo(pcie_device_info_t* out_info) { |
| ZX_ASSERT(out_info); |
| *out_info = info_; |
| return ZX_OK; |
| } |
| |
| template <typename T> |
| zx_status_t ConfigRead(uint16_t offset, T* out_value) { |
| ZX_ASSERT_MSG(offset + sizeof(T) <= PCI_BASE_CONFIG_SIZE, |
| "FakePciProtocol: PciConfigRead reads must fit in the range [%#x, %#x] (offset " |
| "= %#x, io width = %#lx).", |
| 0, PCI_BASE_CONFIG_SIZE - 1, offset, sizeof(T)); |
| return config_.read(out_value, offset, sizeof(T)); |
| } |
| |
| constexpr zx_status_t PciConfigRead8(uint16_t offset, uint8_t* out_value) { |
| return ConfigRead(offset, out_value); |
| } |
| |
| constexpr zx_status_t PciConfigRead16(uint16_t offset, uint16_t* out_value) { |
| return ConfigRead(offset, out_value); |
| } |
| |
| constexpr zx_status_t PciConfigRead32(uint16_t offset, uint32_t* out_value) { |
| return ConfigRead(offset, out_value); |
| } |
| |
| template <typename T> |
| zx_status_t ConfigWrite(uint16_t offset, T value) { |
| ZX_ASSERT_MSG(offset >= PCI_CFG_HEADER_SIZE && offset + sizeof(T) <= PCI_BASE_CONFIG_SIZE, |
| "FakePciProtocol: PciConfigWrite writes must fit in the range [%#x, %#x] (offset " |
| "= %#x, io width = %#lx).", |
| PCI_CFG_HEADER_SIZE, PCI_BASE_CONFIG_SIZE - 1, offset, sizeof(T)); |
| return config_.write(&value, offset, sizeof(T)); |
| } |
| |
| zx_status_t PciConfigWrite8(uint16_t offset, uint8_t value) { return ConfigWrite(offset, value); } |
| |
| zx_status_t PciConfigWrite16(uint16_t offset, uint16_t value) { |
| return ConfigWrite(offset, value); |
| } |
| |
| zx_status_t PciConfigWrite32(uint16_t offset, uint32_t value) { |
| return ConfigWrite(offset, value); |
| } |
| |
| zx_status_t CommonCapabilitySearch(uint8_t id, std::optional<uint8_t> offset, |
| uint8_t* out_offset) { |
| if (!out_offset) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| for (auto& cap : capabilities_) { |
| // Skip until we've caught up to last one found if one was provided. |
| if (offset && cap.position <= offset) { |
| continue; |
| } |
| |
| if (cap.id == id) { |
| *out_offset = cap.position; |
| return ZX_OK; |
| } |
| } |
| |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| zx_status_t PciGetFirstCapability(uint8_t id, uint8_t* out_offset) { |
| return CommonCapabilitySearch(id, std::nullopt, out_offset); |
| } |
| |
| zx_status_t PciGetNextCapability(uint8_t id, uint8_t offset, uint8_t* out_offset) { |
| return CommonCapabilitySearch(id, offset, out_offset); |
| } |
| |
| zx_status_t PciGetFirstExtendedCapability(uint16_t id, uint16_t* out_offset) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t PciGetNextExtendedCapability(uint16_t id, uint16_t offset, uint16_t* out_offset) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t PciGetBti(uint32_t index, zx::bti* out_bti) { |
| if (!out_bti) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| return bti_.duplicate(ZX_RIGHT_SAME_RIGHTS, out_bti); |
| } |
| |
| // Returns a |pci_protocol_t| suitable for use with the C api, or passing to a |
| // ddk::PciProtocolClient. |
| const pci_protocol_t& get_protocol() { return protocol_; } |
| |
| // Support methods for tests |
| |
| // Add an interrupt for the specified PCI interrupt mode. A borrowed copy of the zx::interrupt |
| // is returned for use in tests. |
| zx::unowned_interrupt AddLegacyInterrupt() { return AddInterrupt(PCI_IRQ_MODE_LEGACY); } |
| zx::unowned_interrupt AddMsiInterrupt() { return AddInterrupt(PCI_IRQ_MODE_MSI); } |
| zx::unowned_interrupt AddMsixInterrupt() { return AddInterrupt(PCI_IRQ_MODE_MSI_X); } |
| zx::unowned_interrupt AddInterrupt(pci_irq_mode_t mode) { |
| ZX_ASSERT_MSG(!(mode == PCI_IRQ_MODE_LEGACY && legacy_interrupt_), |
| "FakePciProtocol Error: Legacy interrupt mode only supports 1 interrupt."); |
| ZX_ASSERT_MSG(!(mode == PCI_IRQ_MODE_MSI && msi_interrupts_.size() == MSI_MAX_VECTORS), |
| "FakePciProtocol Error: MSI interrupt mode only supports up to %u interrupts.", |
| MSI_MAX_VECTORS); |
| |
| zx::interrupt interrupt{}; |
| zx_status_t status = zx::interrupt::create(*zx::unowned_resource(ZX_HANDLE_INVALID), 0, |
| ZX_INTERRUPT_VIRTUAL, &interrupt); |
| ZX_ASSERT_MSG(status == ZX_OK, kFakePciInternalError); |
| zx::unowned_interrupt borrow = interrupt.borrow(); |
| |
| switch (mode) { |
| case PCI_IRQ_MODE_LEGACY: |
| legacy_interrupt_ = std::move(interrupt); |
| break; |
| case PCI_IRQ_MODE_MSI: |
| msi_interrupts_.push_back(std::move(interrupt)); |
| break; |
| case PCI_IRQ_MODE_MSI_X: |
| msix_interrupts_.push_back(std::move(interrupt)); |
| break; |
| } |
| |
| return borrow; |
| } |
| |
| pcie_device_info_t SetDeviceInfo(pcie_device_info_t info) { |
| info_ = info; |
| config_.write(&info_.vendor_id, PCI_CFG_VENDOR_ID, sizeof(info_.vendor_id)); |
| config_.write(&info_.device_id, PCI_CFG_DEVICE_ID, sizeof(info_.device_id)); |
| config_.write(&info_.revision_id, PCI_CFG_REVISION_ID, sizeof(info_.revision_id)); |
| config_.write(&info_.base_class, PCI_CFG_CLASS_CODE_BASE, sizeof(info_.base_class)); |
| config_.write(&info_.sub_class, PCI_CFG_CLASS_CODE_SUB, sizeof(info_.sub_class)); |
| config_.write(&info_.program_interface, PCI_CFG_CLASS_CODE_INTR, |
| sizeof(info_.program_interface)); |
| |
| return info; |
| } |
| |
| void AddVendorCapability(uint8_t position, uint8_t size) { |
| ZX_ASSERT_MSG( |
| size > 2, |
| "FakePciProtocol Error: a vendor capability must be at least size 0x3 (size = %#x).", size); |
| AddCapability(PCI_CAP_ID_VENDOR, position, size); |
| // Vendor capabilities store a size at the byte following the next pointer. |
| config_.write(&size, position + 2, sizeof(size)); |
| } |
| |
| // No registers are configured, but most devices that check for this |
| // capability do so just to understand the configuration space they have |
| // available, not to actually attempt to modify this capability. |
| static constexpr uint8_t kPciExpressCapabilitySize = 0x3B; |
| void AddPciExpressCapability(uint8_t position) { |
| AddCapability(PCI_CAP_ID_PCI_EXPRESS, position, kPciExpressCapabilitySize); |
| } |
| |
| // Capabilities are the hardest part to implement because if a device expects a capability |
| // at a given address in configuration space then it's possible they will want to write to it. |
| // Additionally, vendor capabilities are of a variable size which is read from the capability |
| // at runtime. To further complicate things, particular devices will have registers in their |
| // configuration space that the device may be expected to use but which are not exposed |
| // through any PCI base address register mechanism. This makes it risky to lay out a |
| // capability wherever we wish for fear it may overlap with one of these spaces. For this |
| // reason we do no validation of the capability's setup in configuration space besides writing |
| // the capability id and next pointer. The test author is responsible for setting up the |
| // layout of the capabilities as necessary to match their device, but we can provide helper |
| // methods to ensure they're doing it properly. |
| void AddCapability(uint8_t capability_id, uint8_t position, uint8_t size) { |
| ZX_ASSERT_MSG( |
| capability_id > 0 && capability_id <= PCI_CAP_ID_FLATTENING_PORTAL_BRIDGE, |
| "FakePciProtocol Error: capability_id must be non-zero and <= %#x (capability_id = %#x).", |
| PCI_CAP_ID_FLATTENING_PORTAL_BRIDGE, capability_id); |
| ZX_ASSERT_MSG(position >= PCI_CFG_HEADER_SIZE && position + size < PCI_BASE_CONFIG_SIZE, |
| "FakePciProtocolError: capability must fit the range [%#x, %#x] (capability = " |
| "[%#x, %#x]).", |
| PCI_CFG_HEADER_SIZE, PCI_BASE_CONFIG_SIZE - 1, position, position + size - 1); |
| |
| // We need to update the next pointer of the previous capability, or the |
| // original header capabilities pointer if this is the first. |
| uint8_t next_ptr = PCI_CFG_CAPABILITIES_PTR; |
| if (!capabilities_.empty()) { |
| for (auto& cap : capabilities_) { |
| ZX_ASSERT_MSG(!(position <= cap.position && position + size > cap.position) && |
| !(position >= cap.position && position < cap.position + cap.size), |
| "FakePciProtocol Error: New capability overlaps with a previous capability " |
| "[%#x, %#x] (new capability id = %#x @ [%#x, %#x]).", |
| cap.position, cap.position + cap.size - 1, capability_id, position, |
| position + size - 1); |
| } |
| next_ptr = capabilities_[capabilities_.size() - 1].position + 1; |
| } |
| |
| config_.write(&capability_id, position, sizeof(capability_id)); |
| config_.write(&position, next_ptr, sizeof(position)); |
| capabilities_.push_back({.id = capability_id, .position = position, .size = size}); |
| // Not fast, but not as error prone as doing it by hand on insertion with |
| // capability cyles being a possibility. |
| std::sort(capabilities_.begin(), capabilities_.end()); |
| } |
| |
| zx::unowned_vmo SetBar(uint32_t bar_id, size_t size, zx::vmo vmo) { |
| ZX_ASSERT_MSG(bar_id < PCI_DEVICE_BAR_COUNT, |
| "FakePciProtocol Error: valid BAR ids are [0, 5] (bar_id = %u)", bar_id); |
| uint64_t vmo_size = 0; |
| zx_status_t status = vmo.get_size(&vmo_size); |
| ZX_ASSERT_MSG(status == ZX_OK, kFakePciInternalError); |
| ZX_ASSERT_MSG(vmo_size >= size, |
| "FakePciProtocol Error: vmo is not large enough for BAR size (BAR size = %#lx, " |
| "vmo size = %#lx)", |
| size, vmo_size); |
| |
| bars_[bar_id].size = size; |
| bars_[bar_id].vmo = std::move(vmo); |
| return bars_[bar_id].vmo.borrow(); |
| } |
| |
| zx::unowned_vmo CreateBar(uint32_t bar_id, size_t size) { |
| zx::vmo vmo; |
| zx_status_t status = zx::vmo::create(size, /*options=*/0, &vmo); |
| ZX_ASSERT_MSG(status == ZX_OK, |
| "FakePciProtocol Error: failed to create VMO for bar (bar_id = %u, size = %#zx, " |
| "status = %d)", |
| bar_id, size, status); |
| return SetBar(bar_id, size, std::move(vmo)); |
| } |
| |
| zx::unowned_vmo GetBar(uint32_t bar_id) { |
| ZX_ASSERT_MSG(bar_id < PCI_DEVICE_BAR_COUNT, |
| "FakePciProtocol Error: valid BAR ids are [0, 5] (bar_id = %u)", bar_id); |
| ZX_ASSERT_MSG(bars_[bar_id].size > 0, "FakePciProtocol Error: BAR %u has not been set.", |
| bar_id); |
| return bars_[bar_id].vmo.borrow(); |
| } |
| |
| zx::unowned_vmo GetConfigVmo() { return config_.borrow(); } |
| pci_irq_mode_t GetIrqMode() const { return irq_mode_; } |
| uint32_t GetIrqCount() const { return irq_cnt_; } |
| uint32_t GetResetCount() const { return reset_cnt_; } |
| |
| // Returns the state of the device's Bus Mastering setting. Returned as an optional |
| // so that the caller can differentiate between off and "never set" states in driver |
| // testing. |
| std::optional<bool> GetBusMasterEnabled() const { return bus_master_en_; } |
| |
| void reset() { |
| legacy_interrupt_ = std::nullopt; |
| msi_interrupts_.clear(); |
| msix_interrupts_.clear(); |
| irq_mode_ = PCI_IRQ_MODE_DISABLED; |
| irq_cnt_ = 0; |
| |
| bars_ = {}; |
| capabilities_.clear(); |
| |
| bus_master_en_ = std::nullopt; |
| reset_cnt_ = 0; |
| info_ = {}; |
| |
| zx_status_t status = zx::vmo::create(PCI_BASE_CONFIG_SIZE, /*options=*/0, &config_); |
| ZX_ASSERT(status == ZX_OK); |
| status = fake_bti_create(bti_.reset_and_get_address()); |
| ZX_ASSERT(status == ZX_OK); |
| } |
| |
| private: |
| // Interrupts |
| std::optional<zx::interrupt> legacy_interrupt_; |
| std::vector<zx::interrupt> msi_interrupts_; |
| std::vector<zx::interrupt> msix_interrupts_; |
| pci_irq_mode_t irq_mode_; |
| uint32_t irq_cnt_; |
| |
| std::array<FakeBar, PCI_DEVICE_BAR_COUNT> bars_{}; |
| std::vector<FakeCapability> capabilities_; |
| |
| zx::bti bti_; |
| uint32_t reset_cnt_; |
| std::optional<bool> bus_master_en_; |
| pcie_device_info_t info_ = {}; |
| zx::vmo config_; |
| pci_protocol_t protocol_ = {.ops = &pci_protocol_ops_, .ctx = this}; |
| }; |
| |
| } // namespace pci |
| |
| #endif // SRC_DEVICES_PCI_TESTING_PCI_PROTOCOL_FAKE_H_ |