i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT、4G模块、CAN、RS485等接口一应俱全。H264、VP8视频硬编码,H.264、H.265、VP8、VP9视频硬解码,并提供相关历程,支持8路PDM接口、5路SAI接口、2路Speaker。系统支持Android9.0(支持获取root限)Linux4.14.78+Qt5.10.1、Yocto、Ubuntu20、Debian9系统。适用于智能充电桩,物联网,工业控制,医疗,智能交通等,可用于任何通用工业和物联网应用、
第五十八章 中断下文之tasklet
本章导读
上一章节我们已经写了一个简单的按键中断,我们是使用的中断上文,我们并没有使用中断下文。本章节我们来看一下,如果我们使用中断下文又如何来设计我们的程序呢?
58.1章节讲解了中断下文之tasklet的基础理论知识
58.2章节运用58.1章节的理论,在IMX8MM开发板上以按键中断为例,进行实验,实现按一下音量+按键,打印0-99。
本章内容对应视频讲解链接(在线观看):
中断下文之tasklet → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=37
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\015-中断下文之tasklet”路径下。
58.1 中断下文之tasklet
中断的上下文与进程上下文并没有什么瓜葛,当执行一个中断处理函数时,内核处于中断上下文。由于中断相当于打断了当前执行的程序,而且中断也没有后备的进程,所以中断上下文不可以睡眠(注意某些函数会睡眠),中断处理也必须做到迅捷,有一定的时限要求。中断处理程序存在希望中断程序运行的尽量快以及希望中断处理程序完成的工作量多这一对矛盾。因此我们一般将中断分为上下两个部分,分为上半部,下半部。上半部完成有严格时限的工作(必须),例如回复硬件等,这些工作都是在禁止其他中断情况下进行的。能够延后执行的都放在下半部进行。上半部只能通过中断处理程序实现,下半部的实现目前有3种实现方式,分别为:1、软中断、2、tasklet 3、工作队列(work queues)我们主要讲tasklet。调用tasklet以后,tasklet绑定的函数并不会立马执行,而是有中断以后,经过一个很短的不确定时间在来执行,如下图所示:
58.1.1 tasklet的概念
tasklet 是通过软中断实现的,所以它本身也是软中断。软中断用轮询的方式处理,假如正好是最后一
种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。为了提高中断处理数量,顺道改
进处理效率,于是产生了 tasklet 机制。tasklet 采用无差别的队列机制,有中断时才执行,免去了循环查表之苦,tasklet 机制的优点:无类型数量限制,效率高,无需循环查表,支持 SMP 机制,一种特定类型的 tasklet只能运行在一个 CPU 上,不能并行,只能串行执行。多个不同类型的 tasklet 可以并行在多个 CPU 上。软中断是静态分配的,在内核编译好之后,就不能改变。但 tasklet 就灵活许多,可以在运行时改变(比如添加模块时)。
Linux 内核中的 tasklet 结构体:
struct tasklet_struct
{
struct tasklet_struct *next; /* 下一个 tasklet */
unsigned long state; /* tasklet 状态 */
atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数 */
unsigned long data; /* 函数 func 的参数 */
};
- next:链表中的下一个tasklet,方便管理和设置tasklet;
- state: tasklet的状态。
- count:表示tasklet是否处在激活状态,如果是0,就处在激活状态,如果非0,就处在非激活状态
- void (*func)(unsigned long):结构体中的func成员是tasklet的绑定函数,data是它唯一的参数。
- date:函数执行的时候传递的参数。
如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函
数原型如下:
函数 | void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long),unsigned long data); |
t | 要初始化的 tasklet |
func | tasklet 的处理函数 |
data | 要传递给 func 函数的参数 |
返回值 | 没有返回值。 |
功能 | 动态初始化tasklet |
也可以使用宏 DECLARE_TASKLET 一次性完成 tasklet 的定义和初始化,DECLARE_TASKLET 定义在
include/linux/interrupt.h 文件中,定义如下:
DECLARE_TASKLET(name, func, data)
其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量,func 就是
tasklet 的处理函数,data 是传递给 func 函数的参数。
在需要调度 tasklet 的时候引用一个 tasklet_schedule()函数就能使系统在适当的时候进行调度运行,该函数原型为如下所示:
函数 | void tasklet_schedule(struct tasklet_struct *t) |
t | 要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。 |
返回值 | 没有返回值 |
功能 | 调度tasklet |
杀死tasklet使用tasklet_kill函数,函数原型如下表所示:
函数 | tasklet_kill(struct tasklet_struct *t) |
t | 要删除的 tasklet |
功能 | 删除一个tasklet |
注意 | 这个函数会等待tasklet执行完毕,然后再将它移除。该函数可能会引起休眠,所以要禁止在中断上下文中使用。 |
58.1.2 tasklet参考步骤
关于tasklet 的参考使用示例如下所示:
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
总结一下基本步骤为:
步骤一:定义一个tasklet结构体
步骤二:动态初始化tasklet
步骤三:编写tasklet绑定的函数
步骤四:在中断上文调用tasklet
步骤五:卸载模块的时候删除tasklet
58.2 实验程序编写
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\015-中断下文之tasklet\001”路径下。
我们以IMX8MM开发板为例,实现按一下音量+按键,打印0-99。编写驱动代码如下所示:
/*
* @Author:topeet
* @Description: 中断下文之tasklet,实现按键打印0-99
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
//定义结构体表示我们的节点
struct device_node *test_device_node;
//要申请的中断号
int irq;
// GPIO 编号
int gpio_nu;
//定义tasklet结构体
struct tasklet_struct key_test;
/**
* @description: tasklet 的处理函数
* @param {unsignedlong} data:要传递给 func 函数的参数
* @return {*}无
*/
void test(unsigned long data)
{
int i = 100;
while (i--)
printk("test_key is %d \n", i);
}
/**
* @description: 中断处理函数test_key
* @param {int} irq :要申请的中断号
* @param {void} *args :
* @return {*}IRQ_HANDLED
*/
irqreturn_t test_key(int irq, void *args)
{
printk("start\n");
tasklet_schedule(&key_test);
printk("end\n");
return IRQ_HANDLED;
}
/****************************************************************************************
* @brief led_probe : 与设备信息层(设备树)匹配成功后自动执行此函数,
* @param inode : 文件索引
* @param file : 文件
* @return 成功返回 0
****************************************************************************************/
int led_probe(struct platform_device *pdev)
{
int ret = 0;
// 打印匹配成功进入probe函数
printk("led_probe\n");
test_device_node = of_find_node_by_path("/test");
if (test_device_node == NULL)
{
//查找节点失败则打印信息
printk("of_find_node_by_path is error \n");
return -1;
}
gpio_nu = of_get_named_gpio(test_device_node, "gpios", 0);
if (gpio_nu < 0)
{
printk("of_get_namd_gpio is error \n");
return -1;
}
//设置GPIO为输入模式
gpio_direction_input(gpio_nu);
//获取GPIO对应的中断号
irq = gpio_to_irq(gpio_nu);
// irq =irq_of_parse_and_map(test_device_node,0);
printk("irq is %d \n", irq);
/*申请中断,irq:中断号名字
test_key:中断处理函数
IRQF_TRIGGER_RISING:中断标志,意为上升沿触发
"test_key":中断的名字
*/
ret = request_irq(irq, test_key, IRQF_TRIGGER_RISING, "test_key", NULL);
if (ret < 0)
{
printk("request_irq is error \n");
return -1;
}
/* 初始化 tasklet */
tasklet_init(&key_test,test,0 );
return 0;
}
int led_remove(struct platform_device *pdev)
{
printk("led_remove\n");
return 0;
}
const struct platform_device_id led_idtable = {
.name = "keys",
};
const struct of_device_id of_match_table_test[] = {
{.compatible = "keys"},
{},
};
struct platform_driver led_driver = {
//3. 在led_driver结构体中完成了led_probe和led_remove
.probe = led_probe,
.remove = led_remove,
.driver = {
.owner = THIS_MODULE,
.name = "led_test",
.of_match_table = of_match_table_test},
//4 .id_table的优先级要比driver.name的优先级要高,优先与.id_table进行匹配
.id_table = &led_idtable};
/**
* @description: 模块初始化函数
* @param {*}
* @return {*}
*/
static int led_driver_init(void)
{
//1.我们看驱动文件要从init函数开始看
int ret = 0;
//2.在init函数里面注册了platform_driver
ret = platform_driver_register(&led_driver);
if (ret < 0)
{
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok \n");
return 0;
}
/**
* @description: 模块卸载函数
* @param {*}
* @return {*}
*/
static void led_driver_exit(void)
{
free_irq(irq, NULL);
platform_driver_unregister(&led_driver);
printk("gooodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
58.3 运行测试
编译驱动代码为驱动模块,如下图所示:
编译成功加载驱动模块,如下图所示:
我们按一下开发板上面的音量+键,打印信息如下图所示(部分):
如上图所示,和我们预期结果是一样的,先打印start,再打印end,再打印0-99。
在上面的代码中,我们在代码中是直接赋值i是100,我们也可以将100传参进去,完整代码如下图所示;
/*
* @Author:topeet
* @Description: 中断下文之tasklet,实现按键打印0-99
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
//定义结构体表示我们的节点
struct device_node *test_device_node;
//要申请的中断号
int irq;
// GPIO 编号
int gpio_nu;
//定义tasklet结构体
struct tasklet_struct key_test;
/**
* @description: tasklet 的处理函数
* @param {unsignedlong} data:要传递给 func 函数的参数
* @return {*}无
*/
void test(unsigned long data)
{
int i = data;
printk("i is %d \n", i);
while (i--)
printk("test_key is %d \n", i);
}
/**
* @description: 中断处理函数test_key
* @param {int} irq :要申请的中断号
* @param {void} *args :
* @return {*}IRQ_HANDLED
*/
irqreturn_t test_key(int irq, void *args)
{
printk("start\n");
tasklet_schedule(&key_test);
printk("end\n");
return IRQ_HANDLED;
}
/****************************************************************************************
* @brief led_probe : 与设备信息层(设备树)匹配成功后自动执行此函数,
* @param inode : 文件索引
* @param file : 文件
* @return 成功返回 0
****************************************************************************************/
int led_probe(struct platform_device *pdev)
{
int ret = 0;
// 打印匹配成功进入probe函数
printk("led_probe\n");
test_device_node = of_find_node_by_path("/test");
if (test_device_node == NULL)
{
//查找节点失败则打印信息
printk("of_find_node_by_path is error \n");
return -1;
}
gpio_nu = of_get_named_gpio(test_device_node, "gpios", 0);
if (gpio_nu < 0)
{
printk("of_get_namd_gpio is error \n");
return -1;
}
//设置GPIO为输入模式
gpio_direction_input(gpio_nu);
//获取GPIO对应的中断号
//irq = gpio_to_irq(gpio_nu);
irq =irq_of_parse_and_map(test_device_node,0);
printk("irq is %d \n", irq);
/*申请中断,irq:中断号名字
test_key:中断处理函数
IRQF_TRIGGER_RISING:中断标志,意为上升沿触发
"test_key":中断的名字
*/
ret = request_irq(irq, test_key, IRQF_TRIGGER_RISING, "test_key", NULL);
if (ret < 0)
{
printk("request_irq is error \n");
return -1;
}
/* 初始化 tasklet */
tasklet_init(&key_test,test,100);
return 0;
}
int led_remove(struct platform_device *pdev)
{
printk("led_remove\n");
return 0;
}
const struct platform_device_id led_idtable = {
.name = "keys",
};
const struct of_device_id of_match_table_test[] = {
{.compatible = "keys"},
{},
};
struct platform_driver led_driver = {
//3. 在led_driver结构体中完成了led_probe和led_remove
.probe = led_probe,
.remove = led_remove,
.driver = {
.owner = THIS_MODULE,
.name = "led_test",
.of_match_table = of_match_table_test},
//4 .id_table的优先级要比driver.name的优先级要高,优先与.id_table进行匹配
.id_table = &led_idtable};
/**
* @description: 模块初始化函数
* @param {*}
* @return {*}
*/
static int led_driver_init(void)
{
//1.我们看驱动文件要从init函数开始看
int ret = 0;
//2.在init函数里面注册了platform_driver
ret = platform_driver_register(&led_driver);
if (ret < 0)
{
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok \n");
return 0;
}
/**
* @description: 模块卸载函数
* @param {*}
* @return {*}
*/
static void led_driver_exit(void)
{
free_irq(irq, NULL);
platform_driver_unregister(&led_driver);
printk("goodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
我们重新编译下驱动文件,将原来加载的驱动模块卸载掉,再加载新编译好的驱动模块,如下图所示:
我们按一下开发板上面的音量+键,打印信息如下图所示,可以看到i的值是100。