本文将介绍看门狗驱动的实现。
目标平台:TQ2440
CPU:s3c2440
内核版本:2.6.30
看门狗其实就是一个定时器,当该定时器溢出前必须对看门狗进行"喂狗“,如果不这样做,定时器溢出后则将复位CPU。
因此,看门狗通常用于对处于异常状态的CPU进行复位。
具体的概念请自行百度。
s3c2440的看门狗的原理框图如下:
可以看出,看门狗定时器的频率由PCLK提供,其预分频器最大取值为255+1;另外,通过MUX,可以进一步降低频率。
定时器采用递减模式,一旦到0,则可以触发看门狗中断以及RESET复位信号。
看门狗定时器的频率的计算公式如下:
看门狗驱动代码位于: linux/drivers/char/watchdog/s3c2410_wdt.c
static struct platform_driver s3c2410wdt_driver = { .probe = s3c2410wdt_probe, .remove = s3c2410wdt_remove, .shutdown = s3c2410wdt_shutdown, .suspend = s3c2410wdt_suspend, .resume = s3c2410wdt_resume, .driver = { .owner = THIS_MODULE, .name = "s3c2410-wdt", }, }; static char banner[] __initdata = KERN_INFO "S3C2410 Watchdog Timer, (c) 2004 Simtec Electronics\n"; static int __init watchdog_init(void){printk(banner);return platform_driver_register(&s3c2410wdt_driver);} module_init(watchdog_init)
模块的注册函数很简单,直接调用了 platform的驱动注册函数platform_driver_register。
该函数在注册时会调用驱动的probe方法,也即s3c2410wdt_probe函数。
我们来看下这个函数:
static int s3c2410wdt_probe(struct platform_device *pdev) { struct resource *res; struct device *dev; unsigned int wtcon; int started = 0; int ret; int size; DBG("%s: probe=%p\n", __func__, pdev); dev = &pdev->dev; wdt_dev = &pdev->dev; /* get the memory region for the watchdog timer */ /*获取平台资源,寄存器地址范围*/ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res == NULL) { dev_err(dev, "no memory resource specified\n"); return -ENOENT; } /*内存申请*/ size = (res->end - res->start) + 1; wdt_mem = request_mem_region(res->start, size, pdev->name); if (wdt_mem == NULL) { dev_err(dev, "failed to get memory region\n"); ret = -ENOENT; goto err_req; } /*内存映射*/ wdt_base = ioremap(res->start, size); if (wdt_base == NULL) { dev_err(dev, "failed to ioremap() region\n"); ret = -EINVAL; goto err_req; } DBG("probe: mapped wdt_base=%p\n", wdt_base); /*获取平台资源,看门狗定时器中断号*/ wdt_irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0); if (wdt_irq == NULL) { dev_err(dev, "no irq resource specified\n"); ret = -ENOENT; goto err_map; } /*注册看门狗定时器中断*/ ret = request_irq(wdt_irq->start, s3c2410wdt_irq, 0, pdev->name, pdev); if (ret != 0) { dev_err(dev, "failed to install irq (%d)\n", ret); goto err_map; } /*获取看门狗模块的时钟*/ wdt_clock = clk_get(&pdev->dev, "watchdog"); if (IS_ERR(wdt_clock)) { dev_err(dev, "failed to find watchdog clock source\n"); ret = PTR_ERR(wdt_clock); goto err_irq; } /*使能该时钟*/ clk_enable(wdt_clock); /* see if we can actually set the requested timer margin, and if * not, try the default value */ /*设置定时器模块的时钟频率*/ if (s3c2410wdt_set_heartbeat(tmr_margin)) { started = s3c2410wdt_set_heartbeat( CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME); if (started == 0) dev_info(dev, "tmr_margin value out of range, default %d used\n", CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME); else dev_info(dev, "default timer value is out of range, cannot start\n"); } /*注册混杂设备,设备名watchdog,次设备号130*/ ret = misc_register(&s3c2410wdt_miscdev); if (ret) { dev_err(dev, "cannot register miscdev on minor=%d (%d)\n", WATCHDOG_MINOR, ret); goto err_clk; } /* *如果需要在看门狗模块加载时启动看门狗则 *调用s3c2410wdt_start,否则调用s3c2410wdt_stop */ if (tmr_atboot && started == 0) { dev_info(dev, "starting watchdog timer\n"); s3c2410wdt_start(); } else if (!tmr_atboot) { /* if we're not enabling the watchdog, then ensure it is * disabled if it has been left running from the bootloader * or other source */ s3c2410wdt_stop(); } /* print out a statement of readiness */ /*读取控制寄存器,打印目前看门狗的状态*/ wtcon = readl(wdt_base + S3C2410_WTCON); dev_info(dev, "watchdog %sactive, reset %sabled, irq %sabled\n", (wtcon & S3C2410_WTCON_ENABLE) ? "" : "in", (wtcon & S3C2410_WTCON_RSTEN) ? "" : "dis", (wtcon & S3C2410_WTCON_INTEN) ? "" : "en"); return 0; err_clk: clk_disable(wdt_clock); clk_put(wdt_clock); err_irq: free_irq(wdt_irq->start, pdev); err_map: iounmap(wdt_base); err_req: release_resource(wdt_mem); kfree(wdt_mem); return ret; }
该函数的前面几步和其他驱动的类似:获取平台资源后进行相应的注册,并使能时钟。接着,将调用s3c2410wdt_set_heartbeat函数来设置看门狗的工作频率。
然后,注册一个混杂设备,为看门狗注册相应的API到内核中。最后,判断是否需要启动看门狗并调用相应的函数。
上面是probe函数大致的执行过程。随后我们看下其中被调用的s3c2410wdt_set_heartbeat函数,该函数将设置看门狗的工作频率。
PS:probe函数的执行依赖于平台设备,而看门狗平台设备的添加和注册在linux/arch/arm/plat-s3c24xx/devs.c和 linux/arch/arm/mach-s3c2410/mach-smdk2410.c中已经完成,因此对于看门狗驱动无需进行移植。
static int s3c2410wdt_set_heartbeat(int timeout) /*timeout 超时时间,单位秒*/ { unsigned int freq = clk_get_rate(wdt_clock); unsigned int count; unsigned int divisor = 1; unsigned long wtcon; if (timeout < 1) return -EINVAL; freq /= 128; /*时钟源为PCLK/128,不使用16 32 和64*/ count = timeout * freq; /*得出计数器值*/ DBG("%s: count=%d, timeout=%d, freq=%d\n", __func__, count, timeout, freq); /* if the count is bigger than the watchdog register, then work out what we need to do (and if) we can actually make this value */ /*计数器最大值为0xFFFF,如果大于则要计算Prescaler value*/ if (count >= 0x10000) { for (divisor = 1; divisor <= 0x100; divisor++) { /*Prescaler value最大为0xff*/ if ((count / divisor) < 0x10000) break; } /*找不到合适的Prescaler value,报错返回*/ if ((count / divisor) >= 0x10000) { dev_err(wdt_dev, "timeout %d too big\n", timeout); return -EINVAL; } } tmr_margin = timeout; /*保存timeout*/ DBG("%s: timeout=%d, divisor=%d, count=%d (%08x)\n", __func__, timeout, divisor, count, count/divisor); count /= divisor; /*根据Prescaler value计算出新的计数器值*/ wdt_count = count; /*保存计数器值*/ /* update the pre-scaler */ /* *设置预分频计数器和数据寄存器 * NOTE:此时并未使能看门狗定时器 */ wtcon = readl(wdt_base + S3C2410_WTCON); wtcon &= ~S3C2410_WTCON_PRESCALE_MASK; wtcon |= S3C2410_WTCON_PRESCALE(divisor-1); writel(count, wdt_base + S3C2410_WTDAT); writel(wtcon, wdt_base + S3C2410_WTCON); return 0; }
形参timeout为定时时间,单位为秒。
这里唯一需要注意的是freq/= 128这一步。在第2节我们看到,通过MUX,可选择的分频系数为16,32,64和128,但是在这里驱动直接使用了128来计算系数。
在下一节我们将会看到,驱动为什么在这里只使用了128这个分频系数。
当该函数调用结束时,Prescalervalue 和计数器值都将计算完成,并写入寄存器。
定时器的停止由 s3c2410wdt_stop函数完成。
static void __s3c2410wdt_stop(void) { unsigned long wtcon; wtcon = readl(wdt_base + S3C2410_WTCON); /*禁止看门狗,禁止RESET*/ wtcon &= ~(S3C2410_WTCON_ENABLE | S3C2410_WTCON_RSTEN); writel(wtcon, wdt_base + S3C2410_WTCON); } static void s3c2410wdt_stop(void) { spin_lock(&wdt_lock); __s3c2410wdt_stop(); spin_unlock(&wdt_lock); }
定时器的启动由s3c2410wdt_start函数完成。
static void s3c2410wdt_start(void) { unsigned long wtcon; spin_lock(&wdt_lock); __s3c2410wdt_stop(); ./*先禁止看门狗*/ wtcon = readl(wdt_base + S3C2410_WTCON); /*读取控制寄存器*/ /*启动定时器,设置分频系数为128*/ wtcon |= S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128; if (soft_noboot) { /*判断许是否需要RESET*/ wtcon |= S3C2410_WTCON_INTEN; /*使能看门狗中断*/ wtcon &= ~S3C2410_WTCON_RSTEN; /*取消RESET*/ } else { /*复位*/ wtcon &= ~S3C2410_WTCON_INTEN; /*禁止看门狗中断*/ wtcon |= S3C2410_WTCON_RSTEN; /*设置RESET*/ } DBG("%s: wdt_count=0x%08x, wtcon=%08lx\n", __func__, wdt_count, wtcon); writel(wdt_count, wdt_base + S3C2410_WTDAT); writel(wdt_count, wdt_base + S3C2410_WTCNT); /*写入控制器,此时将启动看门狗定时器*/ writel(wtcon, wdt_base + S3C2410_WTCON); spin_unlock(&wdt_lock); }
在这里我们到了一个宏S3C2410_WTCON_DIV128,这里设置了分频系数为128。而s3c2410wdt_start函数的调用肯定在s3c2410wdt_set_heartbeat之后,这也就是为什么在3.2节中使用了freq/= 128这一步。
定时器的保活由s3c2410wdt_keepalive函数完成。
static void s3c2410wdt_keepalive(void) { spin_lock(&wdt_lock); writel(wdt_count, wdt_base + S3C2410_WTCNT); /*重置计数器值*/ spin_unlock(&wdt_lock); }最后需要说明的是,从3.1节probe函数的执行来看,由于tmr_atboot变量的初值为0,因此看门狗定时器是没有工作的。
看门狗驱动提供的API如下:
static const struct file_operations s3c2410wdt_fops = { .owner = THIS_MODULE, .llseek = no_llseek, .write = s3c2410wdt_write, .unlocked_ioctl = s3c2410wdt_ioctl, .open = s3c2410wdt_open, .release = s3c2410wdt_release, };
static int s3c2410wdt_open(struct inode *inode, struct file *file) { if (test_and_set_bit(0, &open_lock))/*看门狗设备文件只能open一次*/ return -EBUSY; if (nowayout) __module_get(THIS_MODULE); /*增加模块引用计数*/ allow_close = CLOSE_STATE_NOT; /*设置标志位,不允许关闭看门狗*/ /* start the timer */ s3c2410wdt_start(); /*启动定时器*/ return nonseekable_open(inode, file); /*告知内核不支持llseek操作*/ }
static int s3c2410wdt_release(struct inode *inode, struct file *file) { /* * Shut off the timer. * Lock it in if it's a module and we set nowayout */ /*状态是允许关闭看门狗,则停止看门狗,否则保活*/ if (allow_close == CLOSE_STATE_ALLOW) s3c2410wdt_stop(); else { dev_err(wdt_dev, "Unexpected close, not stopping watchdog\n"); s3c2410wdt_keepalive(); } allow_close = CLOSE_STATE_NOT; /*设置标志位,不允许关闭看门狗*/ clear_bit(0, &open_lock); return 0; }
static ssize_t s3c2410wdt_write(struct file *file, const char __user *data, size_t len, loff_t *ppos) { /* * Refresh the timer. */ if (len) { /*nowayout 为真,不允许看门狗停止,使其保活*/ if (!nowayout) { size_t i; /* In case it was set long ago */ allow_close = CLOSE_STATE_NOT;/*设置标志位,不允许关闭看门狗*/ for (i = 0; i != len; i++) { char c; if (get_user(c, data + i)) /*从用户空间获取一个字节的数据*/ return -EFAULT; /*读取到字符V,设置标志位,允许关闭看门狗*/ if (c == 'V') allow_close = CLOSE_STATE_ALLOW; } } s3c2410wdt_keepalive(); /*保活*/ } return len; }
只要写入数据的长度不为0,都会调用s3c2410wdt_keepalive函数来重置定时器。
static long s3c2410wdt_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { void __user *argp = (void __user *)arg; int __user *p = argp; int new_margin; switch (cmd) { case WDIOC_GETSUPPORT: return copy_to_user(argp, &s3c2410_wdt_ident, sizeof(s3c2410_wdt_ident)) ? -EFAULT : 0; case WDIOC_GETSTATUS: case WDIOC_GETBOOTSTATUS: return put_user(0, p); case WDIOC_KEEPALIVE: s3c2410wdt_keepalive(); return 0; case WDIOC_SETTIMEOUT: if (get_user(new_margin, p)) return -EFAULT; if (s3c2410wdt_set_heartbeat(new_margin)) return -EINVAL; s3c2410wdt_keepalive(); return put_user(tmr_margin, p); case WDIOC_GETTIMEOUT: return put_user(tmr_margin, p); default: return -ENOTTY; } }
#include <stdio.h> #include <unistd.h> #include <linux/watchdog.h> #include <fcntl.h> int main(void) { int fd, val, ret; fd = open("/dev/watchdog", O_RDWR); if(fd < 0){ printf("open device fail\n"); return -1; } while(1){ ret = write(fd, &val, sizeof(val)); if(ret < 0){ perror("watchdog write wrong\n"); return -1; } sleep(5); } return 0; }
可以将5秒替换为16秒,你会发现系统自动重启了。
本文主要对基于S3C2440的看门狗驱动作出了分析。该驱动只是一个简单的字符设备,比较简单。其中,用于计算预分频系数的s3c2410wdt_set_heartbeat函数比较关键,读者可以好好琢磨下该系数是如何计算出来的。
Thank you for your time。
2013.1.30 添加测试程序