搞定了底层的"收发"和协议层的"规则",现在我们要把它们组装成一个能实际跑起来的系统。
在没有 RTOS(实时操作系统)的 裸机(Bare Metal) 环境下,最大的挑战是:如何在等待从机回复(通常需要几十毫秒)的同时,不阻塞主程序的其他业务(如扫描按键、刷新屏幕)?
绝对不能用 HAL_Delay()!本章将带你构建一个基于 有限状态机(FSM) 和 非阻塞定时器 的高效处理框架。
1. 告别 HAL_Delay:前后台系统设计
初学者写单线通讯经常这样写:
// 错误示范:阻塞式代码
HAL_UART_Transmit(&huart1, tx_data, len, 100); // 1. 阻塞发送
HAL_GPIO_WritePin(DIR_Port, DIR_Pin, RX_MODE); // 2. 切方向
HAL_Delay(50); // 3. 死等从机回复 <--- 致命代码!在这里 CPU 啥也干不了
if(HAL_UART_Receive(&huart1, rx_data, len, 100) == HAL_OK) { ... }
这种写法会导致整个系统卡顿。高效的裸机架构应分为两层:
-
前台(中断层) :负责"快进快出"的工作。如 DMA 搬运完成、IDLE 中断置位。它只负责置标志位。
-
后台(主循环) :负责复杂的协议解析和业务逻辑。它通过轮询标志位 和检查时间戳来推动状态机流转。
2. 核心设计:有限状态机 (FSM)
我们将整个通讯过程抽象为几个状态。主循环每次运行到串口任务时,根据当前状态决定做什么。
状态定义
-
IDLE (空闲):总线无事,等待上层应用发起请求。
-
TX_BUSY (发送中):DMA 正在干活,等待发送完成(TC)。
-
RX_WAIT (等待回复):请求已发出,正在计时等待从机应答。这是最关键的"非阻塞"阶段。
-
PROCESSING (处理中):收到了完整数据,正在校验和解析(耗时操作)。
3. 代码实战:非阻塞驱动框架
3.1 定义状态结构体
为了代码整洁,把所有相关的变量打包。
typedef enum {
STATE_IDLE,
STATE_TX_BUSY,
STATE_RX_WAIT,
STATE_PROCESSING
} UART_State_t;
typedef struct {
UART_State_t State;
uint32_t TxStartTime; // 发送开始时间戳
uint32_t RxStartTime; // 等待接收开始时间戳
uint32_t TimeoutLimit; // 超时阈值
uint8_t RetryCount; // 重发计数
uint8_t TxBuffer[64];
uint8_t RxBuffer[64]; // 这是一个逻辑Buffer,数据从DMA环形区拷过来
} SingleWire_Handle_t;
SingleWire_Handle_t hBus;
3.2 核心任务函数 (Task)
这个函数需要在 main() 的 while(1) 中高频调用。
void SingleWire_Poll_Task(void)
{
uint32_t now = HAL_GetTick(); // 获取当前系统滴答(ms)
switch (hBus.State)
{
/* ----------------------------------------------------
状态 1: 空闲态 - 检查是否有业务要发
---------------------------------------------------- */
case STATE_IDLE:
// 示例:检查是否有一个标志位要求发送
if (App_Need_Send_Request())
{
// 1. 准备数据
Prepare_Frame(hBus.TxBuffer);
// 2. 硬件切为发送 (参考第二章)
UART_ENTER_TX_MODE();
// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, hBus.TxBuffer, LEN);
// 4. 更新状态
hBus.TxStartTime = now;
hBus.State = STATE_TX_BUSY;
}
break;
/* ----------------------------------------------------
状态 2: 发送中 - 等待 DMA TC (传输完成)
---------------------------------------------------- */
case STATE_TX_BUSY:
// 这里有两种方式跳转:
// A. 查询 huart1.gState (不推荐,依赖库版本)
// B. 在 HAL_UART_TxCpltCallback 中置一个 volatile 标志位 (推荐)
if (Flag_Tx_Complete)
{
Flag_Tx_Complete = 0;
// 关键:发送完毕,立刻切回接收模式
UART_ENTER_RX_MODE();
// 如果是广播包,不需要等回复,直接回 IDLE
if (Is_Broadcast_Frame(hBus.TxBuffer)) {
hBus.State = STATE_IDLE;
} else {
hBus.RxStartTime = now;
hBus.TimeoutLimit = 50; // 等待 50ms
hBus.State = STATE_RX_WAIT;
}
}
// 容错:如果 DMA 卡死了 (极少见),超时强制复位
if (now - hBus.TxStartTime > 100) {
Hardware_Reset();
hBus.State = STATE_IDLE;
}
break;
/* ----------------------------------------------------
状态 3: 等待回复 - 核心非阻塞逻辑
---------------------------------------------------- */
case STATE_RX_WAIT:
// 事件 A: 收到数据了 (由第二章的 IDLE 中断触发)
if (Flag_Data_Received)
{
Flag_Data_Received = 0;
// 从 DMA 环形 Buffer 拷出数据到 hBus.RxBuffer
RingBuf_Read(hBus.RxBuffer);
hBus.State = STATE_PROCESSING;
}
// 事件 B: 超时了 (Slave 没理我)
else if (now - hBus.RxStartTime > hBus.TimeoutLimit)
{
Handle_Timeout_Error(); // 记录日志或重发
hBus.State = STATE_IDLE;
}
break;
/* ----------------------------------------------------
状态 4: 数据处理
---------------------------------------------------- */
case STATE_PROCESSING:
if (Check_CRC(hBus.RxBuffer))
{
Execute_Command(hBus.RxBuffer); // 执行业务逻辑
}
else
{
Handle_CRC_Error();
}
hBus.State = STATE_IDLE; // 处理完,回到空闲
break;
}
}
4. 裸机架构的进阶技巧
4.1 软件定时器 (Soft Timer)
如果在 STATE_RX_WAIT 期间你还想闪烁 LED 怎么办? 不要在 Task 里写死循环。利用 HAL_GetTick() 的差值。
// 放在 while(1) 中,独立于串口任务
if (HAL_GetTick() - Led_Last_Toggle > 500) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
Led_Last_Toggle = HAL_GetTick();
}
4.2 应对"分包接收"
如果从机比较慢,先发了一半数据,过了 2ms 发了另一半。
-
现象:你会收到两次 IDLE 中断。
-
处理 :在
STATE_RX_WAIT中,收到第一次数据后,不要立刻切走,而是检查Frame_Len(协议头里的长度)。-
如果
Received_Len < Protocol_Len,说明数据没收全。 -
此时重置超时计时器 ,继续留在
STATE_RX_WAIT等待剩下的字节。这就是我们在第三章设计协议时一定要包含Len字段的原因!
-
4.3 喂狗 (Watchdog) 的位置
在裸机系统中,看门狗(IWDG)通常在 main 循环的末尾喂一次。 切记 :不要在 SingleWire_Poll_Task 内部喂狗!如果串口状态机因为硬件故障死循环在 STATE_TX_BUSY,看门狗必须能复位系统,如果在任务里喂狗,系统就真死机了。
5. 本章小结
通过状态机,我们将线性的时间流打散成了离散的事件片。
-
优点:CPU 利用率极高,等待期间可以处理 UI、按键、传感器。
-
缺点:逻辑比阻塞式代码复杂,需要维护全局变量和状态流转。
对于简单的应用(如温控器),这套裸机架构已经足够完美。但如果你要一边跑 TCP/IP 协议栈,一边做图形界面,一边还要处理单线串口,裸机状态机就会变得极其庞大且难以维护。
这时,我们需要操作系统的力量。
下一章预告 : 《架构实现 II:RTOS 下的高效设计 (CMSIS-RTOS2)》 我们将引入 FreeRTOS,利用 信号量 (Semaphore) 替代标志位,利用 消息队列 (Queue) 传递数据,并使用 互斥锁 (Mutex) 保护我们的单线串口,展示如何在多任务环境下优雅地实现单线通讯。
/*******************************************
* Description:
* 本文为作者《嵌入式开发基础与工程实践》系列文之一。
* 关注我即可订阅后续内容更新,采用异步推送机制。
* 转发本文可视为广播分发,有助于信息传播至更多节点。
*******************************************/