[WIP PATCH linux dev-5.4 1/4] misc: Add ASPEED KCS driver for MCTP purposes
Andrew Jeffery
andrew at aj.id.au
Fri Mar 27 17:31:43 AEDT 2020
IBM have developed a vendor-defined MCTP binding that utilises LPC IO
and FW interfaces to exchange MCTP messages. A KCS device in the IO
space is used to send single-byte control messages initialising the MCTP
channel and exchanging ownership of data buffers.
This driver exposes the KCS message stream to userspace, allowing an
MCTP-capable application to manipulate the data exposed via the FW
space.
Signed-off-by: Andrew Jeffery <andrew at aj.id.au>
---
drivers/misc/Kconfig | 7 +
drivers/misc/Makefile | 1 +
drivers/misc/mctp-lpc.c | 443 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 451 insertions(+)
create mode 100644 drivers/misc/mctp-lpc.c
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 06a2b753cc7c..c90a9cacc19f 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -454,6 +454,13 @@ config XILINX_SDFEC
If unsure, say N.
+config MCTP_LPC
+ tristate "MCTP LPC binding implementation for ASPEED BMCs"
+ depends on REGMAP
+ help
+ Implements the MCTP LPC binding via KCS LPC IO cycles for control and
+ LPC FWH cycles for data
+
config MISC_RTSX
tristate
default MISC_RTSX_PCI || MISC_RTSX_USB
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index b9e6d4c3e906..9ef78d42be9d 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -59,3 +59,4 @@ obj-$(CONFIG_HABANA_AI) += habanalabs/
obj-$(CONFIG_XILINX_SDFEC) += xilinx_sdfec.o
obj-$(CONFIG_NPCM7XX_LPC_BPC) += npcm7xx-lpc-bpc.o
obj-$(CONFIG_NPCM7XX_PCI_MBOX) += npcm7xx-pci-mbox.o
+obj-$(CONFIG_MCTP_LPC) += mctp-lpc.o
diff --git a/drivers/misc/mctp-lpc.c b/drivers/misc/mctp-lpc.c
new file mode 100644
index 000000000000..71fc4ae69de7
--- /dev/null
+++ b/drivers/misc/mctp-lpc.c
@@ -0,0 +1,443 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) 2019, IBM Corp.
+ */
+
+#include <linux/errno.h>
+#include <linux/fs.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/mfd/syscon.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/poll.h>
+#include <linux/regmap.h>
+#include <linux/sched/signal.h>
+#include <linux/uaccess.h>
+#include <linux/wait.h>
+
+#define LPC_HICRB 0x080
+#define LPC_HICRB_IBFIF4 BIT(1)
+#define LPC_HICRB_LPC4E BIT(0)
+#define LPC_HICRC 0x084
+#define LPC_KCS4_IRQSEL_MASK GENMASK(7, 4)
+#define LPC_KCS4_IRQSEL_SHIFT 4
+#define LPC_KCS4_IRQTYPE_MASK GENMASK(3, 2)
+#define LPC_KCS4_IRQTYPE_SHIFT 2
+#define LPC_KCS4_IRQTYPE_LOW 0b00
+#define LPC_KCS4_IRQTYPE_HIGH 0b01
+#define LPC_KCS4_IRQTYPE_RSVD 0b10
+#define LPC_KCS4_IRQTYPE_RISING 0b11
+#define LPC_KCS4_OBF4_AUTO_CLR BIT(1)
+#define LPC_KCS4_IRQ_HOST BIT(0)
+#define LPC_LADR4 0x090
+#define LPC_IDR4 0x094
+#define LPC_ODR4 0x098
+#define LPC_STR4 0x09C
+#define STR4_IBF (1 << 1)
+#define STR4_OBF (1 << 0)
+
+#define HOST_ODR 0xca2
+#define HOST_STR 0xca3
+#define HOST_SERIRQ_ID 11
+#define HOST_SERIRQ_TYPE LPC_KCS4_IRQTYPE_LOW
+
+#define RX_BUF_SIZE 1024
+
+struct mctp_lpc {
+ struct miscdevice miscdev;
+ struct regmap *map;
+
+ wait_queue_head_t rx;
+ bool pending;
+ u8 idr;
+};
+
+static irqreturn_t mctp_lpc_irq(int irq, void *data)
+{
+ struct mctp_lpc *priv = data;
+ unsigned long flags;
+ unsigned int hicrb;
+ struct device *dev;
+ unsigned int str;
+ irqreturn_t ret;
+
+ dev = priv->miscdev.this_device;
+
+ spin_lock_irqsave(&priv->rx.lock, flags);
+
+ regmap_read(priv->map, LPC_STR4, &str);
+ regmap_read(priv->map, LPC_HICRB, &hicrb);
+
+ if ((str & STR4_IBF) && (hicrb & LPC_HICRB_IBFIF4)) {
+ unsigned int val;
+
+ if (priv->pending)
+ dev_err(dev, "Storm brewing!");
+
+ /* Mask the IRQ / Enter polling mode */
+ dev_dbg(dev, "Received IRQ %d, disabling to provide back-pressure\n",
+ irq);
+ regmap_update_bits(priv->map, LPC_HICRB, LPC_HICRB_IBFIF4, 0);
+
+ /*
+ * Extract the IDR4 value to ack the IRQ. Reading IDR clears
+ * IBF and allows the host to write another value, however as
+ * we have disabled IRQs the back-pressure is still applied
+ * until userspace starts servicing the interface.
+ */
+ regmap_read(priv->map, LPC_IDR4, &val);
+ priv->idr = val & 0xff;
+ priv->pending = true;
+
+ dev_dbg(dev, "Set pending, waking waiters\n");
+ wake_up_locked(&priv->rx);
+ ret = IRQ_HANDLED;
+ } else {
+ dev_dbg(dev, "LPC IRQ triggered, but not for us (str=0x%x, hicrb=0x%x)\n",
+ str, hicrb);
+ ret = IRQ_NONE;
+ }
+
+ spin_unlock_irqrestore(&priv->rx.lock, flags);
+
+ return ret;
+}
+
+static inline struct mctp_lpc *to_mctp_lpc(struct file *filp)
+{
+ return container_of(filp->private_data, struct mctp_lpc, miscdev);
+}
+
+static ssize_t mctp_lpc_read(struct file *filp, char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct mctp_lpc *priv;
+ struct device *dev;
+ size_t remaining;
+ ssize_t rc;
+
+ priv = to_mctp_lpc(filp);
+ dev = priv->miscdev.this_device;
+
+ if (!count)
+ return 0;
+
+ if (count > 2 || *ppos > 1)
+ return -EINVAL;
+
+ remaining = count;
+
+ spin_lock_irq(&priv->rx.lock);
+ if (*ppos == 0) {
+ unsigned int val;
+ u8 str;
+
+ /* YOLO blocking, non-block not supported */
+ dev_dbg(dev, "Waiting for IBF\n");
+ regmap_read(priv->map, LPC_STR4, &val);
+ str = val & 0xff;
+ rc = wait_event_interruptible_locked(priv->rx, (priv->pending || str & STR4_IBF));
+ if (rc < 0)
+ goto out;
+
+ if (signal_pending(current)) {
+ dev_dbg(dev, "Interrupted waiting for IBF\n");
+ rc = -EINTR;
+ goto out;
+ }
+
+ /*
+ * Re-enable IRQs prior to possible read of IDR (which clears
+ * IBF) to ensure we receive interrupts for subsequent writes
+ * to IDR. Writes to IDR by the host should not occur while IBF
+ * is set.
+ */
+ dev_dbg(dev, "Woken by IBF, enabling IRQ\n");
+ regmap_update_bits(priv->map, LPC_HICRB, LPC_HICRB_IBFIF4,
+ LPC_HICRB_IBFIF4);
+
+ /* Read data out of IDR into internal storage if necessary */
+ if (!priv->pending) {
+ WARN(!(str & STR4_IBF), "Unknown reason for wakeup!");
+
+ /* Extract the IDR4 value to ack the IRQ */
+ regmap_read(priv->map, LPC_IDR4, &val);
+ priv->idr = val & 0xff;
+ }
+
+ /* Copy data from internal storage to userspace */
+ if (copy_to_user(buf, &priv->idr, sizeof(priv->idr))) {
+ rc = -EFAULT;
+ goto out;
+ }
+
+ /* We're done consuming the internally stored value */
+ priv->pending = false;
+
+ remaining--;
+ buf++;
+ }
+
+ if (remaining) {
+ /* Either:
+ *
+ * 1. (count == 1 && *ppos == 1)
+ * 2. (count == 2 && *ppos == 0)
+ */
+ unsigned int val;
+ u8 str;
+
+ regmap_read(priv->map, LPC_STR4, &val);
+ str = val & 0xff;
+ if (*ppos == 0 || priv->pending)
+ /*
+ * If we got this far with `*ppos == 0` then we've read
+ * data out of IDR, so set IBF when reporting back to
+ * userspace so userspace knows the IDR value is valid.
+ */
+ str |= STR4_IBF;
+
+ dev_dbg(dev, "Read status 0x%x\n", str);
+ if (copy_to_user(buf, &str, sizeof(str))) {
+ rc = -EFAULT;
+ goto out;
+ }
+
+ remaining--;
+ }
+
+ WARN_ON(remaining);
+
+ rc = count;
+
+out:
+ spin_unlock_irq(&priv->rx.lock);
+
+ return rc;
+}
+
+static ssize_t mctp_lpc_write(struct file *filp, const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ uint8_t _data[2], *data = &_data[0];
+ struct mctp_lpc *priv;
+ struct device *dev;
+ size_t remaining;
+ unsigned int str;
+
+ priv = to_mctp_lpc(filp);
+ dev = priv->miscdev.this_device;
+
+ if (!count)
+ return count;
+
+ if (count > 2)
+ return -EINVAL;
+
+ if (*ppos >= 2)
+ return -EINVAL;
+
+ if (*ppos + count > 2)
+ return -EINVAL;
+
+ if (copy_from_user(data, buf, count))
+ return -EFAULT;
+
+ remaining = count;
+
+ if (*ppos == 0) {
+ /* Wait until OBF is clear - we don't get an IRQ */
+ dev_dbg(dev, "Waiting for OBF to clear\n");
+ for (;;) {
+ if (signal_pending(current))
+ return -EINTR;
+
+ regmap_read(priv->map, LPC_STR4, &str);
+ if (!(str & STR4_OBF))
+ break;
+
+ msleep(1);
+ }
+
+ dev_dbg(dev, "Writing 0x%x to ODR\n", *data);
+ regmap_write(priv->map, LPC_ODR4, *data);
+ remaining--;
+ data++;
+ }
+
+ if (remaining) {
+ if (!(*data & STR4_OBF))
+ dev_err(dev, "Clearing OBF with status write: 0x%x\n",
+ *data);
+ dev_dbg(dev, "Writing status 0x%x\n", *data);
+ regmap_write(priv->map, LPC_STR4, *data);
+ remaining--;
+ }
+
+ WARN_ON(remaining);
+
+ regmap_read(priv->map, LPC_STR4, &str);
+ dev_dbg(dev, "Triggering SerIRQ (current str=0x%x)\n", str);
+
+ /*
+ * Trigger Host IRQ on ODR write. Do this after any STR write in case
+ * we need to write ODR to indicate an STR update (which we do).
+ */
+ if (*ppos == 0)
+ regmap_update_bits(priv->map, LPC_HICRC, LPC_KCS4_IRQ_HOST,
+ LPC_KCS4_IRQ_HOST);
+
+ return count;
+}
+
+static __poll_t mctp_lpc_poll(struct file *filp, poll_table *wait)
+{
+ struct mctp_lpc *priv;
+ struct device *dev;
+ unsigned int val;
+ bool ibf;
+
+ priv = to_mctp_lpc(filp);
+ dev = priv->miscdev.this_device;
+
+ regmap_read(priv->map, LPC_STR4, &val);
+
+ spin_lock_irq(&priv->rx.lock);
+
+ ibf = priv->pending || val & STR4_IBF;
+
+ if (!ibf) {
+ dev_dbg(dev, "Polling on IBF\n");
+
+ spin_unlock_irq(&priv->rx.lock);
+
+ poll_wait(filp, &priv->rx, wait);
+ if (signal_pending(current)) {
+ dev_dbg(dev, "Polling IBF was interrupted\n");
+ goto out;
+ }
+
+ spin_lock_irq(&priv->rx.lock);
+
+ regmap_read(priv->map, LPC_STR4, &val);
+
+ ibf = priv->pending || val & STR4_IBF;
+ }
+
+ spin_unlock_irq(&priv->rx.lock);
+
+out:
+ dev_dbg(dev, "Polled IBF state: %s\n", ibf ? "set" : "clear");
+
+ return ibf ? EPOLLIN : 0;
+}
+
+static const struct file_operations mctp_lpc_fops = {
+ .owner = THIS_MODULE,
+ .llseek = no_seek_end_llseek,
+ .read = mctp_lpc_read,
+ .write = mctp_lpc_write,
+ .poll = mctp_lpc_poll,
+};
+
+static int mctp_lpc_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ unsigned int mask, val;
+ struct mctp_lpc *priv;
+ int irq;
+ int rc;
+
+ priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->map = syscon_node_to_regmap(dev->parent->of_node);
+ if (IS_ERR(priv->map)) {
+ dev_err(dev, "Couldn't get regmap\n");
+ return -ENODEV;
+ }
+
+ /*
+ * Set the LPC address. Simultaneously, test our MMIO regmap works. All
+ * subsequent accesses are assumed to work
+ */
+ rc = regmap_write(priv->map, LPC_LADR4, ((HOST_STR) << 16) | HOST_ODR);
+ if (rc < 0)
+ return rc;
+
+ /* Set up the SerIRQ */
+ mask = LPC_KCS4_IRQSEL_MASK
+ | LPC_KCS4_IRQTYPE_MASK
+ | LPC_KCS4_OBF4_AUTO_CLR;
+ val = (HOST_SERIRQ_ID << LPC_KCS4_IRQSEL_SHIFT)
+ | (HOST_SERIRQ_TYPE << LPC_KCS4_IRQTYPE_SHIFT);
+ val &= ~LPC_KCS4_OBF4_AUTO_CLR; /* Unnecessary, just documentation */
+ regmap_update_bits(priv->map, LPC_HICRC, mask, val);
+
+ /* Trigger waiters from IRQ */
+ init_waitqueue_head(&priv->rx);
+
+ dev_set_drvdata(dev, priv);
+
+ /* Set up the miscdevice */
+ priv->miscdev.minor = MISC_DYNAMIC_MINOR;
+ priv->miscdev.name = "mctp0";
+ priv->miscdev.fops = &mctp_lpc_fops;
+
+ /* Configure the IRQ handler */
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0)
+ return irq;
+
+ rc = devm_request_irq(dev, irq, mctp_lpc_irq, IRQF_SHARED,
+ dev_name(dev), priv);
+ if (rc < 0)
+ return rc;
+
+ /* Register the device */
+ rc = misc_register(&priv->miscdev);
+ if (rc) {
+ dev_err(dev, "Unable to register device\n");
+ return rc;
+ }
+
+ /* Enable the channel */
+ regmap_update_bits(priv->map, LPC_HICRB,
+ LPC_HICRB_IBFIF4 | LPC_HICRB_LPC4E,
+ LPC_HICRB_IBFIF4 | LPC_HICRB_LPC4E);
+
+ return 0;
+}
+
+static int mctp_lpc_remove(struct platform_device *pdev)
+{
+ struct mctp_lpc *ctx = dev_get_drvdata(&pdev->dev);
+
+ misc_deregister(&ctx->miscdev);
+
+ return 0;
+}
+
+static const struct of_device_id mctp_lpc_match[] = {
+ { .compatible = "openbmc,mctp-lpc" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, mctp_lpc_match);
+
+static struct platform_driver mctp_lpc = {
+ .driver = {
+ .name = "mctp-lpc",
+ .of_match_table = mctp_lpc_match,
+ },
+ .probe = mctp_lpc_probe,
+ .remove = mctp_lpc_remove,
+};
+module_platform_driver(mctp_lpc);
+
+MODULE_LICENSE("GPL v2+");
+MODULE_AUTHOR("Andrew Jeffery <andrew at aj.id.au>");
+MODULE_DESCRIPTION("OpenBMC MCTP LPC binding on ASPEED KCS");
--
2.20.1
More information about the openbmc
mailing list