RT thread中断管理学习记录 一、 中断的核心基础知识在 RT-Thread 环境下你需要建立这三个最硬核的认知1. 物理本质CPU 的“紧急拨号”当 STM32F429 上的某个引脚电平发生变化按键按下或者硬件定时器数到了 0或者串口接收到了一个字节相关的硬件电路会直接给 CPU 核心发送一个高低电平电信号。 CPU 一旦收到这个信号会立刻冻结当前正在运行的任何线程把 CPU 内部的寄存器状态现场压入栈中保存然后瞬间跳到一个固定的物理内存地址去执行代码——这个代码叫做 ISR中断服务函数Interrupt Service Routine。2. 中断优先级NVIC如果有两个紧急事件同时发生怎么办STM32 内部有一个叫 NVIC嵌套向量中断控制器的硬件管家。它可以给硬件中断排优先级。高优先级的中断甚至可以再次打断低优先级的中断这叫中断嵌套。 注意硬件中断的优先级绝对高于任何操作系统的线程哪怕是优先级为 0 的最高级线程。3. 禁止操作在编写 ISR中断服务函数时遵守一个绝对原则快进快出绝不阻塞 因为 ISR 运行在一种特殊的“中断上下文”中它根本不是一个线程它没有线程控制块TCB。绝对禁止 在中断里调用rt_thread_mdelay()、rt_mutex_take()等任何会导致挂起、休眠等待的 API。一旦调用系统内核找不到可以挂起的线程控制块会瞬间触发 Kernel Panic内核崩溃导致死机。绝对禁止 在中断里调用rt_malloc()申请动态内存因为malloc底层遍历链表时间不可控且带有内部锁。二、 上半部与下半部既然中断里不能干耗时的重活那复杂的运算怎么办 在 RTOS 工程中业界唯一标准的做法是**“中断只负责发信号线程负责干重活”**上半部中断函数 ISR 纯硬件触发。进来后迅速清除硬件的中断标志位然后释放一个信号量或发个邮件立刻return退出。耗时通常在几微秒。下半部处理线程 这是一个普通的死循环线程平时一直阻塞等信号量不占 CPU。中断里的信号量一释放这个线程瞬间被内核唤醒开始从容地执行复杂的算法或数据解析。三、具体函数讲解1.rt_hw_interrupt_install()给硬件拉一根专线底层作用 它是用来修改 CPU 内部的中断向量表。当某个硬件引脚来电平时CPU 默认不知道该去执行哪段代码。这个函数就是告诉 CPU“如果第vector号中断响了请直接跳转到handler这个函数的物理地址去执行。”vector中断号比如 STM32 里的USART1_IRQn本质上是个整数。handler你写的那个中断服务函数ISR的名字。param传给你那个函数的参数通常填RT_NULL。name给这个中断起个名字方便查日志。2.rt_hw_interrupt_mask()/umask()单个阀门的关与开底层作用 修改 NVIC中断控制器的寄存器。mask屏蔽把第vector号中断的电线物理剪断。硬件就算冒烟了CPU 也不会搭理它。umask解除屏蔽把电线接上允许这个中断打断 CPU。例子窜口中断// 假设我们要接管 STM32 的串口 1 接收中断 (中断号假设为 37) #define USART1_IRQ_NUM 37 // 自己写的纯底层中断函数 void my_uart_rx_isr(int vector, void *param) { // 读取串口数据寄存器... rt_kprintf(收到底层硬件中断\n); } void setup_uart_interrupt(void) { // 1. 先屏蔽它防止在安装过程中突然来中断导致崩溃 rt_hw_interrupt_mask(USART1_IRQ_NUM); // 2. 安装专线把中断号 37 和我的函数绑在一起 rt_hw_interrupt_install(USART1_IRQ_NUM, my_uart_rx_isr, RT_NULL, my_uart); // 3. 安装完毕打开阀门正式开始接收 rt_hw_interrupt_umask(USART1_IRQ_NUM); }3.rt_hw_interrupt_disable()与rt_hw_interrupt_enable()一般用来包含临近区的操作。底层作用 修改 Cortex-M 核心的PRIMASK寄存器。disable瞬间关闭全芯片所有受操作系统管理的中断 此时哪怕按键按烂了、CPU 都绝对不会被打断。它用来保护那些“绝对不能执行到一半被别人偷家”的关键代码。enable恢复中断开关。为什么disable要返回一个rt_base_t level假设你在关中断之前中断本来就是关着的。如果你强行执行“关 - 开”就会把原本关着的中断错误地打开。所以disable会先记住当前的开关状态存入 level然后关闸。而enable会拿着这个level把闸门恢复到刚才的状态。例子包含全局共享数组int global_array[10]; void update_array_safely(void) { rt_base_t level; // 1. 记下当前状态并拉下全局物理大闸时间停止 level rt_hw_interrupt_disable(); // 以下是绝对安全的“临界区” // 在这几微秒内绝对没有任何中断能打断我我可以放心修改数组 for(int i0; i10; i) { global_array[i] i * 2; } // // 2. 事情办完根据刚才记下的状态 level恢复系统时间流动 rt_hw_interrupt_enable(level); }4.rt_interrupt_enter()与rt_interrupt_leave()底层作用 操作系统内部有一个极其关键的全局变量叫rt_interrupt_nest中断嵌套层数。enter把这个变量1。告诉调度器“报告我现在进中断了千万别在此时进行线程切换”leave把这个变量-1。告诉调度器“报告我准备退出中断了。如果是 0你可以立刻去看看有没有更高优先级的线程需要抢占 CPU”规定 这两个函数必须在你自己写的每一个物理硬件中断函数的最开头和最结尾成对出现四按键外部中断点灯初始化配置将 LED 引脚设为输出模式默认高电平灯灭。将按键引脚设为上拉输入模式默认高电平按下接地变低。中断绑定与使能为两个按键分别绑定下降沿触发的中断回调函数按下瞬间触发。开启按键的中断响应。中断回调消抖 控制利用static变量记录上一次触发时间通过判断时间差55ms实现软件消抖避免按键抖动误触发。若为有效触发翻转对应 LED 的电平状态并通过串口打印提示信息。#include rtthread.h #include rtdevice.h #include board.h /* 引脚定义 */ #define KEY1 GET_PIN(H, 2) // 定义按键1的引脚为 PH2 #define KEY0 GET_PIN(H, 3) // 定义按键0的引脚为 PH3 #define led1 GET_PIN(B, 0) // 定义LED1的引脚为 PB0 #define led0 GET_PIN(B, 1) // 定义LED0的引脚为 PB1 /** * brief 按键1的中断回调函数 * * param para 中断回调函数的传参此处未使用 */ void hdr_key1_callback(void *para) { /* 定义静态变量 last_tick用于记录上一次触发的时间 static 修饰的局部变量只会在第一次调用时初始化 之后每次调用都会保留上一次的值 */ static rt_tick_t last_tick 0; // 获取当前的系统时间单位tick rt_tick_t current_tick rt_tick_get(); /* 消抖逻辑判断 计算当前时间与上一次有效触发时间的差值 如果差值大于 55ms则认为是有效按键非抖动误触 */ if ((current_tick - last_tick) rt_tick_from_millisecond(55)) { // 翻转 LED1 的电平状态如果当前是高则变低低则变高 rt_pin_write(led1, !rt_pin_read(led1)); // 打印日志到串口控制台 rt_kprintf(【中断触发】按键1 被按下LED1 已翻转\n); // 更新 last_tick 为当前时间以便下一次消抖判断 last_tick current_tick; } } /** * brief 按键0的中断回调函数 * * param para 中断回调函数的传参此处未使用 */ void hdr_key0_callback(void *para) { /* 这里定义的 last_tick 与上面按键1的 last_tick 互不干扰 因为 static 局部变量的作用域仅限于当前函数内部 */ static rt_tick_t last_tick 0; // 获取当前的系统时间 rt_tick_t current_tick rt_tick_get(); // 同样的消抖逻辑55ms if ((current_tick - last_tick) rt_tick_from_millisecond(55)) { // 翻转 LED0 的电平状态 rt_pin_write(led0, !rt_pin_read(led0)); // 打印日志到串口控制台 rt_kprintf(【中断触发】按键0 被按下LED0 已翻转\n); // 更新时间戳 last_tick current_tick; } } int main(void) { /* 1. 初始化 LED 相关 */ // 设置 LED0 引脚为推挽输出模式 rt_pin_mode(led0, PIN_MODE_OUTPUT); // 设置 LED1 引脚为推挽输出模式 rt_pin_mode(led1, PIN_MODE_OUTPUT); // 将 LED0 默认设置为高电平假设高电平为灭低电平为亮具体看硬件连接 rt_pin_write(led0, PIN_HIGH); // 将 LED1 默认设置为高电平 rt_pin_write(led1, PIN_HIGH); /* 2. 初始化按键相关 */ // 设置 KEY1 引脚为上拉输入模式默认高电平按下接地变低 rt_pin_mode(KEY1, PIN_MODE_INPUT_PULLUP); // 设置 KEY0 引脚为上拉输入模式 rt_pin_mode(KEY0, PIN_MODE_INPUT_PULLUP); /* 3. 绑定中断回调函数 */ // 绑定 KEY1 到中断线触发模式为下降沿按下瞬间回调函数为 hdr_key1_callback rt_pin_attach_irq(KEY1, PIN_IRQ_MODE_FALLING, hdr_key1_callback, RT_NULL); // 绑定 KEY0 到中断线触发模式为下降沿回调函数为 hdr_key0_callback rt_pin_attach_irq(KEY0, PIN_IRQ_MODE_FALLING, hdr_key0_callback, RT_NULL); /* 4. 使能中断 */ // 开启 KEY0 的中断响应 rt_pin_irq_enable(KEY0, PIN_IRQ_ENABLE); // 开启 KEY1 的中断响应 rt_pin_irq_enable(KEY1, PIN_IRQ_ENABLE); return 0; }把代码改为上下部分处理上部分中断释放互斥量下部分线程执行任务。在中断回调函数里做两层过滤从源头杜绝误触发第一层时间过滤用static变量记录上一次有效触发的时间只有距离上次触发超过 100ms 才进入下一步拉长时间窗口覆盖抖动期。第二层状态过滤在释放信号量前再次读取引脚电平。因为是上拉输入模式只有读到 低电平才确认按键真的被按住了过滤掉抖动带来的毛刺。3. 系统架构上下分离上半部中断只做 “消抖判断 释放信号量”快进快出不占用过多中断时间。下半部线程平时休眠拿到信号量后再处理 “翻转 LED、打印日志” 等耗时操作不影响中断响应。#include rtthread.h #include rtdevice.h #include board.h /* 引脚定义 */ #define KEY1 GET_PIN(H, 2) #define KEY0 GET_PIN(H, 3) #define led1 GET_PIN(B, 0) #define led0 GET_PIN(B, 1) /* 定义信号量句柄连接中断和线程 */ static rt_sem_t sem_key0 RT_NULL; static rt_sem_t sem_key1 RT_NULL; /*************************** 上半部中断服务函数 *************************** * 功能硬件中断触发 - 软件消抖 - 释放信号量 * 特点执行速度极快不操作LED、不打印日志 **************************************************************************/ void hdr_key1_callback(void *para) { // 静态变量保存上一次触发时间用于消抖 static rt_tick_t last_tick 0; rt_tick_t current_tick rt_tick_get(); // 【核心消抖】间隔大于55ms才认为是有效按键 if ((current_tick - last_tick) rt_tick_from_millisecond(55)) { if(rt_pin_read(KEY1)PIN_LOW) { rt_sem_release(sem_key1); // 释放信号量唤醒线程 last_tick current_tick; // 更新时间戳 } } } void hdr_key0_callback(void *para) { static rt_tick_t last_tick 0; rt_tick_t current_tick rt_tick_get(); // 【核心消抖】 if ((current_tick - last_tick) rt_tick_from_millisecond(55)) { if(rt_pin_read(KEY0)PIN_LOW) { rt_sem_release(sem_key0); // 释放信号量 last_tick current_tick; } } } /*************************** 下半部工作线程 *************************** * 功能等待信号量 - 执行LED翻转、打印日志耗时操作 * 特点无消抖、无延时专注业务逻辑 **********************************************************************/ void key0_thread_entry(void *parameter) { while (1) { // 永久等待信号量无信号时休眠不占CPU if (rt_sem_take(sem_key0, RT_WAITING_FOREVER) RT_EOK) { rt_pin_write(led0, !rt_pin_read(led0)); rt_kprintf(【下半部】按键0 触发LED0 翻转\n); } } } void key1_thread_entry(void *parameter) { while (1) { if (rt_sem_take(sem_key1, RT_WAITING_FOREVER) RT_EOK) { rt_pin_write(led1, !rt_pin_read(led1)); rt_kprintf(【下半部】按键1 触发LED1 翻转\n); } } } int main(void) { /* 1. LED初始化 */ rt_pin_mode(led0, PIN_MODE_OUTPUT); rt_pin_mode(led1, PIN_MODE_OUTPUT); rt_pin_write(led0, PIN_HIGH); rt_pin_write(led1, PIN_HIGH); /* 2. 按键初始化 */ rt_pin_mode(KEY1, PIN_MODE_INPUT_PULLUP); rt_pin_mode(KEY0, PIN_MODE_INPUT_PULLUP); /* 3. 创建信号量 */ sem_key0 rt_sem_create(sem_k0, 0, RT_IPC_FLAG_PRIO); sem_key1 rt_sem_create(sem_k1, 0, RT_IPC_FLAG_PRIO); /* 4. 创建线程 */ rt_thread_t tid0 rt_thread_create(th_k0, key0_thread_entry, RT_NULL, 512, 10, 20); rt_thread_t tid1 rt_thread_create(th_k1, key1_thread_entry, RT_NULL, 512, 10, 20); if (tid0) rt_thread_startup(tid0); if (tid1) rt_thread_startup(tid1); /* 5. 绑定并开启中断 */ rt_pin_attach_irq(KEY1, PIN_IRQ_MODE_FALLING, hdr_key1_callback, RT_NULL); rt_pin_attach_irq(KEY0, PIN_IRQ_MODE_FALLING, hdr_key0_callback, RT_NULL); rt_pin_irq_enable(KEY0, PIN_IRQ_ENABLE); rt_pin_irq_enable(KEY1, PIN_IRQ_ENABLE); return 0; }