| // SPDX-License-Identifier: (GPL-2.0+ OR MIT) |
| /* |
| * drivers/amlogic/memory_ext/ram_dump.c |
| * |
| * Copyright (C) 2017 Amlogic, Inc. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
| * more details. |
| * |
| */ |
| #define DEBUG |
| #include <linux/version.h> |
| #include <linux/types.h> |
| #include <linux/fs.h> |
| #include <linux/init.h> |
| #include <linux/device.h> |
| #include <linux/platform_device.h> |
| #include <linux/mutex.h> |
| #include <linux/io.h> |
| #include <linux/module.h> |
| #include <linux/kernel.h> |
| #include <linux/slab.h> |
| #include <linux/mm.h> |
| #include <linux/reboot.h> |
| #include <linux/memblock.h> |
| #include <linux/sched/clock.h> |
| #include <linux/amlogic/ramdump.h> |
| #include <linux/amlogic/reboot.h> |
| #include <linux/arm-smccc.h> |
| #include <linux/of.h> |
| #include <linux/highmem.h> |
| #include <linux/syscalls.h> |
| #include <linux/fs_struct.h> |
| #include <linux/mount.h> |
| #include <linux/namei.h> |
| #include <linux/capability.h> |
| #include <asm/cacheflush.h> |
| #include <linux/amlogic/gki_module.h> |
| #include <trace/hooks/debug.h> |
| #include <linux/panic_notifier.h> |
| #include <linux/of_address.h> |
| |
| static unsigned long ramdump_base; |
| static unsigned long ramdump_size; |
| static bool ramdump_disable = 1; |
| |
| #define WAIT_TIMEOUT (40ULL * 1000 * 1000 * 1000) |
| #define SAVE_DATA_BY_INIT_RC_SHELL 1 |
| #define RESERVE_MEM_BY_RAMDUMP_DTS_NODE 1 |
| |
| struct ramdump { |
| unsigned long mem_size; |
| unsigned long mem_base; |
| #ifdef SAVE_DATA_BY_INIT_RC_SHELL |
| //void __iomem *mem_base; |
| struct mutex lock; |
| struct kobject *kobj; |
| #else |
| unsigned long long tick; |
| void *mnt_buf; |
| const char *storage_device; |
| #endif |
| struct delayed_work work; |
| int disable; |
| }; |
| |
| static struct ramdump *ram; |
| |
| static void ramdump_parse_info(void) |
| { |
| #if !IS_MODULE(CONFIG_AMLOGIC_MEMORY_DEBUG) |
| pr_info("%s, .text : 0x%px - 0x%px, pa(.text): 0x%lx\n", |
| __func__, (unsigned long *)_text, |
| (unsigned long *)_etext, (unsigned long)__pa_symbol(_text)); |
| pr_info("%s, kimage_voffset = 0x%px\n", |
| __func__, (unsigned long)_text - (unsigned long)__pa_symbol(_text)); |
| #endif |
| |
| #ifdef CONFIG_ARM64 |
| pr_info("%s, kimage_vaddr: 0x%px, v2p: 0x%lx\n", |
| __func__, (unsigned long *)kimage_vaddr, |
| (unsigned long)__pa_symbol(kimage_vaddr)); |
| pr_info("%s, KIMAGE_VADDR: 0x%px, v2p: 0x%lx\n", |
| __func__, (unsigned long *)KIMAGE_VADDR, |
| (unsigned long)__pa_symbol(KIMAGE_VADDR)); |
| pr_info("%s, kimage_voffset = 0x%lx\n", __func__, |
| (unsigned long)kimage_vaddr - (unsigned long)__pa_symbol(kimage_vaddr)); |
| pr_info("%s, kaslr_offset = 0x%lx\n", __func__, kaslr_offset()); |
| pr_info("%s, vabits_actual = %d\n", __func__, (unsigned int)vabits_actual); |
| #endif |
| } |
| |
| static int early_ramdump_para(char *buf) |
| { |
| int ret; |
| #ifdef RESERVE_MEM_BY_RAMDUMP_DTS_NODE |
| struct device_node *node; |
| struct resource res; |
| #endif |
| |
| if (!buf) |
| return -EINVAL; |
| |
| pr_info("%s:%s\n", __func__, buf); |
| if (strcmp(buf, "disabled") == 0) { |
| ramdump_disable = 1; |
| } else { |
| ret = sscanf(buf, "%lx,%lx", &ramdump_base, &ramdump_size); |
| if (ret != 2) { |
| pr_err("invalid boot args\n"); |
| ramdump_disable = 1; |
| } |
| ramdump_disable = 0; |
| pr_info("%s, base:%lx, size:%lx\n", |
| __func__, ramdump_base, ramdump_size); |
| #ifdef RESERVE_MEM_BY_RAMDUMP_DTS_NODE |
| node = of_find_node_by_path("/reserved-memory/ramdump_bl33z"); |
| ret = of_address_to_resource(node, 0, &res); |
| pr_info("%s, dts reserved-memory %lx - %lx\n", |
| __func__, (unsigned long)res.start, (unsigned long)res.end); |
| #else |
| ret = memblock_reserve(ramdump_base, PAGE_ALIGN(ramdump_size)); |
| if (ret < 0) { |
| pr_info("%s, reserve memblock %lx - %lx failed\n", |
| __func__, ramdump_base, |
| ramdump_base + PAGE_ALIGN(ramdump_size)); |
| ramdump_disable = 1; |
| } else { |
| pr_info("%s, reserve memblock %lx - %lx OK\n", |
| __func__, ramdump_base, |
| ramdump_base + PAGE_ALIGN(ramdump_size)); |
| } |
| #endif |
| ramdump_parse_info(); |
| } |
| return 0; |
| } |
| |
| early_param("ramdump", early_ramdump_para); |
| |
| #ifdef SAVE_DATA_BY_INIT_RC_SHELL |
| static ssize_t ramdump_bin_read(struct file *filp, struct kobject *kobj, |
| struct bin_attribute *attr, |
| char *buf, loff_t off, size_t count) |
| { |
| void *p = NULL; |
| |
| if (!ram->mem_base || off >= ram->mem_size) { |
| pr_info("%s, crash sysfsnode data err.\n", __func__); |
| return 0; |
| } |
| |
| if (off + count > ram->mem_size) |
| count = ram->mem_size - off; |
| |
| p = (void *)phys_to_virt(ram->mem_base + off); |
| |
| mutex_lock(&ram->lock); |
| memcpy(buf, p, count); |
| mutex_unlock(&ram->lock); |
| |
| /* debug when read end */ |
| if (off + count >= ram->mem_size) |
| pr_info("%s, p=%p %p, off:%lli, c:%zi\n", |
| __func__, buf, p, off, count); |
| |
| return count; |
| } |
| |
| int ramdump_disabled(void) |
| { |
| if (ram) |
| return ram->disable; |
| return 0; |
| } |
| EXPORT_SYMBOL(ramdump_disabled); |
| |
| static void meson_set_reboot_reason(int reboot_reason) |
| { |
| struct arm_smccc_res smccc; |
| |
| arm_smccc_smc(SET_REBOOT_REASON, |
| reboot_reason, 0, 0, 0, 0, 0, 0, &smccc); |
| } |
| |
| static ssize_t ramdump_bin_write(struct file *filp, |
| struct kobject *kobj, |
| struct bin_attribute *bin_attr, |
| char *buf, loff_t off, size_t count) |
| { |
| if (ram->mem_base && !strncmp("reboot", buf, 6)) |
| kernel_restart("RAM-DUMP finished\n"); |
| |
| if (!strncmp("disable", buf, 7)) { |
| ram->disable = 1; |
| meson_set_reboot_reason(MESON_NORMAL_BOOT); |
| } |
| if (!strncmp("enable", buf, 6)) { |
| ram->disable = 0; |
| meson_set_reboot_reason(MESON_KERNEL_PANIC); |
| } |
| |
| return count; |
| } |
| |
| static struct bin_attribute ramdump_attr = { |
| .attr = { |
| .name = "compmsg", |
| .mode = 0664, |
| }, |
| .read = ramdump_bin_read, |
| .write = ramdump_bin_write, |
| }; |
| |
| #ifdef CONFIG_ARM64 |
| noinline void ramdump_sync_data(void) |
| { |
| /* |
| * back port from old kernel version for function |
| * flush_cache_all(), we need it for ram dump |
| */ |
| asm volatile |
| ("mov x12, x30\n" |
| "dsb sy\n" |
| "mrs x0, clidr_el1\n" |
| "and x3, x0, #0x7000000\n" |
| "lsr x3, x3, #23\n" |
| "cbz x3, finished\n" |
| "mov x10, #0\n" |
| "loop1:\n" |
| "add x2, x10, x10, lsr #1\n" |
| "lsr x1, x0, x2\n" |
| "and x1, x1, #7\n" |
| "cmp x1, #2\n" |
| "b.lt skip\n" |
| "mrs x9, daif\n" |
| "msr daifset, #2\n" |
| "msr csselr_el1, x10\n" |
| "isb\n" |
| "mrs x1, ccsidr_el1\n" |
| "msr daif, x9\n" |
| "and x2, x1, #7\n" |
| "add x2, x2, #4\n" |
| "mov x4, #0x3ff\n" |
| "and x4, x4, x1, lsr #3\n" |
| "clz w5, w4\n" |
| "mov x7, #0x7fff\n" |
| "and x7, x7, x1, lsr #13\n" |
| "loop2:\n" |
| "mov x9, x4\n" |
| "loop3:\n" |
| "lsl x6, x9, x5\n" |
| "orr x11, x10, x6\n" |
| "lsl x6, x7, x2\n" |
| "orr x11, x11, x6\n" |
| "dc cisw, x11\n" |
| "subs x9, x9, #1\n" |
| "b.ge loop3\n" |
| "subs x7, x7, #1\n" |
| "b.ge loop2\n" |
| "skip:\n" |
| "add x10, x10, #2\n" |
| "cmp x3, x10\n" |
| "b.gt loop1\n" |
| "finished:\n" |
| "mov x10, #0\n" |
| "msr csselr_el1, x10\n" |
| "dsb sy\n" |
| "isb\n" |
| "mov x0, #0\n" |
| "ic ialluis\n" |
| "ret x12\n"); |
| } |
| #else |
| noinline void ramdump_sync_data(void) |
| { |
| flush_cache_all(); |
| } |
| #endif |
| |
| #endif /* end SAVE_DATA_BY_INIT_RC_SHELL */ |
| |
| /* |
| * clear memory to avoid large amount of memory not used. |
| * for random data, it's hard to compress |
| */ |
| static void lazy_clear_work(struct work_struct *work) |
| { |
| struct page *page; |
| struct list_head head, *pos, *next; |
| void *virt; |
| int order; |
| gfp_t flags = __GFP_NORETRY | |
| __GFP_NOWARN | |
| __GFP_MOVABLE; |
| unsigned long clear = 0, size = 0, free = 0, tick; |
| |
| INIT_LIST_HEAD(&head); |
| order = MAX_ORDER - 3; |
| tick = sched_clock(); |
| do { |
| page = alloc_pages(flags, order); |
| if (page) { |
| list_add(&page->lru, &head); |
| virt = page_address(page); |
| size = (1 << order) * PAGE_SIZE; |
| memset(virt, 0, size); |
| clear += size; |
| } |
| } while (page); |
| tick = sched_clock() - tick; |
| |
| list_for_each_safe(pos, next, &head) { |
| page = list_entry(pos, struct page, lru); |
| list_del(&page->lru); |
| __free_pages(page, order); |
| free += size; |
| } |
| pr_info("ramdump, clear:%lx, free:%lx, tick:%ld us\n", |
| clear, free, tick / 1000); |
| } |
| |
| #if defined(CONFIG_TRACEPOINTS) && defined(CONFIG_ANDROID_VENDOR_HOOKS) |
| static void do_flush_cpu_cache(void) |
| { |
| int cpu = smp_processor_id(); |
| |
| pr_info("ramdump: CPU-%d flush cache ...\n", cpu); |
| ramdump_sync_data(); |
| pr_info("ramdump: CPU-%d flush cache finish.\n", cpu); |
| } |
| |
| static void flush_all_cache_hook(void *data, struct pt_regs *regs) |
| { |
| do_flush_cpu_cache(); |
| } |
| |
| static int panic_notify(struct notifier_block *self, |
| unsigned long cmd, void *ptr) |
| { |
| do_flush_cpu_cache(); |
| return NOTIFY_DONE; |
| } |
| |
| static struct notifier_block panic_notifier = { |
| .notifier_call = panic_notify, |
| }; |
| #endif |
| |
| static int __init ramdump_probe(struct platform_device *pdev) |
| { |
| unsigned long total_mem; |
| struct resource *res; |
| unsigned int dump_set; |
| void __iomem *base; |
| void *vaddr = NULL; |
| int ret = 0; |
| |
| total_mem = get_num_physpages() << PAGE_SHIFT; |
| pr_info("ramdump, Total Memory:[%ld MB]\n", total_mem / 1024 / 1024); |
| |
| #if defined(CONFIG_TRACEPOINTS) && defined(CONFIG_ANDROID_VENDOR_HOOKS) |
| if (!ramdump_disable) { |
| /* flush cache for online cpu */ |
| ret = register_trace_android_vh_ipi_stop(flush_all_cache_hook, NULL); |
| if (ret) |
| pr_err("%s, register_trace_android_vh_ipi_stop err(%d).\n", __func__, ret); |
| |
| /* flush cache for panic cpu */ |
| ret = atomic_notifier_chain_register(&panic_notifier_list, &panic_notifier); |
| if (ret) |
| pr_err("%s, register panic_notifier err(%d).\n", __func__, ret); |
| } |
| #endif |
| |
| ram = kzalloc(sizeof(*ram), GFP_KERNEL); |
| if (!ram) |
| return -ENOMEM; |
| |
| if (ramdump_disable) |
| ram->disable = 1; |
| |
| ram->mem_base = 0; |
| ram->mem_size = ramdump_size; |
| if (!ramdump_base || !ramdump_size) { |
| pr_info("NO valid ramdump args:%lx %lx\n", |
| ramdump_base, ramdump_size); |
| } else { |
| pr_info("%s, memremap start, paddr area: 0x%08lx - 0x%08lx\n", |
| __func__, ramdump_base, ramdump_base + PAGE_ALIGN(ramdump_size)); |
| //vaddr = ioremap_cache(ramdump_base, PAGE_ALIGN(ramdump_size)); |
| vaddr = memremap(ramdump_base, PAGE_ALIGN(ramdump_size), MEMREMAP_WB); |
| if (vaddr) |
| ram->mem_base = (unsigned long)vaddr; |
| |
| pr_info("%s, memremap end, vaddr_base:%lx, size:%lx\n", |
| __func__, ram->mem_base, ram->mem_size); |
| } |
| |
| if (!ram->disable) { |
| if (!ram->mem_base) { /* No compressed data */ |
| INIT_DELAYED_WORK(&ram->work, lazy_clear_work); |
| schedule_delayed_work(&ram->work, msecs_to_jiffies(100)); |
| } else { /* with compressed data */ |
| #ifdef SAVE_DATA_BY_INIT_RC_SHELL |
| pr_info("%s, SAVE_DATA_BY_INIT_RC_SHELL\n", __func__); |
| ram->kobj = kobject_create_and_add("mdump", kernel_kobj); |
| if (!ram->kobj) { |
| pr_err("%s, create sysfs /mdump failed\n", __func__); |
| goto err; |
| } |
| |
| ramdump_attr.size = ram->mem_size; |
| pr_info("%s, creat /sys/kernel/mdump/compmsg\n", __func__); |
| if (sysfs_create_bin_file(ram->kobj, &ramdump_attr)) { |
| pr_err("%s, create sysfs compmsg failed\n", __func__); |
| goto err1; |
| } |
| #endif /* end SAVE_DATA_BY_INIT_RC_SHELL */ |
| } |
| |
| /* if ramdump is disabled in env, no need to set sticky reg */ |
| res = platform_get_resource_byname(pdev, IORESOURCE_MEM, |
| "SYSCTRL_STICKY_REG6"); |
| if (res) { |
| base = devm_ioremap(&pdev->dev, res->start, |
| res->end - res->start); |
| if (!base) { |
| pr_err("%s, map reg failed\n", __func__); |
| goto err; |
| } |
| dump_set = readl(base); |
| dump_set &= ~RAMDUMP_STICKY_DATA_MASK; |
| dump_set |= ((total_mem >> 20) | AMLOGIC_KERNEL_BOOTED); |
| writel(dump_set, base); |
| pr_info("%s, set sticky(0x%08x) to 0x%x\n", |
| __func__, (unsigned int)res->start, dump_set); |
| } |
| } |
| return ret; |
| #ifdef SAVE_DATA_BY_INIT_RC_SHELL |
| err1: |
| kobject_put(ram->kobj); |
| #endif |
| |
| err: |
| kfree(ram); |
| |
| return -EINVAL; |
| } |
| |
| static int ramdump_remove(struct platform_device *pdev) |
| { |
| #ifdef SAVE_DATA_BY_INIT_RC_SHELL |
| sysfs_remove_bin_file(ram->kobj, &ramdump_attr); |
| iounmap((void *)ram->mem_base); |
| kobject_put(ram->kobj); |
| #endif |
| kfree(ram); |
| return 0; |
| } |
| |
| #ifdef CONFIG_OF |
| static const struct of_device_id ramdump_dt_match[] = { |
| { |
| .compatible = "amlogic, ram_dump", |
| }, |
| {}, |
| }; |
| #endif |
| |
| static struct platform_driver ramdump_driver = { |
| .driver = { |
| .name = "mdump", |
| .owner = THIS_MODULE, |
| #ifdef CONFIG_OF |
| .of_match_table = ramdump_dt_match, |
| #endif |
| }, |
| .remove = ramdump_remove, |
| }; |
| |
| int __init ramdump_init(void) |
| { |
| int ret; |
| |
| ret = platform_driver_probe(&ramdump_driver, ramdump_probe); |
| |
| return ret; |
| } |
| |
| void __exit ramdump_uninit(void) |
| { |
| platform_driver_unregister(&ramdump_driver); |
| } |
| |