首先看一下Openwrt系统中关于按键功能的使用和修改,以18.06版本为例
按键功能实现在脚本中, 比如18.06/package/base-files/files/etc/rc.button/reset
#!/bin/sh
. /lib/functions.sh
OVERLAY="$( grep ' /overlay ' /proc/mounts )"
case "$ACTION" in
pressed)
[ -z "$OVERLAY" ] && return 0
return 5
;;
timeout)
. /etc/diag.sh
set_state failsafe
;;
released)
if [ "$SEEN" -lt 1 ]
then
echo "REBOOT" > /dev/console
sync
reboot
elif [ "$SEEN" -ge 5 -a -n "$OVERLAY" ]
then
echo "FACTORY RESET" > /dev/console
jffs2reset -y && reboot &
fi
;;
esac
return 0
主要有2个参数, ACTION和SEEN,分别代表按键动作(按下/抬起)和按键持续时间
关于按键GPIO的修改位于dts中,比如target/linux/ramips/dts/GL-MT300N-V2.dts
gpio-keys {
compatible = "gpio-keys-polled";
#address-cells = <1>;
#size-cells = <0>;
poll-interval = <20>;
vccin {
label = "BTN_0";
gpios = <&gpio0 6 0>;
linux,code = <BTN_0>;
};
electricity {
label = "BTN_1";
gpios = <&gpio0 11 0>;
linux,code = <BTN_1>;
};
reset {
label = "reset";
gpios = <&gpio1 7 GPIO_ACTIVE_LOW>;
linux,code = <KEY_RESTART>;
};
};
poll-interval = <20>; 表示轮询检测,防抖时间
label = "reset"; 代表功能实现脚本的名称,对应18.06/package/base-files/files/etc/rc.button/reset
gpios = <&gpio1 7 GPIO_ACTIVE_LOW>; 表示使用GPIO1分组,第7个引脚; 低电平生效
linux,code = <KEY_RESTART>; 按键事件代码,对于linux标准输入输出系统,参考Linux内核头文件input/input.h
compatible = "gpio-keys-polled"; 表示加载驱动gpio-keys-polled
位于18.06/package/kernel/gpio-button-hotplug/src/gpio-button-hotplug.c
它具体实现主要内容如下:
1. 注册2种类型驱动,轮询检测和中断检测
2. 使用netlink与用户空间通讯
3. 自定义上报事件内容, 比如前面功能实现脚本中用到的ACTION以及SEEN等
分析内核驱动,首先要看全局结构体定义,这几乎是分析linux所有驱动的共性
2个主要结构
struct gpio_keys_platform_data *pdata; 满足linux内核驱动模型, platform总线驱动设备描述结构,主要存放的是dts中关于设备的描述信息
struct gpio_keys_button_dev *bdev; 驱动私有描述结构
其二者关系为,
1. 取出pdata信息初始化bdev
2. 把bdev设置为pdata的私有数据,与其他驱动接口同步,platform_set_drvdata(pdev, bdev);
以上的操作为linux驱动的标准执行过程,有兴趣的同学可以深入研究下Linux内核驱动模型
对于驱动来说,最重要的是驱动自己的私有结构,即gpio_keys_button_dev
struct gpio_keys_button_dev {
int polled; //是否轮询
struct delayed_work work; //用于轮询时的循环检测
struct device *dev; //设备,来自pdev
struct gpio_keys_platform_data *pdata; //platform总线设备描述
struct gpio_keys_button_data data[0];//事件上报描述结构
};
事件描述gpio_keys_button_data
struct gpio_keys_button_data {
struct delayed_work work; //执行上报动作
struct bh_priv bh;
int last_state; //状态记录
int count; //按键计时
int threshold; //防抖域值
int can_sleep; //是否支持睡眠
struct gpio_keys_button *b; //中断设备描述
};
介绍完驱动结构描述,下面看其使用
首先看轮询检测:
static struct platform_driver gpio_keys_polled_driver = {
.probe = gpio_keys_polled_probe,
.remove = gpio_keys_remove,
.driver = {
.name = "gpio-keys-polled",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(gpio_keys_polled_of_match),
},
};
主要实现函数gpio_keys_polled_probe
static int gpio_keys_polled_probe(struct platform_device *pdev)
{
struct gpio_keys_platform_data *pdata;
struct gpio_keys_button_dev *bdev;
int ret;
int i;
//从platform总线设备描述gpio_keys_platform_data获取信息,
//初始化驱动私有结构gpio_keys_button_dev
ret = gpio_keys_button_probe(pdev, &bdev, 1);
if (ret)
return ret;
//初始化工作队列
INIT_DELAYED_WORK(&bdev->work, gpio_keys_polled_poll);
pdata = bdev->pdata;
if (pdata->enable)
pdata->enable(bdev->dev);
for (i = 0; i < pdata->nbuttons; i++)
//逐个检测button状态,准备上报事件数据gpio_keys_button_data
gpio_keys_polled_check_state(&bdev->data[i]);
//循环检测button状态
gpio_keys_polled_queue_work(bdev);
return ret;
}
具体事件上报位于button_hotplug_create_event
static int button_hotplug_create_event(const char *name, unsigned int type,
unsigned long seen, int pressed)
{
struct bh_event *event;
BH_DBG("create event, name=%s, seen=%lu, pressed=%d\n",
name, seen, pressed);
event = kzalloc(sizeof(*event), GFP_KERNEL);
if (!event)
return -ENOMEM;
// 填充事件信息
event->name = name;
event->type = type;
event->seen = seen;
event->action = pressed ? "pressed" : "released";
// 在工作队列中上报事件
INIT_WORK(&event->work, (void *)(void *)button_hotplug_work);
schedule_work(&event->work);
return 0;
}
上报事件的netlink实现button_hotplug_work
static void button_hotplug_work(struct work_struct *work)
{
struct bh_event *event = container_of(work, struct bh_event, work);
int ret = 0;
//分配skb
event->skb = alloc_skb(BH_SKB_SIZE, GFP_KERNEL);
if (!event->skb)
goto out_free_event;
//填充内容
ret = bh_event_add_var(event, 0, "%s@", event->action);
if (ret)
goto out_free_skb;
ret = button_hotplug_fill_event(event);
if (ret)
goto out_free_skb;
//发送netlink消息
NETLINK_CB(event->skb).dst_group = 1;
broadcast_uevent(event->skb, 0, 1, GFP_KERNEL);
out_free_skb:
if (ret) {
BH_ERR("work error %d\n", ret);
kfree_skb(event->skb);
}
out_free_event:
kfree(event);
}
整个流程总结: 周期循环调用gpio_keys_polled_probe检测按键状态,满足条件发送netlink消息
gpio_keys_polled_probe
--> gpio_keys_polled_check_state
--> button_hotplug_create_event
-->button_hotplug_work
--> broadcast_uevent
有了以上的分析基础,中断检测流程就变得简单了
static struct platform_driver gpio_keys_driver = {
.probe = gpio_keys_probe,
.remove = gpio_keys_remove,
.driver = {
.name = "gpio-keys",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(gpio_keys_of_match),
},
};
主要流程如下:
gpio_keys_probe
--> devm_request_threaded_irq 注册中断
--> button_handle_irq 中断处理函数
--> button_hotplug_create_event
-->button_hotplug_work
-->broadcast_uevent
关于Netlink
Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。
在Linux 内核中,使用netlink 进行应用与内核通信的应用有很多,如
- 路由 daemon(NETLINK_ROUTE)
- 用户态 socket 协议(NETLINK_USERSOCK)
- 防火墙(NETLINK_FIREWALL)
- netfilter 子系统(NETLINK_NETFILTER)
- 内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)
- 通用netlink(NETLINK_GENERIC)
Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。
Openwrt系统中netlink消息接收,位于procd-2018-03-28-dfb68f85/plug/hotplug.c
procd是openwrt系统的init进程,负责内核netlink消息接收处理,watchdog以及执行一些循环任务等等
void hotplug(char *rules)
{
struct sockaddr_nl nls = {};
int nlbufsize = 512 * 1024;
rule_file = strdup(rules);
nls.nl_family = AF_NETLINK;
nls.nl_pid = getpid();
nls.nl_groups = -1;
//创建netlink socket
if ((hotplug_fd.fd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT)) == -1) {
ERROR("Failed to open hotplug socket: %m\n");
exit(1);
}
if (bind(hotplug_fd.fd, (void *)&nls, sizeof(struct sockaddr_nl))) {
ERROR("Failed to bind hotplug socket: %m\n");
exit(1);
}
if (setsockopt(hotplug_fd.fd, SOL_SOCKET, SO_RCVBUFFORCE, &nlbufsize, sizeof(nlbufsize)))
ERROR("Failed to resize receive buffer: %m\n");
json_script_init(&jctx);
queue_proc.cb = queue_proc_cb;
//循环接收netlink消息
uloop_fd_add(&hotplug_fd, ULOOP_READ);
}
调用功能实现脚本时机, procd收到netlink消息后会根据配置文件来搜寻相关功能实现脚本
按键事件的配置文件为 procd/etc/hotplug.json
[ "if",
[ "and",
[ "has", "BUTTON" ],
[ "eq", "SUBSYSTEM", "button" ]
],
[ "button", "/etc/rc.button/%BUTTON%" ]
],
可解读为: procd收到来自BUTTON的消息,最终执行/etc/rc.button/下的脚本,脚本名称为事件中的button字段取值, 在驱动中以数组方式定义,如下:
static struct bh_map button_map[] = {
BH_MAP(BTN_0, "BTN_0"),
BH_MAP(BTN_1, "BTN_1"),
BH_MAP(BTN_2, "BTN_2"),
BH_MAP(BTN_3, "BTN_3"),
BH_MAP(BTN_4, "BTN_4"),
BH_MAP(BTN_5, "BTN_5"),
BH_MAP(BTN_6, "BTN_6"),
BH_MAP(BTN_7, "BTN_7"),
BH_MAP(BTN_8, "BTN_8"),
BH_MAP(BTN_9, "BTN_9"),
BH_MAP(KEY_BRIGHTNESS_ZERO, "brightness_zero"),
BH_MAP(KEY_CONFIG, "config"),
BH_MAP(KEY_COPY, "copy"),
BH_MAP(KEY_EJECTCD, "eject"),
BH_MAP(KEY_HELP, "help"),
BH_MAP(KEY_LIGHTS_TOGGLE, "lights_toggle"),
BH_MAP(KEY_PHONE, "phone"),
BH_MAP(KEY_POWER, "power"),
BH_MAP(KEY_RESTART, "reset"),
BH_MAP(KEY_RFKILL, "rfkill"),
BH_MAP(KEY_VIDEO, "video"),
BH_MAP(KEY_WIMAX, "wwan"),
BH_MAP(KEY_WLAN, "wlan"),
BH_MAP(KEY_WPS_BUTTON, "wps"),
};
BH_MAP第一个参数BTN_0或KEY_POWER对应input子系统的input.h中标准定义;
第二个参数"BTN_0"或"reset"对应/etc/rc.button/下的脚本名称
至此,openwrt按键检测整个过程分析完毕
最后总结:
1. Openwrt系统按键内核驱动分为两种, 循环检测和中断检测, 在dts中配置
2. 按键驱动通过netlink方式发送按键事件到用户空间
3. 在用户空间, init进程procd统一接收处理来自内核的netlink消息,同时根据配置文件,调用/etc/rc.button/下的脚本, 此处需注意脚本的可执行权限
4. dts中关于按键的配置, linux,code为input子系统标准事件定义,即input.h中的定义