STM32F103串口实验详解:标准库USART初始化、中断与DMA实现
发布时间:2026/9/10 9:52:25 锦皓数字建站

简介实验4串口实验压缩包是一份STM32F103串口通信入门与实战资源面向嵌入式学习者覆盖UART配置、中断、DMA传输及模拟串口调试。压缩包共52个文件、约568KB包含11个头文件和7个源文件另有启动文件、编译生成的hex/map/axf及Keil工程文件类型涵盖源代码、工程配置与可执行文件方便直接打开研究。资源内整理了完整的UART初始化代码、数据帧格式定义、中断收发逻辑并穿插RealTerm/PuTTY等模拟串口联调时的排错思路讲解实验步骤时按硬件连接、代码配置、编译下载、模拟串口测试有序组织适合系统性练习。已有230人浏览学习适合正在学习STM32串口通信、需要参考工程模板或希望提升通信调试能力的读者。通过实际操作可快速掌握波特率、奇偶校验等参数配置理解串口收发流程为后续更复杂嵌入式通信开发打下扎实基础。1. 串口实验的关键差异标准库下的 STM32F103 为什么值得拆开看很多包含“实验4 串口实验.rar”的下载包打开都是同一副样子test.uvprojx、HARDWARE 下挂着 KEY、LED、BEEPSYSTEM 下放着 usart、delay、sys。如果只是把 test.hex 烧进 stm32f103 最小系统等串口助手显示几行字那这个实验只发挥了十分之一的价值。值得拆开看的地方在于这个工程八成是标准外设库而不是 HAL 或 LL 库。标准库的 USART 初始化会直接操作 RCC、GPIO、BRR、CR1 这些寄存器你去改波特率、数据位、中断优先级时对“串口是怎么工作”的感知比 HAL 封装强很多。下面就是按这个思路从最小系统的硬件连接开始到中断收发、DMA、模拟串口软件联调把 STM32F103 串口实验的完整链路过一遍。2. USART1 初始化GPIO 复用、时钟分配与波特率误差2.1 共地与电平匹配最小系统调试前必须确认的硬件点拿到 stm32f103 最小系统板先别急着接 TX、RX。USB 转串口模块和开发板之间必须共地这是串口通信最常见却最容易被忽略的前提。不共地时发送端和接收端的电压参考点不一致示波器上波形依然存在但单片机收进的数据就是乱码或者干脆没有反应。正确接法是开发板的 GND 接 USB 转串口模块的 GNDPA9 接 RXDPA10 接 TXD。然后是电平。STM32F103 的串口引脚是 3.3V TTL 电平如果手边是自带 5V 电平的 RS232 模块需要经过 MAX3232 转换。用常见 CH340、CP2102 这类 USB 转 TTL 模块时注意看模块上有没有 3.3V/5V 电平跳线。我一般把跳线放到 3.3V避免长期工作把 PA9、PA10 的内部保护二极管拖垮。这一步检查做完后续的软件配置才有意义。2.2 开启 GPIOA 与 USART1 时钟配置 PA9/PA10 复用模式标准库的配置顺序通常是先开时钟再配 GPIO再配 UART。很多新手把 GPIO_Init 放在 RCC 时钟使能之前导致寄存器写不进值。下面是一个最基础的 USART1 GPIO 初始化void UART1_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); }PA9 作为 TX 要配置成复用推挽GPIO_Mode_AF_PP这样发送数据时引脚会被片内外设拉高或拉低。PA10 作为 RX 一般配浮空输入因为接收方向不需要输出驱动。GPIO_Speed 在标准库中可以任意设为 50MHz这里只是设置翻转速度上限不影响串口波特率。时钟来源必须正确USART1 挂在 APB2 总线上所以这里同时使能了 GPIOA 和 USART1 的 APB2 时钟如果是 USART2/3则挂在 APB1时钟频率计算方式会不同。提示如果初始化后 TX 一直是低电平先查 GPIOA-CRH 的低 16 位是否被其他驱动覆盖很多时候是初始化顺序错误导致复用功能没生效。2.3 USART_Init 参数与波特率误差标准库 v3.5 的寄存器行为网上标着 stm32f103 库 v3.50 下载的包大多对应标准外设库 3.5.0 的 API。不同年份的发布版寄存器定义略有差异但 USART_Init 的用法一致。在标准库体系下USART 初始化函数会直接计算 USARTDIV 并写入 BRR 寄存器。初始化结构体里的每个字段都有实际影响void UART1_Init(uint32_t baudrate) { USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; UART1_GPIO_Init(); USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); USART_Cmd(USART1, ENABLE); }这里有一个很多人没意识到的点USART_BaudRate 不是直接写到寄存器而是由串口时钟 72MHz 除以 16 再除以 USARTDIV 得到。标准库内部会替你做除法和余数处理但晶振不整分频时会产生误差。下面以 8MHz 外部晶振经 PLL 倍频到 72MHz 为例目标波特率实际分频结果实际波特率误差9600468.7596000.00%19200234.375192000.00%11520039.0625115384.60.16%4608009.765625455696.21.10%误差超过 2% 时接收方往往会随机乱码。所以高波特率下要么改用 12MHz 或 25MHz 晶振做整分频要么接受 0.16% 的误差并调低上位机采样宽容度。工程里常用的 115200 在 72MHz 下误差很小可以放心用460800 这类速率最好不要直接用标准波特率需要自己算 BRR。2.4 初始化自检不接上位机也能确认串口在工作配置完先不要连 USB 转串口模块用万用表或示波器量 PA9 的空闲电平。正常状态是接近 3.3V 的高电平UART 协议在空闲时保持高。然后用一根杜邦线把 PA9 和 PA10 短接调用一次发送函数再读取接收寄存器如果收到相同字节说明 GPIO 复用、时钟、波特率链路都通了。这个自检方式在没有电脑的环境下非常有用也能排除掉 USB 转串口模块的问题。如果你用的是 Keil 调试器还可以在 Watch 窗口直接读 USART1-DR。发送后立刻查看 DR 是否从空变成接收值比盲调上位机快得多。3. 查询与中断收发模拟串口软件联调实战3.1 查询发送与状态位的选择TXE 和 TC 的差别发送一个字节的标准库写法是void UART1_SendByte(uint8_t byte) { USART_SendData(USART1, byte); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); }TXE 表示数据已经从 DR 移送到发送移位寄存器此时可以写入下一个字节TC 表示整个帧包括停止位都已经送出。连续发送时等 TXE 就够了但如果连接了 RS485 收发器必须在发送完最后一帧后等待 TC再拉低发送使能脚否则最后一个字节会被截断。这个区别在串口实验里不明显一旦你把手动改造成 Modbus RTU 485 通信时就会踩到。3.2 RXNE 中断与接收缓冲区缓存设计要点接收不能一直用查询因为主循环可能被 delay、按键扫描占用。中断方式在每收到一个字节时及时把数据移走。下面是一个简单的环形缓冲区实现#define RX_BUF_SIZE 256 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_head 0; volatile uint16_t rx_tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint16_t next (rx_head 1) % RX_BUF_SIZE; if (next ! rx_tail) { rx_buf[rx_head] USART_ReceiveData(USART1); rx_head next; } else { (void)USART_ReceiveData(USART1); } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }代码里先判断缓冲区是否满满时丢弃新数据并清掉中断标志防止堆积。rx_head 和 rx_tail 都用无符号类型主循环读取时判断两者是否相等即可。缓冲区容量取 256一般串口实验的帧不会超过这个长度如果要做 Bootloader 升级明显不够就需要用 DMA 配合空闲中断后面第 5 章会展开。3.3 用 RealTerm / PuTTY 建立模拟对端的参数配置通常说的模拟串口软件本质是把电脑模拟成另一个串口设备。它和虚拟串口软件不是一回事不需要安装驱动直接配置串口端口号即可。RealTerm 比串口助手更克制一点适合看十六进制字节PuTTY 的 Serial 模式则适合看 ASCII 文本。我一般用 RealTerm 做联调因为它能同时显示 ASCII 和 HEX还能方便地切换显示模式。参数设置值说明PortCOM3 等实际端口在设备管理器里确认Baud rate115200必须和 USART1_Init 传入值一致Data bits8对应 USART_WordLength_8bParityNone对应 USART_Parity_NoStop bits1对应 USART_StopBits_1Flow controlNone对应 USART_HardwareFlowControl_None这几个参数任何一个对不上都会表现为收到奇怪字符或没有任何响应。尤其是流控很多模块默认没有接 RTS/CTS 线一旦选成 HardwareSTM32 会在收到流控信号前卡住。3.4 回环测试按键触发 STM32 发送并验证链路联调时不要让程序一上电就发数据那样无法区分是初始化问题还是发送问题。建议用按键触发发送uint8_t key KEY_Scan(); if (key KEY0_PRESS) { UART1_SendString(STM32F103 UART Test\r\n); }KEY_Scan 是工程里 HARDWARE 模块提供的轮询函数通常在主循环里周期调用。按键按下后STM32 向串口发送一行文本你在上位机应该逐字收到。如果只收到首字母或者完全没数据优先检查 UART1_SendString 内部是否漏掉了对每个字节的 TXE 等待。很多下载来的工程为了看起来简洁会把发送函数写成不等待状态位这在 STM32F103 内部时钟足够快时可能没问题但模拟串口软件一端一旦开启 ANSI 终端模式或应答机制就会把数据打乱。4. 串口联动 KEY、LED、BEEP标准库工程模块组织与常见坑4.1 从 test.uvprojx 看标准库工程的模块组织打开“实验4 串口实验”的 test.uvprojx能看到工程被分成了 USER、SYSTEM、HARDWARE 等多个分组。SYSTEM 下是 usart、delay、sysHARDWARE 下是 KEY、LED、BEEP。这是国内 STM32F103 教程很常见的组织方式它把串口驱动放到 SYSTEM/usart.c把板级外设放到 HARDWARE 下主逻辑集中在 USER/test.c。模块对应文件作用串口SYSTEM/usart.c/hUSART 初始化、中断服务、发送接收封装延时SYSTEM/delay.c/h基于 SysTick 的毫秒/微秒延时系统SYSTEM/sys.c/hGPIO 时钟初始化、常用内核寄存器操作按键HARDWARE/KEY.c/h按键 GPIO 配置与扫描LEDHARDWARE/LED.c/hLED GPIO 配置与控制蜂鸣器HARDWARE/BEEP.c/h蜂鸣器 GPIO 配置与控制sys.c 里通常做的第一件事是把时钟初始化到 72MHz。串口波特率计算依赖这个值如果从一开始就沿用了系统默认的 HSI 8MHz 而没切换到 PLLUSART_Init 算出的 BRR 会是另一个分频系数。所以拿到下载工程先别急着改串口先在 main 开头确认 SystemInit 是否被调用或者 sys.c 里的 Stm32_Clock_Init 是否执行成功。4.2 按键触发串口发送轮询消抖与 EXTI 的选择串口实验中按键通常用来触发发送。轮询方式占用主循环但逻辑简单、不涉及共享变量uint8_t KEY_Scan(void) { static uint8_t key_up 1; if (key_up KEY0_GPIO 0) { delay_ms(10); key_up 0; if (KEY0_GPIO 0) { return KEY0_PRESS; } } else if (KEY0_GPIO 1) { key_up 1; } return KEY_NONE; }这里的 10ms 消抖和很多模块内部的 10ms 扫描周期是匹配的。使用 EXTI 中断时要注意按键引脚和串口引脚如果共用 EXTI 线会很麻烦好在 KEY 一般接在 PB0 或 PE3 这类引脚上与 PA9/PA10 不冲突。但中断里不能直接调用 delay_ms否则 SysTick 中断优先级高时会卡死主循环。我通常只置一个标志位回到主循环后再发送。4.3 接收数据解析并驱动 LED/BEEP环形缓冲区的消费方式收到数据后除了回显还能驱动 LED 和 BEEP。设计思路是中断只负责填缓冲区主循环负责解析。比如收到大写字母 A 就点亮 LED收到 B 就关闭蜂鸣器while (rx_tail ! rx_head) { uint8_t cmd rx_buf[rx_tail]; rx_tail (rx_tail 1) % RX_BUF_SIZE; switch (cmd) { case A: LED_On(); break; case B: BEEP_Off(); break; default: UART1_SendByte(cmd); break; } }注意这里 rx_tail 和 rx_head 是 volatile 修饰的主循环读取时要先保存到局部变量再消费防止中断在消费过程中修改 rx_head 导致越界。把消费逻辑放在主循环跑能避免中断服务函数里做耗时操作这是嵌入式中断设计的基本边界。4.4 排查串口乱码的常见步骤引脚冲突、时钟、流控串口乱码是这类实验的最高频问题。按下面顺序排查基本能在 5 分钟内定位。查时钟树。ST-Link 调试时 SystemInit 没执行实际串口时钟是 8MHz但用 72MHz 算了波特率会出现 9 倍偏差。查引脚复用。PA9/PA10 在被初始化成普通 GPIO 后再被 USART 占用需要先检查 GPIOA-CRH 的值是多少。查流控。上位机选了 Hardware flow control但板子只有 TX/RX/GND输出会被挂起。查 USB 转串口模块质量。CH340 在 115200 下通常稳定但某些免驱模块的晶振误差偏大可以把波特率降到 9600 对比。查发送函数是否等待 TXE。如果发完一个字节就直接发下一个很可能丢字节。如果上述都没问题用逻辑分析仪抓 PA9 波形看每个 bit 的宽度反向计算实际波特率。这个方法比任何调试器都直接。5. 进阶验证DMA 不定长接收与模拟串口的位采样边界5.1 空闲中断 DMA 实现不定长接收串口实验做到最后你会觉得 RXNE 中断逐个收字节太占资源。工程量大一点后我习惯直接上 DMA 加空闲中断void USART1_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rx_dma_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); }在 USART1_IRQHandler 里判断 USART_IT_IDLE读一次 SR 再读一次 DR 清除空闲标志然后从 DMA 的 CNDTR 计算出当前帧长度。注意 DMA1 通道 5 对应 USART1_RX这是芯片内部硬连线通道用错数据永远不动。这个方案比 RXNE 中断更适合 Modbus 这类长帧CPU 占用低很多。5.2 模拟串口何时可用何时必须用硬件 USART网上讨论“stm32f103 模拟串口”时通常指用 GPIO 配合定时器翻转电平来实现 UART 协议而不是电脑端的模拟串口软件。软件模拟串口不是不能用但位采样的要求很苛刻。每个 bit 只采样一次采样点要求落在位中间附近如果执行代码时被中断打飞就会采到跳变沿数据必然错误。所以软件模拟串口只适合波特率不超过 9600、系统中断负载低的场合。真正的产品设计里如果这组串口需要跑 115200 且要接 RS485或者需要和 CAN 共用一个 MCU老老实实用硬件 USART 加 DMA 才是可靠方案。模拟串口只作为硬件资源不够时的临时手段。5.3 用逻辑分析仪校准波特率的关键手法最后分享一个排查乱码时最有效的动作。在 PA9 发送端夹上逻辑分析仪的探头设置边沿触发抓一帧发送波形。按 UART 的格式起始位是低电平后面 8 个数据位按低位在前排列。测量起始位下降沿到停止位上升沿的宽度除以总的 bit 数就得到实际位宽。比如文本字符 A0x41的帧位宽如果是 8.70us那么实际波特率就是 115000 左右偏差很小如果位宽达到 13.9us那就是 72000 波特说明时钟配置错得离谱。按这个实测值回去修正 SystemInit 或 USARTDIV直到位宽落在目标波特率 ±2% 以内。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。