嵌入式Linux驱动——看门狗(watchdog)驱动

目标平台:TQ2440 

CPU:s3c2440

内核版本:2.6.30


1. 看门狗概述

   看门狗其实就是一个定时器,当该定时器溢出前必须对看门狗进行"喂狗“,如果不这样做,定时器溢出后则将复位CPU。

   因此,看门狗通常用于对处于异常状态的CPU进行复位。

   具体的概念请自行百度。

2. S3C2440看门狗

   s3c2440的看门狗的原理框图如下:


  可以看出,看门狗定时器的频率由PCLK提供,其预分频器最大取值为255+1;另外,通过MUX,可以进一步降低频率。

  定时器采用递减模式,一旦到0,则可以触发看门狗中断以及RESET复位信号。

  看门狗定时器的频率的计算公式如下:



3. 看门狗驱动

  看门狗驱动代码位于: linux/drivers/char/watchdog/s3c2410_wdt.c

3.1 模块注册以及probe函数

[cpp]  view plain  copy
  1. static struct platform_driver s3c2410wdt_driver = {  
  2.     .probe        = s3c2410wdt_probe,  
  3.     .remove        = s3c2410wdt_remove,  
  4.     .shutdown    = s3c2410wdt_shutdown,  
  5.     .suspend    = s3c2410wdt_suspend,  
  6.     .resume        = s3c2410wdt_resume,  
  7.     .driver        = {  
  8.         .owner    = THIS_MODULE,  
  9.         .name    = "s3c2410-wdt",  
  10.     },  
  11. };  
  12.   
  13. static char banner[] __initdata =  
  14.     KERN_INFO "S3C2410 Watchdog Timer, (c) 2004 Simtec Electronics\n";   
  15.   
  16. static int __init watchdog_init(void){printk(banner);return platform_driver_register(&s3c2410wdt_driver);}  
  17.   
  18. module_init(watchdog_init)  

模块的注册函数很简单,直接调用了 platform的驱动注册函数platform_driver_register。

 该函数在注册时会调用驱动的probe方法,也即s3c2410wdt_probe函数。

 我们来看下这个函数:

[cpp]  view plain  copy
  1. static int s3c2410wdt_probe(struct platform_device *pdev)  
  2. {  
  3.     struct resource *res;  
  4.     struct device *dev;  
  5.     unsigned int wtcon;  
  6.     int started = 0;  
  7.     int ret;  
  8.     int size;  
  9.   
  10.     DBG("%s: probe=%p\n", __func__, pdev);  
  11.   
  12.     dev = &pdev->dev;  
  13.     wdt_dev = &pdev->dev;  
  14.   
  15.     /* get the memory region for the watchdog timer */  
  16.     /*获取平台资源,寄存器地址范围*/  
  17.     res = platform_get_resource(pdev, IORESOURCE_MEM, 0);  
  18.     if (res == NULL) {  
  19.         dev_err(dev, "no memory resource specified\n");  
  20.         return -ENOENT;  
  21.     }  
  22.   
  23.     /*内存申请*/  
  24.     size = (res->end - res->start) + 1;  
  25.     wdt_mem = request_mem_region(res->start, size, pdev->name);  
  26.     if (wdt_mem == NULL) {  
  27.         dev_err(dev, "failed to get memory region\n");  
  28.         ret = -ENOENT;  
  29.         goto err_req;  
  30.     }  
  31.   
  32.     /*内存映射*/  
  33.     wdt_base = ioremap(res->start, size);  
  34.     if (wdt_base == NULL) {  
  35.         dev_err(dev, "failed to ioremap() region\n");  
  36.         ret = -EINVAL;  
  37.         goto err_req;  
  38.     }  
  39.   
  40.     DBG("probe: mapped wdt_base=%p\n", wdt_base);  
  41.       
  42.     /*获取平台资源,看门狗定时器中断号*/  
  43.     wdt_irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0);  
  44.     if (wdt_irq == NULL) {  
  45.         dev_err(dev, "no irq resource specified\n");  
  46.         ret = -ENOENT;  
  47.         goto err_map;  
  48.     }  
  49.       
  50.     /*注册看门狗定时器中断*/  
  51.     ret = request_irq(wdt_irq->start, s3c2410wdt_irq, 0, pdev->name, pdev);  
  52.     if (ret != 0) {  
  53.         dev_err(dev, "failed to install irq (%d)\n", ret);  
  54.         goto err_map;  
  55.     }  
  56.     /*获取看门狗模块的时钟*/  
  57.     wdt_clock = clk_get(&pdev->dev, "watchdog");  
  58.     if (IS_ERR(wdt_clock)) {  
  59.         dev_err(dev, "failed to find watchdog clock source\n");  
  60.         ret = PTR_ERR(wdt_clock);  
  61.         goto err_irq;  
  62.     }  
  63.   
  64.     /*使能该时钟*/  
  65.     clk_enable(wdt_clock);  
  66.   
  67.     /* see if we can actually set the requested timer margin, and if 
  68.      * not, try the default value */  
  69.   
  70.     /*设置定时器模块的时钟频率*/  
  71.     if (s3c2410wdt_set_heartbeat(tmr_margin)) {  
  72.         started = s3c2410wdt_set_heartbeat(  
  73.                     CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME);  
  74.   
  75.         if (started == 0)  
  76.             dev_info(dev,  
  77.                "tmr_margin value out of range, default %d used\n",  
  78.                    CONFIG_S3C2410_WATCHDOG_DEFAULT_TIME);  
  79.         else  
  80.             dev_info(dev, "default timer value is out of range, cannot start\n");  
  81.     }  
  82.   
  83.     /*注册混杂设备,设备名watchdog,次设备号130*/  
  84.     ret = misc_register(&s3c2410wdt_miscdev);  
  85.     if (ret) {  
  86.         dev_err(dev, "cannot register miscdev on minor=%d (%d)\n",  
  87.             WATCHDOG_MINOR, ret);  
  88.         goto err_clk;  
  89.     }  
  90.   
  91.     /* 
  92.       *如果需要在看门狗模块加载时启动看门狗则 
  93.       *调用s3c2410wdt_start,否则调用s3c2410wdt_stop 
  94.     */  
  95.       
  96.     if (tmr_atboot && started == 0) {  
  97.         dev_info(dev, "starting watchdog timer\n");  
  98.         s3c2410wdt_start();  
  99.     } else if (!tmr_atboot) {  
  100.         /* if we're not enabling the watchdog, then ensure it is 
  101.          * disabled if it has been left running from the bootloader 
  102.          * or other source */  
  103.   
  104.         s3c2410wdt_stop();  
  105.     }  
  106.   
  107.     /* print out a statement of readiness */  
  108.     /*读取控制寄存器,打印目前看门狗的状态*/  
  109.     wtcon = readl(wdt_base + S3C2410_WTCON);  
  110.   
  111.     dev_info(dev, "watchdog %sactive, reset %sabled, irq %sabled\n",  
  112.          (wtcon & S3C2410_WTCON_ENABLE) ?  "" : "in",  
  113.          (wtcon & S3C2410_WTCON_RSTEN) ? "" : "dis",  
  114.          (wtcon & S3C2410_WTCON_INTEN) ? "" : "en");  
  115.   
  116.     return 0;  
  117.   
  118.  err_clk:  
  119.     clk_disable(wdt_clock);  
  120.     clk_put(wdt_clock);  
  121.   
  122.  err_irq:  
  123.     free_irq(wdt_irq->start, pdev);  
  124.   
  125.  err_map:  
  126.     iounmap(wdt_base);  
  127.   
  128.  err_req:  
  129.     release_resource(wdt_mem);  
  130.     kfree(wdt_mem);  
  131.   
  132.     return ret;  
  133. }  

该函数的前面几步和其他驱动的类似:获取平台资源后进行相应的注册,并使能时钟。接着,将调用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中已经完成,因此对于看门狗驱动无需进行移植

3.2 s3c2410wdt_set_heartbeat

[cpp]  view plain  copy
  1. static int s3c2410wdt_set_heartbeat(int timeout)  /*timeout 超时时间,单位秒*/  
  2. {  
  3.     unsigned int freq = clk_get_rate(wdt_clock);  
  4.     unsigned int count;  
  5.     unsigned int divisor = 1;  
  6.     unsigned long wtcon;  
  7.   
  8.     if (timeout < 1)  
  9.         return -EINVAL;  
  10.   
  11.     freq /= 128;    /*时钟源为PCLK/128,不使用16 32 和64*/  
  12.     count = timeout * freq;  /*得出计数器值*/  
  13.   
  14.     DBG("%s: count=%d, timeout=%d, freq=%d\n",  
  15.         __func__, count, timeout, freq);  
  16.   
  17.     /* if the count is bigger than the watchdog register, 
  18.        then work out what we need to do (and if) we can 
  19.        actually make this value 
  20.     */  
  21.     /*计数器最大值为0xFFFF,如果大于则要计算Prescaler value*/  
  22.     if (count >= 0x10000) {  
  23.         for (divisor = 1; divisor <= 0x100; divisor++) {     /*Prescaler value最大为0xff*/  
  24.             if ((count / divisor) < 0x10000)  
  25.                 break;  
  26.         }  
  27.     /*找不到合适的Prescaler value,报错返回*/  
  28.         if ((count / divisor) >= 0x10000) {  
  29.             dev_err(wdt_dev, "timeout %d too big\n", timeout);  
  30.             return -EINVAL;  
  31.         }  
  32.     }  
  33.   
  34.     tmr_margin = timeout;  /*保存timeout*/  
  35.   
  36.     DBG("%s: timeout=%d, divisor=%d, count=%d (%08x)\n",  
  37.         __func__, timeout, divisor, count, count/divisor);  
  38.   
  39.     count /= divisor;   /*根据Prescaler value计算出新的计数器值*/  
  40.     wdt_count = count; /*保存计数器值*/  
  41.   
  42.     /* update the pre-scaler */   
  43.     /* 
  44.       *设置预分频计数器和数据寄存器 
  45.         * NOTE:此时并未使能看门狗定时器 
  46.        */  
  47.     wtcon = readl(wdt_base + S3C2410_WTCON);  
  48.     wtcon &= ~S3C2410_WTCON_PRESCALE_MASK;  
  49.     wtcon |= S3C2410_WTCON_PRESCALE(divisor-1);  
  50.   
  51.     writel(count, wdt_base + S3C2410_WTDAT);  
  52.     writel(wtcon, wdt_base + S3C2410_WTCON);  
  53.   
  54.     return 0;  
  55. }  

形参timeout为定时时间,单位为秒。

  这里唯一需要注意的是freq/= 128这一步。在第2节我们看到,通过MUX,可选择的分频系数为16,32,64和128,但是在这里驱动直接使用了128来计算系数。

  在下一节我们将会看到,驱动为什么在这里只使用了128这个分频系数。

  当该函数调用结束时,Prescalervalue 和计数器值都将计算完成,并写入寄存器。

3.3 定时器的启动、停止和保活

3.3.1 停止

定时器的停止由 s3c2410wdt_stop函数完成。

[cpp]  view plain  copy
  1. static void __s3c2410wdt_stop(void)  
  2. {  
  3.     unsigned long wtcon;  
  4.   
  5.     wtcon = readl(wdt_base + S3C2410_WTCON);  
  6.     /*禁止看门狗,禁止RESET*/  
  7.     wtcon &= ~(S3C2410_WTCON_ENABLE | S3C2410_WTCON_RSTEN);  
  8.     writel(wtcon, wdt_base + S3C2410_WTCON);  
  9. }  
  10.   
  11. static void s3c2410wdt_stop(void)  
  12. {  
  13.     spin_lock(&wdt_lock);  
  14.     __s3c2410wdt_stop();  
  15.     spin_unlock(&wdt_lock);  
  16. }  

3.3.2 启动

定时器的启动由s3c2410wdt_start函数完成。

[cpp]  view plain  copy
  1. static void s3c2410wdt_start(void)  
  2. {  
  3.     unsigned long wtcon;  
  4.   
  5.     spin_lock(&wdt_lock);  
  6.   
  7.     __s3c2410wdt_stop(); ./*先禁止看门狗*/  
  8.   
  9.     wtcon = readl(wdt_base + S3C2410_WTCON); /*读取控制寄存器*/  
  10.     /*启动定时器,设置分频系数为128*/  
  11.     wtcon |= S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128;   
  12.   
  13.     if (soft_noboot) { /*判断许是否需要RESET*/  
  14.         wtcon |= S3C2410_WTCON_INTEN;   /*使能看门狗中断*/  
  15.         wtcon &= ~S3C2410_WTCON_RSTEN;  /*取消RESET*/  
  16.     } else {  /*复位*/  
  17.         wtcon &= ~S3C2410_WTCON_INTEN; /*禁止看门狗中断*/  
  18.         wtcon |= S3C2410_WTCON_RSTEN;  /*设置RESET*/  
  19.     }  
  20.   
  21.     DBG("%s: wdt_count=0x%08x, wtcon=%08lx\n",  
  22.         __func__, wdt_count, wtcon);  
  23.   
  24.     writel(wdt_count, wdt_base + S3C2410_WTDAT);  
  25.     writel(wdt_count, wdt_base + S3C2410_WTCNT);  
  26.     /*写入控制器,此时将启动看门狗定时器*/  
  27.     writel(wtcon, wdt_base + S3C2410_WTCON);      
  28.     spin_unlock(&wdt_lock);  
  29. }  

在这里我们到了一个宏S3C2410_WTCON_DIV128,这里设置了分频系数为128。而s3c2410wdt_start函数的调用肯定在s3c2410wdt_set_heartbeat之后,这也就是为什么在3.2节中使用了freq/= 128这一步。

3.3.3 保活

定时器的保活由s3c2410wdt_keepalive函数完成。

[cpp]  view plain  copy
  1. static void s3c2410wdt_keepalive(void)  
  2. {  
  3.     spin_lock(&wdt_lock);  
  4.     writel(wdt_count, wdt_base + S3C2410_WTCNT); /*重置计数器值*/  
  5.     spin_unlock(&wdt_lock);  
  6. }  
最后需要说明的是,从3.1节probe函数的执行来看,由于tmr_atboot变量的初值为0,因此看门狗定时器是没有工作的。


3.4 看门狗驱动API

看门狗驱动提供的API如下:

[cpp]  view plain  copy
  1. static const struct file_operations s3c2410wdt_fops = {  
  2.     .owner      = THIS_MODULE,  
  3.     .llseek     = no_llseek,  
  4.     .write      = s3c2410wdt_write,  
  5.     .unlocked_ioctl = s3c2410wdt_ioctl,  
  6.     .open       = s3c2410wdt_open,  
  7.     .release    = s3c2410wdt_release,  
  8. };  

我们可以看到驱动提供了4个API,同时,驱动并不支持llseek方法。

3.4.1 open方法

[cpp]  view plain  copy
  1. static int s3c2410wdt_open(struct inode *inode, struct file *file)  
  2. {  
  3.     if (test_and_set_bit(0, &open_lock))/*看门狗设备文件只能open一次*/  
  4.         return -EBUSY;  
  5.   
  6.     if (nowayout)  
  7.         __module_get(THIS_MODULE); /*增加模块引用计数*/  
  8.   
  9.     allow_close = CLOSE_STATE_NOT;  /*设置标志位,不允许关闭看门狗*/  
  10.   
  11.     /* start the timer */  
  12.     s3c2410wdt_start();     /*启动定时器*/  
  13.     return nonseekable_open(inode, file);  /*告知内核不支持llseek操作*/  
  14. }  

这里需要注意的是,设备文件/dev/watchdog 只能被open一次,大于一次的open都将返回-EBUSY。

3.4.2 release方法

[cpp]  view plain  copy
  1. static int s3c2410wdt_release(struct inode *inode, struct file *file)  
  2. {  
  3.     /* 
  4.      *  Shut off the timer. 
  5.      *  Lock it in if it's a module and we set nowayout 
  6.      */  
  7.       /*状态是允许关闭看门狗,则停止看门狗,否则保活*/  
  8.     if (allow_close == CLOSE_STATE_ALLOW)  
  9.         s3c2410wdt_stop();  
  10.     else {  
  11.         dev_err(wdt_dev, "Unexpected close, not stopping watchdog\n");  
  12.         s3c2410wdt_keepalive();  
  13.     }  
  14.     allow_close = CLOSE_STATE_NOT;  /*设置标志位,不允许关闭看门狗*/  
  15.     clear_bit(0, &open_lock);  
  16.     return 0;  
  17. }  

3.4.3 wirte方法

[cpp]  view plain  copy
  1. static ssize_t s3c2410wdt_write(struct file *file, const char __user *data,  
  2.                 size_t len, loff_t *ppos)  
  3. {  
  4.     /* 
  5.      *  Refresh the timer. 
  6.      */  
  7.     if (len) {  
  8.         /*nowayout 为真,不允许看门狗停止,使其保活*/  
  9.         if (!nowayout) {  
  10.             size_t i;  
  11.   
  12.             /* In case it was set long ago */  
  13.             allow_close = CLOSE_STATE_NOT;/*设置标志位,不允许关闭看门狗*/  
  14.   
  15.             for (i = 0; i != len; i++) {  
  16.                 char c;  
  17.   
  18.                 if (get_user(c, data + i)) /*从用户空间获取一个字节的数据*/  
  19.                     return -EFAULT;  
  20.                 /*读取到字符V,设置标志位,允许关闭看门狗*/  
  21.                 if (c == 'V')         
  22.                     allow_close = CLOSE_STATE_ALLOW;  
  23.             }  
  24.         }  
  25.         s3c2410wdt_keepalive(); /*保活*/  
  26.     }  
  27.     return len;  
  28. }  

只要写入数据的长度不为0,都会调用s3c2410wdt_keepalive函数来重置定时器。

3.4.4 unlocked_ioctl方法

[cpp]  view plain  copy
  1. static long s3c2410wdt_ioctl(struct file *file, unsigned int cmd,  
  2.                             unsigned long arg)  
  3. {  
  4.     void __user *argp = (void __user *)arg;  
  5.     int __user *p = argp;  
  6.     int new_margin;  
  7.   
  8.     switch (cmd) {  
  9.     case WDIOC_GETSUPPORT:  
  10.         return copy_to_user(argp, &s3c2410_wdt_ident,  
  11.             sizeof(s3c2410_wdt_ident)) ? -EFAULT : 0;  
  12.     case WDIOC_GETSTATUS:  
  13.     case WDIOC_GETBOOTSTATUS:  
  14.         return put_user(0, p);  
  15.     case WDIOC_KEEPALIVE:  
  16.         s3c2410wdt_keepalive();  
  17.         return 0;  
  18.     case WDIOC_SETTIMEOUT:  
  19.         if (get_user(new_margin, p))  
  20.             return -EFAULT;  
  21.         if (s3c2410wdt_set_heartbeat(new_margin))  
  22.             return -EINVAL;  
  23.         s3c2410wdt_keepalive();  
  24.         return put_user(tmr_margin, p);  
  25.     case WDIOC_GETTIMEOUT:  
  26.         return put_user(tmr_margin, p);  
  27.     default:  
  28.         return -ENOTTY;  
  29.     }  
  30. }  

4. 测试程序

[cpp]  view plain  copy
  1. #include <stdio.h>  
  2. #include <unistd.h>  
  3. #include <linux/watchdog.h>  
  4. #include <fcntl.h>  
  5.   
  6. int main(void)  
  7. {  
  8.     int fd, val, ret;  
  9.       
  10.     fd = open("/dev/watchdog", O_RDWR);  
  11.     if(fd < 0){  
  12.         printf("open device fail\n");  
  13.         return -1;  
  14.     }  
  15.       
  16.     while(1){  
  17.         ret = write(fd, &val, sizeof(val));  
  18.         if(ret < 0){  
  19.             perror("watchdog write wrong\n");  
  20.             return -1;  
  21.         }  
  22.         sleep(5);  
  23.     }  
  24.       
  25.     return 0;  
  26. }  

该测试程序每隔5秒重置看门狗定时器,而驱动默认的超时时间是15秒。

可以将5秒替换为16秒,你会发现系统自动重启了。

5. 结束语

   本文主要对基于S3C2440的看门狗驱动作出了分析。该驱动只是一个简单的字符设备,比较简单。其中,用于计算预分频系数的s3c2410wdt_set_heartbeat函数比较关键,读者可以好好琢磨下该系数是如何计算出来的。

猜你喜欢

转载自blog.csdn.net/qq_31186123/article/details/79571326