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