| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Microchip Inter-Processor communication (IPC) driver | |
| * | |
| * Copyright (c) 2021 - 2024 Microchip Technology Inc. All rights reserved. | |
| * | |
| * Author: Valentina Fernandez <valentina.fernandezalanis@microchip.com> | |
| * | |
| */ | |
| #include <linux/io.h> | |
| #include <linux/err.h> | |
| #include <linux/smp.h> | |
| #include <linux/init.h> | |
| #include <linux/module.h> | |
| #include <linux/kernel.h> | |
| #include <linux/of_device.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/dma-mapping.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/mailbox/mchp-ipc.h> | |
| #include <asm/sbi.h> | |
| #include <asm/vendorid_list.h> | |
| #define IRQ_STATUS_BITS 12 | |
| #define NUM_CHANS_PER_CLUSTER 5 | |
| #define IPC_DMA_BIT_MASK 32 | |
| #define SBI_EXT_MICROCHIP_TECHNOLOGY (SBI_EXT_VENDOR_START | \ | |
| MICROCHIP_VENDOR_ID) | |
| enum { | |
| SBI_EXT_IPC_PROBE = 0x100, | |
| SBI_EXT_IPC_CH_INIT, | |
| SBI_EXT_IPC_SEND, | |
| SBI_EXT_IPC_RECEIVE, | |
| SBI_EXT_IPC_STATUS, | |
| }; | |
| enum ipc_hw { | |
| MIV_IHC, | |
| }; | |
| /** | |
| * struct mchp_ipc_mbox_info - IPC probe message format | |
| * | |
| * @hw_type: IPC implementation available in the hardware | |
| * @num_channels: number of IPC channels available in the hardware | |
| * | |
| * Used to retrieve information on the IPC implementation | |
| * using the SBI_EXT_IPC_PROBE SBI function id. | |
| */ | |
| struct mchp_ipc_mbox_info { | |
| enum ipc_hw hw_type; | |
| u8 num_channels; | |
| }; | |
| /** | |
| * struct mchp_ipc_init - IPC channel init message format | |
| * | |
| * @max_msg_size: maxmimum message size in bytes of a given channel | |
| * | |
| * struct used by the SBI_EXT_IPC_CH_INIT SBI function id to get | |
| * the max message size in bytes of the initialized channel. | |
| */ | |
| struct mchp_ipc_init { | |
| u16 max_msg_size; | |
| }; | |
| /** | |
| * struct mchp_ipc_status - IPC status message format | |
| * | |
| * @status: interrupt status for all channels associated to a cluster | |
| * @cluster: specifies the cluster instance that originated an irq | |
| * | |
| * struct used by the SBI_EXT_IPC_STATUS SBI function id to get | |
| * the message present and message clear interrupt status for all the | |
| * channels associated to a cluster. | |
| */ | |
| struct mchp_ipc_status { | |
| u32 status; | |
| u8 cluster; | |
| }; | |
| /** | |
| * struct mchp_ipc_sbi_msg - IPC SBI payload message | |
| * | |
| * @buf_addr: physical address where the received data should be copied to | |
| * @size: maximum size(in bytes) that can be stored in the buffer pointed to by `buf` | |
| * @irq_type: mask representing the irq types that triggered an irq | |
| * | |
| * struct used by the SBI_EXT_IPC_SEND/SBI_EXT_IPC_RECEIVE SBI function | |
| * ids to send/receive a message from an associated processor using | |
| * the IPC. | |
| */ | |
| struct mchp_ipc_sbi_msg { | |
| u64 buf_addr; | |
| u16 size; | |
| u8 irq_type; | |
| }; | |
| struct mchp_ipc_cluster_cfg { | |
| void *buf_base; | |
| phys_addr_t buf_base_addr; | |
| int irq; | |
| }; | |
| struct mchp_ipc_sbi_mbox { | |
| struct device *dev; | |
| struct mbox_chan *chans; | |
| struct mchp_ipc_cluster_cfg *cluster_cfg; | |
| void *buf_base; | |
| unsigned long buf_base_addr; | |
| struct mbox_controller controller; | |
| enum ipc_hw hw_type; | |
| }; | |
| static int mchp_ipc_sbi_chan_send(u32 command, u32 channel, unsigned long address) | |
| { | |
| struct sbiret ret; | |
| ret = sbi_ecall(SBI_EXT_MICROCHIP_TECHNOLOGY, command, channel, | |
| address, 0, 0, 0, 0); | |
| if (ret.error) | |
| return sbi_err_map_linux_errno(ret.error); | |
| else | |
| return ret.value; | |
| } | |
| static int mchp_ipc_sbi_send(u32 command, unsigned long address) | |
| { | |
| struct sbiret ret; | |
| ret = sbi_ecall(SBI_EXT_MICROCHIP_TECHNOLOGY, command, address, | |
| 0, 0, 0, 0, 0); | |
| if (ret.error) | |
| return sbi_err_map_linux_errno(ret.error); | |
| else | |
| return ret.value; | |
| } | |
| static struct mchp_ipc_sbi_mbox *to_mchp_ipc_mbox(struct mbox_controller *mbox) | |
| { | |
| return container_of(mbox, struct mchp_ipc_sbi_mbox, controller); | |
| } | |
| static inline void mchp_ipc_prepare_receive_req(struct mbox_chan *chan) | |
| { | |
| struct mchp_ipc_sbi_chan *chan_info = (struct mchp_ipc_sbi_chan *)chan->con_priv; | |
| struct mchp_ipc_sbi_msg request; | |
| request.buf_addr = chan_info->msg_buf_rx_addr; | |
| request.size = chan_info->max_msg_size; | |
| memcpy(chan_info->buf_base_rx, &request, sizeof(struct mchp_ipc_sbi_msg)); | |
| } | |
| static inline void mchp_ipc_process_received_data(struct mbox_chan *chan, | |
| struct mchp_ipc_msg *ipc_msg) | |
| { | |
| struct mchp_ipc_sbi_chan *chan_info = (struct mchp_ipc_sbi_chan *)chan->con_priv; | |
| struct mchp_ipc_sbi_msg sbi_msg; | |
| memcpy(&sbi_msg, chan_info->buf_base_rx, sizeof(struct mchp_ipc_sbi_msg)); | |
| ipc_msg->buf = (u32 *)chan_info->msg_buf_rx; | |
| ipc_msg->size = sbi_msg.size; | |
| } | |
| static irqreturn_t mchp_ipc_cluster_aggr_isr(int irq, void *data) | |
| { | |
| struct mbox_chan *chan; | |
| struct mchp_ipc_sbi_chan *chan_info; | |
| struct mchp_ipc_sbi_mbox *ipc = (struct mchp_ipc_sbi_mbox *)data; | |
| struct mchp_ipc_msg ipc_msg; | |
| struct mchp_ipc_status status_msg; | |
| int ret; | |
| u32 i, chan_index, chan_id; | |
| bool found = false; | |
| /* Find out the hart that originated the irq */ | |
| for_each_online_cpu(i) { | |
| if (irq == ipc->cluster_cfg[i].irq) { | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (unlikely(!found)) | |
| return IRQ_NONE; | |
| status_msg.cluster = cpuid_to_hartid_map(i); | |
| memcpy(ipc->cluster_cfg[i].buf_base, &status_msg, sizeof(struct mchp_ipc_status)); | |
| ret = mchp_ipc_sbi_send(SBI_EXT_IPC_STATUS, ipc->cluster_cfg[i].buf_base_addr); | |
| if (ret < 0) { | |
| dev_err_ratelimited(ipc->dev, "could not get IHC irq status ret=%d\n", ret); | |
| return IRQ_HANDLED; | |
| } | |
| memcpy(&status_msg, ipc->cluster_cfg[i].buf_base, sizeof(struct mchp_ipc_status)); | |
| /* | |
| * Iterate over each bit set in the IHC interrupt status register (IRQ_STATUS) to identify | |
| * the channel(s) that have a message to be processed/acknowledged. | |
| * The bits are organized in alternating format, where each pair of bits represents | |
| * the status of the message present and message clear interrupts for each cluster/hart | |
| * (from hart 0 to hart 5). Each cluster can have up to 5 fixed channels associated. | |
| */ | |
| for_each_set_bit(i, (unsigned long *)&status_msg.status, IRQ_STATUS_BITS) { | |
| /* Find out the destination hart that triggered the interrupt */ | |
| chan_index = i / 2; | |
| /* | |
| * The IP has no loopback channels, so we need to decrement the index when | |
| * the target hart has a greater index than our own | |
| */ | |
| if (chan_index >= status_msg.cluster) | |
| chan_index--; | |
| /* | |
| * Calculate the channel id given the hart and channel index. Channel IDs | |
| * are unique across all clusters of an IPC, and iterate contiguously | |
| * across all clusters. | |
| */ | |
| chan_id = status_msg.cluster * (NUM_CHANS_PER_CLUSTER + chan_index); | |
| chan = &ipc->chans[chan_id]; | |
| chan_info = (struct mchp_ipc_sbi_chan *)chan->con_priv; | |
| if (i % 2 == 0) { | |
| mchp_ipc_prepare_receive_req(chan); | |
| ret = mchp_ipc_sbi_chan_send(SBI_EXT_IPC_RECEIVE, chan_id, | |
| chan_info->buf_base_rx_addr); | |
| if (ret < 0) | |
| continue; | |
| mchp_ipc_process_received_data(chan, &ipc_msg); | |
| mbox_chan_received_data(&ipc->chans[chan_id], (void *)&ipc_msg); | |
| } else { | |
| ret = mchp_ipc_sbi_chan_send(SBI_EXT_IPC_RECEIVE, chan_id, | |
| chan_info->buf_base_rx_addr); | |
| mbox_chan_txdone(&ipc->chans[chan_id], ret); | |
| } | |
| } | |
| return IRQ_HANDLED; | |
| } | |
| static int mchp_ipc_send_data(struct mbox_chan *chan, void *data) | |
| { | |
| struct mchp_ipc_sbi_chan *chan_info = (struct mchp_ipc_sbi_chan *)chan->con_priv; | |
| const struct mchp_ipc_msg *msg = data; | |
| struct mchp_ipc_sbi_msg sbi_payload; | |
| memcpy(chan_info->msg_buf_tx, msg->buf, msg->size); | |
| sbi_payload.buf_addr = chan_info->msg_buf_tx_addr; | |
| sbi_payload.size = msg->size; | |
| memcpy(chan_info->buf_base_tx, &sbi_payload, sizeof(sbi_payload)); | |
| return mchp_ipc_sbi_chan_send(SBI_EXT_IPC_SEND, chan_info->id, chan_info->buf_base_tx_addr); | |
| } | |
| static int mchp_ipc_startup(struct mbox_chan *chan) | |
| { | |
| struct mchp_ipc_sbi_chan *chan_info = (struct mchp_ipc_sbi_chan *)chan->con_priv; | |
| struct mchp_ipc_sbi_mbox *ipc = to_mchp_ipc_mbox(chan->mbox); | |
| struct mchp_ipc_init ch_init_msg; | |
| int ret; | |
| /* | |
| * The TX base buffer is used to transmit two types of messages: | |
| * - struct mchp_ipc_init to initialize the channel | |
| * - struct mchp_ipc_sbi_msg to transmit user data/payload | |
| * Ensure the TX buffer size is large enough to accommodate either message type. | |
| */ | |
| size_t max_size = max(sizeof(struct mchp_ipc_init), sizeof(struct mchp_ipc_sbi_msg)); | |
| chan_info->buf_base_tx = kmalloc(max_size, GFP_KERNEL); | |
| if (!chan_info->buf_base_tx) { | |
| ret = -ENOMEM; | |
| goto fail; | |
| } | |
| chan_info->buf_base_tx_addr = __pa(chan_info->buf_base_tx); | |
| chan_info->buf_base_rx = kmalloc(max_size, GFP_KERNEL); | |
| if (!chan_info->buf_base_rx) { | |
| ret = -ENOMEM; | |
| goto fail_free_buf_base_tx; | |
| } | |
| chan_info->buf_base_rx_addr = __pa(chan_info->buf_base_rx); | |
| ret = mchp_ipc_sbi_chan_send(SBI_EXT_IPC_CH_INIT, chan_info->id, | |
| chan_info->buf_base_tx_addr); | |
| if (ret < 0) { | |
| dev_err(ipc->dev, "channel %u init failed\n", chan_info->id); | |
| goto fail_free_buf_base_rx; | |
| } | |
| memcpy(&ch_init_msg, chan_info->buf_base_tx, sizeof(struct mchp_ipc_init)); | |
| chan_info->max_msg_size = ch_init_msg.max_msg_size; | |
| chan_info->msg_buf_tx = kmalloc(chan_info->max_msg_size, GFP_KERNEL); | |
| if (!chan_info->msg_buf_tx) { | |
| ret = -ENOMEM; | |
| goto fail_free_buf_base_rx; | |
| } | |
| chan_info->msg_buf_tx_addr = __pa(chan_info->msg_buf_tx); | |
| chan_info->msg_buf_rx = kmalloc(chan_info->max_msg_size, GFP_KERNEL); | |
| if (!chan_info->msg_buf_rx) { | |
| ret = -ENOMEM; | |
| goto fail_free_buf_msg_tx; | |
| } | |
| chan_info->msg_buf_rx_addr = __pa(chan_info->msg_buf_rx); | |
| switch (ipc->hw_type) { | |
| case MIV_IHC: | |
| return 0; | |
| default: | |
| goto fail_free_buf_msg_rx; | |
| } | |
| fail_free_buf_msg_rx: | |
| kfree(chan_info->msg_buf_rx); | |
| fail_free_buf_msg_tx: | |
| kfree(chan_info->msg_buf_tx); | |
| fail_free_buf_base_rx: | |
| kfree(chan_info->buf_base_rx); | |
| fail_free_buf_base_tx: | |
| kfree(chan_info->buf_base_tx); | |
| fail: | |
| return ret; | |
| } | |
| static void mchp_ipc_shutdown(struct mbox_chan *chan) | |
| { | |
| struct mchp_ipc_sbi_chan *chan_info = (struct mchp_ipc_sbi_chan *)chan->con_priv; | |
| kfree(chan_info->buf_base_tx); | |
| kfree(chan_info->buf_base_rx); | |
| kfree(chan_info->msg_buf_tx); | |
| kfree(chan_info->msg_buf_rx); | |
| } | |
| static const struct mbox_chan_ops mchp_ipc_ops = { | |
| .startup = mchp_ipc_startup, | |
| .send_data = mchp_ipc_send_data, | |
| .shutdown = mchp_ipc_shutdown, | |
| }; | |
| static struct mbox_chan *mchp_ipc_mbox_xlate(struct mbox_controller *controller, | |
| const struct of_phandle_args *spec) | |
| { | |
| struct mchp_ipc_sbi_mbox *ipc = to_mchp_ipc_mbox(controller); | |
| unsigned int chan_id = spec->args[0]; | |
| if (chan_id >= ipc->controller.num_chans) { | |
| dev_err(ipc->dev, "invalid channel id %d\n", chan_id); | |
| return ERR_PTR(-EINVAL); | |
| } | |
| return &ipc->chans[chan_id]; | |
| } | |
| static int mchp_ipc_get_cluster_aggr_irq(struct mchp_ipc_sbi_mbox *ipc) | |
| { | |
| struct platform_device *pdev = to_platform_device(ipc->dev); | |
| char *irq_name; | |
| int cpuid, ret; | |
| unsigned long hartid; | |
| bool irq_found = false; | |
| for_each_online_cpu(cpuid) { | |
| hartid = cpuid_to_hartid_map(cpuid); | |
| irq_name = devm_kasprintf(ipc->dev, GFP_KERNEL, "hart-%lu", hartid); | |
| if (!irq_name) | |
| return -ENOMEM; | |
| ret = platform_get_irq_byname_optional(pdev, irq_name); | |
| if (ret <= 0) | |
| continue; | |
| ipc->cluster_cfg[cpuid].irq = ret; | |
| ret = devm_request_irq(ipc->dev, ipc->cluster_cfg[cpuid].irq, | |
| mchp_ipc_cluster_aggr_isr, IRQF_SHARED, | |
| "miv-ihc-irq", ipc); | |
| if (ret) | |
| return ret; | |
| ipc->cluster_cfg[cpuid].buf_base = devm_kmalloc(ipc->dev, | |
| sizeof(struct mchp_ipc_status), | |
| GFP_KERNEL); | |
| if (!ipc->cluster_cfg[cpuid].buf_base) | |
| return -ENOMEM; | |
| ipc->cluster_cfg[cpuid].buf_base_addr = __pa(ipc->cluster_cfg[cpuid].buf_base); | |
| irq_found = true; | |
| } | |
| return irq_found; | |
| } | |
| static int mchp_ipc_probe(struct platform_device *pdev) | |
| { | |
| struct device *dev = &pdev->dev; | |
| struct mchp_ipc_mbox_info ipc_info; | |
| struct mchp_ipc_sbi_mbox *ipc; | |
| struct mchp_ipc_sbi_chan *priv; | |
| bool irq_avail = false; | |
| int ret; | |
| u32 chan_id; | |
| ret = sbi_probe_extension(SBI_EXT_MICROCHIP_TECHNOLOGY); | |
| if (ret <= 0) | |
| return dev_err_probe(dev, -ENODEV, "Microchip SBI extension not detected\n"); | |
| ipc = devm_kzalloc(dev, sizeof(*ipc), GFP_KERNEL); | |
| if (!ipc) | |
| return -ENOMEM; | |
| platform_set_drvdata(pdev, ipc); | |
| ipc->buf_base = devm_kmalloc(dev, sizeof(struct mchp_ipc_mbox_info), GFP_KERNEL); | |
| if (!ipc->buf_base) | |
| return -ENOMEM; | |
| ipc->buf_base_addr = __pa(ipc->buf_base); | |
| ret = mchp_ipc_sbi_send(SBI_EXT_IPC_PROBE, ipc->buf_base_addr); | |
| if (ret < 0) | |
| return dev_err_probe(dev, ret, "could not probe IPC SBI service\n"); | |
| memcpy(&ipc_info, ipc->buf_base, sizeof(struct mchp_ipc_mbox_info)); | |
| ipc->controller.num_chans = ipc_info.num_channels; | |
| ipc->hw_type = ipc_info.hw_type; | |
| ipc->chans = devm_kcalloc(dev, ipc->controller.num_chans, sizeof(*ipc->chans), GFP_KERNEL); | |
| if (!ipc->chans) | |
| return -ENOMEM; | |
| ipc->dev = dev; | |
| ipc->controller.txdone_irq = true; | |
| ipc->controller.dev = ipc->dev; | |
| ipc->controller.ops = &mchp_ipc_ops; | |
| ipc->controller.chans = ipc->chans; | |
| ipc->controller.of_xlate = mchp_ipc_mbox_xlate; | |
| for (chan_id = 0; chan_id < ipc->controller.num_chans; chan_id++) { | |
| priv = devm_kmalloc(dev, sizeof(*priv), GFP_KERNEL); | |
| if (!priv) | |
| return -ENOMEM; | |
| ipc->chans[chan_id].con_priv = priv; | |
| priv->id = chan_id; | |
| } | |
| if (ipc->hw_type == MIV_IHC) { | |
| ipc->cluster_cfg = devm_kcalloc(dev, num_online_cpus(), | |
| sizeof(struct mchp_ipc_cluster_cfg), | |
| GFP_KERNEL); | |
| if (!ipc->cluster_cfg) | |
| return -ENOMEM; | |
| if (mchp_ipc_get_cluster_aggr_irq(ipc)) | |
| irq_avail = true; | |
| } | |
| if (!irq_avail) | |
| return dev_err_probe(dev, -ENODEV, "missing interrupt property\n"); | |
| ret = devm_mbox_controller_register(dev, &ipc->controller); | |
| if (ret) | |
| return dev_err_probe(dev, ret, | |
| "Inter-Processor communication (IPC) registration failed\n"); | |
| return 0; | |
| } | |
| static const struct of_device_id mchp_ipc_of_match[] = { | |
| {.compatible = "microchip,sbi-ipc", }, | |
| {} | |
| }; | |
| MODULE_DEVICE_TABLE(of, mchp_ipc_of_match); | |
| static struct platform_driver mchp_ipc_driver = { | |
| .driver = { | |
| .name = "microchip_ipc", | |
| .of_match_table = mchp_ipc_of_match, | |
| }, | |
| .probe = mchp_ipc_probe, | |
| }; | |
| module_platform_driver(mchp_ipc_driver); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_AUTHOR("Valentina Fernandez <valentina.fernandezalanis@microchip.com>"); | |
| MODULE_DESCRIPTION("Microchip Inter-Processor Communication (IPC) driver"); |