1. 项目背景与核心挑战
在嵌入式开发领域,串口通讯是最基础也最考验功底的环节之一。当资源受限的单片机需要同时处理多个任务时,如何确保串口数据不丢失、不重复,就成了工程师们必须面对的难题。STM32系列MCU凭借其DMA控制器和灵活的串口配置,为我们提供了硬件级的解决方案。
这次要探讨的单线半双工串口(Single-Wire Half-Duplex UART)方案,相比传统双线全双工模式,节省了一个GPIO引脚,特别适合引脚资源紧张的场景。但随之而来的问题是:如何在单条线路上实现可靠的双向数据传输?这就涉及到三个关键技术点:
- 硬件层:USART的单线模式配置
- 协议层:收发状态切换的精确时序控制
- 数据层:DMA环形缓冲区的零拷贝管理
2. 硬件架构设计解析
2.1 STM32单线串口硬件配置
在CubeMX中配置USART为单线半双工模式时,有几个关键参数需要注意:
c复制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; // 必须同时启用TX和RX
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_SWAP_INIT;
huart1.AdvancedInit.Swap = UART_ADVFEATURE_SWAP_ENABLE; // 交换TX/RX引脚
关键提示:虽然使用单线传输,但CubeMX配置中必须同时启用TX和RX模式,硬件会自动处理方向控制。
2.2 DMA环形缓冲区设计
DMA环形缓冲区的实现需要精心设计内存布局。我们采用双缓冲方案:一个用于发送,一个用于接收,通过链表结构实现无缝切换:
c复制#define BUF_SIZE 256
typedef struct {
uint8_t buffer[BUF_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
volatile uint8_t isReceiving;
} CircularBuffer;
CircularBuffer rxBuf, txBuf;
缓冲区操作的核心API包括:
c复制void buf_push(CircularBuffer *cb, uint8_t data) {
cb->buffer[cb->head] = data;
cb->head = (cb->head + 1) % BUF_SIZE;
if(cb->head == cb->tail) {
cb->tail = (cb->tail + 1) % BUF_SIZE; // 溢出处理
}
}
uint8_t buf_pop(CircularBuffer *cb) {
if(cb->head == cb->tail) return 0;
uint8_t data = cb->buffer[cb->tail];
cb->tail = (cb->tail + 1) % BUF_SIZE;
return data;
}
3. 链路层协议实现
3.1 收发状态切换时序
单线模式下最关键的挑战是收发切换的时序控制。必须确保:
- 发送完成后立即切换为接收模式
- 接收超时后自动切回发送准备状态
- 避免总线冲突
我们使用USART的TC(传输完成)和IDLE(线路空闲)中断来实现状态机:
c复制void USART1_IRQHandler(void) {
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC)) {
__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_TCF);
switch_to_receive_mode();
}
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_IDLEF);
process_received_data();
}
}
void switch_to_receive_mode(void) {
HAL_UART_DMAStop(&huart1);
huart1.Instance->CR1 &= ~USART_CR1_TE; // 禁用发送
huart1.Instance->CR1 |= USART_CR1_RE; // 启用接收
HAL_UART_Receive_DMA(&huart1, rxBuf.buffer, BUF_SIZE);
}
void switch_to_send_mode(void) {
HAL_UART_DMAStop(&huart1);
huart1.Instance->CR1 &= ~USART_CR1_RE; // 禁用接收
huart1.Instance->CR1 |= USART_CR1_TE; // 启用发送
}
3.2 超时机制实现
为防止总线死锁,必须实现硬件超时检测。使用STM32的基本定时器实现1ms精度的超时检测:
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM6) {
static uint32_t timeout_counter = 0;
if(rxBuf.isReceiving) {
if(++timeout_counter > 100) { // 100ms超时
timeout_counter = 0;
rxBuf.isReceiving = 0;
switch_to_send_mode();
}
}
}
}
4. DMA配置与优化技巧
4.1 DMA内存访问策略
为提高效率,DMA应配置为循环模式,并优化内存访问策略:
c复制hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR; // 循环模式关键配置
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
4.2 零拷贝技巧
通过巧妙的内存布局,可以实现DMA直接访问应用层缓冲区:
c复制__attribute__((section(".dma_buffer"))) uint8_t appRxBuffer[2][BUF_SIZE];
volatile uint8_t currentRxBuf = 0;
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
currentRxBuf = 1;
process_data(appRxBuffer[0]);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
currentRxBuf = 0;
process_data(appRxBuffer[1]);
}
5. 实测性能与问题排查
5.1 带宽测试数据
在不同波特率下的实测性能对比:
| 波特率(bps) | 实际吞吐量(KB/s) | CPU占用率(%) |
|---|---|---|
| 9600 | 0.9 | 2 |
| 115200 | 11.2 | 15 |
| 921600 | 89.7 | 63 |
| 1500000 | 146.3 | 82 |
经验提示:超过1Mbps时建议启用硬件流控或降低协议开销
5.2 常见问题排查指南
-
数据错位问题
- 检查DMA内存对齐设置
- 确认USART时钟与APB总线时钟分频比
- 测试线路终端阻抗匹配
-
切换时序不稳定
- 测量TC中断到RE位设置的实际延迟
- 考虑插入1-2个NOP指令保证时序
- 检查中断优先级配置
-
DMA缓冲区溢出
- 增加水位标记检测
- 使用双缓冲乒乓操作
- 调整DMA突发传输大小
6. 进阶优化方向
对于需要更高可靠性的场景,可以考虑以下增强措施:
-
硬件CRC校验:启用STM32的硬件CRC计算单元
c复制
__HAL_UART_ENABLE_CRC(&huart1); huart1.Instance->CR1 |= USART_CR1_CRCE; -
自适应波特率:通过测量起始位宽度自动校准
c复制uint32_t detect_baudrate(void) { uint32_t start = DWT->CYCCNT; while(!(USART1->ISR & USART_ISR_RXNE)); uint32_t end = DWT->CYCCNT; return SystemCoreClock / (end - start) / 16; } -
动态缓冲区调整:根据负载自动扩展/收缩缓冲区
c复制void adjust_buffer_size(void) { uint16_t usage = (rxBuf.head - rxBuf.tail) % BUF_SIZE; if(usage > BUF_SIZE * 0.8) { realloc_buffer(&rxBuf, BUF_SIZE * 2); } else if(usage < BUF_SIZE * 0.3) { realloc_buffer(&rxBuf, BUF_SIZE / 2); } }
在实际项目中,这套方案已经稳定运行在工业环境监测设备上,单线连接距离可达50米(使用RS485转换芯片),平均无故障时间超过8000小时。最关键的心得是:DMA缓冲区的头尾指针必须使用volatile修饰,并且所有缓冲区操作应该放在临界区保护中。
