1. STM32串口通信实战指南
作为一名嵌入式开发工程师,我深知串口通信在STM32项目开发中的重要性。无论是调试信息输出、设备间数据交互,还是与上位机通信,串口都是最基础也是最常用的外设之一。本文将基于STM32F103系列MCU,详细讲解USART外设的配置和使用方法,涵盖库函数和寄存器两种实现方式。
在实际项目中,我发现很多初学者虽然能照着例程让串口跑起来,但一旦遇到问题就束手无策。因此,本文不仅会介绍标准配置流程,还会深入解析底层原理,分享我在实际项目中积累的经验和教训。
2. 硬件基础与连接
2.1 STM32F103串口资源
STM32F103系列微控制器提供了丰富的串口资源,具体配置如下:
| 串口型号 | 挂载总线 | 时钟频率 | TX引脚 | RX引脚 | 主要特性 |
|---|---|---|---|---|---|
| USART1 | APB2 | 72MHz | PA9 | PA10 | 支持同步/异步模式,中断和DMA |
| USART2 | APB1 | 36MHz | PA2/PD5 | PA3/PD6 | 支持中断和DMA |
| USART3 | APB1 | 36MHz | PB10/PC10 | PB11/PC11 | 支持中断和DMA |
选型建议:对于与PC通信的场景,推荐使用USART1,原因有三:
- 挂载在APB2总线,时钟频率更高(72MHz)
- 波特率计算更精确,通信更稳定
- 引脚固定为PA9/PA10,无需考虑重映射
2.2 硬件连接方案
与PC通信需要USB转TTL模块作为桥梁,常见的CH340G模块连接方式如下:
code复制STM32F103 USB-TTL模块
PA9(TX) ------> RX
PA10(RX) ------> TX
GND ------> GND
关键注意事项:
- TX和RX必须交叉连接,这是最容易犯的错误
- 必须连接GND,否则通信不稳定
- 推荐使用短线连接(<10cm),长线容易引入干扰
- 避免靠近电机、继电器等干扰源
3. 串口初始化配置
3.1 库函数版本实现
c复制#include "stm32f10x.h"
void USART1_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置GPIO
// TX引脚(PA9)配置为复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// RX引脚(PA10)配置为浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置串口参数
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStruct);
// 4. 使能串口
USART_Cmd(USART1, ENABLE);
}
3.2 寄存器版本实现
c复制#include "stm32f10x.h"
void USART1_Init(void) {
// 1. 使能时钟
RCC->APB2ENR |= (1<<14) | (1<<2); // USART1和GPIOA时钟
// 2. 配置GPIO
// PA9(TX)复用推挽输出
GPIOA->CRH &= ~(0x0F<<4);
GPIOA->CRH |= (0x0B<<4);
// PA10(RX)浮空输入
GPIOA->CRH &= ~(0x0F<<8);
GPIOA->CRH |= (0x04<<8);
// 3. 配置波特率(115200@72MHz)
USART1->BRR = 0x271; // 39.0625 -> 0x271
// 4. 配置数据格式(8N1)
USART1->CR1 &= ~(1<<12); // M=0(8位)
USART1->CR1 &= ~(1<<10); // PCE=0(无校验)
USART1->CR2 &= ~(3<<12); // STOP=00(1位停止位)
// 5. 使能收发
USART1->CR1 |= (1<<2) | (1<<3); // RE和TE
// 6. 使能USART1
USART1->CR1 |= (1<<13); // UE=1
}
3.3 关键配置解析
-
波特率计算:
USART1挂载在APB2总线(72MHz),波特率计算公式:code复制USARTDIV = 72000000 / (16 * 115200) ≈ 39.0625 整数部分:39 → 0x27 小数部分:0.0625*16=1 → 0x1 组合后:0x271 -
GPIO模式选择:
- TX引脚必须配置为复用推挽输出(GPIO_Mode_AF_PP)
- RX引脚推荐浮空输入(GPIO_Mode_IN_FLOATING)
-
时钟使能:
必须同时使能USART1和GPIOA的时钟,USART1属于APB2外设
4. 串口发送功能实现
4.1 单字节发送
c复制// 库函数版本
void USART1_SendByte(uint8_t data) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, data);
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
// 寄存器版本
void USART1_SendByte(uint8_t data) {
while(!(USART1->SR & (1<<7))); // 等待TXE置位
USART1->DR = data;
while(!(USART1->SR & (1<<6))); // 等待TC置位
}
关键点说明:
- TXE(发送数据寄存器空)标志表示可以写入新数据
- TC(发送完成)标志表示所有数据(包括停止位)已发送完毕
- 实际项目中,通常只需等待TXE,除非需要确保数据完全发送
4.2 字符串发送
c复制void USART1_SendString(char *str) {
while(*str != '\0') {
USART1_SendByte(*str++);
}
// 可选:添加回车换行
USART1_SendByte('\r');
USART1_SendByte('\n');
}
4.3 printf重定向
c复制#include <stdio.h>
int fputc(int ch, FILE *f) {
USART1_SendByte((uint8_t)ch);
return ch;
}
// 在main函数中使用
int main(void) {
USART1_Init();
printf("System Start!\r\n");
printf("Temperature: %.1fC\r\n", 25.5f);
while(1);
}
工程配置:
- 在Keil中勾选"Use MicroLIB"
- 包含stdio.h头文件
- 实现fputc函数
常见问题:
- 如果printf输出乱码,检查是否启用MicroLIB
- 浮点数打印需要确保链接了相应的库
- printf会显著增加代码体积,在资源紧张的项目中慎用
5. 串口接收功能实现
5.1 查询方式接收
c复制uint8_t USART1_ReceiveByte(void) {
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
return (uint8_t)USART_ReceiveData(USART1);
}
特点:
- 简单易用
- 会阻塞程序运行
- 适合简单场景或调试使用
5.2 中断方式接收
c复制#define RX_BUF_SIZE 128
uint8_t rx_buf[RX_BUF_SIZE];
uint16_t rx_index = 0;
void USART1_Init_IRQ(void) {
// ... 前面的初始化代码相同
// 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
uint8_t data = USART_ReceiveData(USART1);
if(rx_index < RX_BUF_SIZE) {
rx_buf[rx_index++] = data;
}
// 可选:回显接收到的数据
USART_SendData(USART1, data);
}
}
中断接收优势:
- 非阻塞式,不影响主程序运行
- 实时响应,适合高速数据接收
- 结合缓冲区可实现数据帧处理
进阶技巧:
- 使用环形缓冲区提高存储效率
- 添加帧头帧尾检测实现协议解析
- 结合DMA实现大数据量接收
6. 常见问题与解决方案
6.1 通信失败排查步骤
-
检查硬件连接
- 确认TX-RX交叉连接
- 确认共地
- 检查USB-TTL模块驱动是否正常
-
验证配置参数
- 波特率是否一致(115200)
- 数据格式是否匹配(8N1)
- 时钟配置是否正确(USART1使用APB2时钟)
-
信号测量
- 用示波器测量TX引脚是否有信号输出
- 检查信号电平是否符合标准(3.3V)
6.2 典型问题解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无任何输出 | 1. 接线错误 2. 时钟未使能 3. GPIO配置错误 |
1. 检查TX/RX连接 2. 确认时钟使能 3. 检查TX引脚模式 |
| 接收乱码 | 1. 波特率不匹配 2. 时钟源错误 3. 干扰严重 |
1. 核对波特率计算 2. 检查时钟树配置 3. 缩短连线,添加滤波电容 |
| 数据丢失 | 1. 缓冲区溢出 2. 中断优先级低 3. 处理时间过长 |
1. 增大缓冲区 2. 调整中断优先级 3. 优化中断服务函数 |
| printf无法使用 | 1. 未启用MicroLIB 2. 未重定向fputc 3. 链接库缺失 |
1. 勾选MicroLIB 2. 实现fputc函数 3. 添加相应库文件 |
7. 进阶应用与优化
7.1 DMA传输优化
对于高速数据传输场景,可以使用DMA来减轻CPU负担:
c复制// DMA发送配置示例
void USART1_DMA_Send_Init(void) {
DMA_InitTypeDef DMA_InitStruct;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStruct.DMA_BufferSize = 0;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStruct);
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
}
7.2 自定义通信协议
实现简单的帧协议:
c复制#define FRAME_HEADER 0xAA
#define FRAME_TAIL 0x55
typedef struct {
uint8_t header;
uint8_t cmd;
uint8_t len;
uint8_t data[32];
uint8_t checksum;
uint8_t tail;
} USART_Frame;
void USART1_IRQHandler(void) {
static uint8_t state = 0;
static uint8_t index = 0;
static USART_Frame frame;
if(USART_GetITStatus(USART1, USART_IT_RXNE)) {
uint8_t data = USART_ReceiveData(USART1);
switch(state) {
case 0: // 等待帧头
if(data == FRAME_HEADER) {
frame.header = data;
state = 1;
}
break;
case 1: // 接收命令字
frame.cmd = data;
state = 2;
break;
case 2: // 接收长度
frame.len = data;
index = 0;
state = 3;
break;
case 3: // 接收数据
frame.data[index++] = data;
if(index >= frame.len) {
state = 4;
}
break;
case 4: // 接收校验和
frame.checksum = data;
state = 5;
break;
case 5: // 接收帧尾
if(data == FRAME_TAIL) {
frame.tail = data;
// 处理完整帧
ProcessFrame(&frame);
}
state = 0;
break;
}
}
}
7.3 多串口协同工作
在实际项目中,经常需要同时使用多个串口:
c复制// USART1用于调试输出
void USART1_Init(void) {
// 初始化代码...
printf("USART1 Init OK\r\n");
}
// USART2连接蓝牙模块
void USART2_Init(void) {
// 初始化代码...
USART_SendString(USART2, "AT+NAME=MyDevice\r\n");
}
// USART3连接传感器
void USART3_Init(void) {
// 初始化代码...
USART_SendString(USART3, "GETDATA\r\n");
}
int main(void) {
USART1_Init();
USART2_Init();
USART3_Init();
while(1) {
// 各串口协同工作
}
}
8. 性能优化技巧
-
中断优化:
- 保持中断服务函数简洁
- 避免在中断中调用耗时函数
- 合理设置中断优先级
-
缓冲区管理:
- 使用环形缓冲区提高效率
- 双缓冲区减少数据冲突
- 动态内存分配避免浪费
-
功耗优化:
- 空闲时进入低功耗模式
- 动态调整波特率
- 按需启用串口时钟
-
错误处理:
- 添加超时机制
- 实现自动重试
- 完善的错误码系统
9. 实际项目经验分享
在最近的一个工业控制器项目中,我们使用STM32的USART1与HMI触摸屏通信,遇到了几个典型问题:
-
长距离通信不稳定:
- 现象:3米距离通信时出现数据丢失
- 解决方案:改用RS485通信,添加终端电阻
- 经验:超过1米距离建议使用RS485或CAN总线
-
多设备干扰:
- 现象:车间多个设备同时工作时通信错误率上升
- 解决方案:优化电缆屏蔽,调整波特率避开干扰频段
- 经验:工业环境要特别注意EMC设计
-
大数据量阻塞:
- 现象:上传历史数据时系统响应变慢
- 解决方案:实现分页传输,添加流控机制
- 经验:串口不适合传输大量数据,要考虑分包策略
10. 测试与验证方法
完善的测试是保证通信可靠性的关键:
-
单元测试:
- 验证单个字节收发
- 测试边界条件(最大波特率、最长数据包)
-
压力测试:
- 连续发送大量数据
- 模拟恶劣环境(电压波动、温度变化)
-
兼容性测试:
- 不同USB-TTL模块
- 不同上位机软件
- 不同电缆长度
-
长期稳定性测试:
- 72小时连续运行
- 记录错误率和恢复情况
11. 工具与资源推荐
-
开发工具:
- Keil MDK/IAR/STM32CubeIDE
- STM32CubeMX(初始化代码生成)
-
调试工具:
- 逻辑分析仪(Saleae)
- 串口调试助手(SSCOM/TeraTerm)
-
硬件设备:
- 优质USB-TTL模块(FT232/CH340)
- 示波器(观测信号质量)
- 稳压电源(稳定供电)
-
学习资源:
- STM32参考手册(RM0008)
- 标准外设库文档
- 社区论坛(STM32中文社区)
12. 总结与展望
通过本文的详细介绍,相信你已经掌握了STM32串口通信的核心技术。从基础的单字节收发,到中断处理、DMA传输,再到自定义协议实现,串口通信的应用非常灵活。
在实际项目中,建议:
- 根据需求选择合适的通信方式(查询/中断/DMA)
- 设计完善的通信协议
- 实现健壮的错误处理机制
- 进行充分的测试验证
未来可以进一步学习:
- 更高速的通信接口(SPI/I2C)
- 无线通信模块(蓝牙/WiFi)
- 工业总线协议(Modbus/CAN)
- 网络通信(TCP/IP over串口)
串口作为嵌入式系统最基础的通信接口,掌握它的原理和应用技巧,将为你的嵌入式开发之路打下坚实基础。
