【分析笔记】Linux gpio_wdt.c 看门狗设备驱动源码分析
基本原理
该看门狗的设备驱动实现原理很简单,比较主要的有两点:
一、定时器喂狗
通过定时器根据配置文件配置的喂狗方式(如脉冲切换、电平切换),对指定的 gpio 进行脉冲切换或电平切换实现喂狗。
- 脉冲切换 
 指的是喂狗时,会给 gpio 一个 1us 宽度的高电平或低电平(取决于配置的 gpio 电平状态)。如设置为 1600ms,那么每 800ms 就会产生一个这样的脉冲信号。
- 电平切换 
 指的是喂狗时,会以固定的时间间隔,翻转 gpio 的电平状态,如设置看门狗超时时间为 1600ms,那么每 800ms 就会翻转一次 gpio 的电平状态,实现喂狗。
二、软硬件喂狗时间解耦
驱动将喂狗时间分为硬件喂狗时间和软件喂狗时间,很好的解决了软硬件时间的耦合问题,对上提供一个统一的喂狗时间,不受硬件芯片的实际喂狗时间限制,应用软件设计时不需要考虑底层采用了什么硬件。
在应用软件启动喂狗之后,驱动会启动定时器按照硬件看门狗芯片的最长超时复位时间的一半进行喂狗,如看门狗在 1600ms 内没有收到喂狗信号,就会产生复位动作,那么驱动就会取 1600ms 的一半,即定时器设置 800ms 为周期进行喂狗。
而应用软件可以设置喂狗时间范围为 1s ~ 65535s,驱动默认为 60s,如果应用软件没有在所设置的时间内调用 WDIOC_KEEPALIVE 进行喂狗(如60s) ,那么驱动程序就会停止给硬件喂狗,从而让硬件看门狗芯片产生复位信号,也就是说在 60s 内,驱动还是会通过定时器给硬件看门狗芯片继续喂狗,超过 60s 后,应用没有喂狗,那么定时器就会被停止。
- 硬件喂狗时间 
 不同的硬件看门狗芯片的看门狗超时时间都不一样,如 1.6s,那么就需要在 1.6s 内切换一次 gpio 信号进行喂狗,驱动取的是一半的时间,即 1.6s / 2 = 0.8s 喂狗,以便于留有一定的冗余时间。
- 软件喂狗时间 
 指的是应用程序进行喂狗的时间,内核预设最短时间为 1s,最大时间为 65535s,默认喂狗时间为 60s。
// 应用层进行喂狗的回调: 对应应用层 WDIOC_KEEPALIVE
static int gpio_wdt_ping(struct watchdog_device *wdd)
{
	// 更新最后一次喂狗时间
	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd);
	priv->last_jiffies = jiffies;
	return 0;
}
static void gpio_wdt_hwping(unsigned long data)
{
	struct watchdog_device *wdd = (struct watchdog_device *)data;
	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd);
	// 如果应用层已经启动喂狗, 则判断是否超出喂狗时间
	// 注意这里的喂狗时间仅仅只是软件驱动喂狗时间,而不是硬件看门狗时间。
	// 这里的好处是可以将应用层的喂狗时间与硬件看门狗喂狗的时间解耦出来。
	// 若软件喂狗时间超时,那不继续喂硬件看门狗就可以让硬件看门狗复位了。
	if (priv->armed && time_after(jiffies, priv->last_jiffies +
				      msecs_to_jiffies(wdd->timeout * 1000))) {
		dev_crit(wdd->parent, "Timer expired. System will reboot soon!\n");
		return;
	}
	// 重置定时器, 使之可以继续定时
	mod_timer(&priv->timer, jiffies + priv->hw_margin);
	// 根据喂狗方式, 选择电平切换方式或电平脉冲方式喂狗
	switch (priv->hw_algo) {
	case HW_ALGO_TOGGLE:
		/* Toggle output pin */
		priv->state = !priv->state;	// 使用电平切换方式
		gpio_set_value_cansleep(priv->gpio, priv->state);
		break;
	case HW_ALGO_LEVEL:
		/* Pulse */	// 使用电平脉冲方式
		gpio_set_value_cansleep(priv->gpio, !priv->active_low);
		udelay(1);
		gpio_set_value_cansleep(priv->gpio, priv->active_low);
		break;
	}
}
源码分析
以下源码对原生的 Linux 驱动在调用 of_get_gpio_flags() 时,为了适应全志平台做了一点小改动。
/*
 * Driver for watchdog device controlled through GPIO-line
 *
 * Author: 2013, Alexander Shiyan <shc_work@mail.ru>
 *
 * 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.
 */
#include <linux/err.h>
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/watchdog.h>
#include <linux/sunxi-gpio.h>
#define SOFT_TIMEOUT_MIN	1
#define SOFT_TIMEOUT_DEF	60
#define SOFT_TIMEOUT_MAX	0xffff
enum {
	HW_ALGO_TOGGLE,		// 切换方式
	HW_ALGO_LEVEL,		// 脉冲方式
};
struct gpio_wdt_priv {
	int						gpio;
	bool					active_low;
	bool					state;
	bool					always_running;
	bool					armed;
	unsigned int			hw_algo;
	unsigned int			hw_margin;
	unsigned long			last_jiffies;
	struct timer_list		timer;
	struct watchdog_device	wdd;
};
static void gpio_wdt_disable(struct gpio_wdt_priv *priv)
{
	// 根据电平有效性来决定设置什么电平,来关闭看门狗
	gpio_set_value_cansleep(priv->gpio, !priv->active_low);
	// 如果是电平切换方式,则恢复输入状态
	if (priv->hw_algo == HW_ALGO_TOGGLE)
		gpio_direction_input(priv->gpio);
}
static void gpio_wdt_hwping(unsigned long data)
{
	struct watchdog_device *wdd = (struct watchdog_device *)data;
	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd);
	// 如果应用层已经启动喂狗, 则判断是否超出喂狗时间
	// 注意这里的喂狗时间仅仅只是软件驱动喂狗时间,而不是硬件看门狗时间。
	// 这里的好处是可以将应用层的喂狗时间与硬件看门狗喂狗的时间解耦出来。
	// 若软件喂狗时间超时,那不继续喂硬件看门狗就可以让硬件看门狗复位了。
	if (priv->armed && time_after(jiffies, priv->last_jiffies +
				      msecs_to_jiffies(wdd->timeout * 1000))) {
		dev_crit(wdd->parent, "Timer expired. System will reboot soon!\n");
		return;
	}
	// 重置定时器, 使之可以继续定时
	mod_timer(&priv->timer, jiffies + priv->hw_margin);
	// 根据喂狗方式, 选择电平切换方式或电平脉冲方式喂狗
	switch (priv->hw_algo) {
	case HW_ALGO_TOGGLE:
		/* Toggle output pin */
		priv->state = !priv->state;	// 使用电平切换方式
		gpio_set_value_cansleep(priv->gpio, priv->state);
		break;
	case HW_ALGO_LEVEL:
		/* Pulse */	// 使用电平脉冲方式
		gpio_set_value_cansleep(priv->gpio, !priv->active_low);
		udelay(1);
		gpio_set_value_cansleep(priv->gpio, priv->active_low);
		break;
	}
}
// 驱动层自行启动看门狗, 区别于应用层启动看门狗
static void gpio_wdt_start_impl(struct gpio_wdt_priv *priv)
{
	priv->state = priv->active_low;
	gpio_direction_output(priv->gpio, priv->state);
	priv->last_jiffies = jiffies;
	// 这里调用了定时器的回调函数, 内部有 mod_timer() 可以实现启动定时器
	gpio_wdt_hwping((unsigned long)&priv->wdd);
}
// 应用层启动看门狗的回调, 对应应用层 WDIOC_SETOPTIONS->WDIOS_ENABLECARD
static int gpio_wdt_start(struct watchdog_device *wdd)
{
	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd);
	gpio_wdt_start_impl(priv);
	priv->armed = true; // 区分应用层启动还是设备驱动启动的看门狗
	return 0;
}
// 应用层关闭看门狗的回调, 对应应用层 WDIOC_SETOPTIONS->WDIOS_DISABLECARD
static int gpio_wdt_stop(struct watchdog_device *wdd)
{
	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd);
	priv->armed = false;
	// 如果配置 always_running = true 则不会停止定时器喂狗
	// 如果没有配置, 或者配置 always_running = false, 就会停止定时器喂狗并关闭看门狗
	if (!priv->always_running) {
		mod_timer(&priv->timer, 0);	// 停止定时器
		gpio_wdt_disable(priv);	// 关闭看门狗
	}
	return 0;
}
// 应用层进行喂狗的回调: 对应应用层 WDIOC_KEEPALIVE
static int gpio_wdt_ping(struct watchdog_device *wdd)
{
	// 更新最后一次喂狗时间
	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd);
	priv->last_jiffies = jiffies;
	return 0;
}
// 应用层设置超时时间的回调: 对应应用层 WDIOC_SETTIMEOUT
static int gpio_wdt_set_timeout(struct watchdog_device *wdd, unsigned int t)
{
	// 更新超时时间
	wdd->timeout = t;
	// 喂一次狗, 实际上在 watchdog_dev.c 里面调用此回调之后
	// 还会再调用一次 .ping 进行喂狗一次, 所以这里可有可无
	return gpio_wdt_ping(wdd);
}
// 配置此看门狗支持的操作
static const struct watchdog_info gpio_wdt_ident = {
	// WDIOF_MAGICCLOSE: 支持看门狗被应用层关闭
	// WDIOF_KEEPALIVEPING: 支持应用层喂狗
	// WDIOF_SETTIMEOUT: 支持应用层设置超时时间
	.options	= WDIOF_MAGICCLOSE | WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT,
	.identity	= "GPIO Watchdog",
};
// 看门狗对接上层应用的回调接口
static const struct watchdog_ops gpio_wdt_ops = {
	.owner		= THIS_MODULE,
	.start		= gpio_wdt_start,	// 启动看门狗,对应应用层 WDIOC_SETOPTIONS->WDIOS_ENABLECARD
	.stop		= gpio_wdt_stop,	// 关闭看门狗,对应应用层 WDIOC_SETOPTIONS->WDIOS_DISABLECARD
	.ping		= gpio_wdt_ping,	// 进行喂狗操作,对应应用层 WDIOC_KEEPALIVE
	.set_timeout	= gpio_wdt_set_timeout, // 设置看门狗超时时间, 设置的同时也会喂一次狗, 对应应用层 WDIOC_SETTIMEOUT
};
// 匹配上平台设备, 则会被执行
static int gpio_wdt_probe(struct platform_device *pdev)
{
	struct gpio_wdt_priv *priv;
	struct gpio_config gpio_flags;
	unsigned int hw_margin;
	unsigned long f = 0;
	const char *algo;
	int ret;
	// 分配空间,存储私有数据
	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
	// 作为私有数据放入到平台设备
	platform_set_drvdata(pdev, priv);
	// 读取连接看门狗芯片 WDI 引脚的 gpio
	priv->gpio = of_get_gpio_flags(pdev->dev.of_node, 0, (enum of_gpio_flags *)&gpio_flags);
	if (!gpio_is_valid(priv->gpio))
		return priv->gpio;
	// 根据当前 gpio 的电平状态作为电平有效性依据, 来确定关闭看门狗时该 gpio 的电平
	priv->active_low = gpio_flags.data & OF_GPIO_ACTIVE_LOW;
	// 读取清除看门狗计数的方式
	ret = of_property_read_string(pdev->dev.of_node, "hw_algo", &algo);
	if (ret)
		return ret;
	if (!strcmp(algo, "toggle")) { // 高低电平切换方式, 引脚设置为输入, 这样不会误触发看门狗
		priv->hw_algo = HW_ALGO_TOGGLE;
		f = GPIOF_IN;
	} else if (!strcmp(algo, "level")) { // 高电平或低电平脉冲方式, 并根据电平有效性来关闭喂狗
		priv->hw_algo = HW_ALGO_LEVEL;
		f = priv->active_low ? GPIOF_OUT_INIT_HIGH : GPIOF_OUT_INIT_LOW;
	} else {
		return -EINVAL;
	}
	// 申请 gpio 并配置初始状态
	ret = devm_gpio_request_one(&pdev->dev, priv->gpio, f, dev_name(&pdev->dev));
	if (ret)
		return ret;
	// 读取看门狗电路会触发复位的时间(毫秒), 该参数决定定时器定时时间
	ret = of_property_read_u32(pdev->dev.of_node,"hw_margin_ms", &hw_margin);
	if (ret)
		return ret;
	/* Disallow values lower than 2 and higher than 65535 ms */
	// 不能小于2: 安全的喂狗时间取 1/2, 因此不能小于 2
	// 不能大于 65535 : 不清楚为什么这么限制,意味着最大不能超过 65.535 秒
	if (hw_margin < 2 || hw_margin > 65535)
		return -EINVAL;
	// 取超时时间的一半作为喂狗时间比较安全
	/* Use safe value (1/2 of real timeout) */
	priv->hw_margin = msecs_to_jiffies(hw_margin / 2);
	// 读取看门狗配置,是否需要一直运行,配置为 TRUE 意味着此看门狗是不能被应用层关闭的
	priv->always_running = of_property_read_bool(pdev->dev.of_node, "always-running");
	// 作为私有数据放入到看门狗设备
	watchdog_set_drvdata(&priv->wdd, priv);
	// 设置看门狗支持的操作和相应的回调接口, 响应应用层的具体操作, 如启动、停止、设置超时时间等等。
	priv->wdd.info		= &gpio_wdt_ident;
	priv->wdd.ops		= &gpio_wdt_ops;
	priv->wdd.min_timeout	= SOFT_TIMEOUT_MIN;
	priv->wdd.max_timeout	= SOFT_TIMEOUT_MAX;
	priv->wdd.parent	= &pdev->dev;
	// 初始化软件喂狗超时时间的配置, 这里会被设置为默认值 60 秒.
	// 注意, 这里配置的只是应用层多长时间内没有喂狗
	if (watchdog_init_timeout(&priv->wdd, 0, &pdev->dev) < 0)
		priv->wdd.timeout = SOFT_TIMEOUT_DEF;
	// 安装定时器, 通过定时器来实现给硬件看门狗喂狗
	setup_timer(&priv->timer, gpio_wdt_hwping, (unsigned long)&priv->wdd);
	// 设置重启时关闭看门狗
	// 如果应用层启动了看门狗, 则重启时就会触发 stop 接口
	// 通过 watchdog_core.c 注册了重启函数调用链
	// 当应用层通过 watchdog_dev.c 启动了看门狗,在系统重启时在通知了链里面会触发 stop 接口
	watchdog_stop_on_reboot(&priv->wdd);
	// 作为看门狗设备驱动注册到系统中
	ret = watchdog_register_device(&priv->wdd);
	if (ret)
		return ret;
	// 如果需要一直运行, 则启动喂狗操作
	if (priv->always_running)
		gpio_wdt_start_impl(priv);
	return 0;
}
// 在驱动程序移除时执行
static int gpio_wdt_remove(struct platform_device *pdev)
{
	struct gpio_wdt_priv *priv = platform_get_drvdata(pdev);
	// 删除定时器, 不再继续喂狗
	del_timer_sync(&priv->timer);
	// 注销当前看门狗设备驱动
	watchdog_unregister_device(&priv->wdd);
	return 0;
}
// 匹配 dts 的名字
static const struct of_device_id gpio_wdt_dt_ids[] = {
	{ .compatible = "linux,wdt-gpio", },
	{ }
};
MODULE_DEVICE_TABLE(of, gpio_wdt_dt_ids);
static struct platform_driver gpio_wdt_driver = {
	.driver	= {
		.name		= "gpio-wdt",
		.of_match_table	= gpio_wdt_dt_ids,
	},
	.probe	= gpio_wdt_probe,
	.remove	= gpio_wdt_remove,
};
#ifdef CONFIG_GPIO_WATCHDOG_ARCH_INITCALL
static int __init gpio_wdt_init(void)
{
	//注册平台驱动
	return platform_driver_register(&gpio_wdt_driver);
}
arch_initcall(gpio_wdt_init);
#else
module_platform_driver(gpio_wdt_driver);
#endif
MODULE_AUTHOR("Alexander Shiyan <shc_work@mail.ru>");
MODULE_DESCRIPTION("GPIO Watchdog");
MODULE_LICENSE("GPL");
【分析笔记】Linux gpio_wdt.c 看门狗设备驱动源码分析的更多相关文章
- (转)Linux设备驱动之HID驱动 源码分析
		//Linux设备驱动之HID驱动 源码分析 http://blog.chinaunix.net/uid-20543183-id-1930836.html HID是Human Interface De ... 
- OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波
		http://blog.csdn.net/chenyusiyuan/article/details/8710462 OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 201 ... 
- linux设备驱动程序-i2c(0)-i2c设备驱动源码实现
		(基于4.14内核版本) 为了梳理清楚linux内核中的i2c实现框架,从本文开始,博主将分几个章节分别解析i2c总线在linux内核中的形成过程.匹配过程.以及设备驱动程序源码实现. 在介绍linu ... 
- Bytom Dapp 开发笔记(三):Dapp Demo前端源码分析
		本章内容会针对比原官方提供的dapp-demo,分析里面的前端源码,分析清楚整个demo的流程,然后针对里面开发过程遇到的坑,添加一下个人的见解还有解决的方案. 储蓄分红合约简述 为了方便理解,这里简 ... 
- 9.1 IIC驱动源码分析
		学习目标:分析linux内核源码下的i2c总线驱动 drivers/i2c/busses/i2c-s3c2410.c 和 driver/i2c/chips/eeprom.c 设备驱动: 一.i2c驱动 ... 
- 从flink-example分析flink组件(3)WordCount 流式实战及源码分析
		前面介绍了批量处理的WorkCount是如何执行的 <从flink-example分析flink组件(1)WordCount batch实战及源码分析> <从flink-exampl ... 
- Netty源码分析 (十二)----- 心跳服务之 IdleStateHandler 源码分析
		什么是心跳机制? 心跳说的是在客户端和服务端在互相建立ESTABLISH状态的时候,如何通过发送一个最简单的包来保持连接的存活,还有监控另一边服务的可用性等. 心跳包的作用 保活Q:为什么说心跳机制能 ... 
- linux 2.6 互斥锁的实现-源码分析
		http://blog.csdn.net/tq02h2a/article/details/4317211 看了看linux 2.6 kernel的源码,下面结合代码来分析一下在X86体系结构下,互斥锁 ... 
- 基于Netty的RPC架构学习笔记(五):netty线程模型源码分析(二)
		文章目录 小技巧(如何看开源框架的源码) 源码解析 阅读源码技巧 打印查看 通过打断点调试 查看调用栈 小技巧(如何看开源框架的源码) 一断点 二打印 三看调用栈 四搜索 源码解析 //设置nioso ... 
- 基于Netty的RPC架构学习笔记(四):netty线程模型源码分析(一)
		文章目录 如何提高NIO的工作效率 举个 
随机推荐
- 在博客中实现播放音乐功能(QQ,网易,酷狗,虾米,百度)
			1.在页头head标签里添加: <link rel="stylesheet" href="https://static.likepoems.com/cdn/apla ... 
- docker清空网络配置
			docker 网络 故障 相同的 ip 绑定给了 两个 网卡, 需要 清空 网络 ip addr | grep 10.79 inet 10.79.106.1/24 brd 10.79.106.255 ... 
- 某工控图片上传服务 CPU 爆高分析
			一:背景 1.讲故事 今天给大家带来一个入门级的 CPU 爆高案例,前段时间有位朋友找到我,说他的程序间歇性的 CPU 爆高,不知道是啥情况,让我帮忙看下,既然找到我,那就用 WinDbg 看一下. ... 
- 记一次node节点无法加入K8S集群
			#问题现象:root@small-virtual-machine:~# kubeadm join 10.0.0.133:6443 --token d2hyl5.5qt5fzjsdbxm2k5o ... 
- 链表实现-回文palindrome判断
			1.数字回文判断(逆转,分离未位,砍掉个位,保存原来) s = s * 10 + a%10 a = a/10 2.字符串判断回文 package main //思路: 开发一个栈来来存放链表的上半段f ... 
- MSP430中断小实验——通过按键改变小灯闪烁频率
			本小实验基于MSP430f5529,不同的型号可能管脚和中断配置有所不同. 实现的功能为: 第一次按下按键后,系统以每 2 秒钟,指示灯暗 1 秒,亮 1 秒的方式闪烁.程序采用默认时钟配置: 第二次 ... 
- [排序算法] 简单选择排序  (C++)
			简单选择排序原理 简单选择排序 SelectSort 是一种十分直观地排序方法.其原理是每次从未排序的元素中找到当前最小的元素,放在当前未排序序列的首位.一直重复操作直至最后未排序的元素个数为 0,即 ... 
- 小程序canvas2D绘制印章,话不多说,直接上代码
			效果图: CanvasContext 是旧版的接口,不维护了, 新版 Canvas 2D 接口与 Web 一致 官方文档: https://developers.weixin.qq.com/mini ... 
- Android Studio 卡在download fastutil下载慢
			需要替换国内镜像,现在阿里云地址已经更新了.需要使用新的地址.可以参考 https://developer.aliyun.com/mvn/guide 以下是更改buil.gradle文件的代码 // ... 
- 记录一次从linux移动一个项目到windows遇到的问题
			前言 这几天在linux平台写了一个垃圾软件,浪费了我10多天的时间,感觉很垃圾,然后我想在windows平台打包这个软件,然后出现了一个项目中有相同文件名的问题,导致一些文件相互覆盖 问题描述 我把 ... 
