STM32-USART串口通信:从协议解析到实战应用

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(&currentPacket);
            }
            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的过程中,我遇到过各种问题,这里分享一些常见问题的解决方法。

波特率不匹配是最常见的问题,症状是接收到的数据乱码。解决方法:

  1. 确认双方波特率设置一致
  2. 检查时钟配置是否正确
  3. 使用示波器测量实际波特率

数据丢失通常发生在高速传输时,解决方法:

  1. 使用硬件流控制
  2. 增加接收缓冲区大小
  3. 提高中断优先级
  4. 使用DMA传输

电磁干扰问题在工业环境中很常见,解决方法:

  1. 使用屏蔽线缆
  2. 增加滤波电容
  3. 使用差分信号(如RS485)
  4. 降低波特率

性能优化方面,我有几个实用建议:

  1. 使用DMA减少CPU开销
  2. 合理设置缓冲区大小
  3. 使用空闲中断检测帧结束
  4. 优化数据处理算法
// 使能空闲中断
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);
    }
}

调试技巧也很重要:

  1. 使用printf重定向方便调试
  2. 添加状态指示LED
  3. 使用逻辑分析仪查看波形
  4. 实现简单的调试协议
// 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
}

记得在最终产品中关闭调试输出以减少代码大小和提高性能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值