blob: d16b31f322823e272d82b9c9c8816b99ad704949 [file] [log] [blame]
/*
* Copyright (c) 2018 Samsung Electronics Co., Ltd.
* http://www.samsung.com/
*
* EXYNOS - Per-Page Memory Protection Unit(PPMPU) fail detector
* Author: Junho Choi <junhosj.choi@samsung.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/device.h>
#include <linux/dma-mapping.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/irqreturn.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/debugfs.h>
#include <linux/smc.h>
#include <soc/samsung/exynos-ppmpu.h>
static irqreturn_t exynos_ppmpu_irq_handler(int irq, void *dev_id)
{
struct ppmpu_info_data *data = dev_id;
uint32_t dir = 0;
uint32_t irq_idx;
/*
* IRQ_Index PPMPU_INTR
* 0 PPMPU0_R
* 1 PPMPU0_W
* 2 PPMPU1_R
* 3 PPMPU1_W
* 4 PPMPU2_R
* 5 PPMPU2_W
* 6 PPMPU3_R
* 7 PPMPU3_W
*/
for (irq_idx = 0; irq_idx < data->irqcnt; irq_idx++) {
if (irq == data->irq[irq_idx])
break;
}
#ifdef CONFIG_EXYNOS_PPMPU_SUPPORT_SMC
if (irq_idx % 2)
dir = PPMPU_ILLEGAL_ACCESS_WRITE;
else
dir = PPMPU_ILLEGAL_ACCESS_READ;
#endif
/*
* Interrupt status register in PPMPU will clear
* in this SMC handler
*/
data->need_log = exynos_smc(SMC_CMD_GET_PPMPU_FAIL_INFO,
data->fail_info_pa,
(dir << PPMPU_DIRECTION_SHIFT) |
(irq_idx / 2),
data->info_flag);
if ((data->need_log != PPMPU_NEED_FAIL_INFO_LOGGING) &&
(data->need_log != PPMPU_SKIP_FAIL_INFO_LOGGING))
pr_err("%s:ppmpu_fail_info buffer is invalid! ret(%#x)\n",
__func__, data->need_log);
return IRQ_WAKE_THREAD;
}
static irqreturn_t exynos_ppmpu_irq_handler_thread(int irq, void *dev_id)
{
struct ppmpu_info_data *data = dev_id;
unsigned int intr_stat, addr_low;
unsigned long addr_high;
int i;
if (data->need_log == PPMPU_SKIP_FAIL_INFO_LOGGING) {
pr_debug("PPMPU_FAIL_DETECTOR: Ignore PPMPU illegal reads\n");
return IRQ_HANDLED;
}
pr_info("===============[PPMPU FAIL DETECTION]===============\n");
/* Parse fail register information */
for (i = 0; i < data->ch_num; i++) {
pr_info("[Channel %d]\n", i);
intr_stat = data->fail_info[i].ppmpu_intr_stat;
if (!(intr_stat &
(PPMPU_WRITE_INTR_STATUS |
PPMPU_READ_INTR_STATUS))) {
pr_info("NO access failure in this Channel\n\n");
continue;
}
if (intr_stat & PPMPU_READ_INTR_STATUS) {
addr_low = data->fail_info[i].ppmpu_illegal_read_addr_low;
addr_high = data->fail_info[i].ppmpu_illegal_read_addr_high &
PPMPU_ILLEGAL_ADDR_HIGH_MASK;
pr_info("- [READ] Illegal Adddress : %#lx\n",
addr_high ?
(addr_high << 32) | addr_low :
addr_low);
pr_info("- [READ] Fail VC : %#x\n",
data->fail_info[i].ppmpu_illigal_read_field &
PPMPU_ILLEGAL_FIELD_FAIL_VC_MASK);
pr_info("- [READ] Fail ID : %#x\n",
data->fail_info[i].ppmpu_illigal_read_field &
PPMPU_ILLEGAL_FIELD_FAIL_ID_MASK);
pr_info("- [READ] Interrupt Status : %s\n",
(data->fail_info[i].ppmpu_intr_stat &
PPMPU_READ_INTR_STATUS) ?
"Interrupt is asserted" :
"Interrupt is not asserted");
pr_info("- [READ] Interrupt Overrun : %s\n",
(data->fail_info[i].ppmpu_intr_stat &
PPMPU_READ_INTR_STATUS_OVERRUN) ?
"Two or more failures occurred" :
"Only one failure occurred");
pr_info("\n");
}
if (intr_stat & PPMPU_WRITE_INTR_STATUS) {
addr_low = data->fail_info[i].ppmpu_illegal_write_addr_low;
addr_high = data->fail_info[i].ppmpu_illegal_write_addr_high &
PPMPU_ILLEGAL_ADDR_HIGH_MASK;
pr_info("- [WRITE] Illegal Adddress : %#lx\n",
addr_high ?
(addr_high << 32) | addr_low :
addr_low);
pr_info("- [WRITE] Fail VC : %#x\n",
data->fail_info[i].ppmpu_illigal_write_field &
PPMPU_ILLEGAL_FIELD_FAIL_VC_MASK);
pr_info("- [WRITE] Fail ID : %#x\n",
data->fail_info[i].ppmpu_illigal_write_field &
PPMPU_ILLEGAL_FIELD_FAIL_ID_MASK);
pr_info("- [WRITE] Interrupt Status : %s\n",
(data->fail_info[i].ppmpu_intr_stat &
PPMPU_WRITE_INTR_STATUS) ?
"Interrupt is asserted" :
"Interrupt is not asserted");
pr_info("- [WRITE] Interrupt Overrun : %s\n",
(data->fail_info[i].ppmpu_intr_stat &
PPMPU_WRITE_INTR_STATUS_OVERRUN) ?
"Two or more failures occurred" :
"Only one failure occurred");
pr_info("\n");
}
pr_info("- SFR values\n");
pr_info("INT_STATUS : %#x\n",
data->fail_info[i].ppmpu_intr_stat);
pr_info("ILLEGAL_READ_ADDRESS_LOW : %#x\n",
data->fail_info[i].ppmpu_illegal_read_addr_low);
pr_info("ILLEGAL_READ_ADDRESS_HIGH : %#x\n",
data->fail_info[i].ppmpu_illegal_read_addr_high);
pr_info("ILLEGAL_READ_FIELD : %#x\n",
data->fail_info[i].ppmpu_illigal_read_field);
pr_info("ILLEGAL_WRITE_ADDRESS_LOW : %#x\n",
data->fail_info[i].ppmpu_illegal_write_addr_low);
pr_info("ILLEGAL_WRITE_ADDRESS_HIGH : %#x\n",
data->fail_info[i].ppmpu_illegal_write_addr_high);
pr_info("ILLEGAL_WRITE_FIELD : %#x\n",
data->fail_info[i].ppmpu_illigal_write_field);
pr_info("\n");
}
pr_info("====================================================\n");
#ifdef CONFIG_EXYNOS_PPMPU_ILLEGAL_ACCESS_PANIC
BUG();
#endif
return IRQ_HANDLED;
}
static int exynos_ppmpu_probe(struct platform_device *pdev)
{
struct ppmpu_info_data *data;
int ret, i;
data = devm_kzalloc(&pdev->dev, sizeof(struct ppmpu_info_data), GFP_KERNEL);
if (!data) {
dev_err(&pdev->dev, "Fail to allocate memory(ppmpu_info_data)\n");
ret = -ENOMEM;
goto out;
}
platform_set_drvdata(pdev, data);
data->dev = &pdev->dev;
ret = of_property_read_u32(data->dev->of_node, "channel", &data->ch_num);
if (ret) {
dev_err(data->dev,
"Fail to get PPMPU channel number(%d) from dt\n",
data->ch_num);
goto out;
}
ret = exynos_smc(SMC_CMD_CHECK_PPMPU_CH_NUM,
data->ch_num,
sizeof(struct ppmpu_fail_info),
0);
if (ret) {
switch (ret) {
case PPMPU_ERROR_INVALID_CH_NUM:
dev_err(data->dev,
"The channel number(%d) defined in DT is invalid\n",
data->ch_num);
break;
case PPMPU_ERROR_INVALID_FAIL_INFO_SIZE:
dev_err(data->dev,
"The size of struct ppmpu_fail_info(%#x) is invalid\n",
sizeof(struct ppmpu_fail_info));
break;
case SMC_CMD_CHECK_PPMPU_CH_NUM:
dev_err(data->dev,
"DO NOT support this SMC(%#x)\n",
SMC_CMD_CHECK_PPMPU_CH_NUM);
break;
default:
dev_err(data->dev,
"Unknown error from SMC. ret[%#x]\n",
ret);
break;
}
ret = -EINVAL;
goto out;
}
dev_dbg(data->dev,
"PPMPU channel number : %d\n",
data->ch_num);
dev_info(data->dev,
"PPMPU channel number is valid\n");
/*
* Allocate PPMPU fail information buffers as the channel number
*
* DRM LDFW has mapped Kernel region with non-cacheable,
* so Kernel allocates it by dma_alloc_coherent
*/
data->fail_info = dma_alloc_coherent(data->dev,
sizeof(struct ppmpu_fail_info) *
data->ch_num,
&data->fail_info_pa,
GFP_KERNEL);
if (!data->fail_info) {
dev_err(data->dev, "Fail to allocate memory(ppmpu_fail_info)\n");
ret = -ENOMEM;
goto out;
}
dev_dbg(data->dev,
"VA of ppmpu_fail_info : %lx\n",
(unsigned long)data->fail_info);
dev_dbg(data->dev,
"PA of ppmpu_fail_info : %lx\n",
data->fail_info_pa);
ret = of_property_read_u32(data->dev->of_node, "irqcnt", &data->irqcnt);
if (ret) {
dev_err(data->dev,
"Fail to get irqcnt(%d) from dt\n",
data->irqcnt);
goto out_with_dma_free;
}
dev_dbg(data->dev,
"The number of PPMPU interrupt : %d\n",
data->irqcnt);
for (i = 0; i < data->irqcnt; i++) {
data->irq[i] = irq_of_parse_and_map(data->dev->of_node, i);
if (!data->irq[i]) {
dev_err(data->dev,
"Fail to get irq(%d) from dt\n",
data->irq[i]);
ret = -EINVAL;
goto out_with_dma_free;
}
ret = devm_request_threaded_irq(data->dev,
data->irq[i],
exynos_ppmpu_irq_handler,
exynos_ppmpu_irq_handler_thread,
IRQF_ONESHOT,
pdev->name,
data);
if (ret) {
dev_err(data->dev,
"Fail to request IRQ handler. ret(%d) irq(%d)\n",
ret, data->irq[i]);
goto out_with_dma_free;
}
}
#ifdef CONFIG_EXYNOS_PPMPU_ILLEGAL_READ_LOGGING
data->info_flag = STR_INFO_FLAG;
#endif
dev_info(data->dev, "Exynos PPMPU driver probe done!\n");
return 0;
out_with_dma_free:
platform_set_drvdata(pdev, NULL);
dma_free_coherent(data->dev,
sizeof(struct ppmpu_fail_info) *
data->ch_num,
data->fail_info,
data->fail_info_pa);
data->fail_info = NULL;
data->fail_info_pa = 0;
out:
return ret;
}
static int exynos_ppmpu_remove(struct platform_device *pdev)
{
struct ppmpu_info_data *data = platform_get_drvdata(pdev);
int i;
platform_set_drvdata(pdev, NULL);
if (data->fail_info) {
dma_free_coherent(data->dev,
sizeof(struct ppmpu_fail_info) *
data->ch_num,
data->fail_info,
data->fail_info_pa);
data->fail_info = NULL;
data->fail_info_pa = 0;
}
for (i = 0; i < data->ch_num; i++)
data->irq[i] = 0;
data->ch_num = 0;
data->irqcnt = 0;
data->info_flag = 0;
return 0;
}
static const struct of_device_id exynos_ppmpu_of_match_table[] = {
{ .compatible = "samsung,exynos-ppmpu", },
{ },
};
static struct platform_driver exynos_ppmpu_driver = {
.probe = exynos_ppmpu_probe,
.remove = exynos_ppmpu_remove,
.driver = {
.name = "exynos-ppmpu",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(exynos_ppmpu_of_match_table),
}
};
static int __init exynos_ppmpu_init(void)
{
return platform_driver_register(&exynos_ppmpu_driver);
}
static void __exit exynos_ppmpu_exit(void)
{
platform_driver_unregister(&exynos_ppmpu_driver);
}
module_init(exynos_ppmpu_init);
module_exit(exynos_ppmpu_exit);
MODULE_DESCRIPTION("Exynos Per-Page Memory Protection Unit(PPMPU) driver");
MODULE_AUTHOR("<junhosj.choi@samsung.com>");
MODULE_LICENSE("GPL");