STM32单线串口通讯实战(四):裸机架构 —— 事件驱动与状态机设计

搞定了底层的"收发"和协议层的"规则",现在我们要把它们组装成一个能实际跑起来的系统。

在没有 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)

我们将整个通讯过程抽象为几个状态。主循环每次运行到串口任务时,根据当前状态决定做什么。

状态定义

  1. IDLE (空闲):总线无事,等待上层应用发起请求。

  2. TX_BUSY (发送中):DMA 正在干活,等待发送完成(TC)。

  3. RX_WAIT (等待回复):请求已发出,正在计时等待从机应答。这是最关键的"非阻塞"阶段。

  4. 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:
* 本文为作者《嵌入式开发基础与工程实践》系列文之一。
* 关注我即可订阅后续内容更新,采用异步推送机制。
* 转发本文可视为广播分发,有助于信息传播至更多节点。
*******************************************/

相关推荐
Rabbit_QL21 小时前
【水印添加工具】从零设计一个工程级 Python 图片水印工具:WaterMask 架构与实现
开发语言·python
天“码”行空21 小时前
简化Lambda——方法引用
java·开发语言
z203483152021 小时前
C++对象布局
开发语言·c++
Beginner x_u21 小时前
如何解释JavaScript 中 this 的值?
开发语言·前端·javascript·this 指针
数据与后端架构提升之路1 天前
Seata 全景拆解:AT、TCC、Saga 该怎么选?告别“一把梭”的架构误区
分布式·架构
List<String> error_P1 天前
STM32窗口看门狗WWDG详解
stm32·单片机·嵌入式硬件·定时器
java1234_小锋1 天前
Java线程之间是如何通信的?
java·开发语言
檐下翻书1731 天前
在线绘制水流量示意图
论文阅读·架构·毕业设计·流程图·论文笔记
张张努力变强1 天前
C++ Date日期类的设计与实现全解析
java·开发语言·c++·算法
feifeigo1231 天前
基于EM算法的混合Copula MATLAB实现
开发语言·算法·matlab