| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (c) 2020 MediaTek Inc. |
| */ |
| #include <linux/module.h> |
| #include <linux/kernel.h> |
| #include <linux/device.h> |
| #include <linux/platform_device.h> |
| #include <linux/init.h> |
| #include <linux/io.h> |
| #include <linux/interrupt.h> |
| #include <linux/of_irq.h> |
| #include <linux/of_address.h> |
| #include <linux/of_device.h> |
| #include <linux/debugfs.h> |
| #include <linux/random.h> |
| #include <linux/slab.h> |
| #include <linux/uaccess.h> |
| #if IS_ENABLED(CONFIG_MTK_AEE_FEATURE) |
| #include <mt-plat/aee.h> |
| #endif |
| #include "apusys_core.h" |
| #include "dapc.h" |
| #include "dapc_cfg.h" |
| #include "apusys_power.h" |
| |
| struct dapc_driver *dapc_drv; |
| |
| static void dapc_drv_release(struct kref *ref) |
| { |
| kfree(dapc_drv); |
| dapc_drv = NULL; |
| } |
| |
| static void dapc_drv_get(void) |
| { |
| kref_get(&dapc_drv->ref); |
| } |
| |
| static void dapc_drv_put(void) |
| { |
| if (!dapc_drv) |
| return; |
| |
| kref_put(&dapc_drv->ref, dapc_drv_release); |
| } |
| |
| #if IS_ENABLED(CONFIG_MTK_APUSYS_DEBUG) |
| /* apusys devapc debug file operations */ |
| static int dapc_debug_open(struct inode *inode, struct file *file) |
| { |
| file->private_data = inode->i_private; |
| return nonseekable_open(inode, file); |
| } |
| |
| static int dapc_debug_release(struct inode *inode, struct file *file) |
| { |
| return 0; |
| } |
| |
| static int trigger_vio_test(struct dapc_driver *drv, int round) |
| { |
| phys_addr_t test_base = drv->cfg->ut_base; |
| void __iomem *test_virt; |
| int i, num, flag_ke; |
| |
| if (round <= 0) { |
| pr_info("invalid round %d\n", round); |
| return -EINVAL; |
| } |
| |
| test_virt = ioremap(test_base, 4096); |
| if (!test_virt) { |
| pr_info("ioremap test base(0x%lx) virt failed\n", |
| (unsigned long)test_base); |
| return -ENOMEM; |
| } |
| |
| /* do not trigger KE while testing */ |
| flag_ke = drv->enable_ke; |
| drv->enable_ke = 0; |
| |
| for (i = 0; i < round; i++) { |
| get_random_bytes(&num, sizeof(num)); |
| num &= 0xF; |
| /* mdla_core0 0x8 */ |
| iowrite32(num, (unsigned int *)(test_virt + 0x8)); |
| pr_info("write 0x%x to (0x%lx), read: 0x%x\n", |
| num, (unsigned long)(test_base + 0x8), |
| readl((unsigned int *)(test_virt + 0x8))); |
| |
| get_random_bytes(&num, sizeof(num)); |
| num &= 0xF; |
| /* mdla_core1 0xC */ |
| iowrite32(num, (unsigned int *)(test_virt + 0xC)); |
| pr_info("write 0x%x to (0x%lx), read: 0x%x\n", |
| num, (unsigned long)(test_base + 0xC), |
| readl((unsigned int *)(test_virt + 0xC))); |
| } |
| |
| iounmap(test_virt); |
| |
| drv->enable_ke = flag_ke; |
| |
| return 0; |
| } |
| |
| static ssize_t dapc_debug_read(struct file *file, char __user *buf, |
| size_t size, loff_t *offset) |
| { |
| struct dapc_driver *drv = (struct dapc_driver *)file->private_data; |
| char output[64] = {0}; |
| int ret, len; |
| |
| if (!buf) |
| return -EINVAL; |
| |
| ret = snprintf(output, sizeof(output), |
| "%s enable_KE: %d, enable_AEE: %d, enable_IRQ: %d\n", |
| "[APUSYS_DEVAPC]", drv->enable_ke, drv->enable_aee, |
| drv->enable_irq); |
| |
| if (ret <= 0) |
| return 0; |
| |
| len = min((int)size, (int)(strlen(output) - *offset)); |
| if (len <= 0) |
| return 0; |
| |
| ret = copy_to_user(buf, output, len); |
| if (ret) { |
| pr_info("Fail to copy %s, ret %d\n", output, ret); |
| return -EFAULT; |
| } |
| |
| *offset += len; |
| |
| return len; |
| } |
| |
| static void apusys_devapc_start(void *data); |
| |
| static ssize_t dapc_debug_write(struct file *file, const char __user *buf, |
| size_t size, loff_t *ppos) |
| { |
| struct dapc_driver *drv = (struct dapc_driver *)file->private_data; |
| char input[32] = {0}; |
| char *cmd_str, *param_str, *tmp_str; |
| unsigned int param = 0; |
| size_t len; |
| int ret; |
| |
| if (!buf) |
| return -EINVAL; |
| |
| len = min(size, 31UL); |
| |
| if (copy_from_user(input, buf, len)) { |
| pr_info("Fail to copy from user\n"); |
| return -EFAULT; |
| } |
| |
| input[len] = '\0'; |
| tmp_str = input; |
| |
| cmd_str = strsep(&tmp_str, " "); |
| if (!cmd_str) { |
| pr_info("Fail to get cmd\n"); |
| return -EINVAL; |
| } |
| |
| param_str = strsep(&tmp_str, " "); |
| if (!param_str) { |
| pr_info("Fail to get param\n"); |
| return -EINVAL; |
| } |
| |
| ret = kstrtouint(param_str, 10, ¶m); |
| |
| if (ret) |
| return ret; |
| |
| if (!strncmp(cmd_str, "enable_UT", sizeof("enable_UT"))) { |
| if (param & 0x1) { |
| drv->enable_ke = 0; |
| drv->enable_aee = 0; |
| drv->enable_irq = 1; |
| pr_info("APUSYS devapc debugging mode\n"); |
| } else { |
| drv->enable_ke = 1; |
| drv->enable_aee = 1; |
| drv->enable_irq = 1; |
| pr_info("APUSYS devapc violation mode\n"); |
| } |
| } else if (!strncmp(cmd_str, "enable_KE", sizeof("enable_KE"))) { |
| drv->enable_ke = param & 0x1; |
| pr_info("APUSYS devapc %s KE\n", |
| drv->enable_ke ? "enable" : "disable"); |
| } else if (!strncmp(cmd_str, "enable_AEE", sizeof("enable_AEE"))) { |
| drv->enable_aee = param & 0x1; |
| pr_info("APUSYS devapc %s AEE\n", |
| drv->enable_aee ? "enable" : "disable"); |
| } else if (!strncmp(cmd_str, "enable_IRQ", sizeof("enable_IRQ"))) { |
| drv->enable_irq = param & 0x1; |
| if (drv->enable_irq) |
| enable_irq(drv->irq); |
| else |
| disable_irq(drv->irq); |
| pr_info("APUSYS devapc %s IRQ\n", |
| drv->enable_irq ? "enable" : "disable"); |
| } else if (!strncmp(cmd_str, "trigger_vio", sizeof("trigger_vio"))) { |
| pr_info("APUSYS devapc trigger vio test %d +\n", param); |
| trigger_vio_test(drv, param); |
| pr_info("APUSYS devapc trigger vio test %d -\n", param); |
| } else { |
| pr_info("Unknown cmd %s\n", cmd_str); |
| return -EINVAL; |
| } |
| |
| return len; |
| } |
| |
| static const struct file_operations dapc_debug_fops = { |
| .owner = THIS_MODULE, |
| .open = dapc_debug_open, |
| .read = dapc_debug_read, |
| .write = dapc_debug_write, |
| .release = dapc_debug_release, |
| }; |
| /* apusys devapc debug file operations */ |
| |
| static int apusys_devapc_debug_init(struct dapc_driver *drv) |
| { |
| struct dentry *ddevapc = NULL; |
| int ret = 0; |
| |
| if (IS_ERR_OR_NULL(drv->droot)) |
| return 0; |
| |
| ddevapc = debugfs_create_dir("devapc", drv->droot); |
| if (IS_ERR_OR_NULL(ddevapc)) { |
| ret = PTR_ERR(ddevapc); |
| pr_info("%s: unable to create debugfs folder: %d\n", |
| __func__, ret); |
| return ret; |
| } |
| debugfs_create_u32("log", |
| 0644, ddevapc, &drv->debug_log); |
| debugfs_create_file("debug", 0644, |
| ddevapc, drv, &dapc_debug_fops); |
| |
| drv->ddevapc = ddevapc; |
| return ret; |
| } |
| |
| static void apusys_devapc_debug_exit(struct dapc_driver *drv) |
| { |
| debugfs_remove_recursive(drv->ddevapc); |
| } |
| #else |
| static int apusys_devapc_debug_init(struct dapc_driver *drv) |
| { |
| return 0; |
| } |
| |
| static void apusys_devapc_debug_exit(struct dapc_driver *drv) |
| { |
| } |
| #endif |
| |
| static void slv_irq(unsigned int slv, bool enable) |
| { |
| uint32_t reg; |
| uint32_t mask; |
| struct dapc_driver *d = dapc_drv; |
| struct dapc_config *cfg = d->cfg; |
| |
| if (slv > cfg->slv_cnt) { |
| pr_info("%s: slv: %d is out of index, max: %d\n", |
| __func__, slv, cfg->slv_cnt); |
| return; |
| } |
| |
| reg = cfg->vio_mask(slv / cfg->slv_per_dapc); |
| mask = 1 << (slv % cfg->slv_per_dapc); |
| |
| if (enable) |
| dapc_reg_clr(d, reg, mask); |
| else |
| dapc_reg_set(d, reg, mask); |
| } |
| |
| static void do_kernel_exception(struct dapc_driver *drv, unsigned int i, |
| const char *slv_name, |
| struct dapc_exception *ex) |
| { |
| /* mask irq for slv "i" */ |
| slv_irq(i, false); |
| |
| if (drv->enable_ke) { |
| WARN_ON(1); |
| goto out; |
| } |
| |
| #if IS_ENABLED(CONFIG_MTK_AEE_FEATURE) |
| if (drv->enable_aee) { |
| aee_kernel_exception("APUSYS_DEVAPC", |
| "Violation Slave: %s (%s%s): transaction ID:0x%x, Addr:0x%x, HighAddr: %x, Domain: 0x%x\n", |
| slv_name, |
| (ex->read_vio) ? "R" : "", |
| (ex->write_vio) ? " W" : "", |
| ex->trans_id, |
| ex->addr, |
| ex->addr_high, |
| ex->domain_id); |
| } |
| #endif |
| |
| out: |
| /* unmask irq for slv "i" */ |
| slv_irq(i, true); |
| } |
| |
| static uint32_t check_vio_status(unsigned int slv) |
| { |
| uint32_t reg; |
| uint32_t mask; |
| struct dapc_driver *d = dapc_drv; |
| struct dapc_config *cfg = d->cfg; |
| |
| if (slv > cfg->slv_cnt) { |
| pr_info("%s: slv: %d is out of index, max: %d\n", |
| __func__, slv, cfg->slv_cnt); |
| return -EINVAL; |
| } |
| |
| reg = cfg->vio_sta(slv / cfg->slv_per_dapc); |
| mask = 0x1 << (slv % cfg->slv_per_dapc); |
| |
| return dapc_reg_r(d, reg) & mask; |
| } |
| |
| static void clear_vio_status(unsigned int slv) |
| { |
| uint32_t reg; |
| uint32_t mask; |
| struct dapc_driver *d = dapc_drv; |
| struct dapc_config *cfg = d->cfg; |
| |
| if (slv > cfg->slv_cnt) { |
| pr_info("%s: slv: %d is out of index, max: %d\n", |
| __func__, slv, cfg->slv_cnt); |
| return; |
| } |
| |
| reg = cfg->vio_sta(slv / cfg->slv_per_dapc); |
| mask = 0x1 << (slv % cfg->slv_per_dapc); |
| |
| dapc_reg_w(d, reg, mask); |
| } |
| |
| static int shift_vio_dbg(int shift_bit) |
| { |
| int count = 0; |
| struct dapc_driver *d = dapc_drv; |
| struct dapc_config *cfg = d->cfg; |
| uint32_t sel = cfg->vio_shift_sel; |
| uint32_t con = cfg->vio_shift_con; |
| uint32_t mask = cfg->vio_shift_con_mask; |
| |
| /* start shift */ |
| dapc_reg_w(d, sel, (0x1 << shift_bit)); |
| dapc_reg_w(d, con, 0x1); |
| |
| /* wait for shift done */ |
| while (((dapc_reg_r(d, con) & mask) != mask) && (count < 100)) |
| count++; |
| |
| pr_debug("%s: shift done %d, %d, SHIFT_SEL: 0x%x, SHIFT_CON=0x%x\n", |
| __func__, shift_bit, count, |
| dapc_reg_r(d, sel), dapc_reg_r(d, con)); |
| |
| if ((dapc_reg_r(d, con) & mask) != mask) { |
| pr_info("%s: shift bit %d failed\n", __func__, shift_bit); |
| return -EFAULT; |
| } |
| |
| /* disable shift mechanism */ |
| dapc_reg_w(d, con, 0); |
| dapc_reg_w(d, sel, 0); |
| /* SHIFT_STA must be write-cleared before clear VIO_STA */ |
| dapc_reg_w(d, cfg->vio_shift_sta, (0x1 << shift_bit)); |
| |
| return 0; |
| } |
| |
| static void apusys_devapc_dbg(const char *prefix) |
| { |
| struct dapc_driver *d = dapc_drv; |
| struct dapc_config *cfg = d->cfg; |
| |
| if (!dapc_drv->debug_log) |
| return; |
| |
| pr_info("%s: %s: VIO_MASK0~3: 0x%x, 0x%x, 0x%x, 0x%x, VIO_STA0~3: 0x%x, 0x%x, 0x%x, 0x%x\n", |
| __func__, |
| prefix, |
| dapc_reg_r(d, cfg->vio_mask(0)), |
| dapc_reg_r(d, cfg->vio_mask(1)), |
| dapc_reg_r(d, cfg->vio_mask(2)), |
| dapc_reg_r(d, cfg->vio_mask(3)), |
| dapc_reg_r(d, cfg->vio_sta(0)), |
| dapc_reg_r(d, cfg->vio_sta(1)), |
| dapc_reg_r(d, cfg->vio_sta(2)), |
| dapc_reg_r(d, cfg->vio_sta(3))); |
| } |
| |
| /* Return 1 means there are some violations */ |
| static inline int is_violation_irq(void) |
| { |
| struct dapc_driver *d = dapc_drv; |
| struct dapc_config *cfg = d->cfg; |
| |
| return (dapc_reg_r(d, cfg->vio_sta(0)) || |
| dapc_reg_r(d, cfg->vio_sta(1)) || |
| dapc_reg_r(d, cfg->vio_sta(2)) || |
| dapc_reg_r(d, cfg->vio_sta(3))); |
| } |
| |
| static irqreturn_t apusys_devapc_isr(int irq_number, void *data) |
| { |
| int i; |
| struct dapc_driver *d = (struct dapc_driver *)data; |
| struct dapc_config *cfg; |
| unsigned int shift_max; |
| struct dapc_slave *slv; |
| struct dapc_exception ex; |
| |
| if (!is_violation_irq()) |
| return IRQ_NONE; |
| |
| if (!d || IS_ERR_OR_NULL(d->reg)) { |
| pr_info("%s: driver abort\n", __func__); |
| return IRQ_NONE; |
| } |
| |
| cfg = d->cfg; |
| slv = cfg->slv; |
| shift_max = cfg->vio_shift_max_bit; |
| |
| if (irq_number != d->irq) { |
| pr_info("%s: get unknown irq %d\n", __func__, irq_number); |
| return IRQ_NONE; |
| } |
| |
| if (!d->irq) { |
| pr_info("%s: Disable vio irq handler\n", __func__); |
| return IRQ_NONE; |
| } |
| |
| memset(&ex, 0, sizeof(struct dapc_exception)); |
| disable_irq_nosync(irq_number); |
| apusys_devapc_dbg("ISR begin"); |
| |
| for (i = 0; i <= shift_max; ++i) { |
| if (!(dapc_reg_r(d, cfg->vio_shift_sta) & (0x1 << i))) |
| continue; |
| if (shift_vio_dbg(i)) |
| continue; |
| |
| pr_info("%s: VIO_SHIFT_STA=0x%x, VIO_SHIFT_SEL=0x%x, VIO_SHIFT_CON=0x%x,\n", |
| __func__, |
| dapc_reg_r(d, cfg->vio_shift_sta), |
| dapc_reg_r(d, cfg->vio_shift_sel), |
| dapc_reg_r(d, cfg->vio_shift_con)); |
| |
| cfg->excp_info(d, &ex); |
| |
| /* violation information */ |
| pr_info("%s: Violation(%s%s): transaction ID:0x%x, Addr:0x%x, HighAddr: %x, Domain: 0x%x\n", |
| __func__, |
| (ex.read_vio) ? "R" : "", |
| (ex.write_vio) ? " W" : "", |
| ex.trans_id, |
| ex.addr, |
| ex.addr_high, |
| ex.domain_id); |
| } |
| |
| /* Clear VIO_STA */ |
| for_each_dapc_slv(cfg, i) { |
| if (!(slv[i].vio_irq_en && check_vio_status(i))) |
| continue; |
| |
| clear_vio_status(i); |
| pr_info("%s: vio_sta device: %d, slave: %s\n", |
| __func__, i, slv[i].name); |
| do_kernel_exception(d, i, slv[i].name, &ex); |
| } |
| |
| apusys_devapc_dbg("ISR end"); |
| enable_irq(irq_number); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static void apusys_devapc_start(void *data) |
| { |
| int i = 0; |
| struct dapc_driver *d = dapc_drv; |
| struct dapc_config *cfg = d->cfg; |
| struct dapc_slave *slv = cfg->slv; |
| |
| /* Clear APC violation status */ |
| dapc_reg_w(d, cfg->apc_con, cfg->apc_con_vio); |
| apusys_devapc_dbg("Init begin"); |
| |
| /* Enable violation IRQ */ |
| for_each_dapc_slv(cfg, i) { |
| if (!slv[i].vio_irq_en) |
| continue; |
| |
| clear_vio_status(i); |
| slv_irq(i, true); |
| } |
| apusys_devapc_dbg("Init end"); |
| } |
| |
| static int apusys_devapc_probe(struct platform_device *pdev) |
| { |
| int ret = 0; |
| struct device_node *devapc_node; |
| struct dapc_driver *drv = dapc_drv; |
| struct dapc_config *cfg; |
| |
| dev_info(&pdev->dev, "%s\n", __func__); |
| if (!apusys_power_check()) |
| return 0; |
| |
| devapc_node = pdev->dev.of_node; |
| if (!devapc_node) { |
| dev_info(&pdev->dev, "of_node required\n"); |
| return -ENODEV; |
| } |
| |
| if (drv->cfg) { |
| dev_info(&pdev->dev, "duplicated device node\n"); |
| return -ENODEV; |
| } |
| |
| cfg = (struct dapc_config *)of_device_get_match_data(&pdev->dev); |
| if (!cfg) { |
| dev_info(&pdev->dev, "unsupported device: %s\n", pdev->name); |
| return -ENODEV; |
| } |
| |
| drv->cfg = cfg; |
| drv->reg = of_iomap(devapc_node, 0); |
| if (!drv->reg) { |
| ret = -ENOMEM; |
| goto err_iomap; |
| } |
| |
| drv->irq = irq_of_parse_and_map(devapc_node, 0); |
| |
| ret = request_irq(drv->irq, apusys_devapc_isr, |
| irq_get_trigger_type(drv->irq) | IRQF_SHARED, |
| "apusys-devapc", drv); |
| if (ret) { |
| ret = -EFAULT; |
| goto err_request_irq; |
| } |
| |
| drv->enable_ke = drv->enable_aee = 1; |
| |
| if (drv->cfg->irq_enable) { |
| drv->enable_irq = 1; |
| } else { |
| drv->enable_irq = 0; |
| disable_irq(drv->irq); |
| } |
| |
| ret = apu_power_callback_device_register(DEVAPC, |
| apusys_devapc_start, NULL); |
| if (ret) { |
| dev_info(&pdev->dev, |
| "unable to register to apu power: %d\n", ret); |
| goto err_power; |
| } |
| drv->debug_log = 0; |
| apusys_devapc_debug_init(drv); |
| platform_set_drvdata(pdev, drv); |
| dapc_drv_get(); |
| |
| return 0; |
| err_power: |
| free_irq(drv->irq, drv); |
| err_request_irq: |
| iounmap(drv->reg); |
| err_iomap: |
| drv->cfg = NULL; |
| return ret; |
| } |
| |
| static int apusys_devapc_remove(struct platform_device *pdev) |
| { |
| struct dapc_driver *drv = platform_get_drvdata(pdev); |
| |
| dev_info(&pdev->dev, "%s\n", __func__); |
| free_irq(drv->irq, drv); |
| iounmap(drv->reg); |
| apusys_devapc_debug_exit(drv); |
| apu_power_callback_device_unregister(DEVAPC); |
| dapc_drv_put(); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id apusys_devapc_of_match[] = { |
| { .compatible = "mediatek,mt6885-apusys_devapc", &dapc_cfg_mt6885 }, |
| { .compatible = "mediatek,mt6873-apusys_devapc", &dapc_cfg_mt6873 }, |
| { .compatible = "mediatek,mt6853-apusys_devapc", &dapc_cfg_mt6853 }, |
| { .compatible = "mediatek,mt6893-apusys_devapc", &dapc_cfg_mt6885 }, |
| {}, |
| }; |
| |
| MODULE_DEVICE_TABLE(of, apusys_devapc_of_match); |
| |
| static struct platform_driver apusys_devapc_driver = { |
| .probe = apusys_devapc_probe, |
| .remove = apusys_devapc_remove, |
| .driver = { |
| .name = "apusys-devapc", |
| .owner = THIS_MODULE, |
| .of_match_table = apusys_devapc_of_match, |
| }, |
| }; |
| |
| int devapc_init(struct apusys_core_info *info) |
| { |
| dapc_drv = kzalloc(sizeof(struct dapc_driver), GFP_KERNEL); |
| if (!dapc_drv) |
| return -ENOMEM; |
| dapc_drv->droot = info->dbg_root; |
| kref_init(&dapc_drv->ref); |
| return platform_driver_register(&apusys_devapc_driver); |
| } |
| |
| void devapc_exit(void) |
| { |
| platform_driver_unregister(&apusys_devapc_driver); |
| dapc_drv_put(); |
| } |
| |