| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) 2021 Western Digital Corporation or its affiliates. |
| * Copyright (C) 2022 Ventana Micro Systems Inc. |
| */ |
| |
| #define pr_fmt(fmt) "riscv-imsic: " fmt |
| #include <linux/acpi.h> |
| #include <linux/cpu.h> |
| #include <linux/interrupt.h> |
| #include <linux/io.h> |
| #include <linux/irq.h> |
| #include <linux/irqchip.h> |
| #include <linux/irqchip/chained_irq.h> |
| #include <linux/irqchip/riscv-imsic.h> |
| #include <linux/module.h> |
| #include <linux/pci.h> |
| #include <linux/spinlock.h> |
| #include <linux/smp.h> |
| |
| #include "irq-riscv-imsic-state.h" |
| |
| static int imsic_parent_irq; |
| |
| #ifdef CONFIG_SMP |
| static void imsic_ipi_send(unsigned int cpu) |
| { |
| struct imsic_local_config *local = per_cpu_ptr(imsic->global.local, cpu); |
| |
| writel_relaxed(IMSIC_IPI_ID, local->msi_va); |
| } |
| |
| static void imsic_ipi_starting_cpu(void) |
| { |
| /* Enable IPIs for current CPU. */ |
| __imsic_id_set_enable(IMSIC_IPI_ID); |
| } |
| |
| static void imsic_ipi_dying_cpu(void) |
| { |
| /* Disable IPIs for current CPU. */ |
| __imsic_id_clear_enable(IMSIC_IPI_ID); |
| } |
| |
| static int __init imsic_ipi_domain_init(void) |
| { |
| int virq; |
| |
| /* Create IMSIC IPI multiplexing */ |
| virq = ipi_mux_create(IMSIC_NR_IPI, imsic_ipi_send); |
| if (virq <= 0) |
| return virq < 0 ? virq : -ENOMEM; |
| |
| /* Set vIRQ range */ |
| riscv_ipi_set_virq_range(virq, IMSIC_NR_IPI); |
| |
| /* Announce that IMSIC is providing IPIs */ |
| pr_info("%pfwP: providing IPIs using interrupt %d\n", imsic->fwnode, IMSIC_IPI_ID); |
| |
| return 0; |
| } |
| #else |
| static void imsic_ipi_starting_cpu(void) { } |
| static void imsic_ipi_dying_cpu(void) { } |
| static int __init imsic_ipi_domain_init(void) { return 0; } |
| #endif |
| |
| /* |
| * To handle an interrupt, we read the TOPEI CSR and write zero in one |
| * instruction. If TOPEI CSR is non-zero then we translate TOPEI.ID to |
| * Linux interrupt number and let Linux IRQ subsystem handle it. |
| */ |
| static void imsic_handle_irq(struct irq_desc *desc) |
| { |
| struct irq_chip *chip = irq_desc_get_chip(desc); |
| int err, cpu = smp_processor_id(); |
| struct imsic_vector *vec; |
| unsigned long local_id; |
| |
| chained_irq_enter(chip, desc); |
| |
| while ((local_id = csr_swap(CSR_TOPEI, 0))) { |
| local_id >>= TOPEI_ID_SHIFT; |
| |
| if (local_id == IMSIC_IPI_ID) { |
| if (IS_ENABLED(CONFIG_SMP)) |
| ipi_mux_process(); |
| continue; |
| } |
| |
| if (unlikely(!imsic->base_domain)) |
| continue; |
| |
| vec = imsic_vector_from_local_id(cpu, local_id); |
| if (!vec) { |
| pr_warn_ratelimited("vector not found for local ID 0x%lx\n", local_id); |
| continue; |
| } |
| |
| err = generic_handle_domain_irq(imsic->base_domain, vec->hwirq); |
| if (unlikely(err)) |
| pr_warn_ratelimited("hwirq 0x%x mapping not found\n", vec->hwirq); |
| } |
| |
| chained_irq_exit(chip, desc); |
| } |
| |
| static int imsic_starting_cpu(unsigned int cpu) |
| { |
| /* Mark per-CPU IMSIC state as online */ |
| imsic_state_online(); |
| |
| /* Enable per-CPU parent interrupt */ |
| enable_percpu_irq(imsic_parent_irq, irq_get_trigger_type(imsic_parent_irq)); |
| |
| /* Setup IPIs */ |
| imsic_ipi_starting_cpu(); |
| |
| /* |
| * Interrupts identities might have been enabled/disabled while |
| * this CPU was not running so sync-up local enable/disable state. |
| */ |
| imsic_local_sync_all(); |
| |
| /* Enable local interrupt delivery */ |
| imsic_local_delivery(true); |
| |
| return 0; |
| } |
| |
| static int imsic_dying_cpu(unsigned int cpu) |
| { |
| /* Cleanup IPIs */ |
| imsic_ipi_dying_cpu(); |
| |
| /* Mark per-CPU IMSIC state as offline */ |
| imsic_state_offline(); |
| |
| return 0; |
| } |
| |
| static int __init imsic_early_probe(struct fwnode_handle *fwnode) |
| { |
| struct irq_domain *domain; |
| int rc; |
| |
| /* Find parent domain and register chained handler */ |
| domain = irq_find_matching_fwnode(riscv_get_intc_hwnode(), DOMAIN_BUS_ANY); |
| if (!domain) { |
| pr_err("%pfwP: Failed to find INTC domain\n", fwnode); |
| return -ENOENT; |
| } |
| imsic_parent_irq = irq_create_mapping(domain, RV_IRQ_EXT); |
| if (!imsic_parent_irq) { |
| pr_err("%pfwP: Failed to create INTC mapping\n", fwnode); |
| return -ENOENT; |
| } |
| |
| /* Initialize IPI domain */ |
| rc = imsic_ipi_domain_init(); |
| if (rc) { |
| pr_err("%pfwP: Failed to initialize IPI domain\n", fwnode); |
| return rc; |
| } |
| |
| /* Setup chained handler to the parent domain interrupt */ |
| irq_set_chained_handler(imsic_parent_irq, imsic_handle_irq); |
| |
| /* |
| * Setup cpuhp state (must be done after setting imsic_parent_irq) |
| * |
| * Don't disable per-CPU IMSIC file when CPU goes offline |
| * because this affects IPI and the masking/unmasking of |
| * virtual IPIs is done via generic IPI-Mux |
| */ |
| cpuhp_setup_state(CPUHP_AP_IRQ_RISCV_IMSIC_STARTING, "irqchip/riscv/imsic:starting", |
| imsic_starting_cpu, imsic_dying_cpu); |
| |
| return 0; |
| } |
| |
| static int __init imsic_early_dt_init(struct device_node *node, struct device_node *parent) |
| { |
| struct fwnode_handle *fwnode = &node->fwnode; |
| int rc; |
| |
| /* Setup IMSIC state */ |
| rc = imsic_setup_state(fwnode, NULL); |
| if (rc) { |
| pr_err("%pfwP: failed to setup state (error %d)\n", fwnode, rc); |
| return rc; |
| } |
| |
| /* Do early setup of IPIs */ |
| rc = imsic_early_probe(fwnode); |
| if (rc) |
| return rc; |
| |
| /* Ensure that OF platform device gets probed */ |
| of_node_clear_flag(node, OF_POPULATED); |
| return 0; |
| } |
| |
| IRQCHIP_DECLARE(riscv_imsic, "riscv,imsics", imsic_early_dt_init); |
| |
| #ifdef CONFIG_ACPI |
| |
| static struct fwnode_handle *imsic_acpi_fwnode; |
| |
| struct fwnode_handle *imsic_acpi_get_fwnode(struct device *dev) |
| { |
| return imsic_acpi_fwnode; |
| } |
| |
| static int __init imsic_early_acpi_init(union acpi_subtable_headers *header, |
| const unsigned long end) |
| { |
| struct acpi_madt_imsic *imsic = (struct acpi_madt_imsic *)header; |
| int rc; |
| |
| imsic_acpi_fwnode = irq_domain_alloc_named_fwnode("imsic"); |
| if (!imsic_acpi_fwnode) { |
| pr_err("unable to allocate IMSIC FW node\n"); |
| return -ENOMEM; |
| } |
| |
| /* Setup IMSIC state */ |
| rc = imsic_setup_state(imsic_acpi_fwnode, imsic); |
| if (rc) { |
| pr_err("%pfwP: failed to setup state (error %d)\n", imsic_acpi_fwnode, rc); |
| return rc; |
| } |
| |
| /* Do early setup of IMSIC state and IPIs */ |
| rc = imsic_early_probe(imsic_acpi_fwnode); |
| if (rc) { |
| irq_domain_free_fwnode(imsic_acpi_fwnode); |
| imsic_acpi_fwnode = NULL; |
| return rc; |
| } |
| |
| rc = imsic_platform_acpi_probe(imsic_acpi_fwnode); |
| |
| #ifdef CONFIG_PCI |
| if (!rc) |
| pci_msi_register_fwnode_provider(&imsic_acpi_get_fwnode); |
| #endif |
| |
| if (rc) |
| pr_err("%pfwP: failed to register IMSIC for MSI functionality (error %d)\n", |
| imsic_acpi_fwnode, rc); |
| |
| /* |
| * Even if imsic_platform_acpi_probe() fails, the IPI part of IMSIC can |
| * continue to work. So, no need to return failure. This is similar to |
| * DT where IPI works but MSI probe fails for some reason. |
| */ |
| return 0; |
| } |
| |
| IRQCHIP_ACPI_DECLARE(riscv_imsic, ACPI_MADT_TYPE_IMSIC, NULL, |
| 1, imsic_early_acpi_init); |
| #endif |