【嵌入式 C 学习】Day27:中断、ISR、volatile 与事件驱动
【嵌入式 C 学习】Day27:中断、ISR、volatile 与事件驱动
一、前言
前面的学习中,我已经完成了:
GPIO
UART
Ring Buffer
软件定时器
按键消抖
CAN
ADC
SPI
I²C
这些模块大多由主程序主动调用。
但在真实嵌入式系统中,很多事件并不是按照主程序预定顺序发生的,例如:
按键突然被按下
UART 突然收到一个字节
ADC 转换突然完成
定时器到期
CAN 收到数据帧
DMA 数据搬运完成
如果 CPU 一直主动检查每个外设,不仅浪费处理时间,事件响应也可能不及时。
因此,从 Day27 开始学习嵌入式系统中非常重要的一种机制:
中断
Interrupt
本次使用纯 C 模拟:
中断请求 IRQ
中断使能
中断禁止
中断挂起 pending
中断服务函数 ISR
软件事件
主循环事件处理
volatile 共享变量
按键中断
UART 接收中断
多个中断同时发生
二、什么是轮询
轮询是 CPU 不断主动检查外设状态。
例如:
while(1)
{
if(button_is_pressed())
{
process_button();
}
if(uart_has_data())
{
process_uart();
}
if(adc_is_finished())
{
process_adc();
}
}
CPU 会不断询问:
按键按下了吗?
UART 有数据了吗?
ADC 转换完成了吗?
即使当前没有任何事件,CPU 仍然会重复检查。
三、轮询的优缺点
轮询的优点:
程序结构简单
代码执行过程直观
容易调试
适合小型系统
轮询的缺点:
浪费 CPU 时间
事件响应可能不及时
外设越多,主循环越复杂
阻塞操作可能导致其他事件漏处理
不适合高速或突发数据
功耗可能更高
例如主循环中存在:
delay_ms(1000);
在这 1 秒中,CPU 可能无法及时检查 UART、按键或 ADC 状态。
四、什么是中断
中断是外设在事件发生时主动通知 CPU。
基本过程:
CPU 正在执行主程序
↓
外设事件发生
↓
外设产生中断请求
↓
CPU 暂停当前程序
↓
执行中断服务函数
↓
中断处理结束
↓
继续执行原来的程序
可以把中断理解为:
CPU 正在工作
外设突然敲门
CPU 暂停当前工作
快速处理紧急事件
然后返回原位置继续执行
五、什么是 IRQ
IRQ 全称:
Interrupt Request
中文通常称为:
中断请求
例如按键边沿发生后,可以产生:
BUTTON IRQ
UART 收到数据后,可以产生:
UART RX IRQ
本次模拟按键请求:
interrupt_request(
&controller,
INTERRUPT_SOURCE_BUTTON);
该函数执行:
设置 BUTTON pending
IRQ 请求计数加 1
但它不会直接执行 ISR。
六、什么是 ISR
ISR 全称:
Interrupt Service Routine
中文称为:
中断服务函数
本次定义两个 ISR:
void interrupt_button_isr(
InterruptController *controller);
void interrupt_uart_rx_isr(
InterruptController *controller);
真实单片机中可能写成:
void USART0_IRQHandler(void)
{
/* 读取 UART 数据 */
/* 清除 UART 中断标志 */
}
七、ISR 应该完成什么
ISR 应当尽量短,只做必要工作:
确认中断源
读取关键数据
清除中断标志
记录软件事件
更新必要计数
立即退出
例如 UART 接收 ISR:
读取 UART 数据寄存器
保存接收字节
清除 UART pending
设置 UART 软件事件
退出 ISR
八、ISR 中不应该做什么
不建议在 ISR 中执行:
长时间延时
复杂命令解析
大量 printf
文件操作
Flash 擦写
复杂浮点计算
阻塞式通信
长时间循环等待
错误示例:
void uart_rx_isr(void)
{
delay_ms(1000);
parse_command();
save_to_flash();
printf("Long ISR processing\n");
}
可能造成:
其他中断响应延迟
主循环长时间停止
UART 数据丢失
系统实时性下降
中断嵌套问题变复杂
九、正确的 ISR 设计思路
ISR 只记录事件:
void uart_rx_isr(void)
{
received_byte =
uart_data_register;
uart_event_pending = 1U;
}
主循环完成业务处理:
if(uart_event_pending != 0U)
{
uart_event_pending = 0U;
process_uart_byte(
received_byte);
}
核心原则:
ISR 负责快速响应
主循环负责完整处理
十、Day27 模拟的中断源
本次定义两个中断源:
#define INTERRUPT_SOURCE_BUTTON \
(1UL << 0)
#define INTERRUPT_SOURCE_UART_RX \
(1UL << 1)
对应二进制:
BUTTON = 0000 0001
UART_RX = 0000 0010
两个中断同时存在时:
0000 0001
OR
0000 0010
-----------
0000 0011
因此:
INTERRUPT_SOURCE_ALL = 0x03
十一、为什么使用位标志
位标志可以用一个整数保存多个状态。
使能按键中断:
controller->enabled_flags |=
INTERRUPT_SOURCE_BUTTON;
再使能 UART 中断:
controller->enabled_flags |=
INTERRUPT_SOURCE_UART_RX;
最终:
enabled_flags = 0x03
表示:
按键中断已使能
UART 接收中断已使能
十二、按位或设置标志
设置中断使能位:
flags |= source;
例如:
原 flags = 0000 0001
source = 0000 0010
按位或后:
0000 0011
其他已经设置的位不会被清除。
十三、按位与检查标志
检查 UART 中断是否使能:
if((controller->enabled_flags &
INTERRUPT_SOURCE_UART_RX) != 0U)
{
/* UART RX 中断已使能 */
}
如果结果不为 0,说明该位已经设置。
十四、按位取反清除标志
禁止按键中断:
controller->enabled_flags &=
~INTERRUPT_SOURCE_BUTTON;
假设:
enabled_flags = 0000 0011
BUTTON = 0000 0001
~BUTTON = 1111 1110
执行按位与:
0000 0011
AND
1111 1110
-----------
0000 0010
最终只保留 UART 中断。
十五、中断控制器结构体
本次中断控制器定义:
typedef struct
{
volatile uint32_t enabled_flags;
volatile uint32_t pending_flags;
volatile uint32_t event_flags;
volatile uint8_t uart_rx_data_register;
volatile uint8_t uart_rx_event_data;
volatile uint32_t irq_request_count;
volatile uint32_t handled_irq_count;
volatile uint32_t button_isr_count;
volatile uint32_t uart_rx_isr_count;
} InterruptController;
十六、enabled、pending 和 event 的区别
这三个状态是 Day27 最重要的内容。
enabled
表示:
CPU 是否允许响应该中断
pending
表示:
硬件事件已经发生
当前正在等待 ISR 处理
event
表示:
ISR 已经快速处理完毕
主循环还有业务需要处理
完整关系:
外设事件
↓
pending = 1
↓
检查 enabled
↓
进入 ISR
↓
pending = 0
event = 1
↓
主循环处理
↓
event = 0
十七、中断事件的完整生命周期
以按键中断为例。
事件发生前:
pending = 0
event = 0
按键事件发生:
pending = 1
event = 0
ISR 执行完成:
pending = 0
event = 1
主循环处理完成:
pending = 0
event = 0
因此:
pending 由外设产生,由 ISR 清除
event 由 ISR 产生,由主循环清除
十八、中断使能
使能按键中断:
interrupt_enable(
&controller,
INTERRUPT_SOURCE_BUTTON);
使能 UART 中断:
interrupt_enable(
&controller,
INTERRUPT_SOURCE_UART_RX);
最终:
enabled_flags = 0x00000003
十九、中断禁止
禁止按键中断:
interrupt_disable(
&controller,
INTERRUPT_SOURCE_BUTTON);
需要注意:
禁止中断不会自动清除 pending
也就是说:
中断可以被禁止
外设事件仍然可以发生
pending 仍然可能被设置
但 ISR 不会执行
二十、未使能中断时发生了什么
初始化后:
BUTTON enabled = 0
产生按键请求:
interrupt_request(
&controller,
INTERRUPT_SOURCE_BUTTON);
状态变成:
enabled = 0
pending = 1
执行调度:
interrupt_dispatch(
&controller);
因为中断未使能,所以 ISR 不执行。
运行结果:
[DISPATCH] Handled in disabled state = 0
[CHECK] Disabled IRQ remained pending
二十一、重新使能后处理历史请求
pending 保留后,使能按键中断:
interrupt_enable(
&controller,
INTERRUPT_SOURCE_BUTTON);
再次调度:
interrupt_dispatch(
&controller);
此时满足:
pending = 1
enabled = 1
所以进入按键 ISR。
运行结果:
[CHECK] Stored button IRQ entered ISR
二十二、什么是 interrupt_dispatch
在真实 MCU 中,中断控制器和 CPU 会自动选择 ISR。
Linux 普通 C 程序没有真正的 MCU 中断,因此本次使用:
interrupt_dispatch(
&controller);
模拟 CPU 的中断调度。
简化逻辑:
if(button_pending &&
button_enabled)
{
interrupt_button_isr(
controller);
}
if(uart_pending &&
uart_enabled)
{
interrupt_uart_rx_isr(
controller);
}
二十三、dispatch 返回值
interrupt_dispatch() 返回本次实际执行的 ISR 数量。
没有中断可处理:
返回 0
处理一个中断:
返回 1
同时处理按键和 UART:
返回 2
它表示:
本次调度执行了多少个 ISR
累计处理数保存在:
handled_irq_count
二十四、按键 ISR
按键 ISR 的核心逻辑:
void interrupt_button_isr(
InterruptController *controller)
{
if(interrupt_is_pending(
controller,
INTERRUPT_SOURCE_BUTTON) == 0)
{
return;
}
interrupt_clear_pending(
controller,
INTERRUPT_SOURCE_BUTTON);
controller->event_flags |=
INTERRUPT_EVENT_BUTTON;
controller->button_isr_count++;
controller->handled_irq_count++;
}
它只完成:
检查 pending
清除 pending
设置软件事件
增加计数
退出 ISR
没有直接完成:
按键消抖
短按判断
长按判断
LED 控制
状态切换
这些应放在主循环。
二十五、UART 接收中断
模拟 UART 收到字符:
interrupt_uart_receive_byte(
&controller,
(uint8_t)'A');
首先执行:
uart_rx_data_register = 'A'
UART RX pending = 1
IRQ 请求计数加 1
调度后进入 UART ISR:
controller->uart_rx_event_data =
controller->uart_rx_data_register;
随后:
清除 UART pending
设置 UART 软件事件
增加 UART ISR 计数
二十六、UART 数据流
完整数据流:
UART 收到字符 A
↓
uart_rx_data_register
↓
UART RX pending
↓
UART RX ISR
↓
uart_rx_event_data
↓
UART 软件事件
↓
主循环处理字符 A
运行结果:
[UART HARDWARE] Received 'A'
[CHECK] UART ISR saved byte correctly
[MAIN] Processing UART byte 'A' (0x41)
二十七、ASCII 字符
字符:
'A'
对应十六进制:
0x41
字符:
'Z'
对应十六进制:
0x5A
程序最终保存:
Last UART byte = 'Z' (0x5A)
二十八、主循环事件处理
主循环使用:
process_pending_events(
&controller,
&app_stats);
检查软件事件:
if(interrupt_event_is_pending(
controller,
INTERRUPT_EVENT_BUTTON) != 0)
{
/* 处理按键事件 */
}
处理完成后清除事件:
interrupt_clear_event(
controller,
INTERRUPT_EVENT_BUTTON);
UART 事件也采用相同结构。
二十九、为什么主循环统计不需要 volatile
应用统计结构:
typedef struct
{
uint32_t button_event_count;
uint32_t uart_event_count;
uint8_t last_uart_byte;
} ApplicationEventStats;
这些成员只在主循环中修改,没有被 ISR 访问。
因此当前不需要:
volatile
不是所有变量都应该加 volatile,只有确实可能在当前执行流程之外变化的对象才需要考虑。
三十、什么是 volatile
例如:
volatile uint32_t event_flags;
volatile 告诉编译器:
该变量可能在当前代码看不到的位置改变
每次访问时都应重新读取
不要假设它的值一直不变
ISR 和主循环共享的标志常见写法:
volatile uint8_t event_pending;
三十一、没有 volatile 可能出现的问题
例如:
while(uart_event_pending == 0U)
{
/* 等待中断修改变量 */
}
如果变量没有声明为 volatile,编译器可能认为:
循环内部没有代码修改变量
变量不会变化
从而进行不合适的优化。
声明为:
volatile uint8_t uart_event_pending;
后,每次判断都需要重新读取该变量。
三十二、volatile 的局限
必须明确:
volatile 不等于线程安全
volatile 不保证原子性
volatile 不等于临界区
volatile 不等于互斥锁
volatile 不能自动解决竞争条件
它主要解决:
编译器可见性和优化问题
例如:
shared_counter++;
可能被拆成:
读取 shared_counter
加 1
写回 shared_counter
如果 ISR 在中间修改同一个变量,仍可能出现数据错误。
三十三、多个中断同时发生
测试中同时产生:
BUTTON IRQ
UART RX IRQ
pending 位:
BUTTON = 0x01
UART_RX = 0x02
组合后:
pending_flags = 0x03
运行结果:
[STATE] Pending flags = 0x00000003
[DISPATCH] Handled interrupts = 2
说明两个中断都执行了 ISR。
三十四、两个 ISR 完成后的状态
两个 ISR 完成后:
pending_flags = 0x00
event_flags = 0x03
其中:
bit0:按键软件事件
bit1:UART 软件事件
主循环一次处理两个事件:
[MAIN] Processed events = 2
三十五、当前调度顺序
当前软件调度器先检查:
BUTTON
再检查:
UART RX
这只是代码中的固定检查顺序。
它还不是真正的:
硬件中断优先级
抢占优先级
中断嵌套
NVIC 优先级
后续会继续学习中断优先级和临界区。
三十六、手动清除 pending
按键中断被禁止后,再产生一个请求:
BUTTON enabled = 0
BUTTON pending = 1
因为不想保留这个请求,所以程序执行:
interrupt_clear_pending(
&controller,
INTERRUPT_SOURCE_BUTTON);
清除后,即使重新使能按键中断,该请求也不会再被处理。
运行结果:
[IRQ] Disabled button pending cleared manually
[CHECK] Cleared IRQ was not processed later
三十七、请求次数和处理次数为什么不同
本次总共提交五次 IRQ:
1. 未使能状态下的按键请求
2. UART 收到字符 A
3. 多中断测试中的按键请求
4. 多中断测试中的 UART 字符 Z
5. 按键中断被禁止时的请求
前四次最终进入 ISR。
第五次请求被手动清除,没有进入 ISR。
所以:
IRQ requests = 5
Handled IRQs = 4
三十八、ISR 数量与软件事件数量
实际执行:
Button ISR = 2 次
UART ISR = 2 次
主循环处理:
Button event = 2 次
UART event = 2 次
说明:
每次成功执行 ISR
都会设置一个对应的软件事件
每个软件事件
最终都被主循环处理
三十九、同类中断请求可能合并
当前 pending 只是一个位。
连续产生三次按键请求:
interrupt_request(
&controller,
INTERRUPT_SOURCE_BUTTON);
interrupt_request(
&controller,
INTERRUPT_SOURCE_BUTTON);
interrupt_request(
&controller,
INTERRUPT_SOURCE_BUTTON);
结果可能是:
irq_request_count = 3
BUTTON pending = 1
调度器只能知道:
至少发生过一次按键事件
可能只执行一次 ISR。
因此:
同类多次请求可能被合并
四十、为什么 UART 不能一直使用单字节变量
当前 UART 只使用:
uart_rx_event_data
保存一个字节。
如果主循环处理前快速收到:
A
B
C
后面的字节可能覆盖前面的字节。
更合理的方案:
UART RX ISR
↓
每个字节写入 Ring Buffer
↓
主循环逐字节读取
↓
命令解析
后续会把 UART 中断与之前学习的 Ring Buffer 结合起来。
四十一、软件分层
Day27 的结构:
main.c
应用层和事件处理
↓
interrupt.c
中断控制、ISR 与调度
↓
interrupt.h
宏、结构体和接口
main.c 负责:
模拟外设事件
调用中断调度
处理软件事件
统计应用结果
interrupt.c 负责:
中断使能
中断禁止
pending 管理
ISR
软件事件标志
中断统计
四十二、实际运行结果
执行:
cd /root/Embedded_14Days/day27
make clean
make
make run
结果:
===== Day27 Interrupt and Event Test =====
----- Test 1: Initial State -----
[STATE] Enabled flags = 0x00000000
[STATE] Pending flags = 0x00000000
[STATE] Event flags = 0x00000000
[CHECK] Initial interrupt state is correct
----- Test 2: Pending While Disabled -----
[STATE] Button enabled = NO
[IRQ] Button request generated
[STATE] Button pending = YES
[DISPATCH] Handled in disabled state = 0
[CHECK] Disabled IRQ remained pending
[IRQ] Button interrupt enabled
[DISPATCH] Handled after enabling = 1
[CHECK] Stored button IRQ entered ISR
[MAIN] Processing button event
[MAIN] Processed events = 1
[CHECK] Button event processed by main loop
----- Test 3: UART RX Interrupt -----
[IRQ] UART RX interrupt enabled
[UART HARDWARE] Received 'A'
[STATE] UART pending = YES
[DISPATCH] UART ISR count this time = 1
[CHECK] UART ISR saved byte correctly
[MAIN] Processing UART byte 'A' (0x41)
[MAIN] Processed events = 1
[CHECK] UART event processed by main loop
----- Test 4: Multiple Interrupts -----
[IRQ] Button and UART requests generated
[STATE] Pending flags = 0x00000003
[DISPATCH] Handled interrupts = 2
[STATE] Event flags = 0x00000003
[CHECK] Both ISRs completed
[MAIN] Processing button event
[MAIN] Processing UART byte 'Z' (0x5A)
[MAIN] Processed events = 2
[CHECK] Both events processed by main loop
----- Test 5: Interrupt Disable -----
[IRQ] Button interrupt disabled
[IRQ] Button request generated while disabled
[DISPATCH] Handled while disabled = 0
[CHECK] Disabled interrupt did not enter ISR
[IRQ] Disabled button pending cleared manually
[CHECK] Cleared IRQ was not processed later
----- Test 6: Invalid Operations -----
[EXPECTED ERROR] NULL init rejected
[EXPECTED ERROR] NULL enable rejected
[EXPECTED ERROR] NONE source rejected
[EXPECTED ERROR] Combined source rejected
[EXPECTED ERROR] Invalid IRQ source rejected
[EXPECTED ERROR] NULL UART controller rejected
[EXPECTED ERROR] Combined event rejected
[CHECK] Invalid operations changed no state
----- Final Interrupt Status -----
[FINAL] Enabled flags = 0x00000003
[FINAL] Pending flags = 0x00000000
[FINAL] Event flags = 0x00000000
[FINAL] IRQ requests = 5
[FINAL] Handled IRQs = 4
[FINAL] Button ISR count = 2
[FINAL] UART RX ISR count = 2
[FINAL] Button events = 2
[FINAL] UART events = 2
[FINAL] Last UART byte = 'Z' (0x5A)
[FINAL CHECK] All interrupt tests passed
===== Interrupt Test Finished =====
编译过程中没有:
warning
error
四十三、与真实单片机的对应关系
本次模拟:
interrupt_request()
对应真实硬件:
外设设置中断标志
向中断控制器发送 IRQ
本次:
interrupt_dispatch()
对应真实硬件:
CPU 和 NVIC 自动选择并进入 ISR
本次:
interrupt_button_isr()
对应:
GPIO_IRQHandler()
EXTI_IRQHandler()
本次:
interrupt_uart_rx_isr()
对应:
USART_IRQHandler()
UART_IRQHandler()
四十四、今天的核心收获
通过 Day27 学习,我掌握了:
- 轮询的基本方式;
- 轮询的优点和缺点;
- 中断的基本概念;
- IRQ 中断请求;
- ISR 中断服务函数;
- 中断使能与禁止;
- pending 挂起标志;
- 软件事件标志;
- enabled、pending 和 event 的区别;
- 中断源位标志;
- 按位或设置标志;
- 按位与检查标志;
- 按位取反清除标志;
- 中断调度;
- 未使能中断的 pending 保留;
- 重新使能后处理历史请求;
- 手动清除 pending;
- 按键 ISR;
- UART RX ISR;
- ISR 到主循环的数据传递;
- 多个中断同时处理;
- ISR 与主循环的职责分工;
- volatile 的基本作用;
- volatile 的局限;
- 中断请求数与处理数的区别;
- ISR 次数与软件事件次数的关系;
- 同类请求可能合并;
- 单字节 UART 缓冲可能被覆盖;
- 事件驱动程序结构。
最需要记住的是:
外设事件
→ pending
→ ISR
→ 软件事件
→ 主循环处理
以及:
ISR 只做快速、必要的操作
耗时业务放到主循环
四十五、下一步计划
下一天继续学习:
中断共享变量与临界区
重点内容:
volatile 深入理解
共享变量
读-改-写操作
竞争条件 Race Condition
原子操作
临界区
全局中断开关模拟
保护共享计数器
安全读取 ISR 数据
重点解决:
为什么 volatile 不能保证 shared_counter++ 安全
主循环读取数据时 ISR 突然修改怎么办
什么时候需要暂时关闭中断
为什么临界区必须尽量短

更多推荐


所有评论(0)