| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (c) 2014-2018, The Linux Foundation. All rights reserved. |
| */ |
| |
| #define pr_fmt(fmt) "%s:%s " fmt, KBUILD_MODNAME, __func__ |
| |
| #include <linux/interrupt.h> |
| #include <linux/err.h> |
| #include <linux/mutex.h> |
| #include <linux/slab.h> |
| #include <asm/cacheflush.h> |
| #include <soc/qcom/scm.h> |
| #include <linux/dma-mapping.h> |
| #include <linux/uaccess.h> |
| #include <linux/debugfs.h> |
| |
| #include "lmh_dbg.h" |
| |
| #define LMH_MON_NAME "lmh_monitor" |
| #define LMH_DBGFS_READ "data" |
| #define LMH_DBGFS_CONFIG_READ "config" |
| #define LMH_DBGFS_READ_TYPES "data_types" |
| #define LMH_DBGFS_CONFIG_TYPES "config_types" |
| #define LMH_SCM_PAYLOAD_SIZE 10 |
| #define LMH_READ_LINE_LENGTH 10 |
| #define LMH_DEBUG_READ_TYPE 0x0 |
| #define LMH_DEBUG_CONFIG_TYPE 0x1 |
| #define LMH_DEBUG_SET 0x08 |
| #define LMH_DEBUG_READ_BUF_SIZE 0x09 |
| #define LMH_DEBUG_READ 0x0A |
| #define LMH_DEBUG_GET_TYPE 0x0B |
| |
| struct lmh_driver_data { |
| struct device *dev; |
| uint32_t *read_type; |
| uint32_t *config_type; |
| uint32_t read_type_count; |
| uint32_t config_type_count; |
| struct dentry *debugfs_parent; |
| struct dentry *debug_read; |
| struct dentry *debug_config; |
| struct dentry *debug_read_type; |
| struct dentry *debug_config_type; |
| }; |
| |
| enum lmh_read_type { |
| LMH_READ_TYPE = 0, |
| LMH_CONFIG_TYPE, |
| }; |
| |
| static struct lmh_driver_data *lmh_data; |
| |
| static int lmh_debug_read(uint32_t **buf) |
| { |
| int ret = 0, size = 0, tz_ret = 0; |
| static uint32_t curr_size; |
| struct scm_desc desc_arg; |
| static uint32_t *payload; |
| |
| desc_arg.arginfo = SCM_ARGS(0); |
| ret = scm_call2(SCM_SIP_FNID(SCM_SVC_LMH, |
| LMH_DEBUG_READ_BUF_SIZE), &desc_arg); |
| size = desc_arg.ret[0]; |
| if (ret) { |
| pr_err("Error in SCM get debug buffer size call. err:%d\n", |
| ret); |
| goto get_dbg_exit; |
| } |
| if (!size) { |
| pr_err("No Debug data to read\n"); |
| ret = -ENODEV; |
| goto get_dbg_exit; |
| } |
| size = SCM_BUFFER_SIZE(uint32_t) * size * LMH_READ_LINE_LENGTH; |
| if (curr_size != size) { |
| if (payload) |
| devm_kfree(lmh_data->dev, payload); |
| payload = devm_kzalloc(lmh_data->dev, PAGE_ALIGN(size), |
| GFP_KERNEL); |
| if (!payload) { |
| ret = -ENOMEM; |
| goto get_dbg_exit; |
| } |
| curr_size = size; |
| } |
| |
| /* &payload may be a physical address > 4 GB */ |
| desc_arg.args[0] = SCM_BUFFER_PHYS(payload); |
| desc_arg.args[1] = curr_size; |
| desc_arg.arginfo = SCM_ARGS(2, SCM_RW, SCM_VAL); |
| dmac_flush_range(payload, (void *)payload + curr_size); |
| ret = scm_call2(SCM_SIP_FNID(SCM_SVC_LMH, LMH_DEBUG_READ), |
| &desc_arg); |
| dmac_inv_range(payload, (void *)payload + curr_size); |
| tz_ret = desc_arg.ret[0]; |
| /* Have memory barrier before we access the TZ data */ |
| mb(); |
| if (ret) { |
| pr_err("Error in get debug read. err:%d\n", ret); |
| goto get_dbg_exit; |
| } |
| if (tz_ret) { |
| pr_err("TZ API returned error. err:%d\n", tz_ret); |
| ret = tz_ret; |
| goto get_dbg_exit; |
| } |
| |
| get_dbg_exit: |
| if (ret && payload) { |
| devm_kfree(lmh_data->dev, payload); |
| payload = NULL; |
| curr_size = 0; |
| } |
| *buf = payload; |
| |
| return (ret < 0) ? ret : curr_size; |
| } |
| |
| static int lmh_debug_config_write(uint32_t cmd_id, uint32_t *buf, int size) |
| { |
| int ret = 0, size_bytes = 0; |
| struct scm_desc desc_arg; |
| uint32_t *payload = NULL; |
| |
| size_bytes = (size - 3) * sizeof(uint32_t); |
| payload = devm_kzalloc(lmh_data->dev, PAGE_ALIGN(size_bytes), |
| GFP_KERNEL); |
| if (!payload) { |
| ret = -ENOMEM; |
| goto set_cfg_exit; |
| } |
| memcpy(payload, &buf[3], size_bytes); |
| |
| /* &payload may be a physical address > 4 GB */ |
| desc_arg.args[0] = SCM_BUFFER_PHYS(payload); |
| desc_arg.args[1] = size_bytes; |
| desc_arg.args[2] = buf[0]; |
| desc_arg.args[3] = buf[1]; |
| desc_arg.args[4] = buf[2]; |
| desc_arg.arginfo = SCM_ARGS(5, SCM_RO, SCM_VAL, SCM_VAL, SCM_VAL, |
| SCM_VAL); |
| dmac_flush_range(payload, (void *)payload + size_bytes); |
| ret = scm_call2(SCM_SIP_FNID(SCM_SVC_LMH, cmd_id), &desc_arg); |
| /* Have memory barrier before we access the TZ data */ |
| mb(); |
| if (ret) { |
| pr_err("Error in config debug read. err:%d\n", ret); |
| goto set_cfg_exit; |
| } |
| |
| set_cfg_exit: |
| return ret; |
| } |
| |
| static int lmh_parse_and_extract(const char __user *user_buf, size_t count, |
| enum lmh_read_type type) |
| { |
| char *local_buf = NULL, *token = NULL, *curr_ptr = NULL, *token1 = NULL; |
| char *next_line = NULL; |
| int ret = 0, data_ct = 0, i = 0, size = 0; |
| uint32_t *config_buf = NULL; |
| |
| /* Allocate two extra space to add ';' character and NULL terminate */ |
| local_buf = kzalloc(count + 2, GFP_KERNEL); |
| if (!local_buf) { |
| ret = -ENOMEM; |
| goto dfs_cfg_write_exit; |
| } |
| if (copy_from_user(local_buf, user_buf, count)) { |
| pr_err("user buf error\n"); |
| ret = -EFAULT; |
| goto dfs_cfg_write_exit; |
| } |
| size = count + (strnchr(local_buf, count, '\n') ? 1 : 2); |
| local_buf[size - 2] = ';'; |
| local_buf[size - 1] = '\0'; |
| curr_ptr = next_line = local_buf; |
| while ((token1 = strnchr(next_line, local_buf + size - next_line, ';')) |
| != NULL) { |
| data_ct = 0; |
| *token1 = '\0'; |
| curr_ptr = next_line; |
| next_line = token1 + 1; |
| for (token = (char *)curr_ptr; token && |
| ((token = strnchr(token, next_line - token, ' ')) |
| != NULL); token++) |
| data_ct++; |
| if (data_ct < 2) { |
| pr_err("Invalid format string:[%s]\n", curr_ptr); |
| ret = -EINVAL; |
| goto dfs_cfg_write_exit; |
| } |
| config_buf = kzalloc((++data_ct) * sizeof(uint32_t), |
| GFP_KERNEL); |
| if (!config_buf) { |
| ret = -ENOMEM; |
| goto dfs_cfg_write_exit; |
| } |
| pr_debug("Input:%s data_ct:%d\n", curr_ptr, data_ct); |
| for (i = 0, token = (char *)curr_ptr; token && (i < data_ct); |
| i++) { |
| token = strnchr(token, next_line - token, ' '); |
| if (token) |
| *token = '\0'; |
| ret = kstrtouint(curr_ptr, 0, &config_buf[i]); |
| if (ret < 0) { |
| pr_err("Data[%s] scan error. err:%d\n", |
| curr_ptr, ret); |
| kfree(config_buf); |
| goto dfs_cfg_write_exit; |
| } |
| if (token) |
| curr_ptr = ++token; |
| } |
| switch (type) { |
| case LMH_READ_TYPE: |
| case LMH_CONFIG_TYPE: |
| ret = lmh_debug_config_write(LMH_DEBUG_SET, |
| config_buf, data_ct); |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| kfree(config_buf); |
| if (ret) { |
| pr_err("Config error. type:%d err:%d\n", type, ret); |
| goto dfs_cfg_write_exit; |
| } |
| } |
| |
| dfs_cfg_write_exit: |
| kfree(local_buf); |
| return ret; |
| } |
| |
| static ssize_t lmh_dbgfs_config_write(struct file *file, |
| const char __user *user_buf, size_t count, loff_t *ppos) |
| { |
| lmh_parse_and_extract(user_buf, count, LMH_CONFIG_TYPE); |
| return count; |
| } |
| |
| static int lmh_dbgfs_data_read(struct seq_file *seq_fp, void *data) |
| { |
| static uint32_t *read_buf; |
| static int read_buf_size; |
| int idx = 0, ret = 0; |
| |
| if (!read_buf_size) { |
| ret = lmh_debug_read(&read_buf); |
| if (ret <= 0) |
| goto dfs_read_exit; |
| if (!read_buf || ret < sizeof(uint32_t)) { |
| ret = -EINVAL; |
| goto dfs_read_exit; |
| } |
| read_buf_size = ret; |
| ret = 0; |
| } |
| |
| do { |
| seq_printf(seq_fp, "0x%x ", read_buf[idx]); |
| if (seq_has_overflowed(seq_fp)) { |
| pr_err("Seq overflow. idx:%d\n", idx); |
| goto dfs_read_exit; |
| } |
| idx++; |
| if ((idx % LMH_READ_LINE_LENGTH) == 0) { |
| seq_puts(seq_fp, "\n"); |
| if (seq_has_overflowed(seq_fp)) { |
| pr_err("Seq overflow. idx:%d\n", idx); |
| goto dfs_read_exit; |
| } |
| } |
| } while (idx < (read_buf_size / sizeof(uint32_t))); |
| read_buf_size = 0; |
| read_buf = NULL; |
| |
| dfs_read_exit: |
| return ret; |
| } |
| |
| static int lmh_get_recurssive_data(struct scm_desc *desc_arg, uint32_t cmd_idx, |
| uint32_t *payload, uint32_t *size, uint32_t **dest_buf) |
| { |
| int idx = 0, ret = 0; |
| uint32_t next = 0; |
| |
| do { |
| desc_arg->args[cmd_idx] = next; |
| dmac_flush_range(payload, (void *)payload + |
| sizeof(*payload) * LMH_SCM_PAYLOAD_SIZE); |
| ret = scm_call2(SCM_SIP_FNID(SCM_SVC_LMH, LMH_DEBUG_GET_TYPE), |
| desc_arg); |
| dmac_inv_range(payload, (void *)payload + |
| sizeof(*payload) * LMH_SCM_PAYLOAD_SIZE); |
| *size = desc_arg->ret[0]; |
| /* Have barrier before reading from TZ data */ |
| mb(); |
| if (ret) { |
| pr_err("Error in SCM get type. cmd:%x err:%d\n", |
| LMH_DEBUG_GET_TYPE, ret); |
| return ret; |
| } |
| if (!*size) { |
| pr_err("No LMH device supported\n"); |
| return -ENODEV; |
| } |
| if (!*dest_buf) { |
| *dest_buf = devm_kcalloc(lmh_data->dev, *size, |
| sizeof(**dest_buf), GFP_KERNEL); |
| if (!*dest_buf) |
| return -ENOMEM; |
| } |
| |
| for (idx = next; |
| idx < min((next + LMH_SCM_PAYLOAD_SIZE), *size); |
| idx++) |
| (*dest_buf)[idx] = payload[idx - next]; |
| next += LMH_SCM_PAYLOAD_SIZE; |
| } while (next < *size); |
| |
| return ret; |
| } |
| |
| static ssize_t lmh_dbgfs_data_write(struct file *file, |
| const char __user *user_buf, size_t count, loff_t *ppos) |
| { |
| lmh_parse_and_extract(user_buf, count, LMH_READ_TYPE); |
| return count; |
| } |
| |
| static int lmh_dbgfs_data_open(struct inode *inode, struct file *file) |
| { |
| return single_open(file, lmh_dbgfs_data_read, inode->i_private); |
| } |
| |
| static int lmh_debug_get_types(bool is_read, uint32_t **buf) |
| { |
| int ret = 0; |
| uint32_t size = 0; |
| struct scm_desc desc_arg; |
| uint32_t *payload = NULL, *dest_buf = NULL; |
| |
| if (is_read && lmh_data->read_type) { |
| *buf = lmh_data->read_type; |
| return lmh_data->read_type_count; |
| } else if (!is_read && lmh_data->config_type) { |
| *buf = lmh_data->config_type; |
| return lmh_data->config_type_count; |
| } |
| payload = devm_kzalloc(lmh_data->dev, |
| PAGE_ALIGN(LMH_SCM_PAYLOAD_SIZE * |
| sizeof(*payload)), GFP_KERNEL); |
| if (!payload) |
| return -ENOMEM; |
| /* &payload may be a physical address > 4 GB */ |
| desc_arg.args[0] = SCM_BUFFER_PHYS(payload); |
| desc_arg.args[1] = |
| SCM_BUFFER_SIZE(uint32_t) * LMH_SCM_PAYLOAD_SIZE; |
| desc_arg.args[2] = (is_read) ? |
| LMH_DEBUG_READ_TYPE : LMH_DEBUG_CONFIG_TYPE; |
| desc_arg.arginfo = SCM_ARGS(4, SCM_RW, SCM_VAL, SCM_VAL, SCM_VAL); |
| ret = lmh_get_recurssive_data(&desc_arg, 3, payload, &size, &dest_buf); |
| if (ret) |
| goto get_type_exit; |
| pr_debug("Total %s types:%d\n", (is_read) ? "read" : "config", size); |
| if (is_read) { |
| lmh_data->read_type = *buf = dest_buf; |
| lmh_data->read_type_count = size; |
| } else { |
| lmh_data->config_type = *buf = dest_buf; |
| lmh_data->config_type_count = size; |
| } |
| |
| get_type_exit: |
| if (ret) { |
| if (lmh_data->read_type_count) { |
| devm_kfree(lmh_data->dev, lmh_data->read_type); |
| lmh_data->read_type_count = 0; |
| } |
| if (lmh_data->config_type_count) { |
| devm_kfree(lmh_data->dev, lmh_data->config_type); |
| lmh_data->config_type_count = 0; |
| } |
| } |
| if (payload) |
| devm_kfree(lmh_data->dev, payload); |
| |
| return (ret) ? ret : size; |
| } |
| |
| static int lmh_get_types(struct seq_file *seq_fp, enum lmh_read_type type) |
| { |
| int ret = 0, idx = 0, size = 0; |
| uint32_t *type_list = NULL; |
| |
| switch (type) { |
| case LMH_READ_TYPE: |
| ret = lmh_debug_get_types(true, &type_list); |
| break; |
| case LMH_CONFIG_TYPE: |
| ret = lmh_debug_get_types(false, &type_list); |
| break; |
| default: |
| return -EINVAL; |
| } |
| if (ret <= 0 || !type_list) { |
| pr_err("No device information. err:%d\n", ret); |
| return -ENODEV; |
| } |
| size = ret; |
| for (idx = 0; idx < size; idx++) |
| seq_printf(seq_fp, "0x%x ", type_list[idx]); |
| seq_puts(seq_fp, "\n"); |
| |
| return 0; |
| } |
| |
| static int lmh_dbgfs_read_type(struct seq_file *seq_fp, void *data) |
| { |
| return lmh_get_types(seq_fp, LMH_READ_TYPE); |
| } |
| |
| static int lmh_dbgfs_read_type_open(struct inode *inode, struct file *file) |
| { |
| return single_open(file, lmh_dbgfs_read_type, inode->i_private); |
| } |
| |
| static int lmh_dbgfs_config_type(struct seq_file *seq_fp, void *data) |
| { |
| return lmh_get_types(seq_fp, LMH_CONFIG_TYPE); |
| } |
| |
| static int lmh_dbgfs_config_type_open(struct inode *inode, struct file *file) |
| { |
| return single_open(file, lmh_dbgfs_config_type, inode->i_private); |
| } |
| |
| static const struct file_operations lmh_dbgfs_config_fops = { |
| .write = lmh_dbgfs_config_write, |
| }; |
| static const struct file_operations lmh_dbgfs_read_fops = { |
| .open = lmh_dbgfs_data_open, |
| .read = seq_read, |
| .write = lmh_dbgfs_data_write, |
| .llseek = seq_lseek, |
| .release = single_release, |
| }; |
| static const struct file_operations lmh_dbgfs_read_type_fops = { |
| .open = lmh_dbgfs_read_type_open, |
| .read = seq_read, |
| .llseek = seq_lseek, |
| .release = single_release, |
| }; |
| static const struct file_operations lmh_dbgfs_config_type_fops = { |
| .open = lmh_dbgfs_config_type_open, |
| .read = seq_read, |
| .llseek = seq_lseek, |
| .release = single_release, |
| }; |
| |
| static int lmh_check_tz_debug_cmds(void) |
| { |
| if (!scm_is_call_available(SCM_SVC_LMH, LMH_DEBUG_SET) |
| || !scm_is_call_available(SCM_SVC_LMH, LMH_DEBUG_READ_BUF_SIZE) |
| || !scm_is_call_available(SCM_SVC_LMH, LMH_DEBUG_READ) |
| || !scm_is_call_available(SCM_SVC_LMH, LMH_DEBUG_GET_TYPE)) { |
| pr_debug("LMH debug scm not available\n"); |
| return -ENODEV; |
| } |
| |
| return 0; |
| } |
| |
| static int lmh_debug_init(void) |
| { |
| int ret = 0; |
| |
| if (lmh_check_tz_debug_cmds()) { |
| pr_debug("Debug commands not available\n"); |
| return -ENODEV; |
| } |
| |
| lmh_data->debugfs_parent = debugfs_create_dir(LMH_MON_NAME, NULL); |
| if (IS_ERR(lmh_data->debugfs_parent)) { |
| ret = PTR_ERR(lmh_data->debugfs_parent); |
| pr_debug("Error creating debugfs dir:%s. err:%d\n", |
| LMH_MON_NAME, ret); |
| return ret; |
| } |
| lmh_data->debug_read = debugfs_create_file(LMH_DBGFS_READ, 0600, |
| lmh_data->debugfs_parent, NULL, |
| &lmh_dbgfs_read_fops); |
| if (IS_ERR(lmh_data->debug_read)) { |
| pr_err("Error creating" LMH_DBGFS_READ "entry\n"); |
| ret = PTR_ERR(lmh_data->debug_read); |
| goto dbg_reg_exit; |
| } |
| lmh_data->debug_config = debugfs_create_file(LMH_DBGFS_CONFIG_READ, |
| 0200, lmh_data->debugfs_parent, NULL, |
| &lmh_dbgfs_config_fops); |
| if (IS_ERR(lmh_data->debug_config)) { |
| pr_err("Error creating" LMH_DBGFS_CONFIG_READ "entry\n"); |
| ret = PTR_ERR(lmh_data->debug_config); |
| goto dbg_reg_exit; |
| } |
| lmh_data->debug_read_type = debugfs_create_file(LMH_DBGFS_READ_TYPES, |
| 0400, lmh_data->debugfs_parent, NULL, |
| &lmh_dbgfs_read_type_fops); |
| if (IS_ERR(lmh_data->debug_read_type)) { |
| pr_err("Error creating" LMH_DBGFS_READ_TYPES "entry\n"); |
| ret = PTR_ERR(lmh_data->debug_read_type); |
| goto dbg_reg_exit; |
| } |
| lmh_data->debug_read_type = debugfs_create_file( |
| LMH_DBGFS_CONFIG_TYPES, |
| 0400, lmh_data->debugfs_parent, NULL, |
| &lmh_dbgfs_config_type_fops); |
| if (IS_ERR(lmh_data->debug_config_type)) { |
| pr_err("Error creating" LMH_DBGFS_CONFIG_TYPES "entry\n"); |
| ret = PTR_ERR(lmh_data->debug_config_type); |
| goto dbg_reg_exit; |
| } |
| |
| dbg_reg_exit: |
| if (ret) |
| /*Clean up all the dbg nodes*/ |
| debugfs_remove_recursive(lmh_data->debugfs_parent); |
| |
| return ret; |
| } |
| |
| int lmh_debug_register(struct platform_device *pdev) |
| { |
| int ret = 0; |
| |
| if (lmh_data) { |
| pr_debug("Reinitializing lmh hardware driver\n"); |
| return -EEXIST; |
| } |
| lmh_data = devm_kzalloc(&pdev->dev, sizeof(*lmh_data), GFP_KERNEL); |
| if (!lmh_data) |
| return -ENOMEM; |
| lmh_data->dev = &pdev->dev; |
| |
| ret = lmh_debug_init(); |
| if (ret) { |
| pr_debug("LMH debug init failed. err:%d\n", ret); |
| goto probe_exit; |
| } |
| |
| return ret; |
| |
| probe_exit: |
| lmh_data = NULL; |
| return ret; |
| } |
| EXPORT_SYMBOL(lmh_debug_register); |