一、 中断的核心基础知识
在 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。
-
例子:窜口中断
cs
// 假设我们要接管 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,把闸门恢复到刚才的状态。
-
例子:包含全局共享数组
cs
int global_array[10];
void update_array_safely(void)
{
rt_base_t level;
// 1. 记下当前状态,并拉下全局物理大闸!时间停止!
level = rt_hw_interrupt_disable();
// ===== 以下是绝对安全的"临界区" =====
// 在这几微秒内,绝对没有任何中断能打断我,我可以放心修改数组
for(int i=0; i<10; 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 的电平状态,并通过串口打印提示信息。
cs
#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 才进入下一步(拉长时间窗口,覆盖抖动期)。 -
第二层:状态过滤在释放信号量前,再次读取引脚电平。因为是上拉输入模式,只有读到 低电平,才确认按键真的被按住了(过滤掉抖动带来的毛刺)。
- 系统架构:上下分离
-
上半部(中断):只做 "消抖判断 + 释放信号量",快进快出,不占用过多中断时间。
-
下半部(线程):平时休眠,拿到信号量后再处理 "翻转 LED、打印日志" 等耗时操作,不影响中断响应。
cs
#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;
}