| // Copyright 2019 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| #include <lib/arch/intrin.h> |
| #include <lib/counters.h> |
| #include <lib/fit/defer.h> |
| #include <platform.h> |
| #include <reg.h> |
| #include <trace.h> |
| #include <zircon/errors.h> |
| #include <zircon/limits.h> |
| #include <zircon/rights.h> |
| #include <zircon/types.h> |
| |
| #include <array> |
| #include <cstring> |
| #include <limits> |
| |
| #include <dev/interrupt.h> |
| #include <fbl/alloc_checker.h> |
| #include <kernel/auto_lock.h> |
| #include <object/dispatcher.h> |
| #include <object/interrupt_dispatcher.h> |
| #include <object/msi_interrupt_dispatcher.h> |
| #include <vm/vm_address_region.h> |
| #include <vm/vm_object.h> |
| |
| #define LOCAL_TRACE 0 |
| |
| KCOUNTER(dispatcher_msi_create_count, "msi_dispatcher.create") |
| KCOUNTER(dispatcher_msi_interrupt_count, "msi_dispatcher.interrupts") |
| KCOUNTER(dispatcher_msi_mask_count, "msi_dispatcher.mask") |
| KCOUNTER(dispatcher_msi_unmask_count, "msi_dispatcher.unmask") |
| KCOUNTER(dispatcher_msi_destroy_count, "msi_dispatcher.destroy") |
| |
| // Creates an a derived MsiInterruptDispatcher determined by the flags passed in |
| zx_status_t MsiInterruptDispatcher::Create(fbl::RefPtr<MsiAllocation> alloc, uint32_t msi_id, |
| const fbl::RefPtr<VmObject>& vmo, zx_paddr_t vmo_offset, |
| uint32_t options, zx_rights_t* out_rights, |
| KernelHandle<InterruptDispatcher>* out_interrupt, |
| RegisterIntFn register_int_fn) { |
| LTRACEF( |
| "out_rights = %p, out_interrupt = %p\nvmo: %s, %s, %s\nsize = %lu, " |
| "vmo_offset = %lu, options = %#x, cache policy = %u\n", |
| out_rights, out_interrupt, vmo->is_paged() ? "paged" : "physical", |
| vmo->is_contiguous() ? "contiguous" : "not contiguous", |
| vmo->is_resizable() ? "resizable" : "not resizable", vmo->size(), vmo_offset, options, |
| vmo->GetMappingCachePolicy()); |
| |
| bool is_msix = (options & ZX_MSI_MODE_MSI_X) == ZX_MSI_MODE_MSI_X; |
| options &= ~ZX_MSI_MODE_MSI_X; |
| |
| if (!out_rights || !out_interrupt || |
| (vmo->is_paged() && (vmo->is_resizable() || !vmo->is_contiguous())) || |
| vmo_offset >= vmo->size() || options || |
| vmo->GetMappingCachePolicy() != ZX_CACHE_POLICY_UNCACHED_DEVICE) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| uint32_t base_irq_id = 0; |
| { |
| Guard<SpinLock, IrqSave> guard{&alloc->lock()}; |
| if (msi_id >= alloc->block().num_irq) { |
| LTRACEF("msi_id %u is out of range for the block (num_irqs: %u)\n", msi_id, |
| alloc->block().num_irq); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| base_irq_id = alloc->block().base_irq_id; |
| } |
| |
| zx_status_t st = alloc->ReserveId(msi_id); |
| if (st != ZX_OK) { |
| LTRACEF("failed to reserve msi_id %u: %d\n", msi_id, st); |
| return st; |
| } |
| auto cleanup = fit::defer([alloc, msi_id]() { alloc->ReleaseId(msi_id); }); |
| |
| // To handle MSI masking we need to create a kernel mapping for the VMO handed |
| // to us, this will provide access to the register controlling the given MSI. |
| // The VMO must be a contiguous VMO with the cache policy already configured. |
| // Size checks will come into play when we know what type of capability we're |
| // working with. |
| auto vmar = VmAspace::kernel_aspace()->RootVmar(); |
| uint32_t vector = base_irq_id + msi_id; |
| ktl::array<char, ZX_MAX_NAME_LEN> name{}; |
| snprintf(name.data(), name.max_size(), "msi id %u (vector %u)", msi_id, vector); |
| fbl::RefPtr<VmMapping> mapping; |
| st = vmar->CreateVmMapping(0, vmo->size(), 0, 0, vmo, 0, |
| ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE, name.data(), |
| &mapping); |
| if (st != ZX_OK) { |
| LTRACEF("Failed to create MSI mapping: %d\n", st); |
| return st; |
| } |
| |
| st = mapping->MapRange(0, vmo->size(), true); |
| if (st != ZX_OK) { |
| LTRACEF("Falled to MapRange for the mapping: %d\n", st); |
| return st; |
| } |
| |
| LTRACEF("Mapping mapped at %#lx, size %zx, vmo size %lx, vmo_offset = %#lx\n", mapping->base(), |
| mapping->size(), vmo->size(), vmo_offset); |
| fbl::AllocChecker ac; |
| fbl::RefPtr<MsiInterruptDispatcher> disp; |
| |
| // MSI lives inside a device's config space within an MSI Capability. MSI-X has a similar |
| // capability, but has another table mapped elsewhere which contains individually maskable bits |
| // per vector. The capability itself is managed by the PCI bus driver, and the mask bits are |
| // handled by this dispatcher. So in the event of MSI-X there is no capability id to check, since |
| // we don't touch the capability at all at this level. |
| size_t add_result = 0; |
| if (is_msix) { |
| // Most validation for MSI-X is done in the PCI driver since it can confirm that the Table |
| // Structure is appropriately large for the number of interrupts, and the allocation by now has |
| // already been made. |
| if (add_overflow(vmo_offset, (msi_id + 1) * sizeof(MsixTableEntry), &add_result) || |
| add_result > vmo->size()) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| disp = fbl::AdoptRef<MsiInterruptDispatcher>(new (&ac) MsixDispatcherImpl( |
| ktl::move(alloc), base_irq_id, msi_id, ktl::move(mapping), vmo_offset, register_int_fn)); |
| } else { |
| auto* cap = reinterpret_cast<MsiCapability*>(mapping->base() + vmo_offset); |
| if (cap->id != kMsiCapabilityId) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| // MSI capabilities fit within a given device's configuration space which is either 256 |
| // or 4096 bytes. But in most cases the VMO containing config space is going to be at |
| // least the size of a full PCI bus's worth of devices, and physical VMOs cannot be sliced |
| // into children. We can validate that the capability fits within the offset given, but |
| // otherwise cannot rely on the VMO's size for validation. |
| if (add_overflow(vmo_offset, sizeof(MsiCapability), &add_result) || add_result > vmo->size()) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| uint16_t ctrl_val = cap->control; |
| bool has_pvm = !!(ctrl_val & kMsiPvmSupported); |
| bool has_64bit = !!(ctrl_val & kMsi64bitSupported); |
| disp = fbl::AdoptRef<MsiInterruptDispatcher>(new (&ac) MsiInterruptDispatcherImpl( |
| ktl::move(alloc), base_irq_id, msi_id, ktl::move(mapping), vmo_offset, has_pvm, has_64bit, |
| register_int_fn)); |
| } |
| |
| if (!ac.check()) { |
| LTRACEF("Failed to allocate MsiInterruptDispatcher\n"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| // If we allocated MsiInterruptDispatcher successfully then its dtor will release |
| // the id if necessary. |
| cleanup.cancel(); |
| |
| // MSI / MSI-X interrupts share a masking approach and should be masked while |
| // being serviced and unmasked while waiting for an interrupt message to arrive. |
| disp->set_flags(INTERRUPT_UNMASK_PREWAIT | INTERRUPT_MASK_POSTWAIT); |
| |
| disp->UnmaskInterrupt(); |
| st = disp->RegisterInterruptHandler(); |
| if (st != ZX_OK) { |
| LTRACEF("Failed to register interrupt handler for msi id %u (vector %u): %d\n", msi_id, vector, |
| st); |
| return st; |
| } |
| |
| *out_rights = default_rights(); |
| out_interrupt->reset(ktl::move(disp)); |
| LTRACEF("MsiInterruptDispatcher successfully created.\n"); |
| return ZX_OK; |
| } |
| |
| MsiInterruptDispatcher::MsiInterruptDispatcher(fbl::RefPtr<MsiAllocation>&& alloc, |
| fbl::RefPtr<VmMapping>&& mapping, |
| uint32_t base_irq_id, uint32_t msi_id, |
| RegisterIntFn register_int_fn) |
| : alloc_(ktl::move(alloc)), |
| mapping_(ktl::move(mapping)), |
| register_int_fn_(register_int_fn), |
| base_irq_id_(base_irq_id), |
| msi_id_(msi_id) { |
| kcounter_add(dispatcher_msi_create_count, 1); |
| } |
| |
| MsiInterruptDispatcher::~MsiInterruptDispatcher() { |
| zx_status_t st = alloc_->ReleaseId(msi_id_); |
| if (st != ZX_OK) { |
| LTRACEF("MsiInterruptDispatcher: Failed to release MSI id %u (vector %u): %d\n", msi_id_, |
| base_irq_id_ + msi_id_, st); |
| } |
| LTRACEF("MsiInterruptDispatcher: cleaning up MSI id %u\n", msi_id_); |
| kcounter_add(dispatcher_msi_destroy_count, 1); |
| } |
| |
| // This IrqHandler acts as a trampoline to call into the base |
| // InterruptDispatcher's InterruptHandler() routine. Masking and signaling will |
| // be handled there based on flags set in the constructor. |
| interrupt_eoi MsiInterruptDispatcher::IrqHandler(void* ctx) { |
| auto* self = reinterpret_cast<MsiInterruptDispatcher*>(ctx); |
| self->InterruptHandler(); |
| kcounter_add(dispatcher_msi_interrupt_count, 1); |
| return IRQ_EOI_DEACTIVATE; |
| } |
| |
| zx_status_t MsiInterruptDispatcher::RegisterInterruptHandler() { |
| register_int_fn_(&alloc_->block(), msi_id_, IrqHandler, this); |
| return ZX_OK; |
| } |
| |
| void MsiInterruptDispatcher::UnregisterInterruptHandler() { |
| register_int_fn_(&alloc_->block(), msi_id_, nullptr, this); |
| } |
| |
| void MsiInterruptDispatcherImpl::MaskInterrupt() { |
| kcounter_add(dispatcher_msi_mask_count, 1); |
| |
| Guard<SpinLock, IrqSave> guard{&allocation()->lock()}; |
| if (has_platform_pvm_) { |
| msi_mask_unmask(&allocation()->block(), msi_id(), true); |
| } |
| |
| if (has_cap_pvm_) { |
| const uint32_t mask = (1 << msi_id()); |
| if (has_64bit_) { |
| capability_->mask_bits_64 = capability_->mask_bits_64 | mask; |
| } else { |
| capability_->mask_bits_32 = capability_->mask_bits_32 | mask; |
| } |
| arch::DeviceMemoryBarrier(); |
| } |
| } |
| |
| void MsiInterruptDispatcherImpl::UnmaskInterrupt() { |
| kcounter_add(dispatcher_msi_unmask_count, 1); |
| |
| Guard<SpinLock, IrqSave> guard{&allocation()->lock()}; |
| if (has_platform_pvm_) { |
| msi_mask_unmask(&allocation()->block(), msi_id(), false); |
| } |
| |
| if (has_cap_pvm_) { |
| const uint32_t mask = ~(1 << msi_id()); |
| if (has_64bit_) { |
| capability_->mask_bits_64 = capability_->mask_bits_64 & mask; |
| } else { |
| capability_->mask_bits_32 = capability_->mask_bits_32 & mask; |
| } |
| arch::DeviceMemoryBarrier(); |
| } |
| } |
| |
| MsixDispatcherImpl::MsixDispatcherImpl(fbl::RefPtr<MsiAllocation>&& alloc, uint32_t base_irq_id, |
| uint32_t msi_id, fbl::RefPtr<VmMapping>&& mapping, |
| zx_off_t table_offset, RegisterIntFn register_int_fn) |
| : MsiInterruptDispatcher(ktl::move(alloc), ktl::move(mapping), base_irq_id, msi_id, |
| register_int_fn), |
| table_entries_(reinterpret_cast<MsixTableEntry*>(this->mapping()->base() + table_offset)) { |
| // Disable the vector, set up the address and data registers, then re-enable |
| // it for our given msi_id. Per PCI Local Bus Spec v3 section 6.8.2 |
| // implementation notes, all accesses to these registers must be DWORD or |
| // QWORD only. We write upper and lower halves of the address unconditionally |
| // because if the address is 32 bits then we want to write zeroes to the upper |
| // half regardless. The msg_data field is incremented by msi_id because unlike |
| // MSI, MSI-X does not adjust the data payload. This allows us to point |
| // multiple table entries at the same vector, but requires us to specify the |
| // vector in the data field. |
| MaskInterrupt(); |
| writel(allocation()->block().tgt_addr & UINT32_MAX, &table_entries_[msi_id].msg_addr); |
| writel(static_cast<uint32_t>(allocation()->block().tgt_addr >> 32), |
| &table_entries_[msi_id].msg_upper_addr); |
| writel(allocation()->block().tgt_data + msi_id, &table_entries_[msi_id].msg_data); |
| arch::DeviceMemoryBarrier(); |
| } |
| |
| void MsixDispatcherImpl::MaskInterrupt() { |
| kcounter_add(dispatcher_msi_mask_count, 1); |
| RMWREG32(&table_entries_[msi_id()].vector_control, kMsixVectorControlMaskBit, 1, 1); |
| arch::DeviceMemoryBarrier(); |
| } |
| |
| void MsixDispatcherImpl::UnmaskInterrupt() { |
| kcounter_add(dispatcher_msi_unmask_count, 1); |
| RMWREG32(&table_entries_[msi_id()].vector_control, kMsixVectorControlMaskBit, 1, 0); |
| arch::DeviceMemoryBarrier(); |
| } |
| |
| MsixDispatcherImpl::~MsixDispatcherImpl() { |
| MaskInterrupt(); |
| writel(0, &table_entries_[msi_id()].msg_addr); |
| writel(0, &table_entries_[msi_id()].msg_upper_addr); |
| writel(0, &table_entries_[msi_id()].msg_data); |
| arch::DeviceMemoryBarrier(); |
| } |