blob: 1af43d11d6384bd3ddb58952b42e08817d7c99a4 [file] [log] [blame]
// 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