[PATCH 2/2] soc: bcm: iproc: Add Broadcom iProc IDM driver
From: Ray Jui
Date: Mon Dec 02 2019 - 18:32:04 EST
Add Broadcom iProc IDM driver that controls that IDM devices available
on various iProc based SoCs for bus transaction timeout monitoring and
error logging.
Signed-off-by: Ray Jui <ray.jui@xxxxxxxxxxxx>
Signed-off-by: Rayagonda Kokatanur <rayagonda.kokatanur@xxxxxxxxxxxx>
---
drivers/soc/bcm/Kconfig | 10 +
drivers/soc/bcm/Makefile | 1 +
drivers/soc/bcm/iproc/Kconfig | 6 +
drivers/soc/bcm/iproc/Makefile | 1 +
drivers/soc/bcm/iproc/iproc-idm.c | 390 ++++++++++++++++++++++++++++++
5 files changed, 408 insertions(+)
create mode 100644 drivers/soc/bcm/iproc/Kconfig
create mode 100644 drivers/soc/bcm/iproc/Makefile
create mode 100644 drivers/soc/bcm/iproc/iproc-idm.c
diff --git a/drivers/soc/bcm/Kconfig b/drivers/soc/bcm/Kconfig
index 648e32693b7e..30cf0c390c4e 100644
--- a/drivers/soc/bcm/Kconfig
+++ b/drivers/soc/bcm/Kconfig
@@ -33,6 +33,16 @@ config SOC_BRCMSTB
If unsure, say N.
+config SOC_BRCM_IPROC
+ bool "Broadcom iProc SoC drivers"
+ depends on ARCH_BCM_IPROC || COMPILE_TEST
+ default ARCH_BCM_IPROC
+ help
+ Enable SoC drivers for Broadcom iProc based chipsets
+
+ If unsure, say N.
+
source "drivers/soc/bcm/brcmstb/Kconfig"
+source "drivers/soc/bcm/iproc/Kconfig"
endmenu
diff --git a/drivers/soc/bcm/Makefile b/drivers/soc/bcm/Makefile
index d92268a829a9..9db23ab5dacc 100644
--- a/drivers/soc/bcm/Makefile
+++ b/drivers/soc/bcm/Makefile
@@ -2,3 +2,4 @@
obj-$(CONFIG_BCM2835_POWER) += bcm2835-power.o
obj-$(CONFIG_RASPBERRYPI_POWER) += raspberrypi-power.o
obj-$(CONFIG_SOC_BRCMSTB) += brcmstb/
+obj-$(CONFIG_SOC_BRCM_IPROC) += iproc/
diff --git a/drivers/soc/bcm/iproc/Kconfig b/drivers/soc/bcm/iproc/Kconfig
new file mode 100644
index 000000000000..205e0ebbf99c
--- /dev/null
+++ b/drivers/soc/bcm/iproc/Kconfig
@@ -0,0 +1,6 @@
+config IPROC_IDM
+ bool "Broadcom iProc IDM driver"
+ depends on (ARCH_BCM_IPROC || COMPILE_TEST) && OF
+ default ARCH_BCM_IPROC
+ help
+ Enables support for iProc Interconnect and Device Management (IDM) control and monitoring
diff --git a/drivers/soc/bcm/iproc/Makefile b/drivers/soc/bcm/iproc/Makefile
new file mode 100644
index 000000000000..de54aef66097
--- /dev/null
+++ b/drivers/soc/bcm/iproc/Makefile
@@ -0,0 +1 @@
+obj-$(CONFIG_IPROC_IDM) += iproc-idm.o
diff --git a/drivers/soc/bcm/iproc/iproc-idm.c b/drivers/soc/bcm/iproc/iproc-idm.c
new file mode 100644
index 000000000000..5f3b04dbe80a
--- /dev/null
+++ b/drivers/soc/bcm/iproc/iproc-idm.c
@@ -0,0 +1,390 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2019 Broadcom
+ */
+
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/of_irq.h>
+#include <linux/of_platform.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+
+#define IDM_CTRL_OFFSET 0x000
+#define IDM_CTRL_TIMEOUT_ENABLE BIT(9)
+#define IDM_CTRL_TIMEOUT_EXP_SHIFT 4
+#define IDM_CTRL_TIMEOUT_EXP_MASK (0x1f << 4)
+#define IDM_CTRL_TIMEOUT_IRQ BIT(3)
+#define IDM_CTRL_TIMEOUT_RESET BIT(2)
+#define IDM_CTRL_BUS_ERR_IRQ BIT(1)
+#define IDM_CTRL_BUS_ERR_RESET BIT(0)
+
+#define IDM_COMP_OFFSET 0x004
+#define IDM_COMP_OVERFLOW BIT(1)
+#define IDM_COMP_ERR BIT(0)
+
+#define IDM_STATUS_OFFSET 0x008
+#define IDM_STATUS_OVERFLOW BIT(2)
+#define IDM_STATUS_CAUSE_MASK 0x03
+
+#define IDM_ADDR_LSB_OFFSET 0x00c
+#define IDM_ADDR_MSB_OFFSET 0x010
+#define IDM_ID_OFFSET 0x014
+#define IDM_FLAGS_OFFSET 0x01c
+
+#define IDM_ISR_STATUS_OFFSET 0x100
+#define IDM_ISR_STATUS_TIMEOUT BIT(1)
+#define IDM_ISR_STATUS_ERR_LOG BIT(0)
+
+#define ELOG_SIG_OFFSET 0x00
+#define ELOG_SIG_VAL 0x49444d45
+
+#define ELOG_CUR_OFFSET 0x04
+#define ELOG_LEN_OFFSET 0x08
+#define ELOG_HEADER_LEN 12
+#define ELOG_EVENT_LEN 64
+
+#define ELOG_IDM_NAME_OFFSET 0x00
+#define ELOG_IDM_ADDR_LSB_OFFSET 0x10
+#define ELOG_IDM_ADDR_MSB_OFFSET 0x14
+#define ELOG_IDM_ID_OFFSET 0x18
+#define ELOG_IDM_CAUSE_OFFSET 0x20
+#define ELOG_IDM_FLAG_OFFSET 0x28
+
+#define ELOG_IDM_MAX_NAME_LEN 16
+
+#define ELOG_IDM_COMPAT_STR "brcm,iproc-idm-elog"
+
+struct iproc_idm_elog {
+ struct device *dev;
+ void __iomem *buf;
+ u32 len;
+ spinlock_t lock;
+
+ int (*idm_event_log)(struct iproc_idm_elog *elog, const char *name,
+ u32 cause, u32 addr_lsb, u32 addr_msb, u32 id,
+ u32 flag);
+};
+
+struct iproc_idm {
+ struct device *dev;
+ struct iproc_idm_elog *elog;
+ void __iomem *base;
+ const char *name;
+ bool no_panic;
+};
+
+static ssize_t no_panic_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ struct iproc_idm *idm = platform_get_drvdata(pdev);
+ unsigned int no_panic;
+ int ret;
+
+ ret = kstrtouint(buf, 0, &no_panic);
+ if (ret)
+ return ret;
+
+ idm->no_panic = no_panic ? true : false;
+
+ return count;
+}
+
+static ssize_t no_panic_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct platform_device *pdev = to_platform_device(dev);
+ struct iproc_idm *idm = platform_get_drvdata(pdev);
+
+ return sprintf(buf, "%u\n", idm->no_panic ? 1 : 0);
+}
+
+static DEVICE_ATTR_RW(no_panic);
+
+static int iproc_idm_event_log(struct iproc_idm_elog *elog, const char *name,
+ u32 cause, u32 addr_lsb, u32 addr_msb, u32 id,
+ u32 flag)
+{
+ u32 val, cur, len;
+ void *event;
+ unsigned long flags;
+
+ spin_lock_irqsave(&elog->lock, flags);
+
+ /*
+ * Check if signature is already there. If not, clear and restart
+ * everything
+ */
+ val = readl(elog->buf + ELOG_SIG_OFFSET);
+ if (val != ELOG_SIG_VAL) {
+ memset_io(elog->buf, 0, elog->len);
+ writel(ELOG_SIG_VAL, elog->buf + ELOG_SIG_OFFSET);
+ writel(ELOG_HEADER_LEN, elog->buf + ELOG_CUR_OFFSET);
+ writel(0, elog->buf + ELOG_LEN_OFFSET);
+ }
+
+ /* determine offset and length */
+ cur = readl(elog->buf + ELOG_CUR_OFFSET);
+ len = readl(elog->buf + ELOG_LEN_OFFSET);
+
+ /*
+ * Based on the design and how kernel panic is triggered after an IDM
+ * event, it's practically impossible for the storage to be full. In
+ * case if it does happen, we can simply bail out since it's likely
+ * the same category of events that have already been logged
+ */
+ if (cur + ELOG_EVENT_LEN > elog->len) {
+ dev_warn(elog->dev, "IDM ELOG buffer is now full\n");
+ spin_unlock_irqrestore(&elog->lock, flags);
+ return -ENOMEM;
+ }
+
+ /* now log the IDM event */
+ event = elog->buf + cur;
+ memcpy_toio(event + ELOG_IDM_NAME_OFFSET, name, ELOG_IDM_MAX_NAME_LEN);
+ writel(addr_lsb, event + ELOG_IDM_ADDR_LSB_OFFSET);
+ writel(addr_msb, event + ELOG_IDM_ADDR_MSB_OFFSET);
+ writel(id, event + ELOG_IDM_ID_OFFSET);
+ writel(cause, event + ELOG_IDM_CAUSE_OFFSET);
+ writel(flag, event + ELOG_IDM_FLAG_OFFSET);
+
+ cur += ELOG_EVENT_LEN;
+ len += ELOG_EVENT_LEN;
+
+ /* update offset and length */
+ writel(cur, elog->buf + ELOG_CUR_OFFSET);
+ writel(len, elog->buf + ELOG_LEN_OFFSET);
+
+ spin_unlock_irqrestore(&elog->lock, flags);
+
+ return 0;
+}
+
+static irqreturn_t iproc_idm_irq_handler(int irq, void *data)
+{
+ struct iproc_idm *idm = data;
+ struct device *dev = idm->dev;
+ const char *name = idm->name;
+ u32 isr_status, log_status, lsb, msb, id, flag;
+ struct iproc_idm_elog *elog = idm->elog;
+
+ isr_status = readl(idm->base + IDM_ISR_STATUS_OFFSET);
+ log_status = readl(idm->base + IDM_STATUS_OFFSET);
+
+ /* quit if the interrupt is not for IDM */
+ if (!isr_status)
+ return IRQ_NONE;
+
+ /* ACK the interrupt */
+ if (log_status & IDM_STATUS_OVERFLOW)
+ writel(IDM_COMP_OVERFLOW, idm->base + IDM_COMP_OFFSET);
+
+ if (log_status & IDM_STATUS_CAUSE_MASK)
+ writel(IDM_COMP_ERR, idm->base + IDM_COMP_OFFSET);
+
+ /* dump critical IDM information */
+ if (isr_status & IDM_ISR_STATUS_TIMEOUT)
+ dev_err(dev, "[%s] IDM timeout\n", name);
+
+ if (isr_status & IDM_ISR_STATUS_ERR_LOG)
+ dev_err(dev, "[%s] IDM error log\n", name);
+
+ lsb = readl(idm->base + IDM_ADDR_LSB_OFFSET);
+ msb = readl(idm->base + IDM_ADDR_MSB_OFFSET);
+ id = readl(idm->base + IDM_ID_OFFSET);
+ flag = readl(idm->base + IDM_FLAGS_OFFSET);
+
+ dev_err(dev, "Cause: 0x%08x; Address LSB: 0x%08x; Address MSB: 0x%08x; Master ID: 0x%08x; Flag: 0x%08x\n",
+ log_status, lsb, msb, id, flag);
+
+ /* if elog service is available, log the event */
+ if (elog) {
+ elog->idm_event_log(elog, name, log_status, lsb, msb, id, flag);
+ dev_err(dev, "IDM event logged\n\n");
+ }
+
+ /* IDM timeout is fatal and non-recoverable. Panic the kernel */
+ if (!idm->no_panic)
+ panic("Fatal bus error detected by IDM");
+
+ return IRQ_HANDLED;
+}
+
+static int iproc_idm_dev_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct device_node *np = dev->of_node;
+ struct platform_device *elog_pdev;
+ struct device_node *elog_np;
+ struct iproc_idm *idm;
+ const char *name;
+ int ret;
+ u32 val;
+
+ idm = devm_kzalloc(dev, sizeof(*idm), GFP_KERNEL);
+ if (!idm)
+ return -ENOMEM;
+
+ ret = of_property_read_string(np, "brcm,iproc-idm-bus", &name);
+ if (ret) {
+ dev_err(dev, "Unable to parse IDM bus name\n");
+ return ret;
+ }
+ idm->name = name;
+
+ platform_set_drvdata(pdev, idm);
+ idm->dev = dev;
+
+ idm->base = of_iomap(np, 0);
+ if (!idm->base) {
+ dev_err(dev, "Unable to map I/O\n");
+ ret = -ENOMEM;
+ goto err_exit;
+ }
+
+ ret = of_irq_get(np, 0);
+ if (ret <= 0) {
+ dev_err(dev, "Unable to find IRQ number. ret=%d\n", ret);
+ goto err_iounmap;
+ }
+
+ ret = devm_request_irq(dev, ret, iproc_idm_irq_handler, IRQF_SHARED,
+ idm->name, idm);
+ if (ret < 0) {
+ dev_err(dev, "Unable to request irq. ret=%d\n", ret);
+ goto err_iounmap;
+ }
+
+ /*
+ * ELOG phandle is optional. If ELOG phandle is specified, it indicates
+ * ELOG logging needs to be enabled
+ */
+ elog_np = of_parse_phandle(dev->of_node, ELOG_IDM_COMPAT_STR, 0);
+ if (elog_np) {
+ elog_pdev = of_find_device_by_node(elog_np);
+ if (!elog_pdev) {
+ dev_err(dev, "Unable to find IDM ELOG device\n");
+ ret = -ENODEV;
+ goto err_iounmap;
+ }
+
+ idm->elog = platform_get_drvdata(elog_pdev);
+ if (!idm->elog) {
+ dev_err(dev, "Unable to get IDM ELOG driver data\n");
+ ret = -EINVAL;
+ goto err_iounmap;
+ }
+ }
+
+ /* enable IDM timeout and its interrupt */
+ val = readl(idm->base + IDM_CTRL_OFFSET);
+ val |= IDM_CTRL_TIMEOUT_EXP_MASK | IDM_CTRL_TIMEOUT_ENABLE |
+ IDM_CTRL_TIMEOUT_IRQ;
+ writel(val, idm->base + IDM_CTRL_OFFSET);
+
+ ret = device_create_file(dev, &dev_attr_no_panic);
+ if (ret < 0)
+ goto err_iounmap;
+
+ of_node_put(np);
+
+ pr_info("iProc IDM device %s registered\n", idm->name);
+
+ return 0;
+
+err_iounmap:
+ iounmap(idm->base);
+
+err_exit:
+ of_node_put(np);
+ return ret;
+}
+
+static int iproc_idm_elog_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct iproc_idm_elog *elog;
+ struct resource *res;
+ u32 val;
+
+ elog = devm_kzalloc(dev, sizeof(*elog), GFP_KERNEL);
+ if (!elog)
+ return -ENOMEM;
+
+ res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+ elog->buf = (void __iomem *)devm_memremap(dev, res->start,
+ resource_size(res),
+ MEMREMAP_WB);
+ if (IS_ERR(elog->buf)) {
+ dev_err(dev, "Unable to map ELOG buffer\n");
+ return PTR_ERR(elog->buf);
+ }
+
+ elog->dev = dev;
+ elog->len = resource_size(res);
+ elog->idm_event_log = iproc_idm_event_log;
+
+ /*
+ * Check if signature is already there. Only clear memory if there's
+ * no signature detected
+ */
+ val = readl(elog->buf + ELOG_SIG_OFFSET);
+ if (val != ELOG_SIG_VAL)
+ memset_io(elog->buf, 0, elog->len);
+
+ spin_lock_init(&elog->lock);
+ platform_set_drvdata(pdev, elog);
+
+ dev_info(dev, "iProc IDM ELOG registered\n");
+
+ return 0;
+}
+
+static int iproc_idm_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct device_node *np = dev->of_node;
+ int ret;
+
+ if (of_device_is_compatible(np, ELOG_IDM_COMPAT_STR))
+ ret = iproc_idm_elog_probe(pdev);
+ else
+ ret = iproc_idm_dev_probe(pdev);
+
+ return ret;
+}
+
+static const struct of_device_id iproc_idm_of_match[] = {
+ { .compatible = "brcm,iproc-idm", },
+ { .compatible = ELOG_IDM_COMPAT_STR, },
+ { }
+};
+
+static struct platform_driver iproc_idm_driver = {
+ .probe = iproc_idm_probe,
+ .driver = {
+ .name = "iproc-idm",
+ .of_match_table = of_match_ptr(iproc_idm_of_match),
+ },
+};
+
+static int __init iproc_idm_init(void)
+{
+ return platform_driver_register(&iproc_idm_driver);
+}
+arch_initcall(iproc_idm_init);
+
+static void __exit iproc_idm_exit(void)
+{
+ platform_driver_unregister(&iproc_idm_driver);
+}
+module_exit(iproc_idm_exit);
+
+MODULE_LICENSE("GPL v2");
+MODULE_AUTHOR("Broadcom");
+MODULE_DESCRIPTION("iProc IDM driver");
--
2.17.1