目录
- 一.中断是什么
- 二.中断由哪些部分组成
- 三.一次中断从发生到执行的完整流程
- 四."中断"和"轮询"
- [五. STM32F103 常见中断源举例](#五. STM32F103 常见中断源举例)
- [六. 中断工程模板(C程序)](#六. 中断工程模板(C程序))
一.中断是什么
在 STM32F103(Cortex-M3)里,"中断 "可以理解为:当外设/内核发生某个事件时,硬件打断CPU当前执行流程,强制CPU去执行一段预先登记好的处理函数(ISR/IRQHandler) ,处理完再返回继续原来的程序。它的目的不是"更快",而是让系统对外部事件更及时、更可预测,同时避免主循环一直轮询浪费CPU。
二.中断由哪些部分组成

(1)事件源(谁触发)
外设事件:如 GPIO 边沿(EXTI)、定时器溢出/比较(TIM)、串口收到字节(USART RXNE)、DMA传输完成等
内核异常:如 SysTick、HardFault 等(这些也属于异常/中断体系的一部分)
(2)标志位与使能位(触发条件 = Flag + Enable)
几乎所有外设中断都满足这个规律:1.外设先置位 状态标志 Flag(例如:TIM_SR.UIF、USART_SR.RXNE、EXTI_PR某位);2.同时要求对应 中断使能位 Enable/IE 开启(例如:TIM_DIER.UIE、USART_CR1.RXNEIE、EXTI_IMR)。
只有 Flag=1 且 IE=1,才会向 NVIC 发出中断请求。
(3)NVIC(中断控制器:仲裁/屏蔽/优先级)
NVIC 负责:1.这个中断是否 Enable(允许响应);2.是否处于 Pending(挂起等待服务);3.优先级:谁能抢占谁、同级谁先处理
(4)向量表
CPU 进入中断时,会根据中断号去向量表里取出对应的 ISR 地址,然后跳转执行,如下图所示。

三.一次中断从发生到执行的完整流程
1. 外设事件发生(如串口收到字节)。
2.外设置位 Flag(RXNE=1)。
3.若外设 IE=1,则请求 NVIC → NVIC 把该 IRQ 标成 Pending。
4.NVIC 判断:是否使能?是否被屏蔽?优先级是否允许抢占?
5.CPU 进入异常:自动压栈保存现场(部分寄存器),从向量表取 ISR 地址。
6.执行 ISR:判因 + 清标志 + 快速处理(通常只放入缓冲/置事件)。
7.ISR 返回:硬件自动出栈恢复现场,程序继续从被打断处运行。
四."中断"和"轮询"
| 方式 | 你在做什么 | 优点 | 缺点 |
|---|---|---|---|
| 轮询(Polling) | 主循环不断读标志位 | 写法直观 | 浪费CPU、响应不稳定、容易漏瞬时事件 |
| 中断(Interrupt) | 事件发生时硬件通知CPU | 响应更及时、CPU利用率高 | 需要管理优先级/嵌套/临界区;ISR写不好会乱 |
五. STM32F103 常见中断源举例
(1) EXTI(外部中断) :GPIO 上升沿/下降沿触发 → EXTI_PR 置位 → 进 EXTIx_IRQHandler
典型用途:按键、外部告警引脚、传感器中断脚
(2) TIM(定时器) :计数溢出/比较匹配 → TIM_SR.UIF/CCxIF 置位 → 进 TIMx_IRQHandler
典型用途:1ms节拍、PWM更新、输入捕获测频
(3) USART(串口) :收到数据 RXNE=1 → 进 USARTx_IRQHandler,读 DR 清 RXNE
典型用途:协议接收、日志、命令解析。

六. 中断工程模板(C程序)
1.EXTI0 按键(上升沿 + 消抖)
2.TIM2 1ms 中断(系统节拍 + 软定时器框架)
3.USART1 接收(RXNE 环形缓冲)
c
/***************************************************************
* File : bsp_irq_templates.c
* MCU : STM32F103CBT6 (Cortex-M3)
* Goal : EXTI0 button + debounce
* TIM2 1ms tick + soft timers
* USART1 RX (ring buffer) OR (DMA+IDLE recommended)
***************************************************************/
#include "stm32f10x.h" // CMSIS/StdPeriph device header
/*********************** Compile-time options *******************/
// 0: USART1 RXNE interrupt + ring buffer
// 1: USART1 DMA RX + IDLE interrupt (recommended)
#define USART1_USE_DMA_IDLE (1U) // <-- 按需切换
#define SYS_TICK_HZ (1000U) // TIM2 1ms tick => 1000 Hz
#define DEBOUNCE_MS (20U) // 按键消抖时间窗(20ms 常用)
#define UART_BAUDRATE (115200U)
#define UART_RING_SIZE (256U) // 环形缓冲大小(2^n 不是必须,但方便)
#define UART_DMA_BUF_SIZE (256U) // DMA staging buffer(建议 >= 256)
/*********************** Global time base ************************/
static volatile uint32_t g_ms = 0; // 1ms 递增系统时间戳(TIM2 中断更新)
/*********************** Soft timer framework ********************/
// 软定时器:ISR里只做计数递减 + 置位 fired;回调在主循环执行(避免ISR耗时)
typedef void (*soft_cb_t)(void);
typedef struct
{
volatile uint32_t remain; // 剩余 ms
uint32_t period; // 周期 ms(周期定时器使用)
soft_cb_t cb; // 回调函数(在 main/poll 执行)
uint8_t periodic; // 1=周期;0=一次
volatile uint8_t fired; // 1=到期(由ISR置位)
} soft_timer_t;
#define SOFT_TIMER_NUM (8U)
static soft_timer_t g_tmr[SOFT_TIMER_NUM]; // 软定时器数组
/*********************** EXTI0 button state **********************/
static volatile uint32_t g_btn_last_ms = 0; // 上次有效按键时间
static volatile uint8_t g_btn_event = 0; // 1=产生一次"按下事件"(消抖后)
/*********************** USART1 ring buffer **********************/
static volatile uint8_t g_rb[UART_RING_SIZE]; // 环形缓冲区
static volatile uint16_t g_rb_w = 0; // 写指针
static volatile uint16_t g_rb_r = 0; // 读指针
static volatile uint32_t g_uart_ore_cnt = 0; // ORE 计数(用于诊断)
/*********************** USART1 DMA staging **********************/
#if (USART1_USE_DMA_IDLE == 1U)
static uint8_t g_dma_buf[UART_DMA_BUF_SIZE]; // DMA 接收缓冲(循环)
static volatile uint16_t g_dma_old_pos = 0; // 上次处理到的位置
#endif
/*********************** Utility: NVIC priority grouping *********/
// 使用 StdPeriph 的"PriorityGroup_4"等价:4 bits 抢占优先级 / 0 bits 子优先级:contentReference[oaicite:4]{index=4}
static void NVIC_Grouping_PriorityGroup4(void)
{
uint32_t reg = 0; // 临时变量
reg = SCB->AIRCR; // 读 AIRCR
reg &= ~SCB_AIRCR_PRIGROUP_Msk; // 清 PRIGROUP 位域
reg = (0x5FAUL << SCB_AIRCR_VECTKEY_Pos) | // 写 VECTKEY 解锁
(reg) |
(3UL << SCB_AIRCR_PRIGROUP_Pos); // PRIGROUP=3 => PriorityGroup_4
SCB->AIRCR = reg; // 写回 AIRCR
}
/*********************** Utility: clock helpers ******************/
// 注:计时器时钟:当 APB 预分频 != 1 时,TIMxCLK = 2 * PCLKx(STM32 典型规则):contentReference[oaicite:5]{index=5}
static uint32_t decode_ahb_presc(uint32_t hpre_bits)
{
static const uint16_t tbl[16] = {1,1,1,1,1,1,1,1,2,4,8,16,64,128,256,512};
return (uint32_t)tbl[(hpre_bits >> 4) & 0x0F];
}
static uint32_t decode_apb_presc(uint32_t ppre_bits)
{
static const uint8_t tbl[8] = {1,1,1,1,2,4,8,16};
return (uint32_t)tbl[(ppre_bits >> 8) & 0x07];
}
static uint32_t get_hclk_hz(void)
{
uint32_t hpre = (RCC->CFGR & RCC_CFGR_HPRE); // 取 HPRE
uint32_t div = decode_ahb_presc(hpre); // 解码分频
return SystemCoreClock / div; // HCLK = SYSCLK / AHB_div
}
static uint32_t get_pclk1_hz(void)
{
uint32_t hclk = get_hclk_hz(); // 获取 HCLK
uint32_t ppre1 = (RCC->CFGR & RCC_CFGR_PPRE1); // 取 PPRE1
uint32_t div = decode_apb_presc(ppre1); // 解码分频
return hclk / div; // PCLK1 = HCLK / APB1_div
}
static uint32_t get_pclk2_hz(void)
{
uint32_t hclk = get_hclk_hz(); // 获取 HCLK
uint32_t ppre2 = (RCC->CFGR & RCC_CFGR_PPRE2); // 取 PPRE2
uint32_t div = decode_apb_presc(ppre2); // 解码分频
return hclk / div; // PCLK2 = HCLK / APB2_div
}
static uint32_t get_tim2_clk_hz(void)
{
uint32_t pclk1 = get_pclk1_hz(); // TIM2 在 APB1
uint32_t ppre1_bits = (RCC->CFGR & RCC_CFGR_PPRE1); // 取 APB1 分频配置
uint32_t apb1_div = decode_apb_presc(ppre1_bits); // 计算 APB1 分频
if (apb1_div == 1U) // APB1 不分频
{
return pclk1; // TIM2CLK = PCLK1
}
else
{
return (2U * pclk1); // TIM2CLK = 2 * PCLK1(APB分频≠1):contentReference[oaicite:6]{index=6}
}
}
/*********************** Soft timer APIs **************************/
static void soft_timer_init(void)
{
for (uint32_t i = 0; i < SOFT_TIMER_NUM; i++) // 遍历所有定时器槽
{
g_tmr[i].remain = 0; // 初始不计时
g_tmr[i].period = 0; // 周期=0
g_tmr[i].cb = 0; // 回调为空
g_tmr[i].periodic = 0; // 非周期
g_tmr[i].fired = 0; // 未触发
}
}
static void soft_timer_start(uint32_t id, uint32_t delay_ms, uint8_t periodic, soft_cb_t cb)
{
if (id >= SOFT_TIMER_NUM) return; // 参数检查
g_tmr[id].remain = delay_ms; // 设置剩余时间
g_tmr[id].period = delay_ms; // 周期=delay(周期定时器用)
g_tmr[id].cb = cb; // 绑定回调
g_tmr[id].periodic = periodic ? 1U : 0U; // 记录是否周期
g_tmr[id].fired = 0; // 清触发标志
}
static void soft_timer_stop(uint32_t id)
{
if (id >= SOFT_TIMER_NUM) return; // 参数检查
g_tmr[id].remain = 0; // 停止计时
g_tmr[id].fired = 0; // 清触发标志
}
static void soft_timer_tick_isr(void)
{
for (uint32_t i = 0; i < SOFT_TIMER_NUM; i++) // ISR 内遍历(数量少时可接受)
{
if (g_tmr[i].remain > 0U) // 仍在计时
{
g_tmr[i].remain--; // 1ms 递减
if (g_tmr[i].remain == 0U) // 到期
{
g_tmr[i].fired = 1U; // 置位触发(ISR只置位,不调用回调)
if (g_tmr[i].periodic) // 周期定时器
{
g_tmr[i].remain = g_tmr[i].period; // 重新装载周期
}
}
}
}
}
static void soft_timer_poll(void)
{
for (uint32_t i = 0; i < SOFT_TIMER_NUM; i++) // 主循环轮询触发标志
{
if (g_tmr[i].fired) // 已到期
{
g_tmr[i].fired = 0U; // 先清标志(避免回调重入风险)
if (g_tmr[i].cb) // 回调有效
{
g_tmr[i].cb(); // 在主循环执行回调(避免ISR耗时)
}
}
}
}
/*********************** EXTI0 (PA0) init *************************/
// 典型按键接法:PA0 下拉,按下连接到 VCC => 上升沿触发
static void EXTI0_Button_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能 GPIOA 时钟
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; // 使能 AFIO 时钟(EXTI 映射需要)
// GPIOA PA0: 输入上拉/下拉(CNF=10, MODE=00)
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0); // 清 MODE0/CNF0
GPIOA->CRL |= (GPIO_CRL_CNF0_1); // CNF0=10 => Pull-up/Pull-down input
GPIOA->ODR &= ~(GPIO_ODR_ODR0); // ODR0=0 => 选择下拉(Pull-down)
// EXTI0 映射到 PA0:EXTICR1[3:0] = 0000(默认就是PA,但这里显式写一次)
AFIO->EXTICR[0] &= ~(AFIO_EXTICR1_EXTI0); // EXTI0 -> PA0
EXTI->IMR |= EXTI_IMR_MR0; // 取消屏蔽 EXTI0
EXTI->EMR &= ~EXTI_EMR_MR0; // 事件不需要(可选)
EXTI->RTSR |= EXTI_RTSR_TR0; // 上升沿触发
EXTI->FTSR &= ~EXTI_FTSR_TR0; // 关闭下降沿触发
EXTI->PR = EXTI_PR_PR0; // 写1清 pending(rc_w1):contentReference[oaicite:7]{index=7}
NVIC_SetPriority(EXTI0_IRQn, 3U); // EXTI0 优先级(数值越小越高)
NVIC_EnableIRQ(EXTI0_IRQn); // 使能 NVIC EXTI0
}
/*********************** TIM2 1ms tick init ************************/
static void TIM2_1ms_Init(void)
{
uint32_t timclk = get_tim2_clk_hz(); // 获取 TIM2 输入时钟(考虑APB倍频):contentReference[oaicite:8]{index=8}
uint32_t psc = (timclk / 1000000U) - 1U; // 预分频到 1MHz(1us 计数)
uint32_t arr = (1000000U / SYS_TICK_HZ) - 1U; // 1ms => 1000us => ARR=1000-1=999
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; // 使能 TIM2 时钟
TIM2->CR1 = 0U; // 先复位 CR1(关闭计数)
TIM2->PSC = (uint16_t)psc; // 设置 PSC(分频)
TIM2->ARR = (uint16_t)arr; // 设置 ARR(自动重装)
TIM2->EGR = TIM_EGR_UG; // 产生更新事件,把 PSC/ARR 装载生效
TIM2->SR = 0U; // 清 SR(避免上电残留 UIF)
TIM2->DIER |= TIM_DIER_UIE; // 开启更新中断 UIE
NVIC_SetPriority(TIM2_IRQn, 1U); // TIM2 优先级最高(示例=1)
NVIC_EnableIRQ(TIM2_IRQn); // 使能 TIM2 中断
TIM2->CR1 |= TIM_CR1_CEN; // 启动计数器
}
/*********************** USART1 BRR helper ************************/
// Oversampling by 16: BRR = Mantissa<<4 | Fraction (Fraction on /16)
static uint16_t usart_brr_from_pclk(uint32_t pclk_hz, uint32_t baud)
{
uint32_t usartdiv_x16 = (pclk_hz + (baud / 2U)) / baud; // 近似:USARTDIV*16 = PCLK/baud(做简单四舍五入)
uint32_t mantissa = usartdiv_x16 / 16U; // 取整数部分
uint32_t fraction = usartdiv_x16 % 16U; // 取小数部分(0..15)
return (uint16_t)((mantissa << 4) | (fraction & 0x0FU)); // 拼 BRR
}
/*********************** USART1 init (common) *********************/
static void USART1_Common_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // GPIOA 时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // USART1 时钟
// PA9 (TX): 复用推挽输出 50MHz => MODE=11, CNF=10
GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_CNF9); // 清 PA9 配置
GPIOA->CRH |= (GPIO_CRH_MODE9); // MODE9=11 => 50MHz
GPIOA->CRH |= (GPIO_CRH_CNF9_1); // CNF9=10 => AF push-pull
// PA10 (RX): 浮空输入 => MODE=00, CNF=01
GPIOA->CRH &= ~(GPIO_CRH_MODE10 | GPIO_CRH_CNF10); // 清 PA10 配置
GPIOA->CRH |= (GPIO_CRH_CNF10_0); // CNF10=01 => floating input
USART1->CR1 = 0U; // 先关 CR1
USART1->CR2 = 0U; // 1 stop bit
USART1->CR3 = 0U; // 默认无硬件流控
USART1->BRR = usart_brr_from_pclk(get_pclk2_hz(), UART_BAUDRATE); // 配波特率(USART1 在 APB2)
USART1->CR1 |= USART_CR1_TE; // 使能发送
USART1->CR1 |= USART_CR1_RE; // 使能接收
USART1->CR1 |= USART_CR1_UE; // 使能 USART
}
/*********************** USART1 RXNE + ring buffer ****************/
static void USART1_RXNE_Ring_Init(void)
{
USART1_Common_Init(); // 公共初始化
USART1->CR1 |= USART_CR1_RXNEIE; // 开 RXNE 中断(收到字节即进中断)
NVIC_SetPriority(USART1_IRQn, 2U); // USART1 优先级(示例=2)
NVIC_EnableIRQ(USART1_IRQn); // 使能 USART1 中断
}
static inline void rb_put(uint8_t b)
{
uint16_t next = (uint16_t)((g_rb_w + 1U) % UART_RING_SIZE); // 计算下一个写位置
if (next != g_rb_r) // 非满(留一个空位区分满/空)
{
g_rb[g_rb_w] = b; // 写入数据
g_rb_w = next; // 前移写指针
}
else
{
// 缓冲满:可选择丢弃/覆盖/计数,这里直接丢弃(可扩展溢出计数)
}
}
static int rb_get(uint8_t *out)
{
if (g_rb_r == g_rb_w) return 0; // 空
*out = g_rb[g_rb_r]; // 读出
g_rb_r = (uint16_t)((g_rb_r + 1U) % UART_RING_SIZE); // 前移读指针
return 1; // 成功
}
/*********************** USART1 DMA+IDLE (recommended) ************/
#if (USART1_USE_DMA_IDLE == 1U)
static void USART1_DMA_IDLE_Init(void)
{
USART1_Common_Init(); // 公共初始化
RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 使能 DMA1 时钟
DMA1_Channel5->CCR &= ~DMA_CCR1_EN; // 先关 DMA 通道5(USART1_RX)
DMA1_Channel5->CPAR = (uint32_t)&USART1->DR; // 外设地址 = USART1->DR
DMA1_Channel5->CMAR = (uint32_t)g_dma_buf; // 内存地址 = staging buffer
DMA1_Channel5->CNDTR = UART_DMA_BUF_SIZE; // 传输计数 = buffer size
// 配 DMA:外设到内存、8bit、内存自增、循环模式、较高优先级
DMA1_Channel5->CCR = 0U; // 清 CCR
DMA1_Channel5->CCR |= DMA_CCR1_MINC; // 内存地址自增
DMA1_Channel5->CCR |= DMA_CCR1_CIRC; // 循环模式(持续接收)
DMA1_Channel5->CCR |= DMA_CCR1_PL_1; // PL=10(高优先级)
// DIR=0: 外设->内存;PSIZE=00/MSIZE=00: 8bit(默认0)
USART1->CR3 |= USART_CR3_DMAR; // USART1 使能 DMA 接收请求
USART1->CR1 |= USART_CR1_IDLEIE; // 开 IDLE 中断(帧间空闲触发)
DMA1_Channel5->CCR |= DMA_CCR1_EN; // 使能 DMA 通道
NVIC_SetPriority(USART1_IRQn, 2U); // USART1 优先级
NVIC_EnableIRQ(USART1_IRQn); // 使能 USART1 中断
g_dma_old_pos = 0U; // 初始化处理指针
}
static void dma_process_new_data(uint16_t new_pos)
{
// 把 DMA staging buffer 中的新数据段搬到环形缓冲(用于统一上层解析入口)
while (g_dma_old_pos != new_pos) // 逐字节搬运(段通常不大)
{
rb_put(g_dma_buf[g_dma_old_pos]); // 放入环形缓冲
g_dma_old_pos++; // 前移
if (g_dma_old_pos >= UART_DMA_BUF_SIZE) // 到末尾回绕
{
g_dma_old_pos = 0U; // 回绕
}
}
}
#endif
/*********************** Interrupt Handlers ************************/
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF) // UIF=1 表示更新事件
{
TIM2->SR &= ~TIM_SR_UIF; // 清 UIF(避免重复进中断)
g_ms++; // 1ms 时间戳递增
soft_timer_tick_isr(); // 软定时器 tick(ISR里只计数/置位)
}
}
void EXTI0_IRQHandler(void)
{
if (EXTI->PR & EXTI_PR_PR0) // 判断是否 EXTI0 pending
{
uint32_t now = g_ms; // 读取当前毫秒时间戳
// 消抖:同一按键在 DEBOUNCE_MS 内重复触发则忽略
if ((now - g_btn_last_ms) >= DEBOUNCE_MS) // 超过消抖窗
{
g_btn_last_ms = now; // 更新最后触发时间
g_btn_event = 1U; // 置位"按键事件"(主循环消费)
}
EXTI->PR = EXTI_PR_PR0; // 写1清 pending(rc_w1):contentReference[oaicite:9]{index=9}
}
}
void USART1_IRQHandler(void)
{
#if (USART1_USE_DMA_IDLE == 0U)
uint32_t sr = USART1->SR; // 先读 SR(对 ORE 等清除序列有要求)
if (sr & USART_SR_RXNE) // RXNE=1:收到字节
{
uint8_t b = (uint8_t)USART1->DR; // 读 DR:取走数据(同时清 RXNE)
rb_put(b); // 放入环形缓冲
}
else if (sr & USART_SR_ORE) // ORE=1:溢出(说明服务不够快)
{
(void)USART1->DR; // 读 DR:配合先读 SR 清 ORE
g_uart_ore_cnt++; // 计数用于诊断
}
#else
uint32_t sr = USART1->SR; // 先读 SR
if (sr & USART_SR_IDLE) // IDLE=1:总线空闲(常用于"帧结束")
{
(void)USART1->DR; // 读 DR:配合 SR->DR 清 IDLE
uint16_t new_pos = (uint16_t)(UART_DMA_BUF_SIZE - DMA1_Channel5->CNDTR); // 计算 DMA 写入位置
dma_process_new_data(new_pos); // 搬运新增数据到环形缓冲
}
if (sr & USART_SR_ORE) // DMA 情况下仍可保留 ORE 诊断
{
(void)USART1->DR; // SR->DR 清 ORE
g_uart_ore_cnt++; // 计数
}
#endif
}
/*********************** Example application hooks ****************/
//按键事件处理(你可替换为自己的逻辑)
static void on_button_pressed(void)
{
// 这里不要做耗时操作;建议仅置位状态/发消息
}
//软定时器回调(1Hz闪烁等)
static void on_timer0(void)
{
// 示例:翻转 PC13(若你想用PC13,请自行初始化GPIOC)
}
/*********************** Public init ********************************/
void BSP_IRQ_Templates_Init(void)
{
NVIC_Grouping_PriorityGroup4(); // 只用抢占优先级(4bits/0bits)
soft_timer_init(); // 初始化软定时器
TIM2_1ms_Init(); // 启动 1ms 节拍(系统时间基准)
EXTI0_Button_Init(); // 按键 EXTI0 + 消抖
#if (USART1_USE_DMA_IDLE == 0U)
USART1_RXNE_Ring_Init(); // 串口 RXNE + 环形缓冲
#else
USART1_DMA_IDLE_Init(); // 串口 DMA+IDLE(推荐)
#endif
soft_timer_start(0U, 1000U, 1U, on_timer0); // 定时器0:1000ms 周期回调
}
/*********************** Example main loop *************************/
int main(void)
{
SystemInit(); // 启动文件一般会调用;这里写上更直观
SystemCoreClockUpdate(); // 更新 SystemCoreClock(若工程未做)
BSP_IRQ_Templates_Init(); // 初始化:EXTI0 + TIM2 + USART1
while (1)
{
soft_timer_poll(); // 执行到期回调(在主循环,不在 ISR)
if (g_btn_event) // 有按键事件
{
g_btn_event = 0U; // 清事件
on_button_pressed(); // 处理按键
}
// 统一的"串口取数据"入口:两种模式最终都把数据放进环形缓冲
uint8_t ch = 0;
while (rb_get(&ch)) // 读取环形缓冲
{
// TODO: 在这里做协议解析(推荐:状态机/帧解析器)
(void)ch; // 占位避免未使用警告
}
}
}