4.9 中断管理
4.9 中断管理
📚 本节导读
学习时长: 约 30 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: ARM Cortex-M 中断模型(NVIC、PRIMASK、IPSR)、第 4.2 节任务管理
🎯 学习目标
- 理解 OneOS 中断管理机制的整体架构
- 掌握
os_irq_lock/os_irq_unlock的正确使用场景 - 区分
os_irq_lock与os_irq_disable的差异 - 学会在中断上下文中安全编程
- 了解中断栈管理与溢出检测机制
一、概述
中断是嵌入式实时操作系统的核心机制。OneOS 的中断管理通过架构抽象层(arch 层)提供统一的 API,将底层硬件差异屏蔽在 arch_interrupt.h / arch_interrupt.c 中,内核和应用层只需调用统一接口即可。
1.1 中断管理在 OneOS 中的位置
┌──────────────────────────────────────────────┐
│ 应用层 / 内核层 │
│ 调用 os_irq_lock() / os_irq_unlock() 等 API │
├──────────────────────────────────────────────┤
│ 架构抽象层 (arch_interrupt.h) │
│ 统一接口声明,各架构独立实现 │
├──────────┬──────────┬──────────┬─────────────┤
│ ARMv7-M │ ARMv7-R │ RISC-V │ CSKY ... │
│ PRIMASK │ CPSR │ MIE │ PSR ... │
└──────────┴──────────┴──────────┴─────────────┘1.2 核心 API 一览
| API | 功能 | 嵌套安全 | 返回值 |
|---|---|---|---|
os_irq_lock() | 关闭中断,返回之前的中断状态 | ✅ 支持 | os_ubase_t(原状态) |
os_irq_unlock(irq_save) | 恢复中断到指定状态 | ✅ 支持 | 无 |
os_irq_disable() | 全局禁用中断 | ❌ 不支持 | 无 |
os_irq_enable() | 全局启用中断 | ❌ 不支持 | 无 |
os_is_irq_active() | 判断是否在中断上下文 | - | OS_TRUE / OS_FALSE |
os_is_irq_disabled() | 判断中断是否被禁用 | - | OS_TRUE / OS_FALSE |
os_irq_num() | 获取当前中断号 | - | uint32_t(中断号) |
os_is_fault_active() | 判断是否在异常上下文 | - | OS_TRUE / OS_FALSE |
源码参考: arch_interrupt.h 头文件 | arch_interrupt.c 实现
二、临界区保护:os_irq_lock / os_irq_unlock
2.1 原理
os_irq_lock / os_irq_unlock 是 OneOS 中最常用的临界区保护手段。它们通过操作处理器的全局中断屏蔽位(ARM Cortex-M 中为 PRIMASK)来实现:
os_irq_lock():读取当前 PRIMASK 值并保存,然后关闭全局中断(CPSID I),返回保存的值os_irq_unlock(irq_save):将 PRIMASK 恢复为irq_save参数指定的值
2.2 ARMv7-M 实现
os_ubase_t os_irq_lock(void)
{
os_ubase_t primask;
__asm__ __volatile__(
"MRS %0, PRIMASK\n" // 读取当前 PRIMASK 值
"CPSID I" // 关闭全局中断
: "=r"(primask)
:
: "memory");
return primask; // 返回之前的状态
}
void os_irq_unlock(os_ubase_t irq_save)
{
__asm__ __volatile__(
"MSR PRIMASK, %0" // 恢复 PRIMASK 到之前的值
:
: "r"(irq_save)
: "memory");
}2.3 使用模式
/* 标准用法:保护临界区 */
void critical_function(void)
{
os_ubase_t level;
/* 进入临界区:关中断并保存原状态 */
level = os_irq_lock();
/* --- 临界区代码(不可被中断打断)--- */
shared_resource.value = 100;
shared_resource.flag = OS_TRUE;
/* --- 临界区结束 --- */
/* 退出临界区:恢复中断状态 */
os_irq_unlock(level);
}2.4 支持嵌套
void outer_function(void)
{
os_ubase_t level1 = os_irq_lock(); // 关中断,level1 = 0(之前中断是开的)
/* ... 临界区 1 ... */
inner_function();
os_irq_unlock(level1); // 恢复中断,level1 = 0 → 开中断
}
void inner_function(void)
{
os_ubase_t level2 = os_irq_lock(); // 关中断,level2 = 1(之前中断已关)
/* ... 临界区 2 ... */
os_irq_unlock(level2); // 恢复中断,level2 = 1 → 继续关中断
// 注意:此时中断仍然是关的,因为外层还没恢复
}关键点:
os_irq_unlock恢复到irq_save参数指定的状态,而非一定开中断。因此嵌套调用是安全的。
三、全局中断控制:os_irq_disable / os_irq_enable
3.1 与 os_irq_lock 的区别
| 特性 | os_irq_lock/unlock | os_irq_disable/enable |
|---|---|---|
| 保存/恢复状态 | ✅ 保存原状态 | ❌ 不保存状态 |
| 嵌套安全 | ✅ 安全 | ❌ 不安全 |
| 使用场景 | 临界区保护(推荐) | 系统初始化、异常处理 |
| 配对要求 | 必须 lock/unlock 配对 | 必须 disable/enable 配对 |
3.2 ARMv7-M 实现
void os_irq_disable(void)
{
__asm__ __volatile__(
"CPSID I" // 直接关闭全局中断
:
:
:);
}
void os_irq_enable(void)
{
__asm__ __volatile__(
"CPSIE I" // 直接开启全局中断
:
:
:);
}3.3 使用场景
/* 场景1:系统启动时临时禁用中断 */
void system_init(void)
{
os_irq_disable();
/* 初始化中断控制器、中断向量表等 */
nvic_init();
vector_table_init();
os_irq_enable(); // 初始化完成后统一开启
}
/* 场景2:异常处理中强制关闭中断 */
void fault_handler(void)
{
os_irq_disable(); // 强制关闭,防止嵌套异常
/* 收集异常信息,尝试恢复或复位 */
collect_fault_info();
while (1); // 停止系统
}⚠️ 警告:
os_irq_disable()和os_irq_enable()不保存状态,不可嵌套使用。若在函数 A 中调用了os_irq_disable(),然后调用函数 B,函数 B 内部若也调用了os_irq_disable()/os_irq_enable(),则函数 B 返回后,函数 A 以为中断还是关的,实际上已被函数 B 打开。这是危险的 bug!
四、中断状态查询
4.1 os_is_irq_active — 判断是否在中断上下文
os_bool_t os_is_irq_active(void)
{
os_bool_t active;
__asm__ __volatile__(
" MRS %0, IPSR\n" // 读取中断程序状态寄存器
" CBZ %0, in_task_context\n" // 如果 IPSR = 0,在任务上下文
" MOV %0, #1\n"
"in_task_context:"
: "=r"(active)
:
: "memory");
return active;
}原理:ARM Cortex-M 的 IPSR(Interrupt Program Status Register)寄存器存储当前正在执行的中断号。若值为 0,表示当前在线程模式(任务上下文);非 0 表示在处理模式(中断/异常上下文)。
使用场景:
/* 场景1:API 函数中判断上下文,避免在中断中调用阻塞函数 */
os_err_t os_sem_wait(os_semaphore_id sem, os_tick_t timeout)
{
/* 中断上下文中不能阻塞等待 */
if (os_is_irq_active() && (timeout != OS_NO_WAIT))
{
OS_SET_ERRNO(OS_EBUSY);
return OS_EBUSY;
}
/* ... 正常等待逻辑 ... */
}
/* 场景2:根据上下文选择不同的内存分配策略 */
void *my_alloc(uint32_t size)
{
if (os_is_irq_active())
{
/* 中断上下文:从内存池快速分配,不阻塞 */
return os_mempool_alloc(irq_pool, size);
}
else
{
/* 任务上下文:可以使用堆分配 */
return os_malloc(size);
}
}4.2 os_is_irq_disabled — 判断中断是否被禁用
os_bool_t os_is_irq_disabled(void)
{
os_bool_t disabled;
__asm__ __volatile__(
" MRS %0, PRIMASK\n" // 读取 PRIMASK 寄存器
: "=r"(disabled)
:
: "memory");
return disabled;
}原理:PRIMASK 寄存器为 1 表示全局中断被屏蔽,为 0 表示中断开启。
4.3 os_irq_num — 获取当前中断号
uint32_t os_irq_num(void)
{
uint32_t irq_num;
__asm__ __volatile__(
" MRS %0, IPSR\n"
: "=r"(irq_num)
:
:);
return irq_num;
}用途:调试追踪、日志输出、中断统计等场景。
4.4 os_is_fault_active — 判断是否在异常上下文
os_bool_t os_is_fault_active(void)
{
// ARMv7-M 异常向量号范围:HardFault(3) ~ UsageFault(6)
os_bool_t active;
__asm__ __volatile__(
" MRS %0, IPSR\n"
" CMP %0, #3\n"
" BLT noactive\n"
" CMP %0, #6\n"
" BGT noactive\n"
" MOV %0, #1\n"
" B end\n"
"noactive:\n"
" MOV %0, #0\n"
"end:\n"
: "=r"(active)
:
: "cc", "memory");
return active;
}与 os_is_irq_active 的区别:
| 函数 | 判断范围 | 检测对象 |
|---|---|---|
os_is_irq_active() | IPSR ≠ 0 | 所有中断和异常 |
os_is_fault_active() | IPSR ∈ [3, 6] | 仅故障异常(HardFault / MemManage / BusFault / UsageFault) |
五、中断栈管理
5.1 中断栈的概念
在 ARM Cortex-M 架构中,中断处理使用主栈(Main Stack, MSP),而任务使用进程栈(Process Stack, PSP)。OneOS 支持中断栈溢出检测功能,通过 OS_USING_INTERRUPT_STACK_OVERFLOW_CHECK 宏启用。
┌──────────────┐ 高地址
│ 任务栈 1 │ ← PSP (任务1)
├──────────────┤
│ 任务栈 2 │ ← PSP (任务2)
├──────────────┤
│ 任务栈 3 │ ← PSP (任务3)
├──────────────┤
│ │
│ 中断栈 │ ← MSP (中断处理时使用)
│ │
├──────────────┤
│ gs_main_ │ ← 中断栈起始地址
│ stack_start │
└──────────────┘ 低地址5.2 中断栈溢出检测
启用 OS_USING_INTERRUPT_STACK_OVERFLOW_CHECK 后,系统会在启动时初始化中断栈,并在任务切换时检查栈是否溢出:
/* 中断栈初始化:将栈空间填充为特殊字符 '$' (0x24) */
os_err_t os_interrupt_stack_init(void)
{
// 获取中断栈起始和结束地址
gs_main_stack_start_addr = (uint32_t)(&CSTACK_BLOCK_START);
gs_main_stack_end_addr = (uint32_t)(&CSTACK_BLOCK_END);
addr = (uint8_t *)gs_main_stack_start_addr;
level = os_irq_lock();
os_main_stack_init(addr); // 填充 0x24242424
os_irq_unlock(level);
return OS_SUCCESS;
}
/* 中断栈溢出检查:检查栈顶 16 字节是否仍为 0x24242424 */
void os_interrupt_stack_check(void)
{
uint32_t *addr = (uint32_t *)gs_main_stack_start_addr;
for (int i = 0; i < 4; i++)
{
if (*addr == 0x24242424)
addr++;
else
{
os_kprintf("ERROR: Interrupt stack overflow!\r\n");
OS_ASSERT(0);
}
}
}5.3 查看中断栈信息
通过 Shell 命令查看中断栈使用情况:
show_intstk输出示例:
stack_begin stack_end stack_size max_used
----------- ----------- ---------- --------
0x20001000 0x20002000 4096 35%六、中断上下文编程注意事项
6.1 中断处理函数中可用的 API
| 操作类型 | 中断中可用 | 说明 |
|---|---|---|
os_irq_lock/unlock | ✅ | 临界区保护 |
os_sem_post | ✅ | 释放信号量唤醒任务 |
os_event_post | ✅ | 发送事件 |
os_mq_send | ✅ | 发送消息(非阻塞) |
os_mb_send | ✅ | 发送邮件(非阻塞) |
os_sem_wait (timeout=0) | ✅ | 非阻塞获取 |
os_sem_wait (timeout>0) | ❌ | 不能阻塞等待 |
os_task_tsleep | ❌ | 不能延时 |
os_malloc | ⚠️ | 谨慎使用(可能阻塞) |
os_kprintf | ⚠️ | 调试用,生产环境避免 |
6.2 中断处理时间
中断处理函数应尽可能简短,遵循「快进快出」原则:
/* ✅ 推荐:中断中只做必要操作,耗时处理通过信号量/消息队列交给任务 */
void UART_IRQHandler(void)
{
os_ubase_t level = os_irq_lock();
char ch = UART_ReadByte(); // 快速读取数据
ring_buffer_put(&uart_rx_buf, ch); // 放入环形缓冲区
os_irq_unlock(level);
os_sem_post(&uart_rx_sem); // 通知任务处理
}
/* ❌ 不推荐:在中断中做耗时操作 */
void BAD_IRQHandler(void)
{
char ch = UART_ReadByte();
os_kprintf("Received: %c\n", ch); // 打印慢,阻塞中断
process_data(ch); // 业务处理慢,阻塞中断
os_task_tsleep(10); // 不能在中断中延时!
}6.3 中断优先级
ARM Cortex-M 架构中,中断优先级数值越小优先级越高。OneOS 不直接管理中断优先级,而是由用户在 BSP 初始化时配置 NVIC:
/* 在 BSP 初始化中配置中断优先级 */
NVIC_SetPriority(UART0_IRQn, 2); // UART 中断优先级 2
NVIC_SetPriority(TIMER0_IRQn, 1); // 定时器中断优先级 1(更高)
NVIC_SetPriority(SPI0_IRQn, 3); // SPI 中断优先级 3
NVIC_EnableIRQ(UART0_IRQn);
NVIC_EnableIRQ(TIMER0_IRQn);
NVIC_EnableIRQ(SPI0_IRQn);注意:使用
os_irq_lock()时,它只屏蔽了 PRIMASK(全局中断),不区分优先级。但高优先级中断仍然可以抢占低优先级中断处理函数(在os_irq_unlock之后)。
七、与异常处理的关系
中断管理和异常处理是 OneOS 错误处理体系的两个层面:
| 层面 | 机制 | 触发方式 | 处理方式 |
|---|---|---|---|
| 中断管理 | 外设中断、定时器中断等 | 硬件外设触发 | ISR 快速处理,通过 IPC 通知任务 |
| 异常处理 | HardFault、BusFault 等 | 程序错误触发 | 异常钩子 → 安全机制处理 → 系统复位 |
中断/异常触发
│
├─ IPSR = 0 (任务上下文)
│ └─ 正常执行
│
├─ IPSR = 1~15 (系统异常)
│ ├─ 3~6: HardFault/MemManage/BusFault/UsageFault
│ │ └─ os_is_fault_active() → OS_TRUE
│ │ └─ 跳转异常处理流程 (参见 4.6.5 异常处理)
│ │
│ └─ 其他系统异常 (SVCall, PendSV, SysTick)
│ └─ 内核调度使用
│
└─ IPSR ≥ 16 (外设中断)
└─ os_is_irq_active() → OS_TRUE
└─ 跳转外设 ISR 处理八、API 参考
8.1 中断控制
/* 关闭全局中断,返回之前的中断状态(支持嵌套) */
os_ubase_t os_irq_lock(void);
/* 恢复中断到指定状态 */
void os_irq_unlock(os_ubase_t irq_save);
/* 全局禁用中断(不保存状态,不可嵌套) */
void os_irq_disable(void);
/* 全局启用中断(不保存状态,不可嵌套) */
void os_irq_enable(void);8.2 中断状态查询
/* 判断当前是否在中断上下文 */
os_bool_t os_is_irq_active(void);
/* 判断全局中断是否被禁用 */
os_bool_t os_is_irq_disabled(void);
/* 获取当前中断号(IPSR 值) */
uint32_t os_irq_num(void);
/* 判断当前是否在故障异常上下文 */
os_bool_t os_is_fault_active(void);8.3 中断栈管理
#ifdef OS_USING_INTERRUPT_STACK_OVERFLOW_CHECK
/* 中断栈溢出检查 */
void os_interrupt_stack_check(void);
#endif九、最佳实践
9.1 临界区保护
✅ 推荐:使用 os_irq_lock/os_irq_unlock
- 保存/恢复状态,支持嵌套
- 适合大多数临界区场景
❌ 避免:使用 os_irq_disable/os_irq_enable
- 不保存状态,不支持嵌套
- 仅在系统初始化、异常处理等特殊场景使用9.2 中断处理函数设计
✅ DO:
- 保持 ISR 简短
- 使用信号量/消息队列将耗时处理交给任务
- 用 os_irq_lock 保护共享数据
- 检查 os_is_irq_active() 避免阻塞调用
❌ DON'T:
- 在 ISR 中调用阻塞函数(os_sem_wait 带超时、os_task_tsleep 等)
- 在 ISR 中打印大量日志
- 在 ISR 中执行复杂计算
- 嵌套使用 os_irq_disable/os_irq_enable9.3 中断栈保护
/* 开发阶段启用中断栈溢出检测 */
#define OS_USING_INTERRUPT_STACK_OVERFLOW_CHECK
/* 估算中断栈大小 */
// 中断栈 = 最大嵌套深度 × (寄存器保存 + 局部变量 + ISR 调用开销)
// 建议至少预留 1KB~2KB,并在开发阶段通过 show_intstk 监控使用率📖 相关资源
- 4.6.5 异常处理 — 故障异常处理机制
- 4.3.6 自旋锁 — SMP 环境下的临界区保护
- 4.6.3 安全机制 — 异常安全处理
- 附录:arch_interrupt.h 源码 — 中断接口头文件
- 附录:arch_interrupt.c 源码 — 中断实现源码