【嵌入式 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 学习,我掌握了:

  1. 轮询的基本方式;
  2. 轮询的优点和缺点;
  3. 中断的基本概念;
  4. IRQ 中断请求;
  5. ISR 中断服务函数;
  6. 中断使能与禁止;
  7. pending 挂起标志;
  8. 软件事件标志;
  9. enabled、pending 和 event 的区别;
  10. 中断源位标志;
  11. 按位或设置标志;
  12. 按位与检查标志;
  13. 按位取反清除标志;
  14. 中断调度;
  15. 未使能中断的 pending 保留;
  16. 重新使能后处理历史请求;
  17. 手动清除 pending;
  18. 按键 ISR;
  19. UART RX ISR;
  20. ISR 到主循环的数据传递;
  21. 多个中断同时处理;
  22. ISR 与主循环的职责分工;
  23. volatile 的基本作用;
  24. volatile 的局限;
  25. 中断请求数与处理数的区别;
  26. ISR 次数与软件事件次数的关系;
  27. 同类请求可能合并;
  28. 单字节 UART 缓冲可能被覆盖;
  29. 事件驱动程序结构。

最需要记住的是:

外设事件
→ pending
→ ISR
→ 软件事件
→ 主循环处理

以及:

ISR 只做快速、必要的操作
耗时业务放到主循环

四十五、下一步计划

下一天继续学习:

中断共享变量与临界区

重点内容:

volatile 深入理解
共享变量
读-改-写操作
竞争条件 Race Condition
原子操作
临界区
全局中断开关模拟
保护共享计数器
安全读取 ISR 数据

重点解决:

为什么 volatile 不能保证 shared_counter++ 安全
主循环读取数据时 ISR 突然修改怎么办
什么时候需要暂时关闭中断
为什么临界区必须尽量短

duhong2026

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐