硬件架构的根本差异
Cortex-M4(MCU部分): 跑实时操作系统(RTOS)或裸机,追求极低的延迟。
Cortex-A7(MPU部分): 跑 Linux,追求高吞吐量和多任务管理。
html
STM32 MCU (裸机/RTOS) 的逻辑:
中断控制器(NVIC)和 CPU 是紧耦合的。
中断来了 -> CPU 直接跳转执行 ISR。
逻辑简单,不需要软件去"映射"中断号。
STM32MP1 (Linux/Cortex-A7) 的逻辑:
CPU 和外设之间隔着一个复杂的 GIC(通用中断控制器)。
GIC 不仅仅是开关中断,它负责路由。MP157 是双核 A7,GIC 要决定按键中断是发给 CPU0 还是 CPU1。
在写驱动时,虽然不用手写寄存器配置 GIC,但必须理解中断亲和性(Affinity)(这个中断归哪个核管),这对多核系统调试非常关键。
Linux 内核的抽象
html
解决的是"软硬映射"的问题。
物理中断号 (HW IRQ) vs 虚拟中断号 (Virtual IRQ):
GIC 视角:按键可能连接到 GIC 的第 35 号 SPI 中断线(物理号)。
Linux 视角:内核不会直接用物理号,而是会把它映射成一个软件上的中断号。
逻辑递进:你在驱动代码中用 request_irq(irq_num, ...) 时,填入的一定是虚拟中断号。
DTS(设备树)描述物理连接 -> 内核解析 DTS -> 自动映射生成虚拟号 -> 驱动调用 platform_get_irq 获取虚拟号。
中断
中断处理的执行流
html
顶半部 (Top Half):
逻辑:用 request_irq 注册的那个函数。
要求:快进快出。只做最简单的事(比如读取寄存器状态,清除中断标志)。
原因:顶半部运行时,往往会关中断,时间太长会把系统卡死。
底半部 (Bottom Half):
逻辑:如果按键按下后,你需要打印大量日志、或者读写文件、或者做复杂的逻辑判断。
实现:要使用 tasklet、workqueue(工作队列)或者 threaded_irq。
顶半部/底半部机制的核心,就是把"紧急的硬件响应"和"耗时的软件处理"剥离开来,保证系统对外界事件始终保持高响应速度。

中断的三种类型
html
SGI (Software Generated Interrupt) - 软中断
ID范围: 0 ~ 15
专门用于 CPU 核心之间通信(IPI)。比如 Core0 想告诉 Core1 "该醒醒了"或者"调度任务",就写寄存器触发一个 SGI。
PPI (Private Peripheral Interrupt) - 私有外设中断
ID范围: 16 ~ 31
某些外设是每个 CPU 核心独有的。最典型的就是 Generic Timer (通用定时器)。每个核都有自己的定时器中断,互不干扰。
SPI (Shared Peripheral Interrupt) - 共享外设中断
ID范围: 32 ~ 1019
所有外部外设(GPIO按键、串口、I2C、网卡)产生的中断都是 SPI。之所以叫"共享",是因为它们可以被路由到任意一个 CPU 核心去处理。
注意点容易导致内核崩溃 (Kernel Panic)
html
1.中断上下文中不能休眠
现象:在中断处理函数(ISR)中,你不能调用 ssleep(), msleep(), copy_to_user(), kmalloc(..., GFP_KERNEL) 等可能导致线程挂起(Sleep/Block)的函数。
后果:操作系统会直接崩溃(Oops),因为中断上下文没有进程描述符,一旦休眠,CPU 就不知道该切回到哪里去了。
因此在处理按键消抖时,绝对不能用 mdelay 或 msleep 死等,必须使用内核定时器。
2.硬件去抖 vs 软件去抖
现状:机械按键一定会有抖动(按下一次,电平跳变多次)。
如果只依赖硬件中断触发,必须配合软件定时器去抖。否则按一下,中断会触发几十次,导致系统负载瞬间飙升。
驱动逻辑通常是:中断触发 -> 关闭该中断 -> 开启定时器(如20ms)-> 定时器到期 -> 读取电平确认 -> 重新开启中断。
3.中断标志位的返回值
中断处理函数必须返回 irqreturn_t 类型。
IRQ_HANDLED:告诉内核"这个中断是我处理的"。
IRQ_NONE:告诉内核"这个中断不是我的"(用于共享中断线的情况)。
如果驱动一直返回 IRQ_NONE 但中断确实在发生,内核会检测到中断风暴,为了保护系统,会强行屏蔽掉这个中断源。
4.并发与竞争
场景:如果你的按键中断修改了一个全局变量,而你的应用程序也在读取这个变量。
注意:在中断中保护共享资源,不能使用互斥锁(Mutex)(因为Mutex可能导致休眠),只能使用自旋锁(Spinlock)。
5.设备树中的触发方式
在 DTS 中配置中断时,interrupts 属性里的标志位(如 IRQ_TYPE_EDGE_FALLING 下降沿触发)必须与硬件电路匹配。
如果引脚没有外部上拉/下拉电阻,必须在 pinctrl(引脚控制器)节点中配置内部上拉/下拉,否则引脚处于浮空状态,手指还没碰到按键,中断就会乱跳。
相应代码
设备树插件代码
html
/dts-v1/;
/plugin/;
//#include "../stm32mp157c.dtsi"
#include <dt-bindings/pinctrl/stm32-pinfunc.h>
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
/ {
fragment@0 {
target-path = "/";
__overlay__ {
button_interrupt {
compatible = "button_interrupt";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_button>;
button_gpio = <&gpiob 13 GPIO_ACTIVE_LOW>; //默认低电平,按键按下高电平
status = "okay";
interrupts-extended = <&gpiob 13 IRQ_TYPE_EDGE_RISING>;
interrupt-names = "button_interrupt";
//interrupt-parent = <&gpiob>;
//interrupts = <13 IRQ_TYPE_EDGE_RISING>; // 指定中断,触发方式为上升沿触发。
};
};
};
fragment@1 {
target = <&pinctrl>;
__overlay__ {
pinctrl_button: buttongrp {
pins {
pinmux = <STM32_PINMUX('B', 13, GPIO)>;
drive-push-pull;
};
};
};
};
};
驱动代码
c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include "interrupt.h"
/*------------------字符设备内容----------------------*/
#define DEV_NAME "button"
#define DEV_CNT (1)
static dev_t button_devno; //定义字符设备的设备号
static struct cdev button_chr_dev; //定义字符设备结构体chr_dev
struct class *class_button; //保存创建的类
struct device *device_button; // 保存创建的设备
struct device_node *button_device_node = NULL; //定义按键设备节点结构体
unsigned button_GPIO_number = 0; //保存button使用的GPIO引脚编号
u32 interrupt_number = 0; // button 引脚中断编号
atomic_t button_status = ATOMIC_INIT(0); //定义整型原子变量,保存按键状态 ,设置初始值为0
struct work_struct button_work; //定义工作结构体
/*定义工作执行函数*/
void work_hander(struct work_struct *work)
{
int counter = 1;
mdelay(200);
printk(KERN_ERR "work_hander counter = %d \n", counter++);
mdelay(200);
printk(KERN_ERR "work_hander counter = %d \n", counter++);
mdelay(200);
printk(KERN_ERR "work_hander counter = %d \n", counter++);
mdelay(200);
printk(KERN_ERR "work_hander counter = %d \n", counter++);
mdelay(200);
printk(KERN_ERR "work_hander counter = %d \n", counter++);
}
/*定义按键中断服务函数*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
/*按键状态加一*/
atomic_inc(&button_status);
schedule_work(&button_work); //触发工作
return IRQ_HANDLED;
}
static int button_open(struct inode *inode, struct file *filp)
{
int error = -1;
/*获取按键 设备树节点*/
button_device_node = of_find_node_by_path("/button_interrupt");
if(NULL == button_device_node)
{
printk("of_find_node_by_path error!");
return -1;
}
/*获取按键使用的GPIO*/
button_GPIO_number = of_get_named_gpio(button_device_node ,"button_gpio", 0);
if(0 == button_GPIO_number)
{
printk("of_get_named_gpio error");
return -1;
}
/*申请GPIO , 记得释放*/
error = gpio_request(button_GPIO_number, "button_gpio");
if(error < 0)
{
printk("gpio_request error");
gpio_free(button_GPIO_number);
return -1;
}
error = gpio_direction_input(button_GPIO_number);//设置引脚为输入模式
/*获取中断号*/
interrupt_number = irq_of_parse_and_map(button_device_node, 0);
printk("\n irq_of_parse_and_map! = %d \n",interrupt_number);
/*申请中断, 记得释放*/
error = request_irq(interrupt_number,button_irq_hander,IRQF_TRIGGER_RISING,"button_interrupt",device_button);
if(error != 0)
{
printk("request_irq error");
free_irq(interrupt_number, device_button);
return -1;
}
/*初始化button_work*/
INIT_WORK(&button_work, work_hander);
/*申请之后已经开启了,切记不要再次打开,否则运行时报错*/
// // enable_irq(interrupt_number);
return 0;
}
static int button_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int error = -1;
int button_countervc = 0;
/*读取按键状态值*/
button_countervc = atomic_read(&button_status);
/*结果拷贝到用户空间*/
error = copy_to_user(buf, &button_countervc, sizeof(button_countervc));
if(error < 0)
{
printk("copy_to_user error");
return -1;
}
/*清零按键状态值*/
atomic_set(&button_status,0);
return 0;
}
/*字符设备操作函数集,.release函数实现*/
static int button_release(struct inode *inode, struct file *filp)
{
/*释放申请的引脚,和中断*/
gpio_free(button_GPIO_number);
free_irq(interrupt_number, device_button);
return 0;
}
/*字符设备操作函数集*/
static struct file_operations button_chr_dev_fops = {
.owner = THIS_MODULE,
.open = button_open,
.read = button_read,
.release = button_release};
/*
*驱动初始化函数
*/
static int __init button_driver_init(void)
{
int error = -1;
/*采用动态分配的方式,获取设备编号,次设备号为0,*/
error = alloc_chrdev_region(&button_devno, 0, DEV_CNT, DEV_NAME);
if (error < 0)
{
printk("fail to alloc button_devno\n");
goto alloc_err;
}
/*关联字符设备结构体cdev与文件操作结构体file_operations*/
button_chr_dev.owner = THIS_MODULE;
cdev_init(&button_chr_dev, &button_chr_dev_fops);
/*添加设备至cdev_map散列表中*/
error = cdev_add(&button_chr_dev, button_devno, DEV_CNT);
if (error < 0)
{
printk("fail to add cdev\n");
goto add_err;
}
class_button = class_create(THIS_MODULE, DEV_NAME); //创建类
device_button = device_create(class_button, NULL, button_devno, NULL, DEV_NAME);//创建设备 DEV_NAME 指定设备名,
return 0;
add_err:
unregister_chrdev_region(button_devno, DEV_CNT); // 添加设备失败时,需要注销设备号
printk("\n error! \n");
alloc_err:
return -1;
}
/*
*驱动注销函数
*/
static void __exit button_driver_exit(void)
{
pr_info("button_driver_exit\n");
/*删除设备*/
device_destroy(class_button, button_devno); //清除设备
class_destroy(class_button); //清除类
cdev_del(&button_chr_dev); //清除设备号
unregister_chrdev_region(button_devno, DEV_CNT); //取消注册字符设备
}
module_init(button_driver_init);
module_exit(button_driver_exit);
MODULE_LICENSE("GPL");
代码拆解:
第一维度:核心机制(顶半部与底半部)
顶半部 (Top Half):button_irq_hander
c
/*定义按键中断服务函数*/
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
/*按键状态加一*/
atomic_inc(&button_status);
/* 核心动作:调度底半部 */
schedule_work(&button_work); //触发工作
return IRQ_HANDLED; // 告诉内核:这个中断我处理了
}
底半部 (Bottom Half):work_hander
html
/*定义工作执行函数*/
void work_hander(struct work_struct *work)
{
int counter = 1;
/* 核心特征:可以休眠 */
mdelay(200); // 延时 200ms
printk(KERN_ERR "work_hander counter = %d \n", counter++);
mdelay(200);
// ... 后面还有很多延时
}
关联
html
全局定义了 struct work_struct button_work; 用于描述一个工作项。
绑定函数(在 button_open 中):
/*初始化button_work,将结构体与底半部函数绑定*/
INIT_WORK(&button_work, work_hander);
触发调度(在顶半部中):
/* 告诉内核:把这个工作项加入执行队列,尽快执行 */
schedule_work(&button_work);
第二维度:生命周期管理
驱动的生命周期分为两个层次:模块的加载/卸载,和设备的打开/关闭。
模块加载与卸载 (init/exit)
c
static int __init button_driver_init(void) { ... }
static void __exit button_driver_exit(void) { ... }
/*
申请字符设备号,注册字符设备 (cdev_add),在 /sys/class 下创建类,最终在 /dev 下生成设备文件 button。
*/
设备的打开与释放 (open/release)
这里是真正的硬件操作
c
static int button_open(struct inode *inode, struct file *filp)
{
// 1. 解析设备树,找到节点
button_device_node = of_find_node_by_path("/button_interrupt");
// 2. 获取 GPIO 编号
button_GPIO_number = of_get_named_gpio(...);
// 3. 申请 GPIO 资源并设置为输入
gpio_request(button_GPIO_number, ...);
gpio_direction_input(button_GPIO_number);
// 4. 关键:将 GPIO 映射为 Linux 虚拟中断号
interrupt_number = irq_of_parse_and_map(button_device_node, 0);
// 5. 核心:申请中断,注册顶半部函数 button_irq_hander
// IRQF_TRIGGER_RISING 指定上升沿触发
request_irq(interrupt_number, button_irq_hander, IRQF_TRIGGER_RISING, ...);
// 6. 初始化底半部工作项
INIT_WORK(&button_work, work_hander);
return 0;
}
第三维度:数据交互(用户空间如何知道按键)
c
atomic_t button_status = ATOMIC_INIT(0); // 定义原子变量
// 顶半部 ISR 中:
atomic_inc(&button_status); // 按下一次,变量加 1
// read 操作中:
static int button_read(struct file *filp, char __user *buf, ...)
{
int button_countervc = 0;
/*读取按键状态值*/
button_countervc = atomic_read(&button_status);
/*结果拷贝到用户空间*/
copy_to_user(buf, &button_countervc, sizeof(button_countervc));
/*读完后清零*/
atomic_set(&button_status,0);
return 0;
}
第四维度:细节
html
1.用 atomic_t (原子变量)的目的
button_status 这个变量,它会被两个"并发"的实体访问:
中断处理函数 (ISR):随时可能发生,修改它。
button_read 函数:用户进程在读取它。
如果使用普通的 int 变量,可能会出现竞态条件(Race Condition)。例如,read 函数刚读到一半,中断来了把它修改了,导致读出的数据错乱。使用 atomic_t 和对应的 atomic_inc/atomic_read 函数,能保证这些操作是原子的(不可分割的),从而安全地在中断和进程之间共享数据。
2.设备树依赖
button_device_node = of_find_node_by_path("/button_interrupt");
button_GPIO_number = of_get_named_gpio(button_device_node ,"button_gpio", 0);
interrupt_number = irq_of_parse_and_map(button_device_node, 0);
这个驱动依赖于一个配套的设备树插件(.dtbo)。在设备树中,必须定义一个名为 /button_interrupt 的节点,并且该节点需要指定 button_gpio 属性,以及中断控制器父节点等信息。内核会根据设备树的描述,自动帮你找到对应的 GPIO 引脚和物理中断线,并映射为驱动可用的虚拟中断号。
基于工作队列(Workqueue)的 Linux 中断驱动程序的完整生命周期和执行分层

html
阶段一:初始化与资源准备 (Setup Phase)还没有按键按下,系统处于准备状态。
用户态启动:位于最上层蓝色区域的应用程序启动 (App Start)。
发起调用:应用调用标准的 POSIX 接口 open("/dev/button", ...)。
进入内核:通过系统调用 (System Call),执行流陷入内核态的进程上下文 (Process Context, 浅橙色区域)。
驱动响应 (button_open):
驱动程序中的 button_open 函数被执行。
它负责"建立资源"(Sets up resources),包括申请 GPIO 引脚,最重要的是调用 request_irq 注册中断处理函数,并初始化工作队列项。此时,硬件中断被使能,系统开始准备接收按键事件。
阶段二:常规数据读取 (Normal Operation Phase)应用程序主动查询按键状态的过程。
用户态读取:应用程序在一个循环中调用 read(...),试图获取按键数据。
驱动响应 (button_read):
访问共享数据:它去读取位于中央的"全局共享数据"区域 (Global Shared Data),查看原子变量 atomic_t 的值(即按键次数)。
返回数据:将读到的次数拷贝回用户空间,并将内核中的计数器清零。
业务处理:应用程序拿到数据后进行业务逻辑处理 (Process Logic),然后再次循环读取。
阶段三:异步中断事件处理 (The Asynchronous Interrupt Core)描述了当物理按键被按下时发生的瞬间。
硬件触发 (最底层灰色区域):手指按下物理按键 (Physical Button Press),硬件产生电平信号触发中断 (Hardware Trigger)。
顶半部执行 (Top Half - 红色醒目区域):
CPU 立即暂停当前任务,跳转执行注册的中断服务函数 button_irq_hander。
关键约束 (CRITICAL! NOTE: NO SLEEP!):此时处于中断上下文,绝对禁止休眠或执行耗时操作。
动作 A (原子操作):快速将"全局共享数据"中的原子计数器加 1 (atomic increment)。
动作 B (调度底半部):调用 schedule_work,将耗时的后续处理任务"挂"到内核的工作队列中,然后立即退出顶半部。
底半部执行 (Bottom Half - 右下灰色区域):
当系统调度器认为合适时,内核工作线程 (Kernel Worker Thread) 会被唤醒。
它执行之前被调度的 work_hander 函数。
此时运行在进程上下文,可以放心地休眠(执行去抖动延时 mdelay、打印复杂日志等)。
阶段四:资源释放与清理 (Cleanup Phase)应用程序退出时的收尾工作。
用户态关闭:应用程序完成任务,调用 close()。
驱动响应 (button_release):运行在进程上下文。
执行"清理资源"操作 (Clean up resources)。最重要的是调用 free_irq 释放中断号,并释放 GPIO 资源,确保下次加载驱动时不会冲突。