| // 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 <lib/mmio/mmio.h> |
| #include <lib/zx/status.h> |
| #include <zircon/assert.h> |
| #include <zircon/errors.h> |
| |
| #include <ddk/protocol/pci.h> |
| #include <ddktl/protocol/pci.h> |
| #include <fbl/algorithm.h> |
| |
| #include "capabilities/msi.h" |
| #include "common.h" |
| #include "device.h" |
| |
| namespace pci { |
| |
| zx::status<uint32_t> Device::QueryIrqMode(pci_irq_mode_t mode) { |
| fbl::AutoLock dev_lock(&dev_lock_); |
| switch (mode) { |
| case PCI_IRQ_MODE_LEGACY: |
| return zx::error(ZX_ERR_NOT_SUPPORTED); |
| case PCI_IRQ_MODE_MSI: |
| if (caps_.msi) { |
| return zx::ok(caps_.msi->vectors_avail()); |
| } |
| break; |
| case PCI_IRQ_MODE_MSI_X: |
| if (caps_.msix) { |
| return zx::ok(caps_.msix->table_size()); |
| } |
| break; |
| case PCI_IRQ_MODE_DISABLED: |
| default: |
| return zx::error(ZX_ERR_INVALID_ARGS); |
| } |
| return zx::error(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| zx_status_t Device::SetIrqMode(pci_irq_mode_t mode, uint32_t irq_cnt) { |
| if (mode >= PCI_IRQ_MODE_COUNT) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| if (mode != PCI_IRQ_MODE_DISABLED && irq_cnt == 0) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| fbl::AutoLock dev_lock(&dev_lock_); |
| // Before enabling any given interrupt mode we need to ensure no existing |
| // interrupts are configured. Disabling them can fail in cases downstream |
| // drivers have mot freed outstanding interrupt objects allocated off of |
| // an MSI object. |
| if (zx_status_t st = DisableInterrupts(); st != ZX_OK) { |
| return st; |
| } |
| |
| // At this point interrupts have been disabled, so we're already successful |
| // if that was the intent. |
| if (mode == PCI_IRQ_MODE_DISABLED) { |
| return ZX_OK; |
| } |
| |
| switch (mode) { |
| case PCI_IRQ_MODE_MSI: |
| if (caps_.msi) { |
| return EnableMsi(irq_cnt); |
| } |
| break; |
| case PCI_IRQ_MODE_MSI_X: |
| if (caps_.msix) { |
| return EnableMsix(irq_cnt); |
| } |
| break; |
| } |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t Device::DisableInterrupts() { |
| zxlogf(DEBUG, "[%s] disabling IRQ mode %u", cfg_->addr(), irqs_.mode); |
| zx_status_t st = ZX_OK; |
| switch (irqs_.mode) { |
| case PCI_IRQ_MODE_DISABLED: |
| zxlogf(DEBUG, "[%s] disabling interrupts when interrupts are already disabled", cfg_->addr()); |
| break; |
| case PCI_IRQ_MODE_MSI: |
| st = DisableMsi(); |
| break; |
| case PCI_IRQ_MODE_MSI_X: |
| st = DisableMsix(); |
| break; |
| } |
| |
| if (st == ZX_OK) { |
| irqs_.mode = PCI_IRQ_MODE_DISABLED; |
| } |
| return st; |
| } |
| |
| zx::status<zx::interrupt> Device::MapInterrupt(uint32_t which_irq) { |
| fbl::AutoLock dev_lock(&dev_lock_); |
| // MSI support is controlled through the capability held within the device's configuration space, |
| // so the dispatcher needs acess to the given device's config vmo. MSI-X needs access to the table |
| // structure which is held in one of the device BARs, but a view is built ahead of time for it |
| // when the MSI-X capability is initialized. |
| if (irqs_.mode == PCI_IRQ_MODE_DISABLED) { |
| return zx::error(ZX_ERR_BAD_STATE); |
| } |
| |
| if (irqs_.mode == PCI_IRQ_MODE_LEGACY) { |
| return zx::error(ZX_ERR_NOT_SUPPORTED); |
| } |
| |
| zx::interrupt interrupt = {}; |
| zx_status_t status = ZX_OK; |
| switch (irqs_.mode) { |
| case PCI_IRQ_MODE_MSI: { |
| zx::status<ddk::MmioView> view_res = cfg_->get_view(); |
| if (!view_res.is_ok()) { |
| return view_res.take_error(); |
| } |
| |
| status = zx::msi::create(irqs_.msi_allocation, /*options=*/0, which_irq, *view_res->get_vmo(), |
| view_res->get_offset() + caps_.msi->base(), &interrupt); |
| break; |
| } |
| case PCI_IRQ_MODE_MSI_X: { |
| auto& msix = caps_.msix; |
| status = zx::msi::create(irqs_.msi_allocation, ZX_MSI_MODE_MSI_X, which_irq, |
| *msix->table_vmo(), msix->table_offset(), &interrupt); |
| // Disable the function level masking now that at least one interrupt exists for the device. |
| if (status == ZX_OK) { |
| MsixControlReg ctrl = {.value = cfg_->Read(caps_.msix->ctrl())}; |
| ctrl.set_function_mask(0); |
| cfg_->Write(caps_.msix->ctrl(), ctrl.value); |
| } |
| break; |
| } |
| default: |
| return zx::error(ZX_ERR_BAD_STATE); |
| } |
| |
| if (status != ZX_OK) { |
| return zx::error(status); |
| } |
| |
| return zx::ok(std::move(interrupt)); |
| } |
| |
| zx::status<std::pair<zx::msi, zx_info_msi_t>> Device::AllocateMsi(uint32_t irq_cnt) { |
| zx::msi msi; |
| zx_status_t st = bdi_->AllocateMsi(irq_cnt, &msi); |
| if (st != ZX_OK) { |
| return zx::error(st); |
| } |
| |
| zx_info_msi_t msi_info; |
| st = msi.get_info(ZX_INFO_MSI, &msi_info, sizeof(msi_info), nullptr, nullptr); |
| if (st != ZX_OK) { |
| return zx::error(st); |
| } |
| ZX_DEBUG_ASSERT(msi_info.num_irq == irq_cnt); |
| ZX_DEBUG_ASSERT(msi_info.interrupt_count == 0); |
| |
| return zx::ok(std::make_pair(std::move(msi), msi_info)); |
| } |
| |
| zx_status_t Device::EnableMsi(uint32_t irq_cnt) { |
| ZX_DEBUG_ASSERT(irqs_.mode == PCI_IRQ_MODE_DISABLED); |
| ZX_DEBUG_ASSERT(!irqs_.msi_allocation); |
| ZX_DEBUG_ASSERT(caps_.msi); |
| |
| if (!fbl::is_pow2(irq_cnt) || irq_cnt > caps_.msi->vectors_avail()) { |
| zxlogf(DEBUG, "[%s] EnableMsi: bad irq count = %u, available = %u\n", cfg_->addr(), irq_cnt, |
| caps_.msi->vectors_avail()); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| auto result = AllocateMsi(irq_cnt); |
| if (result.is_ok()) { |
| auto [alloc, info] = std::move(result.value()); |
| MsiControlReg ctrl = {.value = cfg_->Read(caps_.msi->ctrl())}; |
| cfg_->Write(caps_.msi->tgt_addr(), info.target_addr); |
| cfg_->Write(caps_.msi->tgt_data(), info.target_data); |
| if (ctrl.mm_capable()) { |
| ctrl.set_mm_enable(MsiCapability::CountToMmc(irq_cnt)); |
| } |
| ctrl.set_enable(1); |
| cfg_->Write(caps_.msi->ctrl(), ctrl.value); |
| |
| irqs_.msi_allocation = std::move(alloc); |
| irqs_.mode = PCI_IRQ_MODE_MSI; |
| } |
| return result.status_value(); |
| } |
| |
| zx_status_t Device::EnableMsix(uint32_t irq_cnt) { |
| ZX_DEBUG_ASSERT(irqs_.mode == PCI_IRQ_MODE_DISABLED); |
| ZX_DEBUG_ASSERT(!irqs_.msi_allocation); |
| ZX_DEBUG_ASSERT(caps_.msix); |
| |
| // MSI-X supports non-pow2 counts, but the MSI allocator still allocates in |
| // pow2 based blocks. |
| uint32_t irq_cnt_pow2 = fbl::roundup_pow2(irq_cnt); |
| auto result = AllocateMsi(irq_cnt_pow2); |
| if (result.is_ok()) { |
| auto [alloc, info] = std::move(result.value()); |
| // Enable MSI-X, but mask off all functions until an interrupt is mapped. |
| MsixControlReg ctrl = {.value = cfg_->Read(caps_.msix->ctrl())}; |
| ctrl.set_function_mask(1); |
| ctrl.set_enable(1); |
| cfg_->Write(caps_.msix->ctrl(), ctrl.value); |
| |
| irqs_.msi_allocation = std::move(alloc); |
| irqs_.mode = PCI_IRQ_MODE_MSI_X; |
| } |
| return result.status_value(); |
| } |
| |
| // In general, if a device driver tries to disable an interrupt mode while |
| // holding handles to individual interrupts then it's considered a bad state. |
| // TODO(fxbug.dev/32978): Are there cases where the bus driver would want to hard disable |
| // IRQs even though the driver holds outstanding handles? In the event of a driver |
| // crash the handles will be released, but in a hard disable path they would still |
| // exist. |
| zx_status_t Device::VerifyAllMsisFreed() { |
| ZX_DEBUG_ASSERT(irqs_.msi_allocation); |
| zx_info_msi_t info; |
| zx_status_t st = |
| irqs_.msi_allocation.get_info(ZX_INFO_MSI, &info, sizeof(info), nullptr, nullptr); |
| if (st != ZX_OK) { |
| return st; |
| } |
| |
| if (info.interrupt_count != 0) { |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Device::DisableMsi() { |
| ZX_DEBUG_ASSERT(irqs_.mode == PCI_IRQ_MODE_MSI); |
| ZX_DEBUG_ASSERT(caps_.msi); |
| if (zx_status_t st = VerifyAllMsisFreed(); st != ZX_OK) { |
| return st; |
| } |
| |
| MsiControlReg ctrl = {.value = cfg_->Read(caps_.msi->ctrl())}; |
| ctrl.set_enable(0); |
| cfg_->Write(caps_.msi->ctrl(), ctrl.value); |
| |
| irqs_.msi_allocation.reset(); |
| irqs_.mode = PCI_IRQ_MODE_DISABLED; |
| return ZX_OK; |
| } |
| |
| zx_status_t Device::DisableMsix() { |
| ZX_DEBUG_ASSERT(irqs_.mode == PCI_IRQ_MODE_MSI_X); |
| ZX_DEBUG_ASSERT(caps_.msix); |
| if (zx_status_t st = VerifyAllMsisFreed(); st != ZX_OK) { |
| return st; |
| } |
| |
| MsixControlReg ctrl = {.value = cfg_->Read(caps_.msix->ctrl())}; |
| ctrl.set_function_mask(1); |
| ctrl.set_enable(0); |
| cfg_->Write(caps_.msix->ctrl(), ctrl.value); |
| |
| irqs_.msi_allocation.reset(); |
| irqs_.mode = PCI_IRQ_MODE_DISABLED; |
| return ZX_OK; |
| } |
| |
| } // namespace pci |