【分析笔记】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");