| /* |
| * Blackfin nmi_watchdog Driver |
| * |
| * Originally based on bfin_wdt.c |
| * Copyright 2010-2010 Analog Devices Inc. |
| * Graff Yang <graf.yang@analog.com> |
| * |
| * Enter bugs at http://blackfin.uclinux.org/ |
| * |
| * Licensed under the GPL-2 or later. |
| */ |
| |
| #include <linux/bitops.h> |
| #include <linux/hardirq.h> |
| #include <linux/sysdev.h> |
| #include <linux/pm.h> |
| #include <linux/nmi.h> |
| #include <linux/smp.h> |
| #include <linux/timer.h> |
| #include <asm/blackfin.h> |
| #include <asm/atomic.h> |
| #include <asm/cacheflush.h> |
| #include <asm/bfin_watchdog.h> |
| |
| #define DRV_NAME "nmi-wdt" |
| |
| #define NMI_WDT_TIMEOUT 5 /* 5 seconds */ |
| #define NMI_CHECK_TIMEOUT (4 * HZ) /* 4 seconds in jiffies */ |
| static int nmi_wdt_cpu = 1; |
| |
| static unsigned int timeout = NMI_WDT_TIMEOUT; |
| static int nmi_active; |
| |
| static unsigned short wdoga_ctl; |
| static unsigned int wdoga_cnt; |
| static struct corelock_slot saved_corelock; |
| static atomic_t nmi_touched[NR_CPUS]; |
| static struct timer_list ntimer; |
| |
| enum { |
| COREA_ENTER_NMI = 0, |
| COREA_EXIT_NMI, |
| COREB_EXIT_NMI, |
| |
| NMI_EVENT_NR, |
| }; |
| static unsigned long nmi_event __attribute__ ((__section__(".l2.bss"))); |
| |
| /* we are in nmi, non-atomic bit ops is safe */ |
| static inline void set_nmi_event(int event) |
| { |
| __set_bit(event, &nmi_event); |
| } |
| |
| static inline void wait_nmi_event(int event) |
| { |
| while (!test_bit(event, &nmi_event)) |
| barrier(); |
| __clear_bit(event, &nmi_event); |
| } |
| |
| static inline void send_corea_nmi(void) |
| { |
| wdoga_ctl = bfin_read_WDOGA_CTL(); |
| wdoga_cnt = bfin_read_WDOGA_CNT(); |
| |
| bfin_write_WDOGA_CTL(WDEN_DISABLE); |
| bfin_write_WDOGA_CNT(0); |
| bfin_write_WDOGA_CTL(WDEN_ENABLE | ICTL_NMI); |
| } |
| |
| static inline void restore_corea_nmi(void) |
| { |
| bfin_write_WDOGA_CTL(WDEN_DISABLE); |
| bfin_write_WDOGA_CTL(WDOG_EXPIRED | WDEN_DISABLE | ICTL_NONE); |
| |
| bfin_write_WDOGA_CNT(wdoga_cnt); |
| bfin_write_WDOGA_CTL(wdoga_ctl); |
| } |
| |
| static inline void save_corelock(void) |
| { |
| saved_corelock = corelock; |
| corelock.lock = 0; |
| } |
| |
| static inline void restore_corelock(void) |
| { |
| corelock = saved_corelock; |
| } |
| |
| |
| static inline void nmi_wdt_keepalive(void) |
| { |
| bfin_write_WDOGB_STAT(0); |
| } |
| |
| static inline void nmi_wdt_stop(void) |
| { |
| bfin_write_WDOGB_CTL(WDEN_DISABLE); |
| } |
| |
| /* before calling this function, you must stop the WDT */ |
| static inline void nmi_wdt_clear(void) |
| { |
| /* clear TRO bit, disable event generation */ |
| bfin_write_WDOGB_CTL(WDOG_EXPIRED | WDEN_DISABLE | ICTL_NONE); |
| } |
| |
| static inline void nmi_wdt_start(void) |
| { |
| bfin_write_WDOGB_CTL(WDEN_ENABLE | ICTL_NMI); |
| } |
| |
| static inline int nmi_wdt_running(void) |
| { |
| return ((bfin_read_WDOGB_CTL() & WDEN_MASK) != WDEN_DISABLE); |
| } |
| |
| static inline int nmi_wdt_set_timeout(unsigned long t) |
| { |
| u32 cnt, max_t, sclk; |
| int run; |
| |
| sclk = get_sclk(); |
| max_t = -1 / sclk; |
| cnt = t * sclk; |
| if (t > max_t) { |
| pr_warning("NMI: timeout value is too large\n"); |
| return -EINVAL; |
| } |
| |
| run = nmi_wdt_running(); |
| nmi_wdt_stop(); |
| bfin_write_WDOGB_CNT(cnt); |
| if (run) |
| nmi_wdt_start(); |
| |
| timeout = t; |
| |
| return 0; |
| } |
| |
| int check_nmi_wdt_touched(void) |
| { |
| unsigned int this_cpu = smp_processor_id(); |
| unsigned int cpu; |
| |
| cpumask_t mask = cpu_online_map; |
| |
| if (!atomic_read(&nmi_touched[this_cpu])) |
| return 0; |
| |
| atomic_set(&nmi_touched[this_cpu], 0); |
| |
| cpu_clear(this_cpu, mask); |
| for_each_cpu_mask(cpu, mask) { |
| invalidate_dcache_range((unsigned long)(&nmi_touched[cpu]), |
| (unsigned long)(&nmi_touched[cpu])); |
| if (!atomic_read(&nmi_touched[cpu])) |
| return 0; |
| atomic_set(&nmi_touched[cpu], 0); |
| } |
| |
| return 1; |
| } |
| |
| static void nmi_wdt_timer(unsigned long data) |
| { |
| if (check_nmi_wdt_touched()) |
| nmi_wdt_keepalive(); |
| |
| mod_timer(&ntimer, jiffies + NMI_CHECK_TIMEOUT); |
| } |
| |
| static int __init init_nmi_wdt(void) |
| { |
| nmi_wdt_set_timeout(timeout); |
| nmi_wdt_start(); |
| nmi_active = true; |
| |
| init_timer(&ntimer); |
| ntimer.function = nmi_wdt_timer; |
| ntimer.expires = jiffies + NMI_CHECK_TIMEOUT; |
| add_timer(&ntimer); |
| |
| pr_info("nmi_wdt: initialized: timeout=%d sec\n", timeout); |
| return 0; |
| } |
| device_initcall(init_nmi_wdt); |
| |
| void touch_nmi_watchdog(void) |
| { |
| atomic_set(&nmi_touched[smp_processor_id()], 1); |
| } |
| |
| /* Suspend/resume support */ |
| #ifdef CONFIG_PM |
| static int nmi_wdt_suspend(struct sys_device *dev, pm_message_t state) |
| { |
| nmi_wdt_stop(); |
| return 0; |
| } |
| |
| static int nmi_wdt_resume(struct sys_device *dev) |
| { |
| if (nmi_active) |
| nmi_wdt_start(); |
| return 0; |
| } |
| |
| static struct sysdev_class nmi_sysclass = { |
| .name = DRV_NAME, |
| .resume = nmi_wdt_resume, |
| .suspend = nmi_wdt_suspend, |
| }; |
| |
| static struct sys_device device_nmi_wdt = { |
| .id = 0, |
| .cls = &nmi_sysclass, |
| }; |
| |
| static int __init init_nmi_wdt_sysfs(void) |
| { |
| int error; |
| |
| if (!nmi_active) |
| return 0; |
| |
| error = sysdev_class_register(&nmi_sysclass); |
| if (!error) |
| error = sysdev_register(&device_nmi_wdt); |
| return error; |
| } |
| late_initcall(init_nmi_wdt_sysfs); |
| |
| #endif /* CONFIG_PM */ |
| |
| |
| asmlinkage notrace void do_nmi(struct pt_regs *fp) |
| { |
| unsigned int cpu = smp_processor_id(); |
| nmi_enter(); |
| |
| cpu_pda[cpu].__nmi_count += 1; |
| |
| if (cpu == nmi_wdt_cpu) { |
| /* CoreB goes here first */ |
| |
| /* reload the WDOG_STAT */ |
| nmi_wdt_keepalive(); |
| |
| /* clear nmi interrupt for CoreB */ |
| nmi_wdt_stop(); |
| nmi_wdt_clear(); |
| |
| /* trigger NMI interrupt of CoreA */ |
| send_corea_nmi(); |
| |
| /* waiting CoreB to enter NMI */ |
| wait_nmi_event(COREA_ENTER_NMI); |
| |
| /* recover WDOGA's settings */ |
| restore_corea_nmi(); |
| |
| save_corelock(); |
| |
| /* corelock is save/cleared, CoreA is dummping messages */ |
| |
| wait_nmi_event(COREA_EXIT_NMI); |
| } else { |
| /* OK, CoreA entered NMI */ |
| set_nmi_event(COREA_ENTER_NMI); |
| } |
| |
| pr_emerg("\nNMI Watchdog detected LOCKUP, dump for CPU %d\n", cpu); |
| dump_bfin_process(fp); |
| dump_bfin_mem(fp); |
| show_regs(fp); |
| dump_bfin_trace_buffer(); |
| show_stack(current, (unsigned long *)fp); |
| |
| if (cpu == nmi_wdt_cpu) { |
| pr_emerg("This fault is not recoverable, sorry!\n"); |
| |
| /* CoreA dump finished, restore the corelock */ |
| restore_corelock(); |
| |
| set_nmi_event(COREB_EXIT_NMI); |
| } else { |
| /* CoreB dump finished, notice the CoreA we are done */ |
| set_nmi_event(COREA_EXIT_NMI); |
| |
| /* synchronize with CoreA */ |
| wait_nmi_event(COREB_EXIT_NMI); |
| } |
| |
| nmi_exit(); |
| } |