| /* |
| * Copyright (C) 2014 Altera Corporation. All rights reserved |
| * |
| * This program is free software; you can redistribute it and/or modify it |
| * under the terms and conditions of the GNU General Public License, |
| * version 2, as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
| * more details. |
| * |
| * You should have received a copy of the GNU General Public License along with |
| * this program. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| |
| #include <linux/device.h> |
| #include <linux/interrupt.h> |
| #include <linux/io.h> |
| #include <linux/kernel.h> |
| #include <linux/kfifo.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/sysfs.h> |
| |
| #define DRV_NAME "altera_ilc" |
| #define CTRL_REG 0x80 |
| #define FREQ_REG 0x84 |
| #define STP_REG 0x88 |
| #define VLD_REG 0x8C |
| #define ILC_MAX_PORTS 32 |
| #define ILC_FIFO_DEFAULT 32 |
| #define ILC_ENABLE 0x01 |
| #define CHAR_SIZE 10 |
| #define POLL_INTERVAL 1 |
| #define GET_PORT_COUNT(_val) ((_val & 0x7C) >> 2) |
| #define GET_VLD_BIT(_val, _offset) (((_val) >> _offset) & 0x1) |
| |
| struct altera_ilc { |
| struct platform_device *pdev; |
| void __iomem *regs; |
| unsigned int port_count; |
| unsigned int irq; |
| unsigned int channel_offset; |
| unsigned int interrupt_channels[ILC_MAX_PORTS]; |
| struct kfifo kfifos[ILC_MAX_PORTS]; |
| struct device_attribute dev_attr[ILC_MAX_PORTS]; |
| struct delayed_work ilc_work; |
| char sysfs[ILC_MAX_PORTS][CHAR_SIZE]; |
| u32 fifo_depth; |
| }; |
| |
| static int ilc_irq_lookup(struct altera_ilc *ilc, int irq) |
| { |
| int i; |
| for (i = 0; i < ilc->port_count; i++) { |
| if (irq == platform_get_irq(ilc->pdev, i)) |
| return i; |
| } |
| return -EPERM; |
| } |
| |
| static ssize_t ilc_show_counter(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| int ret, i, id, fifo_len; |
| unsigned int fifo_buf[ILC_MAX_PORTS]; |
| char temp[10]; |
| struct altera_ilc *ilc = dev_get_drvdata(dev); |
| |
| fifo_len = 0; |
| ret = kstrtouint(attr->attr.name, 0, &id); |
| |
| for (i = 0; i < ilc->port_count; i++) { |
| if (id == (ilc->interrupt_channels[i])) { |
| /*Check for kfifo length*/ |
| fifo_len = kfifo_len(&ilc->kfifos[i]) |
| /sizeof(unsigned int); |
| if (fifo_len <= 0) { |
| dev_info(&ilc->pdev->dev, "Fifo for interrupt %s is empty\n", |
| attr->attr.name); |
| return 0; |
| } |
| /*Read from kfifo*/ |
| ret = kfifo_out(&ilc->kfifos[i], &fifo_buf, |
| kfifo_len(&ilc->kfifos[i])); |
| } |
| } |
| |
| for (i = 0; i < fifo_len; i++) { |
| sprintf(temp, "%u\n", fifo_buf[i]); |
| strcat(buf, temp); |
| } |
| |
| strcat(buf, "\0"); |
| |
| return strlen(buf); |
| } |
| |
| static struct attribute *altera_ilc_attrs[ILC_MAX_PORTS]; |
| |
| struct attribute_group altera_ilc_attr_group = { |
| .name = "ilc_data", |
| .attrs = altera_ilc_attrs, |
| }; |
| |
| static void ilc_work(struct work_struct *work) |
| { |
| unsigned int ilc_value, ret, offset, stp_reg; |
| struct altera_ilc *ilc = |
| container_of(work, struct altera_ilc, ilc_work.work); |
| |
| offset = ilc_irq_lookup(ilc, ilc->irq); |
| if (offset < 0) { |
| dev_err(&ilc->pdev->dev, "Unable to lookup irq number\n"); |
| return; |
| } |
| |
| if (GET_VLD_BIT(readl(ilc->regs + VLD_REG), offset)) { |
| /*Read counter register*/ |
| ilc_value = readl(ilc->regs + (offset) * 4); |
| |
| /*Putting value into kfifo*/ |
| ret = kfifo_in((&ilc->kfifos[offset]), |
| (unsigned int *)&ilc_value, sizeof(ilc_value)); |
| |
| /*Clearing stop register*/ |
| stp_reg = readl(ilc->regs + STP_REG); |
| writel((~(0x1 << offset) & stp_reg), ilc->regs + STP_REG); |
| |
| return; |
| } |
| |
| /*Start workqueue to poll data valid*/ |
| schedule_delayed_work(&ilc->ilc_work, msecs_to_jiffies(POLL_INTERVAL)); |
| } |
| |
| static irqreturn_t ilc_interrupt_handler(int irq, void *p) |
| { |
| unsigned int offset, stp_reg; |
| |
| struct altera_ilc *ilc = (struct altera_ilc *)p; |
| |
| /*Update ILC struct*/ |
| ilc->irq = irq; |
| |
| dev_dbg(&ilc->pdev->dev, "Interrupt %u triggered\n", |
| ilc->irq); |
| |
| offset = ilc_irq_lookup(ilc, irq); |
| if (offset < 0) { |
| dev_err(&ilc->pdev->dev, "Unable to lookup irq number\n"); |
| return IRQ_RETVAL(IRQ_NONE); |
| } |
| |
| /*Setting stop register*/ |
| stp_reg = readl(ilc->regs + STP_REG); |
| writel((0x1 << offset)|stp_reg, ilc->regs + STP_REG); |
| |
| /*Start workqueue to poll data valid*/ |
| schedule_delayed_work(&ilc->ilc_work, 0); |
| |
| return IRQ_RETVAL(IRQ_NONE); |
| } |
| |
| static int altera_ilc_probe(struct platform_device *pdev) |
| { |
| struct altera_ilc *ilc; |
| struct resource *regs; |
| struct device_node *np = pdev->dev.of_node; |
| int ret, i; |
| |
| ilc = devm_kzalloc(&pdev->dev, sizeof(struct altera_ilc), |
| GFP_KERNEL); |
| if (!ilc) |
| return -ENOMEM; |
| |
| ilc->pdev = pdev; |
| |
| regs = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| if (!regs) |
| return -ENXIO; |
| |
| ilc->regs = devm_ioremap_resource(&pdev->dev, regs); |
| if (!ilc->regs) |
| return -EADDRNOTAVAIL; |
| |
| ilc->port_count = GET_PORT_COUNT(readl(ilc->regs + CTRL_REG)); |
| if (ilc->port_count <= 0) { |
| dev_warn(&pdev->dev, "No interrupt connected to ILC\n"); |
| return -EPERM; |
| } |
| |
| /*Check for fifo depth*/ |
| ret = of_property_read_u32(np, "altr,sw-fifo-depth", |
| &(ilc->fifo_depth)); |
| if (ret) { |
| dev_warn(&pdev->dev, "Fifo depth undefined\n"); |
| dev_warn(&pdev->dev, "Setting fifo depth to default value (32)\n"); |
| ilc->fifo_depth = ILC_FIFO_DEFAULT; |
| } |
| |
| /*Initialize Kfifo*/ |
| for (i = 0; i < ilc->port_count; i++) { |
| ret = kfifo_alloc(&ilc->kfifos[i], (ilc->fifo_depth * |
| sizeof(unsigned int)), GFP_KERNEL); |
| if (ret) { |
| dev_err(&pdev->dev, "Kfifo failed to initialize\n"); |
| return ret; |
| } |
| } |
| |
| /*Register each of the IRQs*/ |
| for (i = 0; i < ilc->port_count; i++) { |
| ilc->interrupt_channels[i] = platform_get_irq(pdev, i); |
| |
| ret = devm_request_irq(&pdev->dev, (ilc->interrupt_channels[i]), |
| ilc_interrupt_handler, IRQF_SHARED, "ilc_0", |
| (void *)(ilc)); |
| |
| if (ret < 0) |
| dev_warn(&pdev->dev, "Failed to register interrupt handler"); |
| } |
| |
| /*Setup sysfs interface*/ |
| for (i = 0; (i < ilc->port_count); i++) { |
| sprintf(ilc->sysfs[i], "%d", (ilc->interrupt_channels[i])); |
| ilc->dev_attr[i].attr.name = ilc->sysfs[i]; |
| ilc->dev_attr[i].attr.mode = S_IRUGO; |
| ilc->dev_attr[i].show = ilc_show_counter; |
| altera_ilc_attrs[i] = &ilc->dev_attr[i].attr; |
| altera_ilc_attrs[i+1] = NULL; |
| } |
| ret = sysfs_create_group(&pdev->dev.kobj, &altera_ilc_attr_group); |
| |
| /*Initialize workqueue*/ |
| INIT_DELAYED_WORK(&ilc->ilc_work, ilc_work); |
| |
| /*Global enable ILC softIP*/ |
| writel(ILC_ENABLE, ilc->regs + CTRL_REG); |
| |
| platform_set_drvdata(pdev, ilc); |
| |
| dev_info(&pdev->dev, "Driver successfully loaded\n"); |
| |
| return 0; |
| } |
| |
| static int altera_ilc_remove(struct platform_device *pdev) |
| { |
| int i; |
| struct altera_ilc *ilc = platform_get_drvdata(pdev); |
| |
| /*Remove sysfs interface*/ |
| sysfs_remove_group(&pdev->dev.kobj, &altera_ilc_attr_group); |
| |
| /*Free up kfifo memory*/ |
| for (i = 0; i < ilc->port_count; i++) |
| kfifo_free(&ilc->kfifos[i]); |
| |
| platform_set_drvdata(pdev, NULL); |
| return 0; |
| } |
| |
| static const struct of_device_id altera_ilc_match[] = { |
| { .compatible = "altr,ilc-1.0" }, |
| { /* Sentinel */ } |
| }; |
| |
| MODULE_DEVICE_TABLE(of, altera_ilc_match); |
| |
| static struct platform_driver altera_ilc_platform_driver = { |
| .driver = { |
| .name = DRV_NAME, |
| .owner = THIS_MODULE, |
| .of_match_table = of_match_ptr(altera_ilc_match), |
| }, |
| .remove = altera_ilc_remove, |
| }; |
| |
| static int __init altera_ilc_init(void) |
| { |
| return platform_driver_probe(&altera_ilc_platform_driver, |
| altera_ilc_probe); |
| } |
| |
| static void __exit altera_ilc_exit(void) |
| { |
| platform_driver_unregister(&altera_ilc_platform_driver); |
| } |
| |
| module_init(altera_ilc_init); |
| module_exit(altera_ilc_exit); |
| |
| MODULE_AUTHOR("Chee Nouk Phoon <[email protected]>"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_DESCRIPTION("Altera Interrupt Latency Counter Driver"); |
| MODULE_ALIAS("platform:" DRV_NAME); |