在Linux内核中,定时器通过软件中断实现,而这个软件中断其实依赖于实际的物理定时器中断。概括来说,物理定时器会每隔一段时间发送一次中断,然后有一个全局变量jiffies就会加1,当到达某个阈值时,就会触发定时器软件中断。软件中断是在每次发生了硬件(物理)中断并处理中断后由内核去检查是否有需要执行的软件中断,软件中断处理流程可概括如下:
发生硬件中断 => 内核执行负责获取中断号等一系列操作,然后关中断,执行我们写的中断处理函数,开中断 => 检查有没有需要执行的软件中断,有就执行
在Linux内核中,通常10ms发生一次硬件定时器中断,因此对于软中断,至少10ms就有可能被执行。
1 Linux内核定时器的使用
在Linux内核中,硬件定时器中断通常10ms发生一次,每发生一次全局变量jiffies加1,因此使用内核中的定时器就是修改timer结构体中的expires,当jiffies超过设定值时会触发定时器软件中断。
在内核中使用定时器中断涉及的函数如下:
在驱动中,使用定时器的步骤如下:
(1)设置定时器处理函数,函数原型为 void (*timer_expire)(unsigned long data)
虽然函数参数是整数,但是指针本身也是一个整数,因此可以传入一个结构体指针然后在函数内部再强制类型转换,这样可以一次性传入一组有关的数据。
(2)设置定时器setup_timer(timer, fn, data)
(3)设置timer结构体中的expire变量确定定时时间
(4)使用add_timer向内核添加定时器
从上面的定时器相关函数以及使用流程,可以看到每一个软件定时器其实都通过一个struct timer_list的结构体相关联,这个结构体会被放进一个timer链表中,内核在处理软件中断时会检查这条链表上是否有超时的timer_list结构体,如果有就取出其中对应的函数执行。
总体使用还是很简单的,接下来可以使用定时器进行按键消抖
添加了定时器消抖的按键中断异步通知代码如下:
点击查看代码
/*
基于总线设备驱动模型进行驱动开发,platform_device由设备树产生,并且使用GPIO子系统和pinctr子系统
在该驱动程序中需要自己构造platform_driver结构体并且要和设备树生成的platform_device结构体匹配
1、使用循环缓冲区记录按键值
2、采用异步通知机制,当发生按键中断时通过向进程发信号,然后信号处理函数会被调用,在其中读取按键值
*/
#include "asm-generic/errno-base.h"
#include "asm-generic/poll.h"
#include "asm-generic/siginfo.h"
#include "asm/signal.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/gpio/driver.h"
#include "linux/irqreturn.h"
#include "linux/kdev_t.h"
#include "linux/nfs_fs.h"
#include "linux/of.h"
#include "linux/wait.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/ktime.h>
#include <linux/delay.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
static int major; //主设备号
static struct class *key_class;
static int count; //引脚树,也即按键数量
/*
设置按键中断时,会使用到引脚的引脚编号(老版本),引脚描述结构体,引脚中断号,引脚标志位
因此这些信息可以使用一个结构体来保存,一个引脚按键就对应一个,如果有多个按键就可以用结构体数组
按键中断本质是引脚输入电平变化引起的中断,因此如果有外设在完成工作后发出电平变化信号的话,可以使用这种中断方式
比如AD7606在完成转换后BUSY引脚会变成持续一段时间的高电平,那么可以使用这个高电平产生引脚中断然后去读取AD7606的AD转换值
*/
struct gpio_interrupt{
int gpio_num; //GPIO引脚编号,老版本
struct gpio_desc *desc; //gpio引进描述结构体
int irq; //中断号
int flag;
struct timer_list key_timer; //定时器结构体
};
struct fasync_struct *key_fasync_struct;
struct gpio_interrupt *key_gpio; //定义一个结构体指针,在probe函数中再根据设备树中定义的引脚数量动态分配内存
#define BUF_LEN 128 //缓冲区大小
static int key_buf[BUF_LEN];
static int r, w; //定义缓冲区写指针,r=w时表示缓冲区空,(w+1) % BUF_LEN = r时表示缓冲区满
//实际上BUF_LEN个缓存单元只能用BUF_LEN-1个,因为要判断是否满/空
#define next_position(x) (((x) + 1) % BUF_LEN)
static bool is_buf_empty(void)
{
// printk("%s %s line %d, w = %d, r = %d\n", __FILE__, __FUNCTION__, __LINE__, w, r);
return (r == w);
}
static bool is_buf_full(void)
{
// printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return (r == next_position(w));
}
static void put_key_value(int key)
{
// printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
if(!is_buf_full())
{
key_buf[w] = key;
w = next_position(w); //更新写指针
}
}
static int get_key_value(void)
{
int key;
if(!is_buf_empty())
{
key = key_buf[r];
r = next_position(r); //更新指针
}
else
{
key = -1;
printk("%s %s line %d, buf is empty! failed to read\n", __FILE__, __FUNCTION__, __LINE__);
}
return key;
}
int key_open(struct inode *node, struct file *file)
{
/*打开文件时会执行,在这里可以做一些初始化操作
设置GPIO为输入
*/
int i;
r = 0;
w = 0;
for(i = 0; i < count; i++)
{
gpiod_direction_input(key_gpio[i].desc);
}
return 0;
}
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
/*用户程序使用read系统调用时会调用,返回值最终会返回给用户程序中的read
读取按键值,如果循环缓冲区有内容,那么直接读取缓冲区内容,否则休眠
因此可以使用缓冲区是否为空作为休眠的信号
*/
int err;
int key;
printk("%s %s line %d, w = %d, r = %d\n", __FILE__, __FUNCTION__, __LINE__, w, r);
//如果buf空,is_buf_empty()返回1, !is_buf_empty()是0,那么陷入睡眠
wait_event_interruptible(gpio_key_wait, !is_buf_empty());
//如果没有休眠或者从休眠唤醒,说明缓冲区有内容了,读缓冲区
key = get_key_value();
//把按键值写入用户空间
err = copy_to_user(buf, &key, sizeof(key));
if(err)
{
printk("failed copy data to user\n");
return -EFAULT;
}
return sizeof(key);
}
int key_fasync(int fd, struct file *file, int on)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
if(fasync_helper(fd, file, on, &key_fasync_struct) >= 0) //在file结构体中的flag的FASYNC位发生变化时调用
{
return 0;
}
else
{
return -EIO;
}
}
int key_close(struct inode *node, struct file *file)
{
/*用户程序调用close时会执行*/
int i;
for(i = 0; i < count; i++)
{
gpiod_direction_output(key_gpio[i].desc, 0);
}
return 0;
}
struct file_operations key_opr = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.fasync = key_fasync,
.release = key_close,
};
static void key_timer_expire(unsigned long data)
{
/*按键定时器消抖
(1)读取按键状态并放入缓存
(2)唤醒进程
(3)发送信号
*/
struct gpio_interrupt *key = (struct gpio_interrupt *)data; //强制类型转换
int val;
int key_val;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
mod_timer(&(key->key_timer), ~0); //重新设置定时器为无限长,如果没有的话,即使按键没有按下,也会每个几十ms触发一次定时器中断.实验了一下,好像不影响
val = gpiod_get_value(key->desc);
val ^= (1);
key_val = (key->gpio_num << 8) | val;
put_key_value(key_val); //把按键状态放入缓冲区
wake_up_interruptible(&gpio_key_wait); //唤醒进程
kill_fasync(&key_fasync_struct, SIGIO, POLL_IN); //发送信号
}
static irqreturn_t gpio_isr(int irq, void *dev_id)
{
/*中断处理函数上半部
发生中断后,会执行相应的中断处理函数,在内核中已经提供了相应的中断处理函数
在里面会判断中断号,关中断,我们所写的中断处理函数被注册进内核后就作为内核中断
处理函数的回调函数被调用,在里面我们可以写上我们想执行的操作
在这里不执行读取按键值的操作,只是修改重置定时器定时时间,进行消抖
在定时器函数中再执行按键读取操作
*/
struct gpio_interrupt *key = dev_id; //获取引脚信息
mod_timer(&key->key_timer, jiffies + HZ / 20); //定时50ms
return IRQ_HANDLED; //返回IRQ_HANDLED表示已经处理中断,否则内核还会继续执行action链表中其他函数
}
int key_probe(struct platform_device *pdev)
{
/*
驱动和设备节点匹配时执行
在这里会做一些初始化操作:获取设备树信息,设置中断,注册驱动file_operation结构体
*/
struct device_node *node = pdev->dev.of_node; //获取设备树中引脚对应的device_node结构体
int err;
int i;
enum of_gpio_flags flag;
count = of_gpio_named_count(node, "key-gpios"); //获取设备树节点中定义了多少个GPIO
if(count <= 0)
{
printk("%s %s line %d, No available gpio, count = %d\n", __FILE__, __FUNCTION__, __LINE__, count);
return -1;
}
key_gpio = kzalloc(sizeof(struct gpio_interrupt) * count, GFP_KERNEL); //为自定义的引脚结构体分配内存
//获取设备树中定义的节点信息
for(i = 0; i < count; i++)
{
//获取引脚编号
// key_gpio[i].gpio_num = of_get_gpio_flags(node, i, &flag); //以后尽量使用命名式的函数
key_gpio[i].gpio_num = of_get_named_gpio_flags(node, "key-gpios", 0, &flag);
if(key_gpio[i].gpio_num < 0)
{
printk("%s %s line %d, of_get_gpio_flags failed\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
//获取引脚描述结构体
key_gpio[i].desc = gpiod_get_index(&pdev->dev, "key", i); //这里使用get,那么在remove函数里就需要使用put释放引脚
key_gpio[i].flag = flag & OF_GPIO_ACTIVE_LOW;
key_gpio[i].irq = gpiod_to_irq(key_gpio[i].desc); //获取对应的中断号
/*添加定时器*/
setup_timer(&(key_gpio[i].key_timer), key_timer_expire, (int)&key_gpio[i]);
key_gpio[i].key_timer.expires = ~0; //设置定时时间无限长
add_timer(&key_gpio[i].key_timer);
}
//注册中断
for(i = 0; i < count; i++)
{
err = request_irq(key_gpio[i].irq, gpio_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "gpio_key", &key_gpio[i]);
}
/*注册file_operaton结构体,创建类和设备*/
major = register_chrdev(0, "key_interrupt", &key_opr);
key_class = class_create(THIS_MODULE, "key_class");
if(IS_ERR(key_class))
{
printk("%s %s line %d, class create failed\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "key_interrupt");
return PTR_ERR(key_class);
}
//创建设备节点,不需要为每个引脚都创建设备节点,所有引脚(按键)共用一个设备节点
device_create(key_class, NULL, MKDEV(major, 0), NULL, "key");
return 0;
}
int key_remove(struct platform_device *pdev)
{
int i;
/*卸载驱动时执行,在这里要撤销中断,注销驱动、类和设备节点,释放动态分配的内存*/
//删除设备节点
device_destroy(key_class, MKDEV(major, 0));
//删除类
class_destroy(key_class);
//撤销驱动
unregister_chrdev(major, "key_interrupt");
//撤销中断,释放GPIO
for(i = 0; i < count; i++)
{
free_irq(key_gpio[i].irq, &key_gpio[i]);
gpiod_put(key_gpio[i].desc);
del_timer(&key_gpio[i].key_timer);
}
kfree(key_gpio); //释放内存
return 0;
}
static const struct of_device_id keys_match_table[] = {
{
.compatible = "mykey_driver_interrupt"
}
};
struct platform_driver key_driver = {
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "key_interrupt",
.of_match_table = keys_match_table,
},
};
static int __init key_drv_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&key_driver);
return err;
}
static void __exit key_drv_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&key_driver);
}
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
使用定时器消抖后,按键按下读取按键,明显看到几乎不受抖动影响了。
2 Linux内核中定时器的机制
未完待续,后面再更