一、串口中断到底是什么?(What)
你可以把 CPU 想象成一个正在专心做饭的厨师(执行主循环代码),串口外设就是小区的快递员。当有串口数据(快递)到达时,快递员不会一直敲门等厨师忙完(查询式),而是打一个电话(触发中断)------厨师暂停炒菜,去门口取快递(执行中断服务函数),取完后回到厨房继续炒菜(返回主循环)。
**串口接收中断(RXNE)** 是最常用的串口中断:当串口收到 1 字节数据并存入"接收数据寄存器"时,硬件自动触发中断,提醒 CPU"有数据要处理",这是比"厨师每隔 10 秒去门口看一眼(查询式)"更高效的方式。
核心概念通俗解
-
中断源:触发中断的"事件"(这里是 USART1 收到数据)
-
中断向量表 :CPU 的"电话簿",记录"哪个中断对应哪个处理函数"(比如 USART1 中断对应
USART1_IRQHandler),位于 STM32F103 存储器映射的 0x08000000 起始处 -
NVIC:嵌套向量中断控制器,相当于"总机",管理所有中断的优先级、是否允许响铃(使能),Cortex-M3 内核内置,支持 60 个可屏蔽中断通道和 16 个优先级
二、串口中断接收的核心硬件逻辑(Why)
串口接收数据的硬件流程就像快递入柜的过程,用 ASCII 流程图表示如下:
串口外设接收串行数据 → 移位寄存器(USART_DR的高8位)拼装成1字节 → 存入接收数据寄存器(USART_DR低8位)
→ 硬件置位RXNE标志(USART_SR位5) → NVIC检测到使能的中断 → CPU保存现场 → 跳转到中断服务函数
→ 读取USART_DR(取快递,自动清RXNE灯) → 处理数据 → CPU恢复现场 → 返回主循环
核心寄存器完整说明(基于 STM32F103xCDE 数据手册)
| 寄存器组 | 寄存器名 | 地址偏移 | 作用(通俗类比) | 关键位/操作 |
|---|---|---|---|---|
| USART 通用 | USART_SR | 0x00 | 串口状态寄存器(快递柜状态提示灯) | • RXNE(位 5): 1=有新数据,0=无数据 • TXE(位 7): 1=发送寄存器空 • TC(位 6): 1=发送完成 • ORE(位 3): 1=溢出错误 • FE(位 1): 1=帧错误 |
| USART_DR | 0x04 | 串口数据寄存器(快递柜储物格) | • 低 8 位: 存 1 字节收发数据 • 读操作: 取接收数据,自动清 RXNE • 写操作: 存发送数据,自动清 TXE | |
| USART_BRR | 0x08 | 波特率寄存器(快递速度调节器) | • DIV_Mantissa[15:4]: 波特率分频器整数部分 • DIV_Fraction[3:0]: 波特率分频器小数部分 | |
| USART_CR1 | 0x0C | 控制寄存器 1(总开关) | • UE(位 13): 1=使能 USART • RE(位 2): 1=使能接收 • TE(位 3): 1=使能发送 • RXNEIE(位 5): 1=使能 RXNE 中断 • M(位 12): 0=8 位数据,1=9 位数据 • PCE(位 10): 1=使能奇偶校验 | |
| USART_CR2 | 0x10 | 控制寄存器 2(停止位等) | • STOP[13:12]: 00=1 位停止位 | |
| USART_CR3 | 0x14 | 控制寄存器 3(流控等) | • CTSE(位 9): 1=使能 CTS 流控 • RTSE(位 8): 1=使能 RTS 流控 | |
| RCC 时钟 | RCC_APB2ENR | 0x18 | APB2 外设时钟使能寄存器 | • USART1EN(位 14): 1=使能 USART1 时钟 • IOPAEN(位 2): 1=使能 GPIOA 时钟 |
| GPIO | GPIOA_CRH | 0x04 | GPIOA 高 8 位配置寄存器 | • MODE9[1:0]: PA9 模式 • CNF9[1:0]: PA9 配置 • MODE10[1:0]: PA10 模式 • CNF10[1:0]: PA10 配置 |
| NVIC | NVIC_ISER0 | 0xE000E100 | 中断使能寄存器 0 | • USART1_IRQn(位 37): 1=使能 USART1 中断 |
| NVIC_IPR9 | 0xE000E424 | 中断优先级寄存器 9 | • USART1_PRIO[7:4]: 抢占优先级 • USART1_PRIO[3:0]: 子优先级 |
三、从 0 到 1 配置串口中断(How)
配置分 5 步,以下是提取后的最小可用代码,每行都标注"功能+硬件逻辑+寄存器操作+C 语言知识点"。
步骤 0:为什么选择在 stm32f10x_it.c文件中添加串口中断服务函数
stm32f10x_it.c是 STM32 标准库模板中**专门用于集中存放所有中断服务例程(ISR)**的核心文件,包含两部分核心内容:
-
Cortex-M3 内核异常处理函数(如 HardFault、SysTick、PendSV 等);
-
外设中断服务函数的实现/模板(如串口、定时器、GPIO 等外设的中断处理)。
STM32 的中断服务函数命名必须严格匹配启动文件(如 startup_stm32f10x_xx.s)中中断向量表定义的函数名(例如 DEBUG_USART_IRQHandler),而 stm32f10x_it.c是 ST 官方推荐的中断服务函数实现位置,既符合 STM32 工程的标准化开发规范,也便于集中管理所有中断逻辑,因此串口中断服务函数需要在此文件中实现。
// 串口中断服务函数(函数名需匹配启动文件中断向量表,不可随意修改)
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp; // 定义临时变量,用于暂存串口接收到的1字节数据
// 检查DEBUG_USARTx的"接收数据寄存器非空(RXNE)"中断标志是否置位
// 作用:判断是否有新的串口数据被接收,避免无中断时误处理
// USART_GetITStatus:返回指定中断标志的状态(RESET/SET)
if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) != RESET)
{
ucTemp = USART_ReceiveData(DEBUG_USARTx); // 读取接收寄存器中的数据到临时变量
USART_SendData(DEBUG_USARTx, ucTemp); // 将接收到的数据回发(实现串口echo回显功能)
}
}
补充说明
-
中断标志判断 :
USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE)是核心判断,确保仅在"有数据接收"时执行后续操作,避免无效的寄存器读写; -
数据回显 :代码中
USART_SendData是"回显"示例逻辑,实际项目中可替换为数据解析、缓存、协议处理等业务逻辑; -
函数名约束 :
DEBUG_USART_IRQHandler的命名由串口外设(如 USART1 对应USART1_IRQHandler)决定,需与启动文件中断向量表完全一致,否则中断无法触发。
步骤 1:宏定义与头文件(bsp_usart.h)
#ifndef __BSP_USART_H
#define __BSP_USART_H
#include "stm32f10x.h"
#include <stdio.h>
// 宏定义:改这里就能切换串口,无需改核心逻辑(C语言宏的复用价值)
// STM32F103: USART1在APB2(72MHz), USART2-5在APB1(36MHz)
#define DEBUG_USART1 1
#if DEBUG_USART1
#define DEBUG_USARTx USART1 // 目标串口
#define DEBUG_USART_CLK RCC_APB2Periph_USART1 // 串口时钟
#define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmd // 时钟使能函数
#define DEBUG_USART_BAUDRATE 115200 // 波特率
// GPIO配置:USART1_TX=PA9,RX=PA10(数据手册表5)
#define DEBUG_USART_GPIO_CLK RCC_APB2Periph_GPIOA
#define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10
// 中断配置(数据手册表58)
#define DEBUG_USART_IRQ USART1_IRQn // 中断通道号(37)
#define DEBUG_USART_IRQHandler USART1_IRQHandler // 中断函数名(必须和向量表一致)
#endif
// 函数声明
void USART_Config(void);
#endif
步骤 2:配置 NVIC(中断优先级)
// bsp_usart.c
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure; // C语言:定义结构体,存NVIC配置参数
// 配置优先级分组:2位抢占优先级+2位子优先级(整个工程只能设1次)
// 抢占优先级:高优先级中断能打断低优先级(比如快递电话能打断外卖电话)
// 子优先级:同抢占优先级时,先处理子优先级高的
// 寄存器操作:SCB->AIRCR = 0x05FA0000 | (0x2 << 8)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 选择要配置的中断通道(USART1)
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
// 设置抢占优先级为1(0-3,数字越小越紧急)
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
// 设置子优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
// 使能该中断通道(允许总机转接这个电话)
// 寄存器操作:NVIC->ISER[1] |= 1 << (DEBUG_USART_IRQ - 32)
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
// 将配置写入硬件寄存器(C语言:传结构体地址,函数修改硬件)
NVIC_Init(&NVIC_InitStructure);
}
步骤 3:初始化 GPIO 与 USART
// bsp_usart.c
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 1. 使能GPIO时钟(给PA9/PA10供电)
// 寄存器操作:RCC->APB2ENR |= RCC_APB2ENR_IOPAEN
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 2. 使能USART1时钟(给串口外设供电)
// 寄存器操作:RCC->APB2ENR |= RCC_APB2ENR_USART1EN
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 3. 配置TX引脚(PA9):复用推挽输出
// 复用推挽:引脚既可以当普通IO,也能作为串口TX(专门对外发数据的喇叭)
// 寄存器操作:GPIOA->CRH &= ~(0xF << 4); GPIOA->CRH |= (0xB << 4)
// 即MODE9=11(50MHz), CNF9=10(复用推挽)
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 引脚响应速度
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 4. 配置RX引脚(PA10):浮空输入
// 浮空输入:不接高/低电平,只接收外部串口信号(专门听数据的麦克风)
// 寄存器操作:GPIOA->CRH &= ~(0xF << 8); GPIOA->CRH |= (0x4 << 8)
// 即MODE10=00(输入), CNF10=01(浮空输入)
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 5. 配置USART参数
// 波特率计算:USARTDIV = 72000000 / (16 * 115200) = 39.0625
// 整数部分39,小数部分0.0625 * 16=1 → USART_BRR=0x271
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE; // 波特率115200
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8位数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 1位停止位
USART_InitStructure.USART_Parity = USART_Parity_No; // 无校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式
// 寄存器操作:写入USART_BRR、USART_CR1、USART_CR2、USART_CR3
USART_Init(DEBUG_USARTx, &USART_InitStructure);
// 6. 配置中断优先级
NVIC_Configuration();
// 7. 使能RXNE中断(允许快递员打电话)
// 寄存器操作:USART1->CR1 |= USART_CR1_RXNEIE
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 8. 使能USART外设(打开串口)
// 寄存器操作:USART1->CR1 |= USART_CR1_UE
USART_Cmd(DEBUG_USARTx, ENABLE);
}
步骤 4:编写中断服务函数
// stm32f10x_it.c
#include "bsp_usart.h"
// 中断服务函数:函数名必须和中断向量表一致(不能随便改)
// 向量表定义在startup_stm32f10x_hd.s中
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp; // C语言:uint8_t=无符号8位整数,存1字节数据
// 好写法:先检查中断标志,防止处理非RXNE中断(比如串口错误中断)
// 库函数实现:return ((USARTx->SR & USART_SR_RXNE) && (USARTx->CR1 & USART_CR1_RXNEIE))
if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) != RESET)
{
// 读取接收数据(自动清除RXNE标志,无需手动清)
// 寄存器操作:ucTemp = (uint8_t)(USART1->DR & 0xFF)
ucTemp = USART_ReceiveData(DEBUG_USARTx);
// 回显数据:把收到的字节发回去(测试用)
// 寄存器操作:USART1->DR = ucTemp
USART_SendData(DEBUG_USARTx, ucTemp);
// 坏写法:缺少发送完成等待,高波特率下可能丢数据
// 好写法补充:等待TXE标志置1(发送寄存器空)
// 寄存器操作:while(!(USART1->SR & USART_SR_TXE))
while(USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
}
}
四、实战:串口命令控制 RGB 灯
方式 1:查询式接收(简单但低效)
// main.c
#include "stm32f10x.h"
#include "bsp_usart.h"
#include "bsp_led.h"
int main(void)
{
uint8_t ch; // 存接收的字符
// 初始化串口(含中断配置,但这里先关闭中断,用查询式)
USART_Config();
// 寄存器操作:USART1->CR1 &= ~USART_CR1_RXNEIE
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, DISABLE); // 关闭中断
// 初始化RGB灯GPIO
LED_GPIO_Config();
// printf能串口输出:因为重写了fputc(底层调用USART_SendData)
printf("串口控制RGB灯\r\n");
printf("发送'1'亮红灯 | '2'亮绿灯 | '3'亮蓝灯 | 其他灭灯\r\n");
while(1) // 主循环(厨师一直炒菜)
{
// 查询:有没有新数据(每隔10秒看一眼门口)
// 寄存器操作:if(USART1->SR & USART_SR_RXNE)
if(USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == SET)
{
ch = USART_ReceiveData(DEBUG_USARTx); // 读取数据
printf("收到:%c(0x%02X)\r\n", ch, ch);
// 命令解析:注意是字符'1'(ASCII=0x31),不是数字1
switch(ch)
{
case '1': LED_RED; break; // 亮红灯
case '2': LED_GREEN; break; // 亮绿灯
case '3': LED_BLUE; break; // 亮蓝灯
default: LED_RGBOFF; break; // 灭所有灯
}
}
}
}
方式 2:中断式接收(高效)
查询式会让 CPU 一直"查有没有数据",中断式只在有数据时才处理,核心是用全局变量共享数据:
// bsp_usart.h 新增全局变量声明
// volatile关键字:告诉编译器该变量可能被中断修改,不要优化
extern volatile uint8_t g_rx_data; // 存接收的字节
extern volatile uint8_t g_rx_flag; // 接收完成标志(1=有新数据)
// bsp_usart.c 新增全局变量定义
volatile uint8_t g_rx_data = 0;
volatile uint8_t g_rx_flag = 0;
// 修改中断服务函数
void DEBUG_USART_IRQHandler(void)
{
if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) != RESET)
{
g_rx_data = USART_ReceiveData(DEBUG_USARTx);
g_rx_flag = 1; // 标记有新数据
}
}
// main.c 修改
int main(void)
{
USART_Config(); // 开启中断
LED_GPIO_Config();
printf("中断式串口控制RGB灯\r\n");
printf("发送'1'或0x01亮红灯 | '2'或0x02亮绿灯 | '3'或0x03亮蓝灯\r\n");
while(1)
{
if(g_rx_flag == 1) // 检测到新数据
{
g_rx_flag = 0; // 清除标志
// 支持ASCII和16进制命令
if(g_rx_data == '1' || g_rx_data == 0x01) LED_RED;
else if(g_rx_data == '2' || g_rx_data == 0x02) LED_GREEN;
else if(g_rx_data == '3' || g_rx_data == 0x03) LED_BLUE;
else LED_RGBOFF;
printf("执行命令:0x%02X\r\n", g_rx_data);
}
// 主循环可执行其他任务(比如灯闪烁),不被串口接收阻塞
}
}
五、新手必避的 7 个坑(注意事项)
-
中断标志未检查:中断服务函数直接读数据,可能处理非 RXNE 中断(如错误中断),导致数据错误。
-
混淆 ASCII 与 16 进制:串口助手发"1"是 ASCII=0x31,不是数字 1;勾选"16 进制发送"时,发"01"才是 0x01。
-
发送未等完成 :连续发送数据时,未等待
TXE标志置 1 就发下一个,会覆盖未发送的数据。 -
优先级分组重复设置 :整个工程只能调用 1 次
NVIC_PriorityGroupConfig,多次设置会导致所有中断优先级混乱。 -
中断与查询混用:既开中断又在主循环查询,数据会被中断先读走,主循环拿不到数据。
-
缺少 volatile 关键字 :全局变量未加
volatile,编译器可能优化掉变量读取,导致主循环看不到中断修改的值。 -
错误中断未处理:未使能错误中断,发生溢出、帧错误时无法及时发现,导致后续数据全部错误。
六、扩展练习
-
实现多字节命令解析:比如接收"RED ON"亮红灯、"RED OFF"灭红灯。
-
用环形缓冲区(数组)实现批量数据接收,解决连续发送多字节时的数据丢失问题。
-
给串口中断加错误处理:使能 ORE、FE、PE 错误中断,在中断服务函数中清除错误标志并打印错误信息。
-
实现 printf 重定向:重写
fputc函数,支持printf串口输出(已隐含在代码中,可自行实现)。
参考资料
-
《零死角玩转 STM32F103-指南者》第 26 章及 USART 相关章节
-
STM32F103xCDE 官方数据手册(USART 章节,第 5.3.16 节)