基于S3C2440的嵌入式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函数

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中已经完成,因此对于看门狗驱动无需进行移植

3.2 s3c2410wdt_set_heartbeat

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 和计数器值都将计算完成,并写入寄存器。

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

3.3.1 停止

定时器的停止由 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);
}

3.3.2 启动

定时器的启动由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这一步。

3.3.3 保活

定时器的保活由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,因此看门狗定时器是没有工作的。


3.4 看门狗驱动API

看门狗驱动提供的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,
};

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

3.4.1 open方法

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操作*/
}

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

3.4.2 release方法

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;
}

3.4.3 wirte方法
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函数来重置定时器。

3.4.4 unlocked_ioctl方法

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;
	}
}

4. 测试程序

#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秒重置看门狗定时器,而驱动默认的超时时间是15秒。

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

5. 结束语

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

  Thank you for your time。

2013.1.30 添加测试程序

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值