1. 单线通讯的“至暗时刻”:收发切换 (Turnaround)
在全双工(两根线)模式下,发送和接收是独立的。但在单线模式下,你必须在一个循环中不断地切换身份:说 -> 听 -> 说。
1.1 致命的“TC 标志位”陷阱
这是新手最容易犯的错误:使用 TXE (Transmit Data Register Empty) 标志来切换方向。
-
错误做法:
-
填入数据到 TDR。
-
等待
TXE置位(表示数据从寄存器移到了移位寄存器)。 -
立刻切换为接收模式。
-
后果:移位寄存器里还有 8-10 个 bit 没发完,你把 TX 关了,最后那个字节被“截断”,总线上出现乱码。
-
-
正确做法: 必须等待
TC(Transmission Complete) 标志。它表示移位寄存器也空了,最后一个 Stop Bit 已经完整发到了物理线路上。
1.2 状态机控制代码 (STM32G0 寄存器操作)
为了极致的切换速度(HAL 库函数调用有几微秒开销),在中断或高频任务中,建议直接操作寄存器控制 TE (Transmitter Enable) 和 RE (Receiver Enable)。
/* 定义方向控制宏,提高代码可读性 */
#define UART_ENTER_RX_MODE() do { \
USART1->CR1 &= ~USART_CR1_TE; /* 关闭发送 */ \
USART1->CR1 |= USART_CR1_RE; /* 开启接收 */ \
} while(0)
#define UART_ENTER_TX_MODE() do { \
USART1->CR1 &= ~USART_CR1_RE; /* 关闭接收 (避免回显) */ \
USART1->CR1 |= USART_CR1_TE; /* 开启发送 */ \
} while(0)
/* 发送函数示例 */
void SingleWire_Send(uint8_t *pData, uint16_t Len)
{
// 1. 切为发送模式
UART_ENTER_TX_MODE();
// 2. 使用 HAL 库或 DMA 发送
if(HAL_UART_Transmit_DMA(&huart1, pData, Len) != HAL_OK)
{
Error_Handler();
}
// 注意:这里不能立刻切回 RX!
// 必须在 DMA 完成中断 (TC中断) 中切回 RX
}
1.3 怎么处理 Echo (自发自收)?
在单线模式(特别是 Open-Drain 物理连接)中,TX 引脚的电平变化会被 RX 引脚同步捕捉到。
-
笨办法:软件层接收所有数据,发现是自己发的就丢弃。缺点是浪费 DMA 空间和 CPU 算力解析。
-
聪明办法:如上代码所示,在发送期间,硬件关闭 RE (Receiver Enable)。
-
STM32G0 允许在初始化时只开启 TE,发送完后再开启 RE。这样物理层根本不会把数据送入接收 FIFO,彻底根除 Echo。
-
2. 接收端的“黄金标准”:DMA Circular + IDLE
处理单线总线上的数据(通常是不定长的)时,“DMA 循环模式 + 空闲中断” 是目前最高效的架构,没有之一。
2.1 为什么是 Circular (环形)?
如果用 Normal 模式,每次收满 Buffer 都要重新调用 HAL_UART_Receive_DMA。在这个“重新调用”的微秒级空隙里,如果有数据进来,就会发生 Overrun Error (ORE),导致丢包。 Circular 模式下,DMA 指针指到 Buffer 尾部会自动回到头部,硬件永不停歇。
2.2 为什么是 IDLE (空闲中断)?
单线协议通常不知道下一包数据有多长。
-
IDLE 定义:当总线上检测到一帧数据传输结束,且维持了一个字节时间的空闲(High Level),硬件置位
IDLE标志。 -
这相当于硬件告诉你:“刚才那波数据发完了,你可以处理了”。
2.3 实战代码:STM32G0 的 DMA 接收实现
步骤 1:初始化启动 DMA 在 main() 初始化部分调用一次即可:
#define RX_BUF_SIZE 256
uint8_t RxBuffer[RX_BUF_SIZE];
void Start_Listening(void)
{
// 开启空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
// 启动 DMA 循环接收
// 注意:G0 的 DMA 初始化代码需确保配置为 DMA_CIRCULAR
HAL_UART_Receive_DMA(&huart1, RxBuffer, RX_BUF_SIZE);
}
步骤 2:编写中断服务函数 (ISR) 这是核心逻辑。我们需要计算“上次读到了哪”和“现在 DMA 写到了哪”,中间的部分就是新收到的数据。
/* 全局变量记录处理进度 */
volatile uint16_t Last_Read_Index = 0;
void USART1_IRQHandler(void)
{
uint32_t isrflags = USART1->ISR;
uint32_t cr1its = USART1->CR1;
// 检测 IDLE 标志 (注意:G0 清除 IDLE 标志是写 ICR 寄存器)
if ((isrflags & USART_ISR_IDLE) && (cr1its & USART_CR1_IDLEIE))
{
// 1. 清除 IDLE 标志
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 2. 获取当前 DMA 写入位置
// G0 的 DMA 计数器是递减的 (NDTR),也就是 "剩余传输量"
// 需换算为 "Buffer 中的索引"
uint16_t dma_remaining = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
uint16_t current_write_index = RX_BUF_SIZE - dma_remaining;
// 3. 计算数据长度并处理
Process_New_Data(current_write_index);
}
// 处理 HAL 库的其他中断
HAL_UART_IRQHandler(&huart1);
}
步骤 3:环形数据提取 (Ring Buffer Logic)
void Process_New_Data(uint16_t current_index)
{
uint16_t len = 0;
if (current_index == Last_Read_Index) return; // 无新数据
if (current_index > Last_Read_Index)
{
// 情况 A: 线性数据 (未回卷)
// [ ... old ... | NEW DATA | ... empty ... ]
// ^ ^
// Last Curr
len = current_index - Last_Read_Index;
Parse_Protocol(&RxBuffer[Last_Read_Index], len);
}
else
{
// 情况 B: 数据回卷 (Wrap around)
// [ DATA_PART2 | ... old ... | DATA_PART1 ]
// ^ ^
// Curr Last
// 先处理尾部 (Part 1)
uint16_t tail_len = RX_BUF_SIZE - Last_Read_Index;
Parse_Protocol(&RxBuffer[Last_Read_Index], tail_len);
// 再处理头部 (Part 2)
if (current_index > 0)
{
Parse_Protocol(&RxBuffer[0], current_index);
}
}
// 更新指针
Last_Read_Index = current_index;
}
3. 进阶:如何调试“看不见”的方向?
调试单线串口最痛苦的是:示波器接上去,只有一根线在跳,你根本分不清哪段波形是 Master 发的,哪段是 Slave 回的。
推荐技巧:GPIO 辅助调试法 找一个空闲 GPIO(例如 PB5),作为调试探针。
-
在
UART_ENTER_TX_MODE()里,拉高 PB5。 -
在 DMA 发送完成中断(切回 RX 时),拉低 PB5。
示波器设置:
-
CH1: 单线串口数据。
-
CH2: PB5。
效果: 当 CH2 为高电平时,CH1 的波形是你发的;当 CH2 为低电平时,CH1 的波形是别人回的。通过测量 CH2 下降沿到 CH1 对方数据起始位的间隔,你可以精确测量 响应时间 (Response Time),这是优化总线效率的关键依据。
4. 本章小结
在这一章,我们完成了链路层的坚实地基:
-
收发切换:明确了必须等待
TC标志,并利用硬件寄存器快速切换TE/RE以屏蔽回显。 -
高效接收:实现了 DMA Circular + IDLE 机制,无论数据包多长,都能在总线空闲瞬间触发处理。
-
数据提取:解决了环形缓冲区的数据回卷(Wrap-around)处理逻辑。
/*******************************************
* Description:
* 本文为作者《嵌入式开发基础与工程实践》系列文之一。
* 关注我即可订阅后续内容更新,采用异步推送机制。
* 转发本文可视为广播分发,有助于信息传播至更多节点。
*******************************************/
943

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



