如果说裸机开发是在“走钢丝”,小心翼翼地平衡各个模块的时间片;那么 RTOS(实时操作系统)就是给了你“分身术”。
在 STM32G0 这种资源受限(RAM 可能只有 8KB-36KB)的 MCU 上跑 RTOS,处理高速单线串口,我们必须解决三大难题:
-
资源竞争:两个任务同时想发串口,总线谁说了算?
-
实时响应:数据来了,如何最快唤醒处理任务?
-
内存效率:不要在队列里反复拷贝数据(Memcpy is evil),要用“零拷贝”。
本章将基于 CMSIS-RTOS2 标准接口(底层通常是 FreeRTOS),带你构建一个企业级的单线通讯架构。
1. 架构总览:生产者-消费者模型
在 RTOS 下,我们不再写一个巨大的 while(1)。我们将系统拆分为三个核心角色:
-
ISR (中断服务):生产者 A。负责接收硬件原始数据,不做逻辑,只发信号。
-
Protocol Task (协议任务):消费者。高优先级,被 ISR 唤醒,解析数据包。
-
App Task (业务任务):生产者 B。低优先级,负责发起发送请求。
2. 核心机制 I:同步与快速唤醒 (Thread Flags)
在裸机中,我们轮询标志位。在 RTOS 中,我们使用 事件标志 (Thread Flags) 或 信号量 (Semaphore)。 推荐:使用 osThreadFlagsSet。比信号量更轻量,且可以直接指定唤醒哪个任务。
代码实战:ISR 唤醒协议任务
/* 定义接收任务 ID */
osThreadId_t tid_Protocol;
/* 串口中断服务函数 (ISR) */
void USART1_IRQHandler(void)
{
// ... 前面的 IDLE 判断逻辑 (参考第二章) ...
if (is_idle_detected)
{
// 1. 停止 DMA (或记录当前位置)
// 2. 发送信号给协议任务,参数 0x01 是自定义标志位
osThreadFlagsSet(tid_Protocol, 0x01);
// 3. 强制上下文切换 (让高优先级任务立刻执行)
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
3. 核心机制 II:资源保护 (Mutex)
单线串口是 半双工 的。这就意味着“总线”是一个 临界资源 (Critical Resource)。 如果 任务 A 正在发送“开灯”指令的前半段,任务 B 突然切进来发送“关窗”指令,总线上的波形就变成了乱码,且 GPIO 方向切换会冲突。
解决方案:必须使用 互斥锁 (Mutex)。
为什么不用二值信号量?
-
优先级翻转 (Priority Inversion):Mutex 具有“优先级继承”机制,能防止低优先级任务持有锁时被中优先级任务卡死,导致高优先级任务一直等不到锁。
代码实战:线程安全的发送函数
/* 定义互斥锁 ID */
osMutexId_t SingleWire_Mutex_ID;
/* 初始化 */
void Bus_Init(void) {
SingleWire_Mutex_ID = osMutexNew(NULL); // 创建互斥锁
}
/* 线程安全的发送接口 (任意任务均可调用) */
int32_t SingleWire_Send_Safe(uint8_t *data, uint16_t len)
{
osStatus_t status;
// 1. 申请锁 (等待 100ms,拿不到就放弃)
status = osMutexAcquire(SingleWire_Mutex_ID, 100);
if (status != osOK) return -1; // 总线忙
// ---------------- 临界区开始 ----------------
// 2. 硬件切发送
UART_ENTER_TX_MODE();
// 3. DMA 发送
HAL_UART_Transmit_DMA(&huart1, data, len);
// 4. 等待发送完成 (使用信号量或标志位挂起等待,不要死等!)
// 这里假设我们在 TC 中断里发了一个 flag
osThreadFlagsWait(0x02, osFlagsWaitAny, 50);
// 5. 硬件切接收 (防止 Echo)
UART_ENTER_RX_MODE();
// ---------------- 临界区结束 ----------------
// 6. 释放锁
osMutexRelease(SingleWire_Mutex_ID);
return 0;
}
4. 核心机制 III:零拷贝 (Zero-Copy) 与内存池
STM32G0 的 RAM 很宝贵。如果 ISR 收到了 256 字节数据:
-
Copy 到 Queue。
-
Task 从 Queue Copy 出来。 这是极大的浪费!
解决方案:Memory Pool (内存池) + Message Queue (指针队列)。 ISR 只把数据的 指针 扔进队列,Task 读指针直接访问 DMA 缓冲区。
方案设计
由于我们使用的是 DMA Circular Buffer,数据已经在 RAM 里了。
-
简单做法:直接传递 RingBuffer 的
Start_Index和Length。 -
消息结构体:
typedef struct {
uint16_t start_idx; // 数据在环形缓冲区的起始位置
uint16_t len; // 数据长度
uint32_t timestamp; // 接收时间戳
} UART_Event_t;
/* 定义消息队列 */
osMessageQueueId_t UART_Queue_ID;
/* 协议处理任务 */
void Task_Protocol(void *argument)
{
UART_Event_t event;
uint8_t *pRealData;
while (1)
{
// 1. 挂起等待消息 (从 ISR 发来的)
// 这里的 &event 只是接收了几个字节的结构体,而不是 Payload
osMessageQueueGet(UART_Queue_ID, &event, NULL, osWaitForever);
// 2. 核心:通过索引直接定位 DMA Buffer,无需拷贝
// 注意处理环形回卷 (Wrap-around)
Process_RingBuffer_Data(event.start_idx, event.len);
}
}
5. RTOS 常见问题
5.1 栈溢出 (Stack Overflow)
-
printf和 DMA 回调是栈溢出的重灾区。 -
建议:协议任务分配 256-512 Bytes 栈空间(根据
Parse函数深度);osMutex相关操作不占栈。
5.2 低功耗 (Tickless Mode)
如果你的设备是电池供电,RTOS 的 Tick 会让 CPU 无法深度休眠。
-
在 STM32G0 上,配合 LPUART(低功耗串口),可以在 Stop Mode 下通过地址匹配唤醒。
-
策略:当
Task_Protocol没事干挂起时,RTOS 进入 IDLE 任务,调用__WFI()。但要注意 DMA 传输期间不能进入 Stop Mode(DMA 需要时钟),可以使用osKernelLock()或特定的电源管理锁。
本专栏系列总结
通过这五章内容,我们完成了一次从“电线”到“操作系统”的完整技术栈构建:
-
物理层:看懂了原理图,学会了用 STM32G0 的
HDSEL和Open-Drain避坑硬件连接。 -
链路层:掌握了 DMA Circular + IDLE 的黄金接收方案,解决了收发切换的时序痛点。
-
协议层:设计了包含 Header/Len/CRC 的 Mini-Frame,并利用 9-bit 模式实现了硬件地址过滤。
-
裸机架构:用状态机 (FSM) 实现了非阻塞的前后台系统。
-
RTOS 架构:用 Mutex 保护总线,用 Thread Flags 和 Queue 实现了高效的线程通信。
1231

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



