用STM32F103的USART1和PC串口助手玩‘聊天室’:一个项目理解全双工通信
基于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模块。注意避免以下常见错误:
- 混淆TTL电平(3.3V)与RS232电平(±12V)
- 忘记共地连接
- 波特率不匹配导致乱码
以下是经过优化的初始化代码,增加了错误处理和状态反馈:
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 中断服务程序优化
传统的中断处理方式常因处理时间过长导致数据丢失。我们采用分层中断策略:
- 顶层ISR:仅做最必要的操作(保存数据)
- 底层处理:在主循环中完成协议解析等耗时操作
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架构可以轻松应对高速数据流。另一个实用技巧是在协议设计中加入心跳包机制,这对于检测连接状态异常有效。
更多推荐
所有评论(0)