/*! \file ngbde_intr.c * * API for controlling a thread-based user-mode interrupt handler. * */ /* * $Copyright: Copyright 2018-2020 Broadcom. All rights reserved. * The term 'Broadcom' refers to Broadcom Inc. and/or its subsidiaries. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * A copy of the GNU General Public License version 2 (GPLv2) can * be found in the LICENSES folder.$ */ #include /*! \cond */ static int intr_debug = 0; module_param(intr_debug, int, 0); MODULE_PARM_DESC(intr_debug, "Interrupt debug output enable (default 0)."); /*! \endcond */ static int ngbde_intr_shared_write32(struct ngbde_dev_s *sd, struct ngbde_intr_ctrl_s *ic, uint32_t reg_offs, uint32_t reg_val, uint32_t shr_mask) { unsigned long flags; struct ngbde_shr_reg_s *sr; int idx; sr = NULL; for (idx = 0; idx < NGBDE_NUM_INTR_SHR_REGS_MAX; idx++) { if (sd->intr_shr_reg[idx].reg_offs == 0) { /* If not found, then we add a new entry */ sd->intr_shr_reg[idx].reg_offs = reg_offs; } if (sd->intr_shr_reg[idx].reg_offs == reg_offs) { sr = &sd->intr_shr_reg[idx]; break; } } if (sr == NULL) { return -1; } spin_lock_irqsave(&sd->lock, flags); sr->cur_val &= ~shr_mask; sr->cur_val |= (reg_val & shr_mask); NGBDE_IOWRITE32(sr->cur_val, ic->iomem + reg_offs); spin_unlock_irqrestore(&sd->lock, flags); return 0; } /*! * \brief Interrupt handler for user mode thread. * * This function will determine whether a user-mode interrupt has * occurred by reading the configured interrupt status and mask * registers. * * If an interrupt has occurred, any waiting user-mode thread is woken * up. * * \param [in] ic Interrupt control information. * * \retval 1 One or more user mode interrupts occurred. * \retval 0 No user mode interrupts occurred. */ static int ngbde_user_isr(ngbde_intr_ctrl_t *ic) { int idx; int active_interrupts = 0; uint32_t stat = 0, mask = 0; uint32_t kmask; /* Check if any enabled interrupts are active */ for (idx = 0; idx < ic->num_regs; idx++) { ngbde_irq_reg_t *ir = &ic->regs[idx]; /* Get mask of all kernel interrupt sources for this register address */ kmask = ir->kmask; stat = NGBDE_IOREAD32(&ic->iomem[ir->status_reg]); mask = NGBDE_IOREAD32(&ic->iomem[ir->mask_reg]); if (stat & mask & ~kmask) { active_interrupts = 1; break; } } /* No active interrupts to service */ if (!active_interrupts) { return 0; } /* Disable (mask off) all interrupts */ for (idx = 0; idx < ic->num_regs; idx++) { ngbde_irq_reg_t *ir = &ic->regs[idx]; /* Get mask of all kernel interrupt sources for this register address */ kmask = ir->kmask; if (kmask == 0xffffffff) { /* Kernel driver owns all interrupts in this register */ continue; } else if (kmask) { /* Synchronized write */ struct ngbde_dev_s *sd = ngbde_swdev_get(ic->kdev); if (ngbde_intr_shared_write32(sd, ic, ir->mask_reg, 0, ~kmask) < 0) { printk(KERN_WARNING "%s: Failed to write shared register for device %d\n", MOD_NAME, ic->kdev); /* Fall back to normal write to ensure interrupts are masked */ NGBDE_IOWRITE32(0, &ic->iomem[ir->mask_reg]); } } else { NGBDE_IOWRITE32(0, &ic->iomem[ir->mask_reg]); } } atomic_set(&ic->run_user_thread, 1); wake_up_interruptible(&ic->user_thread_wq); return 1; } /*! * \brief Interrupt handler for kernel driver. * * Typically used by the KNET driver. * * \param [in] ic Interrupt control information. * * \retval 1 One or more kernel mode interrupts occurred. * \retval 0 No kernel mode interrupts occurred. */ static int ngbde_kernel_isr(ngbde_intr_ctrl_t *ic) { if (ic->isr_func) { return ic->isr_func(ic->isr_data); } return 0; } /*! * \brief Acknowledge interrupt * * \param [in] data Interrupt control information * * \retval 0 */ static int ngbde_intr_ack(ngbde_intr_ctrl_t *ic) { struct ngbde_dev_s *sd = ngbde_swdev_get(ic->kdev); struct ngbde_intr_ack_reg_s *ar = &ic->intr_ack; if (sd->use_msi) { if (ar->flags & NGBDE_INTR_ACK_F_PAXB) { NGBDE_IOWRITE32(ar->ack_val, &sd->paxb_mem[ar->ack_reg]); } else { NGBDE_IOWRITE32(ar->ack_val, &sd->pio_mem[ar->ack_reg]); } } return 0; } /*! * \brief Linux ISR * * Will call the user-mode interrupts handler and optionally also a * kernel mode interrupt handler (typically KNET). * * \param [in] irq_num Interrupt vector from kernel. * \param [in] data Interrupt control information * * \retval IRQ_NONE Interrupt not recognized. * \retval IRQ_HANDLED Interrupt recognized and handled (masked off). */ static irqreturn_t ngbde_isr(int irq_num, void *data) { struct ngbde_intr_ctrl_s *ic = (struct ngbde_intr_ctrl_s *)data; irqreturn_t rv = IRQ_NONE; ngbde_intr_ack(ic); if (ngbde_user_isr(ic)) { rv = IRQ_HANDLED; } if (ngbde_kernel_isr(ic)) { rv = IRQ_HANDLED; } return rv; } int ngbde_intr_connect(int kdev, unsigned int irq_num) { struct ngbde_dev_s *sd; struct ngbde_intr_ctrl_s *ic; unsigned long irq_flags; sd = ngbde_swdev_get(kdev); if (!sd) { return -1; } if (irq_num >= NGBDE_NUM_IRQS_MAX) { return -1; } ic = &sd->intr_ctrl[irq_num]; if (ic->irq_active) { return 0; } if (sd->irq_line >= 0) { if (sd->pio_mem == NULL) { printk(KERN_WARNING "%s: No memory-mapped I/O for device %d\n", MOD_NAME, kdev); return -1; } ic->kdev = kdev; ic->iomem = sd->pio_mem; if (sd->iio_mem) { if (intr_debug) { printk("INTR: Using dedicated interrupt controller\n"); } ic->iomem = sd->iio_mem; } init_waitqueue_head(&ic->user_thread_wq); atomic_set(&ic->run_user_thread, 0); irq_flags = IRQF_SHARED; ic->irq_vect = sd->irq_line; /* * The pci_enable_msi function must be called after enabling * BAR0_PAXB_OARR_FUNC0_MSI_PAGE, otherwise, MSI interrupts * cannot be triggered! */ if (sd->use_msi) { if (pci_enable_msi(sd->pci_dev) == 0) { irq_flags = 0; ic->irq_vect = sd->pci_dev->irq; if (intr_debug) { printk("INTR: Enabled MSI interrupts\n"); } } else { printk(KERN_WARNING "%s: Failed to enable MSI for device %d\n", MOD_NAME, kdev); sd->use_msi = 0; } } if (intr_debug) { printk("INTR: Request IRQ %d\n", ic->irq_vect); } if (request_irq(ic->irq_vect, ngbde_isr, irq_flags, MOD_NAME, ic) < 0) { printk(KERN_WARNING "%s: Could not get IRQ %d for device %d\n", MOD_NAME, ic->irq_vect, kdev); return -1; } ic->irq_active = 1; } return 0; } int ngbde_intr_disconnect(int kdev, unsigned int irq_num) { struct ngbde_dev_s *sd; struct ngbde_intr_ctrl_s *ic; sd = ngbde_swdev_get(kdev); if (!sd) { return -1; } if (irq_num >= NGBDE_NUM_IRQS_MAX) { return -1; } ic = &sd->intr_ctrl[irq_num]; if (!ic->irq_active) { return 0; } if (ic->isr_func) { printk(KERN_WARNING "%s: Disconnecting IRQ %d blocked by kernel ISR\n", MOD_NAME, irq_num); return 0; } if (ic->irq_vect >= 0) { free_irq(ic->irq_vect, ic); if (sd->use_msi) { pci_disable_msi(sd->pci_dev); } ic->irq_active = 0; } return 0; } void ngbde_intr_cleanup(void) { struct ngbde_dev_s *swdev; unsigned int num_swdev, idx, irq_num; ngbde_swdev_get_all(&swdev, &num_swdev); for (idx = 0; idx < num_swdev; idx++) { for (irq_num = 0; irq_num < NGBDE_NUM_IRQS_MAX; irq_num++) { ngbde_intr_disconnect(idx, irq_num); } } } int ngbde_intr_wait(int kdev, unsigned int irq_num) { struct ngbde_dev_s *sd; struct ngbde_intr_ctrl_s *ic; sd = ngbde_swdev_get(kdev); if (!sd) { return -1; } if (irq_num >= NGBDE_NUM_IRQS_MAX) { return -1; } ic = &sd->intr_ctrl[irq_num]; if (!ic->irq_active) { return 0; } wait_event_interruptible(ic->user_thread_wq, atomic_read(&ic->run_user_thread) != 0); atomic_set(&ic->run_user_thread, 0); return 0; } int ngbde_intr_stop(int kdev, unsigned int irq_num) { struct ngbde_dev_s *sd; struct ngbde_intr_ctrl_s *ic; sd = ngbde_swdev_get(kdev); if (!sd) { return -1; } if (irq_num >= NGBDE_NUM_IRQS_MAX) { return -1; } ic = &sd->intr_ctrl[irq_num]; if (!ic->irq_active) { return 0; } /* Wake up user thread */ atomic_set(&ic->run_user_thread, 1); wake_up_interruptible(&ic->user_thread_wq); return 0; } int ngbde_intr_regs_clr(int kdev, unsigned int irq_num) { struct ngbde_dev_s *sd; struct ngbde_intr_ctrl_s *ic; sd = ngbde_swdev_get(kdev); if (!sd) { return -1; } if (irq_num >= NGBDE_NUM_IRQS_MAX) { return -1; } ic = &sd->intr_ctrl[irq_num]; if (ic->irq_active) { /* Do not clear configuration with interrupt connected */ return 0; } ic->num_regs = 0; memset(ic->regs, 0, sizeof(ic->regs)); return 0; } int ngbde_intr_reg_add(int kdev, unsigned int irq_num, struct ngbde_irq_reg_s *ireg) { struct ngbde_dev_s *sd; struct ngbde_intr_ctrl_s *ic; struct ngbde_irq_reg_s *ir; unsigned int idx; sd = ngbde_swdev_get(kdev); if (!sd) { return -1; } if (irq_num >= NGBDE_NUM_IRQS_MAX) { return -1; } ic = &sd->intr_ctrl[irq_num]; if (ic->irq_active) { /* * If the interrupt is connected, then we only update the * kernel mask for existing entries. */ for (idx = 0; idx < ic->num_regs; idx++) { ir = &ic->regs[idx]; if (ir->status_reg == ireg->status_reg && ir->mask_reg == ireg->mask_reg) { ir->kmask = ireg->kmask; if (intr_debug) { printk("INTR: Updating interrupt register " "0x%08x/0x%08x (0x%08x)\n", ir->status_reg, ir->mask_reg, ir->kmask); } return 0; } } return -1; } if (ic->num_regs >= NGBDE_NUM_IRQ_REGS_MAX) { return -1; } ir = &ic->regs[ic->num_regs++]; memcpy(ir, ireg, sizeof (*ir)); if (intr_debug) { printk("INTR: Adding interrupt register 0x%08x/0x%08x (0x%08x)\n", ir->status_reg, ir->mask_reg, ir->kmask); } return ic->num_regs; } int ngbde_intr_ack_reg_add(int kdev, unsigned int irq_num, struct ngbde_intr_ack_reg_s *ackreg) { struct ngbde_dev_s *sd; struct ngbde_intr_ctrl_s *ic; struct ngbde_intr_ack_reg_s *ar; sd = ngbde_swdev_get(kdev); if (!sd) { return -1; } if (irq_num >= NGBDE_NUM_IRQS_MAX) { return -1; } ic = &sd->intr_ctrl[irq_num]; if (ic->irq_active) { /* Ignore request if interrupt is connected */ return 0; } ar = &ic->intr_ack; memcpy(ar, ackreg, sizeof (*ar)); if (intr_debug) { printk("INTR: Adding interrupt ACK register 0x%08x/0x%08x (0x%08x)\n", ar->ack_reg, ar->ack_val, ar->flags); } return 0; } int ngbde_intr_mask_write(int kdev, unsigned int irq_num, int kapi, uint32_t status_reg, uint32_t mask_val) { struct ngbde_dev_s *sd; struct ngbde_intr_ctrl_s *ic; struct ngbde_irq_reg_s *ir; unsigned int idx; uint32_t bmask; sd = ngbde_swdev_get(kdev); if (!sd) { return -1; } if (irq_num >= NGBDE_NUM_IRQS_MAX) { return -1; } ic = &sd->intr_ctrl[irq_num]; ir = ic->regs; for (idx = 0; idx < ic->num_regs; idx++) { if (ir->status_reg == status_reg) { bmask = kapi ? ir->kmask : ~ir->kmask; ngbde_intr_shared_write32(sd, ic, ir->mask_reg, mask_val, bmask); return 0; } ir++; } return -1; }