Qualcomm memory dump driver initializes system memory dump table. Firmware dumps system cache, internal memory, peripheral registers to DDR as per this table after system crashes and warm resets. The driver reserves memory, populates ids and sizes for firmware dumping according to the configuration. Signed-off-by: Zhenhua Huang <quic_zhenhuah@xxxxxxxxxxx> --- MAINTAINERS | 7 + drivers/soc/qcom/Kconfig | 11 + drivers/soc/qcom/Makefile | 1 + drivers/soc/qcom/memory_dump.c | 540 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 559 insertions(+) create mode 100644 drivers/soc/qcom/memory_dump.c diff --git a/MAINTAINERS b/MAINTAINERS index 90f1328..096e0f7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -17792,6 +17792,13 @@ S: Maintained F: Documentation/devicetree/bindings/regulator/vqmmc-ipq4019-regulator.yaml F: drivers/regulator/vqmmc-ipq4019-regulator.c +QUALCOMM MEMORY DUMP DRIVER +M: Zhenhua Huang <quic_zhenhuah@xxxxxxxxxxx> +L: linux-arm-msm@xxxxxxxxxxxxxxx +S: Maintained +F: Documentation/devicetree/bindings/soc/qcom/qcom,memory_dump.yaml +F: drivers/soc/qcom/memory_dump.c + QUALCOMM NAND CONTROLLER DRIVER M: Manivannan Sadhasivam <manivannan.sadhasivam@xxxxxxxxxx> L: linux-mtd@xxxxxxxxxxxxxxxxxxx diff --git a/drivers/soc/qcom/Kconfig b/drivers/soc/qcom/Kconfig index 7153488..1842f4e 100644 --- a/drivers/soc/qcom/Kconfig +++ b/drivers/soc/qcom/Kconfig @@ -74,6 +74,17 @@ config QCOM_KRYO_L2_ACCESSORS bool depends on (ARCH_QCOM || COMPILE_TEST) && ARM64 +config QCOM_MEMORY_DUMP + bool "QCOM Memory Dump Support" + depends on ARCH_QCOM || COMPILE_TEST + select BOOT_CONFIG + help + Qualcomm memory dump driver initializes system memory dump table. + Firmware dumps system cache, internal memory, peripheral registers to + DDR as per this table after system crash and warm reset. + The driver allocates reserved memory and populates ids, sizes for + firmware to dump according to configuration. + config QCOM_MDT_LOADER tristate select QCOM_SCM diff --git a/drivers/soc/qcom/Makefile b/drivers/soc/qcom/Makefile index bbca2e1..988880c 100644 --- a/drivers/soc/qcom/Makefile +++ b/drivers/soc/qcom/Makefile @@ -28,6 +28,7 @@ obj-$(CONFIG_QCOM_STATS) += qcom_stats.o obj-$(CONFIG_QCOM_WCNSS_CTRL) += wcnss_ctrl.o obj-$(CONFIG_QCOM_APR) += apr.o obj-$(CONFIG_QCOM_LLCC) += llcc-qcom.o +obj-$(CONFIG_QCOM_MEMORY_DUMP) += memory_dump.o obj-$(CONFIG_QCOM_KRYO_L2_ACCESSORS) += kryo-l2-accessors.o obj-$(CONFIG_QCOM_ICC_BWMON) += icc-bwmon.o qcom_ice-objs += ice.o diff --git a/drivers/soc/qcom/memory_dump.c b/drivers/soc/qcom/memory_dump.c new file mode 100644 index 0000000..57cd897 --- /dev/null +++ b/drivers/soc/qcom/memory_dump.c @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2023 Qualcomm Innovation Center, Inc. All rights reserved. + * + * Qualcomm memory dump driver dynamically reserves memory and provides + * hints(id and size) of debugging information based on specified + * protocols with firmware into pre-allocated memory. Firmware then does the + * real data capture. The debugging information includes cache contents, + * internal memory, registers. + * After crash and warm reboot, firmware scans ids, sizes and stores contents + * into reserved memory accordingly. Firmware then enters into full dump mode + * which dumps whole DDR to host through USB. + * + */ +#include <asm/barrier.h> +#include <linux/bootconfig.h> +#include <linux/cma.h> +#include <linux/device.h> +#include <linux/dma-map-ops.h> +#include <linux/dma-mapping.h> +#include <linux/err.h> +#include <linux/errno.h> +#include <linux/firmware/qcom/qcom_scm.h> +#include <linux/io.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_reserved_mem.h> +#include <linux/platform_device.h> +#include <linux/types.h> + +#define MAX_NUM_ENTRIES 0x150 +#define QCOM_DUMP_MAKE_VERSION(major, minor) (((major) << 20) | (minor)) +#define QCOM_DUMP_TABLE_VERSION QCOM_DUMP_MAKE_VERSION(2, 0) +#define QCOM_DUMP_DATA_SIZE sizeof(struct qcom_dump_data) + +enum qcom_dump_table_ids { + QCOM_DUMP_TABLE_LINUX, + QCOM_DUMP_TABLE_MAX = MAX_NUM_ENTRIES, +}; + +enum qcom_dump_type { + QCOM_DUMP_TYPE_DATA, + QCOM_DUMP_TYPE_TABLE, +}; + +/* + * +----------+ 1st level + * |IMEM |------+-----------------+ + * +----------+ | qcom_dump_table | + * |---------------- | + * | version | + * | num_entryies | + * | .. | + * |---------------- | + * +-----|qcom_dump_entry | + * | |qcom_dump_entry | + * | | ... | + * | +-----------------+ + * | + * | + * | 2nd level + * | +-----------------+ + * ------| qcom_dump_table | + * |---------------- | + * | version | + * | num_entryies | + * | .. | + * |---------------- | +-------------+ +----------+ + * |qcom_dump_entry |-----|qcom_dump_data|----| data | + * |qcom_dump_entry | +-------------+ +----------+ + * | ... |---- +-------------+ +----------+ + * +-----------------+ |qcom_dump_data|----| data | + * +-------------+ +----------+ + * + * Structures can not be packed due to protocols with firmware. + */ +struct qcom_dump_data { + __le32 version; + __le32 magic; + char name[32]; + __le64 addr; + __le64 len; + __le32 reserved; +}; + +struct qcom_dump_entry { + __le32 id; + char name[32]; + __le32 type; + __le64 addr; +}; + +struct qcom_dump_table { + __le32 version; + __le32 num_entries; + struct qcom_dump_entry entries[MAX_NUM_ENTRIES]; +}; + +struct qcom_memory_dump { + u64 table_phys; + struct qcom_dump_table *table; + struct xbc_node *mem_dump_node; + /* Cached 2nd level table */ + struct qcom_dump_table *cached_2nd_table; +}; + +static void __init mem_dump_entry_set(struct device *dev, + struct qcom_dump_entry *entry, + u32 id, + u32 type, uint64_t addr) +{ + entry->id = id; + entry->type = type; + entry->addr = addr; +} + +/* 1st level table register */ +static int __init mem_dump_table_register(struct device *dev, + struct qcom_dump_entry *entry) +{ + struct qcom_memory_dump *memdump = dev_get_drvdata(dev); + struct qcom_dump_entry *last_entry; + struct qcom_dump_table *table = memdump->table; + + if (!table || table->num_entries >= MAX_NUM_ENTRIES) + return -EINVAL; + + last_entry = &table->entries[table->num_entries]; + mem_dump_entry_set(dev, last_entry, entry->id, + QCOM_DUMP_TYPE_TABLE, entry->addr); + table->num_entries++; + + return 0; +} + +/* Get 2nd level table */ +static struct qcom_dump_table * __init +mem_dump_get_table(struct device *dev, + enum qcom_dump_table_ids id) +{ + struct qcom_memory_dump *memdump = dev_get_drvdata(dev); + struct qcom_dump_table *table = memdump->table; + unsigned long offset; + int i; + + if (memdump->cached_2nd_table) + return memdump->cached_2nd_table; + + if (!table) { + dev_err(dev, "Mem dump base table does not exist\n"); + return ERR_PTR(-EINVAL); + } + + for (i = 0; i < MAX_NUM_ENTRIES; i++) { + if (table->entries[i].id == id) + break; + } + + if (i == MAX_NUM_ENTRIES || !table->entries[i].addr) { + dev_err(dev, "Mem dump base table entry %d invalid\n", id); + return ERR_PTR(-EINVAL); + } + + offset = table->entries[i].addr - memdump->table_phys; + + /* Get the table pointer. Phy and virt addr has same offset */ + table = (void *)memdump->table + offset; + /* Cache it for next time visit */ + memdump->cached_2nd_table = table; + + return table; +} + +/* register in 2nd level table */ +static int __init mem_dump_data_register(struct device *dev, + enum qcom_dump_table_ids id, + struct qcom_dump_entry *entry) +{ + struct qcom_dump_entry *last_entry; + struct qcom_dump_table *table; + + /* Get 2nd level table */ + table = mem_dump_get_table(dev, id); + if (IS_ERR(table)) + return PTR_ERR(table); + + if (!table || table->num_entries >= MAX_NUM_ENTRIES) + return -EINVAL; + + last_entry = &table->entries[table->num_entries]; + mem_dump_entry_set(dev, last_entry, entry->id, QCOM_DUMP_TYPE_DATA, + entry->addr); + table->num_entries++; + + return 0; +} + +static int __init qcom_init_memdump_imem_area(struct device *dev, size_t size) +{ + struct qcom_memory_dump *memdump = dev_get_drvdata(dev); + void __iomem *table_offset; + void __iomem *table_base; + struct device_node *np; + + np = of_find_compatible_node(NULL, NULL, + "qcom,qcom-imem-mem-dump-table"); + if (!np) { + dev_err_probe(dev, -ENODEV, + "Mem dump base table DT node does not exist\n"); + return -ENODEV; + } + + table_base = devm_of_iomap(dev, np, 0, NULL); + if (!table_base) { + dev_err_probe(dev, -ENOMEM, + "Mem dump base table imem offset mapping failed\n"); + return -ENOMEM; + } + + np = of_find_compatible_node(NULL, NULL, + "qcom,qcom-imem-mem-dump-table-size"); + if (!np) { + dev_err_probe(dev, -ENODEV, + "Mem dump base table size DT node does not exist\n"); + devm_iounmap(dev, table_base); + return -ENODEV; + } + + table_offset = devm_of_iomap(dev, np, 0, NULL); + if (!table_offset) { + dev_err_probe(dev, -ENOMEM, + "Mem dump base table size imem offset mapping failed\n"); + devm_iounmap(dev, table_base); + return -ENOMEM; + } + + memcpy_toio(table_base, &memdump->table_phys, + sizeof(memdump->table_phys)); + memcpy_toio(table_offset, + &size, sizeof(size_t)); + + /* Ensure write to table_base is complete before unmapping */ + mb(); + dev_dbg(dev, "QCOM Memory Dump base table set up in IMEM\n"); + + devm_iounmap(dev, table_base); + devm_iounmap(dev, table_offset); + return 0; +} + +/* Helper function for applying both vaddr and phys addr */ +static void __init mem_dump_apply_offset(void **dump_vaddr, + phys_addr_t *phys_addr, size_t offset) +{ + *dump_vaddr += offset; + *phys_addr += offset; +} + +/* Populate 1st level: QCOM_DUMP_TABLE_LINUX */ +static int __init mem_dump_register_data_table(struct device *dev, + void *dump_vaddr, + phys_addr_t phys_addr) +{ + struct qcom_memory_dump *memdump = dev_get_drvdata(dev); + struct qcom_dump_table *table; + struct qcom_dump_entry entry; + int ret; + + memdump->table = dump_vaddr; + memdump->table->version = QCOM_DUMP_TABLE_VERSION; + memdump->table_phys = phys_addr; + mem_dump_apply_offset(&dump_vaddr, &phys_addr, sizeof(*table)); + + table = dump_vaddr; + table->version = QCOM_DUMP_TABLE_VERSION; + entry.id = QCOM_DUMP_TABLE_LINUX; + entry.addr = phys_addr; + ret = mem_dump_table_register(dev, &entry); + if (ret) { + dev_err(dev, "Mem dump apps data table register failed\n"); + return ret; + } + + return 0; +} + +static int __init mem_dump_reserve_mem(struct device *dev) +{ + int ret; + + if (of_property_present(dev->of_node, "memory-region")) { + ret = of_reserved_mem_device_init_by_idx(dev, + dev->of_node, 0); + if (ret) + dev_err_probe(dev, ret, + "Failed to initialize reserved mem\n"); + return ret; + } + + /* Using default CMA region is fallback choice */ + dev_dbg(dev, "Using default CMA region\n"); + return 0; +} + +static struct page * __init +mem_dump_alloc_mem(struct device *dev, size_t *total_size) +{ + struct qcom_memory_dump *memdump = dev_get_drvdata(dev); + struct xbc_node *linked_list; + int num_of_nodes = 0; + struct page *page; + const char *size_p; + const char *id_p; + int ret = 0; + int size; + int id; + + memdump->mem_dump_node = xbc_find_node("memory_dump_config"); + if (!memdump->mem_dump_node) { + dev_err(dev, "xbc config not found\n"); + return ERR_PTR(-EINVAL); + } + + *total_size = sizeof(struct qcom_dump_table) * 2; + + xbc_node_for_each_subkey(memdump->mem_dump_node, linked_list) { + const char *name = xbc_node_get_data(linked_list); + + if (!name) + continue; + + id_p = xbc_node_find_value(linked_list, "id", NULL); + size_p = xbc_node_find_value(linked_list, "size", NULL); + + if (id_p && size_p) { + ret = kstrtoint(id_p, 0, &id); + if (ret) + continue; + + ret = kstrtoint(size_p, 0, &size); + if (ret) + continue; + + if (check_add_overflow(*total_size, size, total_size)) + return ERR_PTR(-EOVERFLOW); + + num_of_nodes++; + } else { + continue; + } + } + + if (!num_of_nodes) + return ERR_PTR(-EINVAL); + + if (check_add_overflow(*total_size, + (QCOM_DUMP_DATA_SIZE * num_of_nodes), + total_size)) + return ERR_PTR(-EOVERFLOW); + + /* Align total_size */ + if (*total_size > ALIGN(*total_size, PAGE_SIZE)) + return ERR_PTR(-EOVERFLOW); + *total_size = ALIGN(*total_size, PAGE_SIZE); + + /* + * Physical continuous buffer. + */ + page = cma_alloc(dev_get_cma_area(dev), (*total_size / PAGE_SIZE), + 0, false); + if (page) + memset(page_address(page), 0, *total_size); + else + return ERR_PTR(-ENOMEM); + + return page; +} + +/* populate allocated region */ +static int __init mem_dump_populate_mem(struct device *dev, + struct page *start_page, + size_t total_size) +{ + struct qcom_memory_dump *memdump = dev_get_drvdata(dev); + struct qcom_dump_entry dump_entry; + struct qcom_dump_data *dump_data; + struct xbc_node *linked_list; + phys_addr_t phys_end_addr; + phys_addr_t phys_addr; + const char *size_p; + void *dump_vaddr; + const char *id_p; + int ret = 0; + int size; + int id; + + phys_addr = page_to_phys(start_page); + phys_end_addr = phys_addr + total_size; + dump_vaddr = page_to_virt(start_page); + + ret = mem_dump_register_data_table(dev, dump_vaddr, phys_addr); + if (ret) { + dev_err_probe(dev, ret, "Mem Dump table set up is failed\n"); + return ret; + } + + ret = qcom_init_memdump_imem_area(dev, total_size); + if (ret) + return ret; + + /* Apply two tables: QCOM_DUMP_TYPE_TABLE and QCOM_DUMP_TYPE_DATA */ + mem_dump_apply_offset(&dump_vaddr, &phys_addr, + sizeof(struct qcom_dump_table) * 2); + + /* Both "id" and "size" must be present */ + xbc_node_for_each_subkey(memdump->mem_dump_node, linked_list) { + const char *name = xbc_node_get_data(linked_list); + + if (!name) + continue; + + id_p = xbc_node_find_value(linked_list, "id", NULL); + size_p = xbc_node_find_value(linked_list, "size", NULL); + + if (id_p && size_p) { + ret = kstrtoint(id_p, 0, &id); + if (ret) + continue; + + ret = kstrtoint(size_p, 0, &size); + + if (ret) + continue; + + /* + * Physical layout: starting from two qcom_dump_data. + * Following are respective dump meta data and reserved regions. + * Qcom_dump_data is populated by the driver, fw parse it + * and dump respective info into dump mem. + * Illustrate the layout: + * + * +------------------------+------------------------+ + * | qcom_dump_table(TABLE) | qcom_dump_table(DATA) | + * +------------------------+------------------------+ + * +-------------+----------+-------------+----------+ + * |qcom_dump_data| dump mem|qcom_dump_data| dump mem | + * +-------------+----------+-------------+----------+ + * +-------------+----------+-------------+----------+ + * |qcom_dump_data| dump mem|qcom_dump_data| dump mem | + * +-------------+----------+-------------+----------+ + * ... + */ + dump_data = dump_vaddr; + dump_data->addr = phys_addr + QCOM_DUMP_DATA_SIZE; + dump_data->len = size; + dump_entry.id = id; + strscpy(dump_data->name, name, + sizeof(dump_data->name)); + dump_entry.addr = phys_addr; + ret = mem_dump_data_register(dev, QCOM_DUMP_TABLE_LINUX, + &dump_entry); + if (ret) { + dev_err_probe(dev, ret, "Dump data setup failed, id = %d\n", + id); + return ret; + } + + mem_dump_apply_offset(&dump_vaddr, &phys_addr, + size + QCOM_DUMP_DATA_SIZE); + if (phys_addr > phys_end_addr) { + dev_err_probe(dev, -ENOMEM, "Exceeding allocated region\n"); + return -ENOMEM; + } + } else { + continue; + } + } + + return ret; +} + +static int __init mem_dump_probe(struct platform_device *pdev) +{ + struct qcom_memory_dump *memdump; + struct device *dev = &pdev->dev; + struct page *page; + size_t total_size; + int ret = 0; + + memdump = devm_kzalloc(dev, sizeof(struct qcom_memory_dump), + GFP_KERNEL); + if (!memdump) + return -ENOMEM; + + dev_set_drvdata(dev, memdump); + + /* check and initiate CMA region */ + ret = mem_dump_reserve_mem(dev); + if (ret) + return ret; + + /* allocate and populate */ + page = mem_dump_alloc_mem(dev, &total_size); + if (IS_ERR(page)) { + ret = PTR_ERR(page); + dev_err_probe(dev, ret, "mem dump alloc failed\n"); + goto release; + } + + ret = mem_dump_populate_mem(dev, page, total_size); + if (!ret) + dev_info(dev, "Mem dump region populated successfully\n"); + else + goto free; + + return 0; + +free: + cma_release(dev_get_cma_area(dev), page, (total_size / PAGE_SIZE)); + +release: + of_reserved_mem_device_release(dev); + return ret; +} + +static const struct of_device_id mem_dump_match_table[] = { + {.compatible = "qcom,mem-dump",}, + {} +}; + +static struct platform_driver mem_dump_driver = { + .driver = { + .name = "qcom_mem_dump", + .of_match_table = mem_dump_match_table, + }, +}; +module_platform_driver_probe(mem_dump_driver, mem_dump_probe); + +MODULE_DESCRIPTION("Memory Dump Driver"); +MODULE_LICENSE("GPL"); -- 2.7.4