STM32F103 学习笔记-21-串口通信(第6节)-串口发送命令控制RGB灯

STM32F103 学习笔记-21-串口通信(第6节)-串口发送命令控制RGB灯 一、串口中断到底是什么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_SR0x00串口状态寄存器快递柜状态提示灯• RXNE(位 5): 1有新数据0无数据• TXE(位 7): 1发送寄存器空• TC(位 6): 1发送完成• ORE(位 3): 1溢出错误• FE(位 1): 1帧错误USART_DR0x04串口数据寄存器快递柜储物格• 低 8 位: 存 1 字节收发数据• 读操作: 取接收数据自动清 RXNE• 写操作: 存发送数据自动清 TXEUSART_BRR0x08波特率寄存器快递速度调节器• DIV_Mantissa[15:4]: 波特率分频器整数部分• DIV_Fraction[3:0]: 波特率分频器小数部分USART_CR10x0C控制寄存器 1总开关• UE(位 13): 1使能 USART• RE(位 2): 1使能接收• TE(位 3): 1使能发送• RXNEIE(位 5): 1使能 RXNE 中断• M(位 12): 08 位数据19 位数据• PCE(位 10): 1使能奇偶校验USART_CR20x10控制寄存器 2停止位等• STOP[13:12]: 001 位停止位USART_CR30x14控制寄存器 3流控等• CTSE(位 9): 1使能 CTS 流控• RTSE(位 8): 1使能 RTS 流控RCC 时钟​RCC_APB2ENR0x18APB2 外设时钟使能寄存器• USART1EN(位 14): 1使能 USART1 时钟• IOPAEN(位 2): 1使能 GPIOA 时钟GPIO​GPIOA_CRH0x04GPIOA 高 8 位配置寄存器• MODE9[1:0]: PA9 模式• CNF9[1:0]: PA9 配置• MODE10[1:0]: PA10 模式• CNF10[1:0]: PA10 配置NVIC​NVIC_ISER00xE000E100中断使能寄存器 0• USART1_IRQn(位 37): 1使能 USART1 中断NVIC_IPR90xE000E424中断优先级寄存器 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_TXPA9RXPA10数据手册表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; // 设置抢占优先级为10-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) // 即MODE911(50MHz), CNF910(复用推挽) 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) // 即MODE1000(输入), CNF1001(浮空输入) 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 * 161 → USART_BRR0x271 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); // 命令解析注意是字符1ASCII0x31不是数字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是 ASCII0x31不是数字 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 节