手把手教你用 Keil 玩转 STM32 串口通信:从零到“Hello World”的完整实战
当你的 STM32 不说话?可能是 UART 没调通
你有没有遇到过这样的场景:
代码烧进去了,开发板也上电了,但串口助手却一片漆黑——没有一个字输出。
或者更糟,屏幕上全是乱码,像外星人发来的密文。
别急,这几乎是每个嵌入式新手都会踩的坑。而问题的核心,往往就出在UART 配置上。
今天我们就来彻底解决这个问题。不讲虚的,只说干货——带你用Keil MDK + STM32F103C8T6实现稳定可靠的串口收发,做到“我说你听,你说我回”,真正打通 MCU 和 PC 之间的第一条数据通道。
整个过程将涵盖:硬件连接、工程搭建、时钟配置、GPIO映射、波特率计算、数据收发逻辑,以及常见问题排查。全程基于标准外设库(StdPeriph),让你看懂每一行代码背后的含义。
准备好了吗?我们开始。
为什么是 UART?它凭什么成为嵌入式的“普通话”
在五花八门的通信协议中,UART 虽然古老,却是最实用的一种。它不需要同步时钟线,只需要两根线(TX 和 RX)就能完成全双工通信,适合点对点、低速到中速的数据传输。
STM32 几乎每款芯片都集成了多个 USART/UART 外设。以经典的STM32F103C8T6(俗称“蓝丸子”)为例:
- USART1:挂载在 APB2 总线,最高时钟 72MHz,支持高速通信
- USART2:挂载在 APB1 总线,最高时钟 36MHz
- UART3:同样在 APB1,功能略少
它们都能实现异步串行通信,支持可编程波特率(如 9600、115200)、8位或9位数据位、奇偶校验、1~2位停止位等参数。
更重要的是,所有调试日志、AT指令交互、Bootloader升级,几乎都依赖 UART。可以说,不会 UART,等于不会嵌入式开发。
先搞明白:UART 是怎么传数据的?
UART 的通信是“异步”的,意味着发送方和接收方没有共享时钟信号。那它是如何保持同步的呢?
答案是:双方提前约定好波特率。
比如我们都设为 115200 bps,即每秒传送 115200 个比特。然后靠这个节奏来采样每一位数据。
一个典型的数据帧结构如下:
| 字段 | 内容说明 |
|---|---|
| 起始位 | 1 bit,低电平,表示帧开始 |
| 数据位 | 8 bit(常用),低位先发 |
| 校验位(可选) | 1 bit,用于简单错误检测 |
| 停止位 | 1 或 2 bit,高电平,帧结束标志 |
举个例子,你要发字符'A'(ASCII = 0x41 =0b01000001),实际在线上传输的顺序是:
[起始位] 1 0 0 0 0 0 1 0 [停止位] ↑ LSB ↑ MSB注意:低位在前!
STM32 的 UART 模块通过几个关键寄存器控制这一切:
- USART_DR:写入要发送的数据,或读取接收到的数据
- USART_SR:查看状态,比如 TXE(发送缓冲空)、RXNE(接收非空)
- USART_BRR:设置波特率分频系数
- USART_CR1~CR3:启用发送/接收、中断、DMA 等功能
只要把这些寄存器配对了,UART 就能跑起来。
引脚怎么接?别让 GPIO 成了拦路虎
再好的配置,如果引脚没接对,也是白搭。
对于 STM32F103C8T6 来说:
- USART1_TX→ PA9
- USART1_RX→ PA10
这两个引脚必须配置为复用功能模式:
- PA9(TX):复用推挽输出(AF_PP)
- PA10(RX):浮空输入(IN_FLOATING)
为什么这么配?
- TX 是输出,要驱动外部电路,所以用推挽;
- RX 是输入,等待对方送信号进来,不用上下拉,避免干扰原始电平。
同时别忘了:开启对应外设时钟!
这是很多初学者忽略的关键一步。STM32 默认关闭所有外设时钟以省电,所以我们得手动打开:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);APB2?因为 USART1 属于高速总线 APB2,而 GPIOA 也在同一组,必须一起开。
波特率怎么算?别让误差毁了一切
波特率不准,通信必崩。理想情况下,接收端应在每位中间采样,但如果频率偏差太大(>±3%),就会采样错位,导致乱码。
STM32 的波特率由以下公式决定:
$$
\text{BaudRate} = \frac{f_{PCLK}}{16 \times (\text{DIV_Mantissa} + \frac{\text{DIV_Fraction}}{16})}
$$
其中:
- 若是 USART1(APB2),$ f_{PCLK} = 72MHz $
- 若是 USART2/3(APB1),$ f_{PCLK} = 36MHz $
我们想要115200 bps,代入计算:
$$
\frac{72000000}{16 \times 115200} ≈ 39.0625
$$
所以整数部分DIV_Mantissa = 39,小数部分DIV_Fraction = 1(0.0625 × 16 = 1)
幸运的是,标准库已经帮我们封装好了:
USART_InitStructure.USART_BaudRate = 115200;内部会自动计算并写入 BRR 寄存器。
但建议你在设计时优先选择能让分频结果为整数的波特率,例如 72MHz 下 115200 刚好接近整除,误差仅 0.15%,非常安全。
上手写代码:从初始化到回环测试
下面这段代码,是你实现 UART 通信的“最小可行系统”。
#include "stm32f10x.h" #define BUFFER_SIZE 64 void USART1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 1. 开启时钟:GPIOA 和 USART1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置 PA9 为复用推挽输出(TX) 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); // 3. 配置 PA10 为浮空输入(RX) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 4. 配置 USART1 参数 USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_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_Init(USART1, &USART_InitStructure); // 5. 启动 USART1 USART_Cmd(USART1, ENABLE); } // 发送单字节 void USART_SendChar(USART_TypeDef* USARTx, uint8_t ch) { while (!USART_GetFlagStatus(USARTx, USART_FLAG_TXE)); // 等待发送缓冲空 USART_SendData(USARTx, ch); } // 发送字符串 void USART_SendString(USART_TypeDef* USARTx, char *str) { while (*str) { USART_SendChar(USARTx, *str++); } } int main(void) { char rxBuffer[BUFFER_SIZE]; int len = 0; USART1_Config(); USART_SendString(USART1, "STM32 UART通信已启动!\r\n"); while (1) { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE)) { // 收到数据? rxBuffer[len++] = USART_ReceiveData(USART1); // 判断是否收到换行符或缓冲满 if (len >= BUFFER_SIZE - 1 || rxBuffer[len-1] == '\n') { rxBuffer[len] = '\0'; // 添加字符串结束符 USART_SendString(USART1, "你输入的是: "); USART_SendString(USART1, rxBuffer); len = 0; // 清空缓冲区 } } } }关键点解读:
RCC_APB2PeriphClockCmd():必须先开时钟,否则后面配置无效。GPIO_Mode_AF_PP:复用推挽,TX 才能正常输出高/低电平。USART_FLAG_RXNE:表示接收数据寄存器非空,可以读取。- 主循环采用轮询方式监听数据,适合入门学习;后续可升级为中断或 DMA。
运行效果:你在串口助手输入任意内容,MCU 会原样回显,并加上前缀"你输入的是: "。
Keil 工程怎么建?一步步带你走通流程
现在回到 Keil uVision,教你从零创建一个可用的工程。
第一步:新建项目
- 打开 Keil uVision
- Project → New uVision Project
- 保存路径不要有中文
- 选择芯片型号:
STM32F103C8
✅ 提示:Keil 会自动加载对应的启动文件(startup_stm32f10x_md.s)
第二步:添加源文件
你需要把以下文件加入项目:
system_stm32f10x.c:系统时钟初始化startup_stm32f10x_md.s:启动汇编(Keil 自动加)stm32f10x_usart.c,stm32f10x_gpio.c,stm32f10x_rcc.c:标准外设库源码- 你的主程序
main.c
建议建立如下分组:
Project ├── User ← main.c ├── Drivers ← stm32f10x_xx.c ├── CMSIS ← core_cm3.c, startup file └── Config ← system_stm32f10x.c第三步:配置编译选项
右键“Target” → Options for Target:
【Output】标签页
- ✔ Generate HEX File:方便后期使用下载工具
【Debug】标签页
- Use ST-Link Debugger:选择仿真器
- Settings → Connect: SWD → Max Clock 设为 4MHz(稳定优先)
【C/C++】标签页
- Define:
USE_STDPERIPH_DRIVER, STM32F10X_MD - Include Paths:
.\Inc .\Libraries\CMSIS .\Libraries\StdPeriph_Driver\inc
【User】标签页(可选)
- 在 After Build/Rebuild 中勾选 “Run #1”
- 输入:
"C:\Program Files\STMicroelectronics\Software\Flash Loader Demonstrator\FlashLoader.exe" $L@H
这样编译后可自动调用 STM32 Flash Loader 工具下载程序。
硬件怎么连?一根 USB-TTL 搞定一切
你需要一块USB 转 TTL 模块(常用 CH340G 或 CP2102 芯片):
| 模块引脚 | 接 STM32 |
|---|---|
| GND | GND |
| TXD | PA10 (RX) |
| RXD | PA9 (TX) |
| VCC | 3.3V(慎接5V!) |
⚠️ 注意事项:
-不要同时接 ST-Link 和 USB-TTL 的 VCC,可能导致电源冲突
- 如果使用独立供电,确保共地(GND 连在一起)
- PA9/PA10 不要接其他负载,避免影响通信电平
PC 端使用串口助手(如 XCOM、SSCOM、PuTTY)打开对应 COM 口,波特率设为 115200,8-N-1。
上电后你应该立刻看到:
STM32 UART通信已启动!接着输入任何内容,比如hello+ 回车,会收到:
你输入的是: hello恭喜,你已经打通了第一道通信关卡!
遇到问题怎么办?这些“坑”我都替你踩过了
别慌,下面是高频故障排查清单:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 完全无输出 | 未开启 RCC 时钟 | 检查RCC_APB2PeriphClockCmd是否包含 USART1 和 GPIOA |
| 输出乱码 | 波特率不一致 | 双方确认都是 115200,且主频配置正确 |
| 下载失败 | ST-Link 驱动未装 / 接线松 | 更新驱动,检查 SWCLK/SWDIO 是否接触良好 |
| 收不到数据 | RX 引脚配置错误 | 确保 PA10 是IN_FLOATING,不是模拟输入或其他模式 |
| 程序跑飞 | 堆栈溢出或中断未处理 | 增大栈大小,添加HardFault_Handler打印异常 |
| 串口助手收不到回车 | 缺少\r\n | Windows 串口通常需要\r\n才能换行显示 |
还有一个隐藏陷阱:串口助手默认发送的是 ASCII 字符,但可能带 CR/LF。如果你只判断\n,记得在助手里勾选“发送新行”。
进阶思路:下一步你可以做什么?
你现在掌握的是“轮询 + 缓冲区”的基础模型。接下来可以尝试:
1. 改用中断接收
减少 CPU 占用,提升响应速度:
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_EnableIRQ(USART1_IRQn);在USART1_IRQHandler()中处理接收事件。
2. 重定向printf
让 C 标准库的printf直接输出到串口:
int fputc(int ch, FILE *f) { USART_SendChar(USART1, ch); return ch; }之后就可以直接printf("ADC Value: %d\r\n", value);调试了。
3. 结合传感器做数据上报
比如读取 DHT11 温湿度,通过串口传给 PC 显示。
4. 对接 ESP8266/WiFi 模块
用 AT 指令控制模块联网,实现远程监控。
写在最后:UART 是起点,不是终点
当你第一次看到自己的 STM32 在串口助手中说出“Hello World”,那种成就感,就像第一次点亮 LED 一样纯粹。
但请记住:UART 不是用来炫技的,而是解决问题的工具。
它是你调试系统的耳朵和嘴巴,是你与设备对话的语言。掌握了它,你就拿到了进入嵌入式世界的第一把钥匙。
未来你可以用 HAL 库、CubeMX 快速生成代码,也可以玩 FreeRTOS 多任务调度,甚至用 DMA 实现零拷贝通信。
但无论技术如何演进,理解底层原理的人,永远拥有更强的掌控力。
如果你正在学习 STM32,不妨现在就打开 Keil,照着这篇文章动手试一遍。
只有亲手让代码跑起来,才算真正学会。
有任何问题,欢迎留言讨论。
我们一起,把每一个“不说话”的板子,变成会思考的智能终端。
🔧热词索引:Keil、STM32、UART、USART、串口通信、波特率、GPIO、RCC、中断、DMA、Hex文件、SWD、标准外设库、嵌入式系统、调试器