1. 项目概述
作为一名嵌入式开发工程师,我经常需要在STM32平台上实现串口通信功能。今天我想分享的是关于STM32F103系列芯片串口通信的详细实现方法,特别是串口初始化结构体和固件库的使用技巧。这些内容是我在实际项目中积累的经验总结,希望能帮助到正在学习STM32开发的同行们。
STM32的串口通信是嵌入式系统中最基础也最重要的外设之一,它承担着与上位机通信、设备调试、模块控制等多种功能。在STM32F103系列中,USART(通用同步异步收发器)模块提供了灵活可靠的串行通信能力。掌握好串口初始化的方法,是每个STM32开发者必须跨过的门槛。
2. 串口通信基础概念
2.1 串口通信基本原理
串口通信是一种通过单根数据线按位顺序传输数据的通信方式。在STM32中,USART模块支持全双工通信,即可以同时进行数据的发送和接收。通信的基本参数包括:
- 波特率:数据传输速率,常见的有9600、115200等
- 数据位:通常为8位或9位
- 停止位:1位或2位
- 校验位:可选无校验、奇校验或偶校验
在STM32F103中,USART模块具有以下特点:
- 支持同步和异步通信
- 可编程的波特率发生器
- 独立的发送和接收数据寄存器
- 支持硬件流控制(CTS/RTS)
- 多种中断源和DMA请求
2.2 STM32固件库架构
STM32标准外设库(Standard Peripheral Library)是ST官方提供的一套用于简化外设编程的软件接口。它采用分层架构:
- 寄存器层:直接操作硬件寄存器
- 外设驱动层:提供外设初始化、配置和控制函数
- 应用层:用户程序
使用固件库的好处是:
- 屏蔽底层硬件细节
- 提高代码可移植性
- 减少开发时间
- 降低出错概率
3. 串口初始化结构体详解
3.1 USART_InitTypeDef结构体解析
在STM32固件库中,串口的初始化主要通过USART_InitTypeDef结构体来完成。这个结构体包含了配置串口所需的所有参数:
c复制typedef struct {
uint32_t USART_BaudRate; // 波特率
uint16_t USART_WordLength; // 数据位长度
uint16_t USART_StopBits; // 停止位
uint16_t USART_Parity; // 校验位
uint16_t USART_Mode; // 收发模式
uint16_t USART_HardwareFlowControl; // 硬件流控制
} USART_InitTypeDef;
让我们详细看看每个成员的作用:
-
USART_BaudRate:
- 设置通信波特率,如9600、115200等
- 实际波特率由系统时钟和USARTDIV值决定
- 计算公式:波特率 = fCK/(16 × USARTDIV)
-
USART_WordLength:
- 可选8位或9位数据长度
- 定义:USART_WordLength_8b 或 USART_WordLength_9b
-
USART_StopBits:
- 设置停止位数量
- 可选:0.5、1、1.5或2个停止位
- 常用USART_StopBits_1
-
USART_Parity:
- 设置校验方式
- 可选:USART_Parity_No(无校验)
- USART_Parity_Even(偶校验)
- USART_Parity_Odd(奇校验)
-
USART_Mode:
- 设置收发模式
- 可以单独或同时启用发送和接收
- 使用位或操作组合:USART_Mode_Rx | USART_Mode_Tx
-
USART_HardwareFlowControl:
- 硬件流控制设置
- 可选:USART_HardwareFlowControl_None(无)
- USART_HardwareFlowControl_RTS
- USART_HardwareFlowControl_CTS
- USART_HardwareFlowControl_RTS_CTS
3.2 初始化结构体的典型配置
下面是一个常见的串口初始化配置示例:
c复制USART_InitTypeDef USART_InitStructure;
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_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_Init(USART1, &USART_InitStructure);
这个配置设置了115200波特率、8位数据位、1位停止位、无校验、启用收发功能、无硬件流控制的串口通信参数。
4. 固件库串口操作函数详解
4.1 串口初始化流程
完整的串口初始化通常包括以下步骤:
- 使能时钟:必须先使能USART和对应GPIO端口的时钟
- GPIO配置:设置TX为推挽输出,RX为浮空输入
- USART初始化:使用USART_Init()函数
- 使能USART:调用USART_Cmd()
示例代码:
c复制void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置GPIO
// USART1_TX -> PA9
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);
// USART1_RX -> PA10
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. USART初始化
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_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_Init(USART1, &USART_InitStructure);
// 4. 使能USART
USART_Cmd(USART1, ENABLE);
}
4.2 常用串口操作函数
STM32固件库提供了丰富的串口操作函数,下面介绍几个最常用的:
-
数据发送函数:
c复制void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);- 功能:发送一个数据
- 参数:USARTx指定USART外设,Data是要发送的数据(8位或9位)
-
数据接收函数:
c复制uint16_t USART_ReceiveData(USART_TypeDef* USARTx);- 功能:接收一个数据
- 返回值:接收到的数据
-
状态标志函数:
c复制FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);- 功能:获取指定标志位的状态
- 常用标志:
- USART_FLAG_TXE:发送数据寄存器空
- USART_FLAG_RXNE:接收数据寄存器非空
- USART_FLAG_TC:发送完成
-
中断配置函数:
c复制void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);- 功能:使能或禁用指定的USART中断
- 常用中断源:
- USART_IT_RXNE:接收中断
- USART_IT_TXE:发送中断
- USART_IT_TC:发送完成中断
4.3 串口数据收发示例
查询方式发送数据:
c复制void USART_SendString(USART_TypeDef* USARTx, char *str)
{
while(*str)
{
// 等待发送数据寄存器空
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
// 发送一个字符
USART_SendData(USARTx, *str++);
}
// 等待发送完成
while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET);
}
中断方式接收数据:
c复制// 在初始化代码中添加中断配置
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_EnableIRQ(USART1_IRQn);
// 中断服务函数
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 读取接收到的数据
uint8_t data = USART_ReceiveData(USART1);
// 处理接收到的数据
// ...
// 清除中断标志
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
5. 串口初始化常见问题与解决方案
5.1 波特率不匹配问题
现象:通信双方能够建立连接,但接收到的数据乱码。
原因分析:
- 波特率计算错误
- 系统时钟配置不正确
- 时钟源选择错误
解决方案:
- 确认系统时钟频率是否正确
- 检查USART时钟源(APB1或APB2)
- 使用示波器测量实际波特率
- 使用以下公式验证波特率设置:
c复制// 计算实际波特率
float realBaudRate = (float)SystemCoreClock / (16 * (USART1->BRR & 0xFFF));
5.2 数据接收不完整问题
现象:接收数据丢失或数据包不完整。
原因分析:
- 接收缓冲区溢出
- 中断优先级设置不当
- 数据处理速度跟不上接收速度
解决方案:
- 增加接收缓冲区大小
- 提高接收中断优先级
- 使用DMA进行数据接收
- 实现硬件流控制(RTS/CTS)
5.3 硬件连接问题
现象:完全无法通信。
原因分析:
- TX/RX线接反
- 地线未连接
- 电平不匹配(如3.3V与5V设备直接连接)
解决方案:
- 检查TX/RX连接是否正确
- 确保通信双方共地
- 使用电平转换芯片(如MAX3232)进行电平转换
- 使用万用表测量信号线是否正常
6. 串口初始化高级技巧
6.1 使用DMA进行高效数据传输
对于高速或大数据量传输,使用DMA可以大幅降低CPU负载。配置步骤:
- 使能DMA时钟
- 配置DMA通道
- 配置USART的DMA请求
- 使能DMA传输
示例代码:
c复制void USART1_DMA_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置DMA发送通道
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SendBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = BufferSize;
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_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
// 使能USART的DMA发送请求
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
// 使能DMA通道
DMA_Cmd(DMA1_Channel4, ENABLE);
}
6.2 多串口管理技巧
在需要同时使用多个串口的项目中,可以采用以下策略:
- 统一接口设计:为所有串口提供一致的API接口
- 使用结构体封装串口参数:
c复制typedef struct { USART_TypeDef* USARTx; uint32_t baudrate; uint8_t *rxBuffer; uint16_t bufferSize; // 其他参数... } UART_HandleTypeDef; - 实现环形缓冲区管理接收数据
- 使用状态机处理协议解析
6.3 低功耗模式下的串口使用
在低功耗应用中,需要注意:
- 在进入低功耗模式前,确保所有数据传输完成
- 使用串口唤醒功能(如USART的IDLE中断)
- 合理配置串口时钟源以降低功耗
- 在唤醒后重新初始化串口(某些低功耗模式会复位外设)
7. 串口调试技巧与工具
7.1 常用调试工具
- 逻辑分析仪:用于观察串口信号波形,验证波特率
- 串口调试助手:如SecureCRT、Putty、Tera Term等
- 示波器:测量信号质量,检查噪声和干扰
- STM32 ST-LINK Utility:用于查看寄存器状态
7.2 调试技巧
- 回环测试:将TX和RX短接,测试自发自收
- 分步验证:
- 先验证GPIO配置是否正确
- 再验证USART初始化是否正确
- 最后验证数据传输
- 打印调试信息:在关键位置通过串口输出状态信息
- 使用断言:在代码中添加断言检查关键条件
7.3 性能优化建议
- 对于高速通信,考虑使用DMA
- 减少中断服务程序中的处理时间
- 使用硬件流控制防止数据丢失
- 合理设置中断优先级,避免被其他中断阻塞
8. 实际项目中的应用案例
8.1 与PC通信的调试接口
在许多嵌入式项目中,我们使用串口作为与PC通信的调试接口。实现方案:
- 重定向printf到串口:
c复制int fputc(int ch, FILE *f) { USART_SendData(DEBUG_USART, (uint8_t)ch); while(USART_GetFlagStatus(DEBUG_USART, USART_FLAG_TXE) == RESET); return ch; } - 实现简单的命令行接口
- 添加调试信息分级输出功能
8.2 与无线模块通信
许多无线模块(如蓝牙、Wi-Fi、LoRa)都使用串口作为通信接口。实现要点:
- 根据模块要求正确配置串口参数
- 实现稳定的AT指令解析器
- 添加超时重传机制
- 处理模块的唤醒和休眠序列
8.3 工业设备通信协议实现
在工业应用中,常用Modbus RTU等基于串口的通信协议。实现注意事项:
- 严格遵循协议规定的时序要求
- 正确处理3.5个字符的帧间隔
- 实现CRC校验
- 处理异常情况和超时
9. 从标准库过渡到HAL库
虽然标准外设库(SPL)已经非常成熟,但ST现在主推HAL库。了解HAL库中的串口实现有助于项目迁移:
9.1 HAL库串口初始化差异
HAL库使用不同的结构体和函数:
c复制UART_HandleTypeDef huart1;
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
9.2 HAL库的优势
- 更好的跨系列兼容性
- 更完善的错误处理机制
- 支持更复杂的使用场景
- 更活跃的维护和更新
9.3 迁移注意事项
- 函数命名和参数变化
- 初始化流程差异
- 中断处理方式不同
- DMA配置变化
10. 串口通信的未来发展
虽然串口是一种古老的通信方式,但在嵌入式领域仍然占据重要地位。未来的发展趋势包括:
- 更高的通信速率
- 更低的功耗设计
- 更好的错误检测和纠正机制
- 与新型接口(如USB、以太网)的桥接
在实际项目中,我发现合理使用串口可以解决80%以上的通信需求。掌握好串口初始化和使用的技巧,是每个嵌入式工程师的基本功。
