| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Cortex A72 EDAC L1 and L2 cache error detection | |
| * | |
| * Copyright (c) 2020 Pengutronix, Sascha Hauer <s.hauer@pengutronix.de> | |
| * Copyright (c) 2025 Microsoft Corporation, <vijayb@linux.microsoft.com> | |
| * | |
| * Based on Code from: | |
| * Copyright (c) 2018, NXP Semiconductor | |
| * Author: York Sun <york.sun@nxp.com> | |
| */ | |
| #include <linux/module.h> | |
| #include <linux/of.h> | |
| #include <linux/bitfield.h> | |
| #include <asm/smp_plat.h> | |
| #include "edac_module.h" | |
| #define DRVNAME "a72-edac" | |
| #define SYS_CPUMERRSR_EL1 sys_reg(3, 1, 15, 2, 2) | |
| #define SYS_L2MERRSR_EL1 sys_reg(3, 1, 15, 2, 3) | |
| #define CPUMERRSR_EL1_RAMID GENMASK(30, 24) | |
| #define L2MERRSR_EL1_CPUID_WAY GENMASK(21, 18) | |
| #define CPUMERRSR_EL1_VALID BIT(31) | |
| #define CPUMERRSR_EL1_FATAL BIT(63) | |
| #define L2MERRSR_EL1_VALID BIT(31) | |
| #define L2MERRSR_EL1_FATAL BIT(63) | |
| #define L1_I_TAG_RAM 0x00 | |
| #define L1_I_DATA_RAM 0x01 | |
| #define L1_D_TAG_RAM 0x08 | |
| #define L1_D_DATA_RAM 0x09 | |
| #define TLB_RAM 0x18 | |
| #define MESSAGE_SIZE 64 | |
| struct mem_err_synd_reg { | |
| u64 cpu_mesr; | |
| u64 l2_mesr; | |
| }; | |
| static struct cpumask compat_mask; | |
| static void report_errors(struct edac_device_ctl_info *edac_ctl, int cpu, | |
| struct mem_err_synd_reg *mesr) | |
| { | |
| u64 cpu_mesr = mesr->cpu_mesr; | |
| u64 l2_mesr = mesr->l2_mesr; | |
| char msg[MESSAGE_SIZE]; | |
| if (cpu_mesr & CPUMERRSR_EL1_VALID) { | |
| const char *str; | |
| bool fatal = cpu_mesr & CPUMERRSR_EL1_FATAL; | |
| switch (FIELD_GET(CPUMERRSR_EL1_RAMID, cpu_mesr)) { | |
| case L1_I_TAG_RAM: | |
| str = "L1-I Tag RAM"; | |
| break; | |
| case L1_I_DATA_RAM: | |
| str = "L1-I Data RAM"; | |
| break; | |
| case L1_D_TAG_RAM: | |
| str = "L1-D Tag RAM"; | |
| break; | |
| case L1_D_DATA_RAM: | |
| str = "L1-D Data RAM"; | |
| break; | |
| case TLB_RAM: | |
| str = "TLB RAM"; | |
| break; | |
| default: | |
| str = "Unspecified"; | |
| break; | |
| } | |
| snprintf(msg, MESSAGE_SIZE, "%s %s error(s) on CPU %d", | |
| str, fatal ? "fatal" : "correctable", cpu); | |
| if (fatal) | |
| edac_device_handle_ue(edac_ctl, cpu, 0, msg); | |
| else | |
| edac_device_handle_ce(edac_ctl, cpu, 0, msg); | |
| } | |
| if (l2_mesr & L2MERRSR_EL1_VALID) { | |
| bool fatal = l2_mesr & L2MERRSR_EL1_FATAL; | |
| snprintf(msg, MESSAGE_SIZE, "L2 %s error(s) on CPU %d CPUID/WAY 0x%lx", | |
| fatal ? "fatal" : "correctable", cpu, | |
| FIELD_GET(L2MERRSR_EL1_CPUID_WAY, l2_mesr)); | |
| if (fatal) | |
| edac_device_handle_ue(edac_ctl, cpu, 1, msg); | |
| else | |
| edac_device_handle_ce(edac_ctl, cpu, 1, msg); | |
| } | |
| } | |
| static void read_errors(void *data) | |
| { | |
| struct mem_err_synd_reg *mesr = data; | |
| mesr->cpu_mesr = read_sysreg_s(SYS_CPUMERRSR_EL1); | |
| if (mesr->cpu_mesr & CPUMERRSR_EL1_VALID) { | |
| write_sysreg_s(0, SYS_CPUMERRSR_EL1); | |
| isb(); | |
| } | |
| mesr->l2_mesr = read_sysreg_s(SYS_L2MERRSR_EL1); | |
| if (mesr->l2_mesr & L2MERRSR_EL1_VALID) { | |
| write_sysreg_s(0, SYS_L2MERRSR_EL1); | |
| isb(); | |
| } | |
| } | |
| static void a72_edac_check(struct edac_device_ctl_info *edac_ctl) | |
| { | |
| struct mem_err_synd_reg mesr; | |
| int cpu; | |
| cpus_read_lock(); | |
| for_each_cpu_and(cpu, cpu_online_mask, &compat_mask) { | |
| smp_call_function_single(cpu, read_errors, &mesr, true); | |
| report_errors(edac_ctl, cpu, &mesr); | |
| } | |
| cpus_read_unlock(); | |
| } | |
| static int a72_edac_probe(struct platform_device *pdev) | |
| { | |
| struct edac_device_ctl_info *edac_ctl; | |
| struct device *dev = &pdev->dev; | |
| int rc; | |
| edac_ctl = edac_device_alloc_ctl_info(0, "cpu", | |
| num_possible_cpus(), "L", 2, 1, | |
| edac_device_alloc_index()); | |
| if (!edac_ctl) | |
| return -ENOMEM; | |
| edac_ctl->edac_check = a72_edac_check; | |
| edac_ctl->dev = dev; | |
| edac_ctl->mod_name = dev_name(dev); | |
| edac_ctl->dev_name = dev_name(dev); | |
| edac_ctl->ctl_name = DRVNAME; | |
| dev_set_drvdata(dev, edac_ctl); | |
| rc = edac_device_add_device(edac_ctl); | |
| if (rc) | |
| goto out_dev; | |
| return 0; | |
| out_dev: | |
| edac_device_free_ctl_info(edac_ctl); | |
| return rc; | |
| } | |
| static void a72_edac_remove(struct platform_device *pdev) | |
| { | |
| struct edac_device_ctl_info *edac_ctl = dev_get_drvdata(&pdev->dev); | |
| edac_device_del_device(edac_ctl->dev); | |
| edac_device_free_ctl_info(edac_ctl); | |
| } | |
| static const struct of_device_id cortex_arm64_edac_of_match[] = { | |
| { .compatible = "arm,cortex-a72" }, | |
| {} | |
| }; | |
| MODULE_DEVICE_TABLE(of, cortex_arm64_edac_of_match); | |
| static struct platform_driver a72_edac_driver = { | |
| .probe = a72_edac_probe, | |
| .remove = a72_edac_remove, | |
| .driver = { | |
| .name = DRVNAME, | |
| }, | |
| }; | |
| static struct platform_device *a72_pdev; | |
| static int __init a72_edac_driver_init(void) | |
| { | |
| int cpu; | |
| for_each_possible_cpu(cpu) { | |
| struct device_node *np __free(device_node) = of_cpu_device_node_get(cpu); | |
| if (np) { | |
| if (of_match_node(cortex_arm64_edac_of_match, np) && | |
| of_property_read_bool(np, "edac-enabled")) { | |
| cpumask_set_cpu(cpu, &compat_mask); | |
| } | |
| } else { | |
| pr_warn("failed to find device node for CPU %d\n", cpu); | |
| } | |
| } | |
| if (cpumask_empty(&compat_mask)) | |
| return 0; | |
| a72_pdev = platform_device_register_simple(DRVNAME, -1, NULL, 0); | |
| if (IS_ERR(a72_pdev)) { | |
| pr_err("failed to register A72 EDAC device\n"); | |
| return PTR_ERR(a72_pdev); | |
| } | |
| return platform_driver_register(&a72_edac_driver); | |
| } | |
| static void __exit a72_edac_driver_exit(void) | |
| { | |
| platform_device_unregister(a72_pdev); | |
| platform_driver_unregister(&a72_edac_driver); | |
| } | |
| module_init(a72_edac_driver_init); | |
| module_exit(a72_edac_driver_exit); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_AUTHOR("Sascha Hauer <s.hauer@pengutronix.de>"); | |
| MODULE_DESCRIPTION("Cortex A72 L1 and L2 cache EDAC driver"); |