参考的教程是正点原子linux驱动开发教程
使用的板子是正点原子stm32mp157开发板
裸机中,使用中断的处理方法:
- 使能中断,初始化相应的寄存器
- 注册中断服务函数,向irqTable数组指定的标号处写入中断服务函数
- 中断发生以后进入IRQ中断服务函数,中断服务函数在irqTable里面查找具体的中断处理函数,找到以后执行相应的中断处理函数
上半部与下半部
我们在使用request_irq申请中断的时候注册的中断服务函数属于中断处理的上半部。
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上班部分完成
下半部:如果中断处理过程比较耗时,那么就将这些耗时的代码提出来,交给下半部去执行,这样中断函数就会快进快出。
分为上半部和下半部的主要目的就是:实现中断处理函数快进快出。
主要有以下几个参考点:
- 如果要处理的内容不希望被其他中断打断,那么可以放到上半部
- 如果要处理的任务对时间敏感,可以放到上半部
- 如果要处理的任务与硬件有关,可以放到上半部
- 除了上述的三点以外的任务,可以优先考虑放到下半部
下半部的方法有好几种,暂时用不到,大概学习了一下,等具体用到了在深入学习,咱不好高骛远。
实验内容:
利用中断对按键进行消抖,这里利用了定时器。
首先,对设备树的节点进行修改。
//dada2023.3.17
key0{
compatible = "dada,key";
status = "okay";
key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpiog>;
interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
};
我在上面标注了一下是谁改的,改的时间,为了方便。
这里分别设置了一下中断类型,以及触发的类型,选用的是上下沿都触发。
至于compatible属性就按照自己来定义吧,我写的是“dada,key”。
这些都是以前的知识了,只不过多了一个中断,也很好理解的,但是设备树这里中断有两种写法,我用的是第二种。
然后把设备树编译一下,放进tftp里面重新加载。
可以看到设备树里面已经有了。
接下来就是编写驱动。
照例,先定义结构体。
//定义key设备结构体
struct key_dev{
dev_t devid;//设备号
int major; //主设备号
int minor; //次设备号
struct cdev cdev;//注册函数
struct class *class;//节点
struct device *device;//设备
struct device_node *nd;//设备节点
int keydev; //使用的设备号
struct timer_list timer; //定时器的结构体
int irq_num; //中断号
spinlock_t spinlock; //自旋锁,用于中断
};
用枚举表示一下按键的状态。
//按键的状态
enum key_status {
KEY_PRESS = 0,//按键按下
KEY_RELEASE , //按键松开
KEY_KEEP , //按键保持
};
定义一个全局变量,用来获取按键的状态。
static int status = KEY_KEEP; //按键的状态
然后,获取设备树属性。
//获取设备树属性内容
//获取设备节点
key.nd = of_find_node_by_path("/key0");
if(key.nd == NULL){
return -EINVAL;
}
//获取compatible属性
ret = of_property_read_string(key.nd, "compatible", &str);
if (ret<0)
{
printk("Compatible find failed\r\n");
return -EINVAL;
}
else if (strcmp(str,"dada,key"))
{
printk("key: Compatible match failed!\n");
}
//获取status属性
ret = of_property_read_string(key.nd, "status", &str);
if(ret < 0){
printk("status read failed!\r\n");
return -EINVAL;
}else if(strcmp(str,"okay")){
return -EINVAL;
}
//获取设备树中的gpio属性,得到key的编号
key.keydev = of_get_named_gpio(key.nd,"key-gpio",0);
if(key.keydev<0){
printk("Can't get key-gpio!\n");
return -EINVAL;
}
printk("key-gpio num = %d\r\n",key.keydev);
//获取GPIO对应的中断号
key.irq_num = irq_of_parse_and_map(key.nd,0);
if(!key.irq_num){
return -EINVAL;
}
//向gpio子系统申请使用gpio,使能gpio
ret = gpio_request(key.keydev,"KEY-GPIO");
if(ret){
printk(KERN_ERR "gpioled: Failed to request key-gpio\n");//疑问点
return ret;
}
//设置pi0为输出,并且默认上拉
ret = gpio_direction_input(key.keydev);
if(ret < 0)
{
printk("Can't set gpio!\r\n");
}
//获取设备树中指定的中断触发类型
irq_flags = irq_get_trigger_type(key.irq_num);
if(IRQF_TRIGGER_NONE == irq_flags){
irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;//上下沿都触发
}
//申请中断
ret = request_irq(key.irq_num,key_interrupt,irq_flags,"key0_IRQ",NULL);
if(ret){
gpio_free(key.keydev);
return ret;
}
return 0;
在申请中断的时候,发现少了中断函数,没有关系,我们定义一个。
static irqreturn_t key_interrupt(int irq,void *dev_id){
return IRQ_HANDLED;
}
别问,问就是从内核文件里学的。
好了,我们要清楚一件事情,这个中断是用来干啥的。
对,是用来消抖的。
当我们按下去或者松开,要触发中断并实现一个消抖的功能。
那么,就要用到延时。
这就想到之前学的内核定时器,完全可以实现这个功能。
好,现在去初始化一下我们的内核定时器。
在这之前,我们还需要把字符驱动设备的注册流程走下去。
//注册字符
//构造设备号
if(key.major)//定义了主设备号
{
key.devid = MKDEV(key.major,0);
ret = register_chrdev_region(key.devid,DTSLED_CNT,DTSLED_NAME);
if(ret<0){
printk("key driver register failed!\r\n");
goto free_gpio;
}
}else {
ret = alloc_chrdev_region(&key.devid,0,DTSLED_CNT,DTSLED_NAME);//没有定义就从内核申请
if(ret<0){
printk("key driver register failed!\r\n");
goto free_gpio;
}
}
//添加字符设备
key.cdev.owner = THIS_MODULE;
cdev_init(&key.cdev,&key_fops);//初始化
ret = cdev_add(&key.cdev,key.devid,DTSLED_CNT);//添加字符设备
if(ret<0){
goto del_unregister;
}
//自动创建设备节点
key.class = class_create(THIS_MODULE, DTSLED_NAME);
if (IS_ERR(key.class)) {
pr_err("QAT: class_create failed for adf_ctl\n");
goto err_cdev_del;
}
//创建设备
key.device = device_create(key.class, NULL,
key.devid,
NULL, DTSLED_NAME);
if (IS_ERR( key.device)) {
pr_err("QAT: failed to create device\n");
goto destroy_class;
}
//初始化定时器
timer_setup(&key.timer,key_timer_fuction,0);
printk("key_init\r\n");
return 0;
destroy_class:
class_destroy(key.class);
err_cdev_del:
cdev_del(&key.cdev);
del_unregister:
unregister_chrdev_region(key.devid, DTSLED_CNT);
free_gpio:
gpio_free(key.keydev);
return -EIO;
对了,别忘了初始化一下自旋锁,毕竟这里用到了中断,自旋锁是可以用到中断里的,因为它不会引发休眠!!!
int ret;
//初始化自旋锁
spin_lock_init(&key.spinlock);
//设备树属性获取
ret = keygpio_init();
if(ret)
return ret;
这段加在前面那段的前面就行。
那么问题又来了,初始化定时器的时候,需要一个定时器中断函数。
也就是到了时间,我们要做一个什么的动作。
当然是获取稳定之后的键值,然后给当前的状态了。
有了这个思路,上代码!!
static void key_timer_fuction(struct timer_list * arg){
static int last_val = 1;
unsigned long flags ;
int current_val;
//自旋锁上锁
spin_lock_irqsave(&key.spinlock,flags);
//读取当前的状态值,status是一个全局变量!
current_val = gpio_get_value(key.keydev);
//如果current_val为0,无论last_val是什么,结果都0,当前肯定是被按下的
if(0 == current_val && last_val)
status = KEY_PRESS;
else if(1 == current_val && !last_val)
status = KEY_RELEASE; //如果c为1,那么就判断一下上一次是不是1或者2,如果是就是保持
else
status = KEY_KEEP;
last_val = current_val;
//解锁
spin_unlock_irqrestore(&key.spinlock,flags);
}
注释写的已经很明白了,如果还看不懂怎么判断的话,可以评论区留言,因为我也怕我理解错了,毕竟我还是个小白。
然后再去完善一下中断。
其实就是中断调用定时器了。
这里采用mod_timer函数,是老朋友了。
static irqreturn_t key_interrupt(int irq,void *dev_id){
mod_timer(&key.timer,jiffies + msecs_to_jiffies(15));
return IRQ_HANDLED;
}
这样大部分就已经完全了。
想要从应用层调用,那么还得用上file_operations这个结构体定义的函数。
因为是读取键值,所以采用的是read函数,很简单,只要获取一下键值就行了。
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt,
loff_t *offt)
{
int ret = 0;
unsigned long flags;
spin_lock_irqsave(&key.spinlock,flags);
ret = copy_to_user(buf,&status,sizeof(int));
status = KEY_KEEP;
spin_unlock_irqrestore(&key.spinlock,flags);
return ret;
}
这样,驱动就已经编写好了。
然后测试就更简单了,循环读取按键值并且打印出来就行。
接下来,上开发板测试一下吧!
我快速按然后松开,发现都能完美的读取出来,程序很成功,如果没有这个消抖的话,还是会出问题的。
成功了!!