简介:一份基于STM32F103C8T6(M3内核)的UART串口通信完整项目资料,结合HAL库与STM32CubeMX,面向嵌入式初学者与STM32开发者,解决从CubeMX图形化配置到USART收发、中断处理及DMA优化等问题,可直接导入开发环境进行编译下载。压缩包共146个文件,涵盖C源码、H头文件、Hex固件、IOC工程配置、Keil工程文件及编译中间文件,整体约4.31MB,目录清晰,适合对照源码与配置逐行学习。已有5985人学习/下载,具有较高参考热度。资料包含CubeMX时钟树与串口参数的配置思路、HAL_UART_Transmit/Receive等API调用示例,并涉及NV_USART模块的非易失性存储扩展;同时介绍了串口调试终端连接、常见错误排查和DMA大量数据传输的优化方法,能够帮助读者从零搭建稳定串口通信功能,为后续RS485、蓝牙等外设通信打下基础。 前几天整理资料,翻出一个老项目包,名字就叫“基于STM32F103C8T6(HAL库)和CubeMX实现UART串口通信数据收发.rar”。这其实是我给实验室新生准备的第一份入门例程,别看串口通信听起来基础,很多新手第一次接触STM32,卡就卡在环境、配置、收发这三关上。今天干脆把这个项目拆开,从CubeMX配置到HAL库代码,再到实际接线和踩坑过程,完整聊一遍。
选择STM32F103C8T6最小系统板,原因很直接:便宜、资料多、引脚够用。C8T6是64引脚LQFP封装,Flash 64KB、RAM 20KB,主频最高72MHz,跑串口、I2C、SPI这类常规外设绰绰有余。板子本身可以通过USB转串口模块连接电脑,也能用ST-Link下载调试。CubeMX负责图形化配置外设,HAL库把寄存器操作封装成结构清晰、可读性强的函数接口,两者结合,能把开发周期压缩得很短。有人可能问,直接用标准外设库或者寄存器操作不是更底层吗?不是不好,而是对大多数应用场景来说,HAL库更符合“先跑通再优化”的思路。对于学生作业、竞赛原型、快速验证方案,这套组合就是最优解。
1. 项目拆解:这个UART例程到底解决了什么问题
1.1 为什么选STM32F103C8T6+HAL库+CubeMX
先说芯片。STM32F103C8T6属于F1主流系列,虽然推出时间比较早,但市场验证非常充分,最小系统板的资源也极其丰富。核心板一般只需要一个8MHz晶振、复位电路、BOOT配置和3.3V供电,就能稳定跑起来。这个项目用UART作为第一个外设,是因为串口几乎是所有嵌入式开发的“调试窗口”。程序跑到哪里、传感器数据是多少、协议交互是否正常,都可以通过串口打印出来,比单纯用Keil仿真观察变量直观得多。
CubeMX在这个项目里的作用,是把最繁琐的初始化阶段自动化。以前用标准库写UART,要手动计算波特率寄存器、配置GPIO复用模式、打开USART时钟、使能中断,任何一个寄存器写错,现象就是乱码或者完全没反应。CubeMX生成工程之后,这些底层配置全部由工具完成,剩下的精力可以放在业务逻辑上。HAL库则提供了一套统一的API,初始化结构体、发送接收函数、中断回调机制,结构清晰。遇到问题去社区搜,几乎都能找到现成答案。对于刚接触单片机的新手来说,这套组合的学习成本远低于从寄存器裸写开始啃。
1.2 UART通信的核心知识点有哪些
UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。它只需要TX和RX两根数据线,外加共地,就能实现全双工通信。为什么叫异步?因为收发双方没有共享时钟,而是靠约定波特率来采样。这就像两个人约好语速,一个人说,另一个人按同样的节奏听,如果语速对不上,内容必然听岔。
一个完整的数据帧包括起始位、数据位(通常8位)、可选的奇偶校验位、停止位。我们常用的115200 8 N 1,意思就是波特率115200bps,8个数据位,无校验,1个停止位。波特率决定每秒钟传输多少个bit,两个设备不一致,收到的数据必然乱码。STM32的波特率由时钟分频得到,比如USART1挂载在APB2总线,时钟72MHz,想要得到115200bps,分频系数大约是39.06。CubeMX会根据你填的波特率自动计算寄存器值,配置起来很快,但前提是时钟树必须正确,否则算出来的波特率同样会偏。理解这几个基本概念,后续对接蓝牙模块、WiFi模块、GPS模块时就不会被各种参数搞晕。
2. CubeMX配置细节:从新建工程到生成代码
2.1 芯片选型、调试接口与时钟树
打开CubeMX,新建工程,在MCU选择器里搜索STM32F103C8Tx。双击选中后,第一步先把SYS里的Debug改成Serial Wire。这一步不做,ST-Link可能没法通过SWD接口连接板子,尤其是板载SWD引脚被复用的情况。很多最小系统板都引出了SWCLK和SWDIO,但CubeMX默认调试接口是No Debug,生成代码后调试器会识别不到芯片。
第二步配置RCC,HSE选择Crystal/Ceramic Resonator。绝大多数最小系统板都焊了8MHz晶振,用外部晶振才能稳定跑到72MHz主频。如果偷懒选择HSI内部时钟,实际主频会变成8MHz,CubeMX里虽然填了72MHz,但串口波特率会整体偏移,现象就是乱码,这点后面会重点说。接着进入Clock Configuration页面,把PLL Source选为HSE,输入8MHz,倍频到72MHz。F103的时钟树有限制,APB1总线最高36MHz,APB2最高72MHz。USART1挂在APB2上,所以它能跑满72MHz。正因如此,例程里优先使用USART1,配置简单,通信稳定。
2.2 USART1参数配置:这些选项不能乱填
在左侧Categories里展开Connectivity,选中USART1,Mode改为Asynchronous。下方Parameter Settings会列出波特率、数据长度、校验、停止位等参数。我习惯设115200 8 N 1,因为和绝大多数串口助手、蓝牙模块、GPS模块默认参数一致。实际项目如果走长线或者电磁环境差,可以降到9600,误码概率更低。波特率越高,对时钟精度和线路质量的要求就越高。
引脚方面,USART1默认映射到PA9(TX)和PA10(RX),这也是最小系统板引出的排针,直接能用杜邦线连接USB转TTL模块。有人问为什么不用USART2?USART2也可以,但它挂在APB1上,时钟上限只有36MHz,而且PA2/PA3有时会被其他外设占用。C8T6上USART1最方便,兼容性最好。另外要注意,STM32的串口引脚输出的是TTL电平,不是RS232电平,所以不能直接接电脑的DB9串口,必须通过USB转TTL模块转换。
2.3 三个容易踩的配置项
工程生成后如果发现不能下载、不能调试,先回头检查SYS里的Debug是否是Serial Wire。这是新手最常见的第一个坑。
第二个坑在Project Manager。Toolchain / IDE要选对,用Keil就选MDK-ARM,用STM32CubeIDE就选STM32CubeIDE,否则生成的工程根本打不开。很多资料默认用的是MDK,但高版本CubeMX生成的工程,直接用旧版Keil打开也会报错,建议升级到较新的Keil版本。
第三个坑是Timebase Source。如果后续要用FreeRTOS或者复杂中断,建议把时基从SysTick改成一个定时器,比如TIM1。SysTick本身被HAL_Delay占用,FreeRTOS也依赖它,配置冲突时程序会莫名其妙卡死。CubeMX里这个选项在SYS页面,默认是SysTick。没有特殊需求可以保持默认,但作为稳定习惯,我通常在初期就把它改成TIM1,给系统多留一条后路。
3. HAL库代码实现:三种收发方式实测
3.1 轮询方式:适合打印日志和固定帧交互
CubeMX生成代码后,串口初始化已经完成,用户代码主要加在main函数的while(1)里。最简单的发送是调用HAL_UART_Transmit:
uint8_t tx_buf[] = "Hello STM32 UART\r\n"; while (1) { HAL_UART_Transmit(&huart1, tx_buf, sizeof(tx_buf) - 1, 1000); HAL_Delay(500); }这个函数有四个参数:串口句柄、待发送数据指针、发送长度、超时时间。超时时间单位是毫秒,如果发送在超时时间内没完成,函数会返回超时错误码。轮询发送的特点是简单直接,适合打印日志、调试信息,或者发送固定长度的协议帧,但要避免在中断回调里调用,否则可能阻塞中断处理。
接收对应HAL_UART_Receive,但它有个特点:必须收满指定字节数才返回。比如设len=5,如果只来了3个字节,程序会一直阻塞在那里,直到超时或者凑够5个字节。所以轮询接收适合长度固定的协议帧,不适合不定长字符串。第一次上手时,我先用发送函数打印测试信息,再接USB转串口模块看返回,确认链路通。
3.2 中断方式:真正解决“只收一次”的坑
串口收发最常用的是中断方式,但网上“串口中断接收只收一次”的求助特别多。先看正确写法:
uint8_t rx_byte = 0; int main(void) { // 初始化代码... HAL_UART_Receive_IT(&huart1, &rx_byte, 1); while (1) { // 主循环不处理串口接收 } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { HAL_UART_Transmit(&huart1, &rx_byte, 1, 100); HAL_UART_Receive_IT(&huart1, &rx_byte, 1); } }关键在于,HAL_UART_Receive_IT每调用一次,只能完成一次接收。进入中断回调后,HAL库会把串口状态从READY置为BUSY,如果不重新调用Receive_IT,串口就再也不收新数据了。很多教程没强调这点,代码跑一遍只收一次,就以为板子坏了,其实只是缺少最后一行重新使能。另外注意回调函数名不能改,而且要判断huart实例,防止多个串口回调互相干扰。中断回调里尽量别做耗时操作,比如HAL_Delay或协议解析,否则实时性会变差。更好的做法是把收到的字节丢进环形缓冲区,在主循环里统一处理。
3.3 DMA方式:大批量数据收发的进阶路径
当数据量很大,比如每秒几KB的传感器数据,中断方式频繁进中断会占用CPU资源。DMA可以让外设直接把数据搬进内存,搬完再通知CPU。HAL库提供了HAL_UART_Transmit_DMA和HAL_UART_Receive_DMA。配置DMA时,发送一般用Normal模式,接收做不定长数据时用Circular模式,配合USART的IDLE空闲中断,能实现“收一批数据再统一处理”的效果。
不过C8T6的DMA资源有限,只有一个DMA1控制器,共7个通道,串口收发分别占用通道,使用前要查数据手册确认,别跟ADC、SPI抢通道。DMA适合做传输层优化,基础项目可以先不碰,但值得列入下一个学习目标。等把轮询、中断、DMA三种方式都跑一遍,对HAL库的串口模型就会有比较完整的认识。
4. 实操接线、驱动与回环验证
4.1 最小系统板接线与USB转串口选型
STM32F103C8T6最小系统板的串口引脚通常印在板上,PA9是USART1_TX,PA10是USART1_RX。与USB转TTL模块连接时一定要交叉,MCU的TX接模块RX,MCU的RX接模块TX,GND必须共地。如果不交叉,发出去的信号跑到了自己的接收脚,什么都收不到。
| STM32F103C8T6 | USB转TTL模块 |
|---|---|
| PA9 (TX) | RX |
| PA10 (RX) | TX |
| GND | GND |
市面上USB转串口模块主控常见的有CH340、FT232R、FT231X。CH340便宜,多数Windows系统免驱;FT232R和FT231X贵一些,但兼容性和抗干扰更好。用FT232R或FT231X时,部分新装系统或者精简系统会识别成未知设备,需要去官网下载对应驱动,安装后设备管理器里才能看到COM口。驱动装不上时,试着禁用Windows驱动强制签名,或者换个USB口,很多玄学问题其实是接触不良。
4.2 串口助手与回环测试:先排除链路问题
把USB转串口模块插到电脑,设备管理器里出现COM口,然后打开任意串口助手,波特率设成和代码一致。最稳妥的验证方式是“回环测试”:先把模块TX和RX短接,在串口助手里发送数据,如果自己能收到,说明USB串口链路是通的。再把模块和STM32接好,发送数据给MCU,MCU用中断回显,如果串口助手能收到同样的数据,说明整个收发链路没问题。
我在调试时还会开一个逻辑分析仪,抓PA9和PA10的电平波形,确认有数据帧在跑。没有逻辑分析仪也不要紧,回环测试加串口助手已经能覆盖大部分情况。如果状态指示灯不亮或者电压不对,优先用万用表量3.3V供电。很多最小系统板供电能力一般,外接多个设备时容易掉电压,导致串口异常。
5. 常见问题排查与避坑实录
5.1 串口中断接收只收一次的完整复盘
这是最典型的问题,值得专门展开。现象很好复现:上电后第一次发数据,MCU能收到并回显,再发就没反应了。原因就是3.2节说的,HAL_UART_Receive_IT只配置一次,中断完成后没有重新使能。但还有两个隐藏原因:一是回调函数中执行了HAL_UART_Transmit,发送期间会产生发送相关中断,如果NVIC里收发中断优先级配置不当,接收中断可能一直被别的中断抢占;二是回调里处理了复杂逻辑,导致再一次调用Receive_IT时,串口状态还处于BUSY。解决办法很简单:回调里只做数据搬运,不要在里面做协议解析和耗时操作。优先级分组在CubeMX的NVIC配置里统一设置,中断优先级不要全都一样,两个不同外设的中断如果抢占优先级相同,调度时会出问题。
5.2 串口乱码:八成是时钟或波特率问题
串口助手显示乱码,第一反应不要怀疑代码,先查CubeMX时钟树。F103默认HSI内部时钟是8MHz,如果你没有配置外部晶振,却在时钟树里把SYSCLK设成72MHz,实际主频就错了,波特率也跟着错,乱码就这么来的。另一个容易忽略的点是串口助手、USB转串口模块、STM32三者的波特率必须完全一致。传输距离长时,建议降低波特率,并在模块端接好地线。短接回环测试时正常,但一接MCU就乱码,优先检查共地和电源纹波。
5.3 串口工具收不到数据或设备无法识别
如果串口助手完全收不到数据,按这个顺序排查:先看设备管理器里COM口是否存在,没有就装驱动,FT232R和FT231X用户重点查驱动版本;再看接线是否交叉、是否共地;然后做回环测试,区分模块和MCU的故障;最后检查代码里USART1是否真的使能了接收中断。硬件问题用万用表量电压,软件问题用调试器打断点。把排查步骤固定下来,比漫无目的地猜快得多。
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 中断只收一次 | 接收中断未重新使能 | 在RxCpltCallback里再次调用HAL_UART_Receive_IT |
| 串口乱码 | 时钟树未配置外部晶振 | 配置HSE为8MHz,PLL倍频到72MHz |
| 设备管理器无COM口 | USB转串口驱动缺失 | 安装CH340/FT232R/FT231X对应驱动 |
| 完全收不到数据 | TX/RX没有交叉或未共地 | 按接线表重新连线 |
| 回环正常但MCU不返回 | 中断优先级或代码问题 | 检查NVIC配置,确认回调里重新调用Receive_IT |
最后再说说我在实际项目里的一个习惯。串口这个外设,看起来简单,但坑都藏在配置细节里。每次新建工程,我都会先用回环测试确认硬件链路,再用轮询发送打印一个版本号,最后才写中断接收。这样每一步出问题,都能快速定位是硬件、配置还是代码的问题。如果你第一次跑通这套UART例程,建议下一步试着加一个环形缓冲区,把接收到的字节先存起来,再在主循环里解析;再往后可以研究DMA加IDLE中断的接收方式,基本能覆盖绝大多数串口应用场景。串口玩明白了,再学I2C、SPI、CAN这些外设,思路都是相通的。
本文还有配套的精品资源,点击获取