基于STM32F103的全双工串口聊天室实战:从协议设计到中断优化

在嵌入式开发领域,串口通信堪称最基础却又最实用的技能之一。许多工程师的职业生涯都是从点亮第一个LED、完成第一次串口通信开始的。但你是否想过,这个看似简单的UART接口,其实蕴含着嵌入式系统设计的精髓?本文将带你用STM32F103的USART1构建一个真正的全双工聊天室系统,不仅实现基本的收发功能,更深入探讨如何设计通信协议、优化中断处理,以及构建稳定的双向数据通道。

1. 硬件架构与初始化配置

1.1 STM32F103的USART外设特性

STM32F103系列微控制器内置多个USART(通用同步异步收发器)接口,其中USART1挂载在APB2总线上,最高支持4.5Mbps的通信速率。与许多初学者想象的不同,USART并非简单的"发送引脚+接收引脚",而是一个完整的通信子系统:

  • 双缓冲机制:独立的发送和接收数据寄存器
  • 灵活的中断源:包括发送完成、接收就绪、校验错误等
  • 可编程波特率:支持从几百bps到数Mbps的速率设置
  • 全双工操作:真正意义上的同时收发能力
// 典型USART初始化结构体
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;

1.2 硬件连接与初始化代码

USART1默认使用PA9(TX)和PA10(RX)引脚,与PC连接通常需要USB转TTL模块。注意避免以下常见错误:

  1. 混淆TTL电平(3.3V)与RS232电平(±12V)
  2. 忘记共地连接
  3. 波特率不匹配导致乱码

以下是经过优化的初始化代码,增加了错误处理和状态反馈:

void USART1_Init(uint32_t baudrate) {
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  USART_InitTypeDef USART_InitStruct = {0};
  NVIC_InitTypeDef NVIC_InitStruct = {0};
  
  // 时钟使能
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
  
  // 配置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);
  
  // USART参数配置
  USART_InitStruct.USART_BaudRate = baudrate;
  USART_InitStruct.USART_WordLength = USART_WordLength_8b;
  USART_InitStruct.USART_StopBits = USART_StopBits_1;
  USART_InitStruct.USART_Parity = USART_Parity_No;
  USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  USART_Init(USART1, &USART_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);
  
  // 使能接收中断
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
  
  // 使能USART
  USART_Cmd(USART1, ENABLE);
}

提示:实际项目中建议添加USART_GetFlagStatus检查初始化状态,并实现重试机制

2. 全双工通信的核心实现

2.1 中断驱动架构设计

真正的全双工通信需要同时处理发送和接收事件而不互相阻塞。我们采用"生产者-消费者"模型:

  • 接收端:硬件中断触发数据接收,存入环形缓冲区
  • 发送端:主程序从缓冲区取出数据发送
  • 状态标志:使用原子操作保证线程安全
#define BUF_SIZE 256

typedef struct {
  uint8_t buffer[BUF_SIZE];
  volatile uint16_t head;
  volatile uint16_t tail;
} RingBuffer;

RingBuffer rx_buf, tx_buf;

// 环形缓冲区操作函数
uint8_t RB_Read(RingBuffer *rb) {
  uint8_t data = rb->buffer[rb->tail];
  rb->tail = (rb->tail + 1) % BUF_SIZE;
  return data;
}

void RB_Write(RingBuffer *rb, uint8_t data) {
  rb->buffer[rb->head] = data;
  rb->head = (rb->head + 1) % BUF_SIZE;
}

uint16_t RB_Available(RingBuffer *rb) {
  return (rb->head - rb->tail) % BUF_SIZE;
}

2.2 中断服务程序优化

传统的中断处理方式常因处理时间过长导致数据丢失。我们采用分层中断策略:

  1. 顶层ISR:仅做最必要的操作(保存数据)
  2. 底层处理:在主循环中完成协议解析等耗时操作
void USART1_IRQHandler(void) {
  // 接收中断处理
  if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
    uint8_t data = USART_ReceiveData(USART1);
    if(RB_Available(&rx_buf) < BUF_SIZE - 1) {
      RB_Write(&rx_buf, data);
    } else {
      // 缓冲区溢出处理
    }
    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
  }
  
  // 发送完成中断处理
  if(USART_GetITStatus(USART1, USART_IT_TXE) != RESET) {
    if(RB_Available(&tx_buf) > 0) {
      USART_SendData(USART1, RB_Read(&tx_buf));
    } else {
      USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
    }
    USART_ClearITPendingBit(USART1, USART_IT_TXE);
  }
}

2.3 非阻塞式发送实现

与阻塞式发送相比,非阻塞方式显著提高系统响应速度:

void USART_SendString(const char *str) {
  __disable_irq(); // 临界区保护
  while(*str) {
    while(RB_Available(&tx_buf) == BUF_SIZE - 1) {
      __enable_irq();
      // 可在此添加超时处理
      __disable_irq();
    }
    RB_Write(&tx_buf, *str++);
  }
  USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
  __enable_irq();
}

3. 通信协议设计与实现

3.1 帧结构设计

可靠的通信需要定义明确的协议帧。我们设计一个简单高效的文本协议:

字段 长度 说明
STX 1字节 起始符(0x02)
LEN 1字节 数据长度(0-255)
DATA N字节 实际数据
ETX 1字节 结束符(0x03)
CRC 1字节 校验和
typedef struct {
  uint8_t stx;
  uint8_t len;
  uint8_t data[256];
  uint8_t etx;
  uint8_t crc;
} UART_Frame;

uint8_t Calculate_CRC(const UART_Frame *frame) {
  uint8_t crc = 0;
  crc ^= frame->stx;
  crc ^= frame->len;
  for(int i=0; i<frame->len; i++) {
    crc ^= frame->data[i];
  }
  crc ^= frame->etx;
  return crc;
}

3.2 协议状态机实现

使用状态机解析协议帧,提高鲁棒性:

typedef enum {
  STATE_IDLE,
  STATE_HEADER,
  STATE_LENGTH,
  STATE_DATA,
  STATE_END,
  STATE_CRC
} ParserState;

void Parse_Protocol(uint8_t byte) {
  static ParserState state = STATE_IDLE;
  static UART_Frame frame;
  static uint8_t data_index = 0;
  
  switch(state) {
    case STATE_IDLE:
      if(byte == 0x02) {
        state = STATE_LENGTH;
      }
      break;
      
    case STATE_LENGTH:
      frame.len = byte;
      if(frame.len > 0) {
        state = STATE_DATA;
        data_index = 0;
      } else {
        state = STATE_END;
      }
      break;
      
    case STATE_DATA:
      frame.data[data_index++] = byte;
      if(data_index >= frame.len) {
        state = STATE_END;
      }
      break;
      
    case STATE_END:
      if(byte == 0x03) {
        state = STATE_CRC;
      } else {
        state = STATE_IDLE; // 错误恢复
      }
      break;
      
    case STATE_CRC:
      if(byte == Calculate_CRC(&frame)) {
        Process_Frame(&frame); // 有效帧处理
      }
      state = STATE_IDLE;
      break;
  }
}

4. 上位机交互与调试技巧

4.1 串口调试助手高级用法

常用串口工具如Tera Term、Putty等支持以下高级功能:

  • 十六进制显示:排查二进制协议
  • 时间戳:分析通信时序
  • 数据记录:长期运行测试
  • 脚本功能:自动化测试

推荐配置参数:

  • 波特率:115200
  • 数据位:8
  • 停止位:1
  • 校验位:无
  • 流控:无

4.2 Python上位机实现

使用PySerial库快速构建测试工具:

import serial
import threading

class ChatTerminal:
    def __init__(self, port='COM3', baudrate=115200):
        self.ser = serial.Serial(port, baudrate, timeout=1)
        self.running = True
        
    def receive_thread(self):
        while self.running:
            data = self.ser.read(self.ser.in_waiting or 1)
            if data:
                print(f"Received: {data.decode('ascii', errors='replace')}", end='')
    
    def send_thread(self):
        while self.running:
            message = input("> ")
            self.ser.write((message + '\r\n').encode('ascii'))
    
    def start(self):
        threading.Thread(target=self.receive_thread, daemon=True).start()
        self.send_thread()
    
    def close(self):
        self.running = False
        self.ser.close()

if __name__ == "__main__":
    terminal = ChatTerminal()
    try:
        terminal.start()
    except KeyboardInterrupt:
        terminal.close()

4.3 常见问题排查表

现象 可能原因 解决方案
无任何通信 接线错误 检查TX/RX交叉连接,确认共地
收到乱码 波特率不匹配 核对双方波特率设置
数据截断 缓冲区溢出 增大缓冲区或提高处理速度
偶发丢包 中断冲突 调整中断优先级,减少ISR处理时间
CRC错误 电磁干扰 检查接地,缩短连线,增加校验强度

5. 性能优化与扩展思路

5.1 DMA加速数据传输

对于高速通信场景,使用DMA可以大幅降低CPU负载:

void USART1_DMA_Init(void) {
  DMA_InitTypeDef DMA_InitStructure;
  
  // 使能DMA时钟
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
  
  // 配置DMA发送通道
  DMA_DeInit(DMA1_Channel4);
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
  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);
  
  // 配置DMA接收通道
  DMA_DeInit(DMA1_Channel5);
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buffer;
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
  DMA_InitStructure.DMA_BufferSize = BUF_SIZE;
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  DMA_Init(DMA1_Channel5, &DMA_InitStructure);
  
  // 使能USART DMA请求
  USART_DMACmd(USART1, USART_DMAReq_Tx | USART_DMAReq_Rx, ENABLE);
}

5.2 多线程安全策略

在RTOS环境中,需要额外的保护措施:

// FreeRTOS示例
void USART_SendTask(void *pvParameters) {
  while(1) {
    xSemaphoreTake(uart_mutex, portMAX_DELAY);
    if(RB_Available(&tx_buf) > 0) {
      USART_SendData(USART1, RB_Read(&tx_buf));
    }
    xSemaphoreGive(uart_mutex);
    vTaskDelay(1);
  }
}

void USART_ReceiveTask(void *pvParameters) {
  while(1) {
    xSemaphoreTake(uart_mutex, portMAX_DELAY);
    if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE)) {
      uint8_t data = USART_ReceiveData(USART1);
      RB_Write(&rx_buf, data);
    }
    xSemaphoreGive(uart_mutex);
    vTaskDelay(1);
  }
}

5.3 无线扩展方案

通过蓝牙或WiFi模块实现无线串口:

  • HC-05蓝牙模块:AT指令配置,透明传输模式
  • ESP8266:串口转WiFi,支持TCP/IP协议栈
  • nRF24L01+:2.4GHz无线通信,适合点对点应用
// HC-05蓝牙模块初始化
void BT_Init(void) {
  USART_SendString("AT+NAME=STM32-Chat\r\n");
  USART_SendString("AT+PSWD=1234\r\n");
  USART_SendString("AT+UART=115200,0,0\r\n");
}

在实际项目中,我发现DMA结合环形缓冲区的方案能显著提升系统稳定性。特别是在处理突发数据时,传统的查询方式往往会导致数据丢失,而精心设计的中断+DMA架构可以轻松应对高速数据流。另一个实用技巧是在协议设计中加入心跳包机制,这对于检测连接状态异常有效。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐