1. USART串口通信基础:从硬件到协议的全面解析
大家好,我是有10年嵌入式开发经验的工程师,今天来聊聊STM32的USART串口通信。在实际项目中,串口通信是最基础也是最常用的通信方式之一,无论是调试信息输出还是设备间数据交换,都离不开它。
什么是USART? USART(Universal Synchronous/Asynchronous Receiver/Transmitter)是STM32内部集成的硬件外设,支持同步和异步两种通信模式。我们平时用的最多的是异步模式,也就是常说的UART。
硬件连接其实很简单,只需要三根线:TX(发送)、RX(接收)和GND(地线)。这里有个容易踩坑的地方:TX和RX要交叉连接!你的设备TX要接对方RX,你的RX要接对方TX。记得我刚入门时就接反过,调试了半天才发现问题。
电平标准也很重要,STM32用的是3.3V TTL电平。如果你要连接电脑,需要USB转TTL模块(如CH340、CP2102)。如果要远距离传输,可以考虑RS232或RS485电平转换。
来看一个最简单的接线示例:
STM32 USB转TTL模块
TX ----> RX
RX <---- TX
GND ---- GND
数据帧格式是串口通信的核心。一个完整的数据帧包括:
- 起始位:1位低电平
- 数据位:5-9位(常用8位)
- 校验位:0或1位(可选)
- 停止位:0.5、1、1.5或2位高电平
最常用的配置是8N1:8位数据位、无校验、1位停止位。这种格式下,每传输一个字节实际需要发送10位数据(1起始+8数据+1停止)。
2. STM32的USART硬件架构深入剖析
STM32的USART模块设计得很巧妙,理解其硬件架构能帮助我们更好地使用它。以STM32F103系列为例,通常有多个USART外设,其中USART1挂载在APB2总线(最高72MHz),其他USART挂载在APB1总线(最高36MHz)。
关键组成部分包括:
- 数据寄存器(USART_DR):实际是双缓冲结构,包含发送数据寄存器(TDR)和接收数据寄存器(RDR)
- 波特率发生器:16位分频器,支持小数分频
- 控制寄存器:配置工作模式、中断等
- 状态寄存器:查看各种状态标志
波特率计算是个重点。公式为:波特率 = fPCLK / (16 * USARTDIV) 其中USARTDIV是个16位数,高12位是整数部分,低4位是小数部分。
举个例子,如果PCLK2=72MHz,要得到115200的波特率: USARTDIV = 72000000 / (16 * 115200) = 39.0625 整数部分39(0x27),小数部分0.0625*16=1(0x1) 所以BRR寄存器值应该是0x271
实际配置时,我建议使用标准库的USART_Init函数,它会自动帮你计算这些值。但理解原理很重要,特别是在使用非标准波特率时。
3. 串口发送实战:从字节到字符串的完整实现
现在我们来实战配置串口发送功能。先初始化GPIO和USART外设:
void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// 配置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);
// 配置RX引脚(PA10)为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置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_Tx;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_Init(USART1, &USART_InitStructure);
// 使能USART
USART_Cmd(USART1, ENABLE);
}
发送单个字节的函数很简单:
void USART1_SendByte(uint8_t data)
{
USART_SendData(USART1, data);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
这里用USART_FLAG_TXE标志判断发送数据寄存器是否为空,可以写入下一个数据。如果要等待整个帧发送完成,可以用USART_FLAG_TC标志。
发送字符串也很常用:
void USART1_SendString(char *str)
{
while(*str != '\0')
{
USART1_SendByte(*str++);
}
}
实际使用中,我经常需要发送各种格式的数据,比如数字、数组等。这里分享一个实用的发送数组函数:
void USART1_SendArray(uint8_t *array, uint16_t length)
{
for(uint16_t i = 0; i < length; i++)
{
USART1_SendByte(array[i]);
}
}
4. 串口接收与中断处理:高效数据接收方案
串口接收相比发送要复杂一些,因为需要实时响应接收到的数据。最简单的方式是轮询方式:
uint8_t USART1_ReceiveByte(void)
{
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
return USART_ReceiveData(USART1);
}
但轮询方式会阻塞程序运行,在实际项目中很少使用。更常用的方式是中断接收。
配置中断接收需要几步:
void USART1_IRQ_Init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
// 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 配置NVIC
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
中断服务函数中处理接收:
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t receivedData = USART_ReceiveData(USART1);
// 处理接收到的数据
// ...
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
在实际项目中,我通常会用环形缓冲区来缓存接收到的数据,避免在中断中处理复杂逻辑:
#define RX_BUFFER_SIZE 256
typedef struct {
uint8_t buffer[RX_BUFFER_SIZE];
uint16_t head;
uint16_t tail;
} RingBuffer;
RingBuffer rxBuffer = {0};
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 写入环形缓冲区
uint16_t next = (rxBuffer.head + 1) % RX_BUFFER_SIZE;
if(next != rxBuffer.tail) // 缓冲区未满
{
rxBuffer.buffer[rxBuffer.head] = data;
rxBuffer.head = next;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
5. 数据包解析:从原始数据到结构化信息
在实际应用中,我们很少直接传输原始字节,而是按照一定的协议格式打包数据。常见的数据包格式包括:
- 固定包头和包尾
- 长度字段
- 校验字段
- 数据内容
最简单的数据包格式可以是:包头(1字节)+ 数据长度(1字节)+ 数据内容(N字节)+ 校验和(1字节)
#pragma pack(1)
typedef struct {
uint8_t header; // 包头,如0xFF
uint8_t length; // 数据长度
uint8_t data[256]; // 数据内容
uint8_t checksum; // 校验和
} USART_Packet;
#pragma pack()
校验和计算通常使用简单的累加和或CRC校验:
uint8_t CalculateChecksum(uint8_t *data, uint8_t length)
{
uint8_t sum = 0;
for(uint8_t i = 0; i < length; i++)
{
sum += data[i];
}
return sum;
}
解析数据包可以使用状态机的方式,这是我在实际项目中常用的方法:
typedef enum {
STATE_HEADER,
STATE_LENGTH,
STATE_DATA,
STATE_CHECKSUM
} ParserState;
ParserState currentState = STATE_HEADER;
USART_Packet currentPacket;
uint8_t dataIndex = 0;
void ParseData(uint8_t data)
{
switch(currentState)
{
case STATE_HEADER:
if(data == 0xFF) // 找到包头
{
currentPacket.header = data;
currentState = STATE_LENGTH;
}
break;
case STATE_LENGTH:
currentPacket.length = data;
dataIndex = 0;
currentState = STATE_DATA;
break;
case STATE_DATA:
currentPacket.data[dataIndex++] = data;
if(dataIndex >= currentPacket.length)
{
currentState = STATE_CHECKSUM;
}
break;
case STATE_CHECKSUM:
currentPacket.checksum = data;
// 验证校验和
if(CalculateChecksum(currentPacket.data, currentPacket.length) == data)
{
// 处理完整数据包
ProcessPacket(¤tPacket);
}
currentState = STATE_HEADER;
break;
}
}
6. 高级应用:DMA传输与硬件流控制
当需要高速或大量数据传输时,直接使用CPU处理每个字节效率太低。这时就需要DMA(直接存储器访问)来帮忙。
DMA传输可以让数据直接在USART和内存之间传输,不占用CPU资源。配置USART DMA发送:
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)txBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = 0;
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);
}
硬件流控制在高速通信中很重要,可以防止数据丢失。使用RTS(请求发送)和CTS(清除发送)信号:
void USART1_HardwareFlowControl_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 配置RTS和CTS引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; // CTS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; // RTS
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置USART硬件流控制
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_RTS_CTS;
USART_Init(USART1, &USART_InitStructure);
}
7. 常见问题排查与性能优化
在多年使用USART的过程中,我遇到过各种问题,这里分享一些常见问题的解决方法。
波特率不匹配是最常见的问题,症状是接收到的数据乱码。解决方法:
- 确认双方波特率设置一致
- 检查时钟配置是否正确
- 使用示波器测量实际波特率
数据丢失通常发生在高速传输时,解决方法:
- 使用硬件流控制
- 增加接收缓冲区大小
- 提高中断优先级
- 使用DMA传输
电磁干扰问题在工业环境中很常见,解决方法:
- 使用屏蔽线缆
- 增加滤波电容
- 使用差分信号(如RS485)
- 降低波特率
性能优化方面,我有几个实用建议:
- 使用DMA减少CPU开销
- 合理设置缓冲区大小
- 使用空闲中断检测帧结束
- 优化数据处理算法
// 使能空闲中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
// 在中断中处理空闲状态
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
// 处理一帧完整数据
ProcessCompleteFrame();
USART_ClearITPendingBit(USART1, USART_IT_IDLE);
}
}
调试技巧也很重要:
- 使用printf重定向方便调试
- 添加状态指示LED
- 使用逻辑分析仪查看波形
- 实现简单的调试协议
// printf重定向
int fputc(int ch, FILE *f)
{
USART_SendData(USART1, (uint8_t)ch);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
return ch;
}
在实际项目中,我建议实现一个简单的日志系统,可以方便地开关调试信息,又不影响程序性能:
#define DEBUG_LEVEL 1
void Debug_Print(char *format, ...)
{
#if DEBUG_LEVEL > 0
char buffer[128];
va_list args;
va_start(args, format);
vsprintf(buffer, format, args);
va_end(args);
USART1_SendString(buffer);
#endif
}
记得在最终产品中关闭调试输出以减少代码大小和提高性能。

434

被折叠的 条评论
为什么被折叠?



