中断简介
中断指 CPU 接收到某些事件,暂停正在执行的程序,转而去执行处理该事件的程序(中断处理函数),当这段程序执行完毕后再继续执行之前的程序,整个过程称为中断处理,简称中断,而引起这一过程的事件称为中断事件。
中断处理函数原形
/**
* irq 中断号
* dev 注册时传递的参数
* 返回 IRQ_NONE 表示不是此设备产生的中断, IRQ_HANDLED 中断被正常处理, IRQ_WAKE_THREAD 唤醒中断处理线
* 程,适用于常用 request_threaded_irq 注册的中断处理函数
*/
irqreturn_t (*irq_handler)(int irq, void *dev)
注册中断处理函数
/**
* irq 中断号
* handler 中断处理函数,即中断上半部,为 NULL 则使用默认的中断处理函数
* thread_fn 中断线程化函数,中断处理函数返回 IRQ_WAKE_THREAD 会执行此函数,为 NULL 则不创建中断处理线程
* flags 标志,IRQF_SHARED 共享中断,表示此中断源可能有多个中断事件,共享中断void *dev不能为NULL
* IRQF_ONESHOT 中断线程化函数结束前不再接收此中断
* name 中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字
* dev 如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断
*/
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);
注销中断处理函数
/**
* irq 中断号
* dev 如果中断设置为共享中断,此参数用来区分具体的中断,必须与注册时传递的参数一致。共享中断只有在最后一
* 次释放中断处理函数的时候才会被禁止掉
*/
void free_irq(unsigned int irq, void *dev)
内核中的中断控制函数
//禁止本核中断,并保存当前中断使能状态
local_irq_save(flags)
//恢复本核中断状态
local_irq_restore(flags)
//使能指定中断
enable_irq(unsigned int irq)
//禁止指定中断,需要等待中断处理函数执行完成
disable_irq(unsigned int irq)
//立即禁止指定中断
disable_irq_nosync(unsigned int irq)
中断编程注意事项
- 因为中断函数中不存在进程上下文,所以不能在中断函数中调用可能会引起休眠的函数
- linux默认不支持中断嵌套,所以中断处理时间要尽可能的短
中断延迟处理
在中断处理函数中只能完成一些紧急或耗时极短的操作,对于不紧急且耗时较长的操作需要放到中断处理函数结束后进行处理,如采用 tasklet 、工作队列中、中断线程化等方法
中断上半部和下半部
在很多文档中都谈到了中断上半部和下半部(有些是前半部和后半部),其实中断上半部就是中断处理函数中的内容,用于处理一些紧急且耗时短的事务,中断下半部就是中断上半部结束后唤起的tasklet 、工作队列中或中断线程,用于执行一些不紧急或耗时长的事务。
tasklet
tasklet 是一种中断延迟手段,在中断程序中可以调度 tasklet ,当中断程序结束后相应的 tasklet 回调函数将会被执行, tasklet 是利用软中断实现的,它处于软中断上下文(可能处于进程上下文,也可能处于中断上下文),所以不能调用可能会引起休眠的函数,但是对执行时间的要求相对宽松些,如下是tasklet的常用函数:
/**
* 静态定义并初始化一个tasklet
* name 变量名
* func 回调函数
* data 回调函数参数
*/
DECLARE_TASKLET(name, func, data)
/**
* 初始化tasklet
* t 要初始化的 tasklet
* func 回调函数
* data 回调函数参数
*/
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
/**
* 调度tasklet
* t 要调度的 tasklet
*/
void tasklet_schedule(struct tasklet_struct *t)
工作队列
工作队列与 tasklet 类似,但是工作队列是在进程上下文执行,可以调用可能会引起休眠的函数,如下是工作队列的常用函数:
/**
* 静态定义并初始化一个工作队列
* n 变量名
* f 工作队列回调函数
*/
DECLARE_WORK(n, f)
/**
* 初始化工作队列
* work 要初始化的工作队列
* _func 工作队列回调函数
*/
INIT_WORK(_work, _func)
/**
* 调度工作队列
* work 要调度的工作队列
*/
bool schedule_work(struct work_struct *work)
设备树里使用中断
设备树中与中断修改的属性
nterrupt-controller :一个空属性,表示此节点是一个中断控制器。
#interrupt-cells :类似#address-cells 和 #size-cells ,表示引用此中断控制器时需要传递多少个参数。
interrupt-parent :设置节点所使用的中断控制器,子节点默认使用父节点的中断控制器。
interrupts :描述节点的中断信息,包括中断类型、中断号、触发方式等,具体含义与中断控制器相关。
interrupts-extended :描述节点的中断信息和所使用的中断控制器
在设备树中使用GIC中断控制器
//设置此节点所使用的中断控制器
interrupt-parent = <&intc>;
/*
* 描述中断信息,依次是:
* 中断类型(GIC_SPI或GIC_PPI)
* 中断物理编号
* 触发方式,其中bit[3:0]表示中断触发类型,bit[15:8]为 PPI 中断的 CPU 掩码
*/
interrupts = <GIC_SPI 68 IRQ_TYPE_LEVEL_HIGH>,
<GIC_PPI 13 IRQ_TYPE_LEVEL_HIGH>;
在设备树中使用GPIO中断控制器:
//设置此节点所使用的中断控制器
interrupt-parent = <&gpiog>;
//描述中断信息,分别是GPIO号、触发方式
interrupts = <1 IRQ_TYPE_EDGE_FALLING>;
通过设备树节点获取逻辑中断号(实际上是根据设备树的描述分配一个逻辑中断号,它通过 irq_create_of_mapping 函
数实现):
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
//相对于irq_of_parse_and_map多了错误检测
int of_irq_get(struct device_node *dev, int index)
通过gpio号获取中断号:
int gpio_to_irq(unsigned int gpio)
获取中断触发类型:
u32 irq_get_trigger_type(unsigned int irq)
对于大多数总线设备内核都会解析设备树中描述的中断信息,在驱动中可以获得相应的中断号,如 iic 设备可以通过i2c_client 的 irq 成员获取中断, spi 设备可以通过 spi_device 的 irq 成员获取中断(平台设备在匹配过程中不会解析设备树中的中断信息,但是可以通过 platform_get_irq 或 platform_get_irq_byname 获取中断)
GPIO中断实验程序
这里以实现一个利用GPIO中断捕获脉冲周期和宽度的驱动程序来演示如何在Linux内核中实验GPIO中断,电路原理图如下,这里利用按键来产生脉冲,按键按下时GPIO为低电平,松开时为高电平,通过捕获相应的电平变化然后计算脉冲宽度和周期(以按键按下时为有效状态,即对应逻辑1,按键松开为无效状态,即对应逻辑0)。
设备树编写
因为按键按下时为低电平,松开时为高电平,且按键按下为逻辑1,松开为逻辑0,所以GPIO应该为低电平有效,相应的设备树如下
//在顶层设备树根节点中加入如下节点
gpio_capture@0 {
compatible = "atk,gpio_capture"; /* 用于设备树与驱动进行匹配 */
status = "okay"; /* 状态 */
label = "gpio_capture0"; /* 标签,对应设备文件名 */
input-gpios = <&gpiog 3 GPIO_ACTIVE_LOW>; /* 中断所使用的引脚,低电平有效 */
interrupt-parent = <&gpiog>; /* 节点的父中断控制器 */
interrupts = <3 IRQ_TYPE_EDGE_BOTH>; /* 中断源,采用双边沿触发 */
};
//用make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- dtbs -j8编译设备树
//用新的.dtb文件启动系统
驱动程序编写
驱动程序的核心内容主要包括工作队列初始化、注册/注销、中断函数编写、工作队列函数编写
- 初始化工作队列
struct capture_handle{
....
//工作队列,用于中断后半部
struct work_struct work;
....
}
static int capture_probe(struct platform_device *pdev)
{
struct capture_handle *capture;
......
//初始化工作队列
INIT_WORK(&capture->work, capture_work);
......
}
- 注册/注销中断
struct capture_handle{
....
//GPIO的中断号
int irq;
....
}
static int capture_probe(struct platform_device *pdev)
{
struct capture_handle *capture;
......
//获取中断号
capture->irq = of_irq_get(pdev->dev.of_node, 0);
if(capture->irq <= 0)
{
printk("irq get failed");
return capture->irq;
}
//注册中断,devm表示在模块卸载时自动注销中断
irq_flags = IRQF_SHARED;
result = devm_request_irq(&pdev->dev, capture->irq, capture_handler, irq_flags, "capture", (void*)capture);
if(result != 0)
{
printk("request irq failed\r\n");
return result;
}
......
}
- 中断函数
//中断处理函数
static irqreturn_t capture_handler(int irq, void *dev)
{
struct capture_handle *capture = (struct capture_handle*)dev;
//调度工作队列
schedule_work(&capture->work);
//返回IRQ_HANDLED,表示中断被成功处理
return IRQ_HANDLED;
}
- 工作队列函数
//工作队列函数
static void capture_work(struct work_struct *w)
{
int input_state;
long long cur_rising;
struct capture_handle *capture = container_of(w, struct capture_handle, work);
//读取GPIO电平,电平为有效值返回1,有效值在设备树中指定,这里再设备树中指定的低有效,所以按键按下返回1,松开返回0
input_state = gpiod_get_value(capture->gpio);
//检查GPIO状态是否改变,若未改变则认为是误触发
if(capture->last_state == input_state)
return;
if(input_state == 1)
{
//上升沿捕获周期
//记录上升沿时间
cur_rising = ktime_get_boottime_ns();
if(capture->last_rising != 0)
{
//计算周期
capture->pulse_par.period = cur_rising - capture->last_rising;
//发送信号通知相应进程
kill_fasync(&capture->fasync, SIGIO, POLL_IN);
//唤醒正在等待脉冲捕获事件的进程
capture->update = 1;
wake_up_interruptible(&capture->wait_queue);
}
//更新last_rising
capture->last_rising = cur_rising;
}
else
{
//下降沿捕获脉宽
//计算脉宽
if(capture->last_rising != 0)
capture->pulse_par.width = ktime_get_boottime_ns() - capture->last_rising;
}
//记录按键状态
capture->last_state = input_state;
}
上机实验
- 修改设备树,再根节点中加入如下内容,然后使用命令make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- dtbs -j8编译设备树,再使用新的设备树启动开发板
- 从这里下载代码并进行编译,然后拷贝到目标板的根文件系统中
- 执行命令insmod gpio_capture.ko加载内核模块
- 执行命令./app.out /dev/gpio_capture0运行测试程序,此时再按键按下时会输出上一次按键与这次按键之间的周期,以及上次按下的时间(得是ns)