embedded software

    嵌入式软件架构:源自真实产品的 RTOS 任务设计

    基于三款已量产的嵌入式产品,分享我们如何用 FreeRTOS 组织固件——任务划分、实时调度和文件系统集成。

    ·XTELL 工程团队

    为什么固件架构决定产品的可靠性

    大多数嵌入式产品的失效,并不是因为某个函数写得不好,而是因为职责被混在了一起:控制逻辑和日志记录共用一个任务,通信和文件系统共用一个缓冲区,一个临时的权宜之计变成了永久方案。在现场,这种混乱表现为死机的屏幕、丢失的告警记录、损坏的文件,或者重启后行为迥异的设备。好的嵌入式软件很少是关于巧妙的代码,而是在早期就决定好:哪个任务负责什么。

    我们在工业控制器、安防设备和语音产品中都体会到了这一点。规律很一致:当每个任务只有一份职责、有一个明确的归属,并且有清晰的方式与系统其他部分通信时,产品就更容易测试、更容易调试,在实际使用中也稳定得多。当任务自然野蛮生长时,可靠性就只能靠运气。

    本文介绍我们如何用 FreeRTOS 解决这个问题,以三款已量产的产品为参照。它写给已经了解嵌入式开发基础、想建立实用架构思维的团队。作为一家嵌入式开发服务团队,我们把这些选择当作产品决策,而非实现细节。

    用 FreeRTOS 做任务划分:来自 STM32 PLC 工业控制系统的经验

    在我们的STM32 PLC 工业控制系统中,硬件决定了基调:一颗跑 FreeRTOS 的 STM32F407,FatFS 负责文件系统,SDIO 接 SD 卡存储,SRAM 扩展运行内存,再加上 UART 和 GPIO/LED 处理工业通信与状态指示。产品需要可靠的实时调度、稳定的文件系统操作,以及高速的 SDIO 数据记录。这种组合,恰恰是糟糕的任务设计最容易出问题的地方。

    我们的做法是让每一层做它最擅长的事:STM32F407 的 HAL 驱动硬件,FreeRTOS 管理多任务调度,FatFS 加 SDIO 提供持久化存储,SRAM 扩展运行内存,UART 支撑工业通信。这些职责都不允许互相渗透。

    把控制、通信和日常维护分开

    第一个决定是划分时序域。控制类工作只属于那些只需关心控制的任务;通信类工作只属于那些只需关心搬运字节的任务;日常维护——LED、GPIO 状态、诊断——则放在一个不会干扰两者的地方。

    这听起来显而易见,但很容易被破坏。常见错误是让 UART 中断处理函数直接修改应用状态,或者让一次日志调用在等待存储时阻塞了控制路径。我们通过明确归属来避免这两者:一个任务拥有它的输入、输出和失败行为。如果 UART 数据需要改变控制行为,它必须经过定义好的接口传递,而不是跨越整个系统去伸手。

    好处不只是代码更干净,它改变了调试方式。出现时序问题时,我们知道该查哪个任务;通信卡住了,我们知道该测哪个接口。架构把模糊的症状变成了局部问题。

    让存储变得无趣:SDIO 上的 FatFS

    文件系统代码是最容易混入职责的地方之一。FatFS 很强大,但不该被每个想存数据的任务随意调用。在 PLC 系统里,存储只有一个主人:记录任务负责准备数据,一个专职的存储任务执行 FatFS 操作,通过 SDIO 写入 SD 卡。

    这种分离之所以重要,是因为存储是突发且偶尔缓慢的:卡可能正忙,写操作可能比预期耗时。如果这些延迟发生在控制任务里,它们就变成了控制问题;如果发生在存储任务里,它们就只是存储问题,系统其余部分照常运行。

    我们还把记录看作一条流水线,而不是副作用:数据从清晰的边界进入,在 SRAM 扩展支撑的运行内存中缓冲,由存储任务统一提交。这种结构让高速 SDIO 数据记录变得可控,同时不让存储的顾虑污染调度。

    稳定的存储不是文件系统的特性,而是架构的特性:一个主人、一条路径,没有来自无关任务的意外调用。

    给每个外设一个家:UART、GPIO、LED 和 SRAM

    UART、GPIO 和 LED 的处理看起来微不足道,直到它们散落在整个代码库中:一个任务用 LED 显示状态,另一个用同一颗 LED 报错误,第三个在特殊模式下挪用某个 GPIO。结果是硬件在向操作员撒谎。

    我们给外设安家:UART 处理归通信所有,GPIO 和 LED 管理归系统状态所有。每个都有小而明确的接口,应用任务只能请求行为,不能直接操作引脚。这在第一天可能像多余的结构,但它能防止那种缓慢漂移——正是这种漂移让现场诊断变得不可靠。

    SRAM 扩展在这里扮演配角:它为缓冲和记录路径扩展了运行内存,但我们不会拿它当作归属不清的借口。内存依然有主人,缓冲区依然有生产者和消费者。扩展只是让架构有喘息的空间,它替代不了架构本身。

    当 MCU 很小:在约束下做设计

    不是每个产品都能用上 STM32F407。有时约束本身就是产品定义:设备必须小巧、便宜,并且把一件事做好。在这种情况下,架构意味着拒绝让一颗小 MCU 包办一切。

    STC32G128K:一颗小 MCU,多个 433MHz 终端

    我们的 433MHz 安防联动系统用一颗跑 FreeRTOS 的 STC32G128K 驱动 433MHz 终端。它从没想过把这颗小 MCU 变成摄像头处理器或云网关:摄像头的工作属于 TXW82x SDK 的 IPC/FPV 摄像头,状态显示属于 LCD/OLED 屏。嵌入式层是按能力刻意拆分的。

    这是约束型设计的关键一课:按子系统划分,而不只按任务划分。FreeRTOS 依然能组织好终端的工作,但更大的架构决策是:哪块硬件承担哪份职责。一颗小 MCU 在职责足够窄、足够明确时,才能变得可靠。

    同样的纪律也用在周边系统上:一个 Flutter 手机 App 负责配对、WiFi 配网、实时视频、告警推送和布防;后端负责协同;嵌入式设备专注于终端、摄像头接入和本地状态。谁都不需要当英雄。

    ESP8266:在没有现成协议栈的情况下实现实时语音对讲

    IP 对讲系统把这个想法推得更远。在低成本的 ESP8266 硬件上,团队证明了实时语音对讲可以在入门级硬件上跑通,支持点对点和多方对讲。项目没有硬套通用的现成协议栈,而是用了自研的音频传输协议。ESP8266 把音频采集、编码和网络传输当作一条完整链路来处理。

    这并不是说自研协议永远更好,而是说架构应该服从约束。在入门级硬件上,每一个不必要的层次都有代价。自研协议让整条音频链路归同一个设计主体,采集、编码、传输可以放在一起推演。

    更普适的一课是:问题平凡的地方用无聊的技术,约束真实的地方用定制的技术。文件系统集成是故意做得无聊的,小设备上的语音传输是故意做得定制的。两个决定都来自同一个问题:这个产品到底要在什么地方可靠?

    从固件想到云端:一个后端,三层同步

    嵌入式架构并不止于设备边缘。在 433MHz 系统里,挑战是让 433MHz 终端、IP 摄像头和手机 App 通过一个后端保持同步,还要保证可靠的实时视频和告警推送。这要求同时思考三层。

    嵌入式层保持专注:STC32G128K 加 FreeRTOS 管 433MHz 终端,TXW82x SDK 驱动摄像头,LCD/OLED 显示状态。移动层用 Flutter App 覆盖 iOS 和 Android,支持扫码配网、经 VLC 的 RTSP/HLS 视频、告警推送和布防控制。后端用 NestJS 加 PostgreSQL,Redis 做缓存,EMQX 做设备消息,ZLMediaKit 做流转发,Docker Compose 部署。

    这套架构能跑通,是因为每一层都有清晰的契约:后端管 JWT 鉴权、缓存、设备消息和媒体转发;App 管配网和用户意图;设备管终端和摄像头。实时视频和告警不是事后钉到设备代码上的补丁,而是横跨三层、各自有主人的链路。

    很多 IoT 项目就是在这里跑偏的:设备团队发明一种消息格式,App 团队发明另一种,后端沦为翻译。我们更愿意早早明确契约:谁做鉴权、谁发布设备消息、谁转发流、配网如何把设备绑定到账号。这些答案应该写在架构里,而不是写在 bug 报告里。

    实用的 RTOS 架构检查清单

    在设计被认为稳定之前,用它做一次评审清单。它反映的是上面的通用模式,不针对某家厂商或某块板子。

    • 给每个时序域独立的任务:控制、通信、存储和日常维护不应共享执行路径。
    • 存储只设一个主人:让一个任务拥有 FatFS 和 SDIO 的访问权,其他任务通过定义好的接口提交工作。
    • 把控制和通信分开:UART 和网络处理不应直接触碰控制状态。
    • 给外设明确的家:UART、GPIO、LED、显示屏各自需要一个主人和一个接口。
    • 有节制地使用 SRAM 扩展:为缓冲和记录路径扩展运行内存,但缓冲区的归属要明确。
    • 小型系统按子系统划分:让小 MCU 只承担狭窄的职责,把摄像头、UI、网关的工作交给为此而生的硬件。
    • 自研协议只留给真实约束:只在入门级硬件或实时性要求逼不得已时才用自研传输协议。
    • 设备、App、后端的契约要明确:鉴权、设备消息、流转发、配网、告警链路都要有主人。
    • 尽早设计失败路径:存储繁忙、链路丢失、重启、重新配网都是架构的输入,不是意外。
    • 加功能时复查任务边界:每个新功能都应该落到某个已有主人身上,或者刻意新增一个主人。

    结论:架构是产品特性

    纵观这些产品,同一个原则反复出现:STM32 PLC 系统之所以可靠,是因为调度、存储、外设都有主人;433MHz 系统之所以协调,是因为小 MCU 只承担狭窄的职责,后端、App、设备各有契约;ESP8266 对讲之所以好用,是因为音频链路被当作一条完整的路来设计,而不是拿顺手的零件拼凑。

    这就是为什么固件架构值得和硬件选型同等的重视。客户看不到任务划分图,但他们能感受到结果:持续记录的设备、准时到达的告警、清晰可辨的语音、干净恢复的系统。如果你的团队正在和纠缠不清的固件、飘忽不定的存储,或者长大到装不下原来结构的设备搏斗,我们的固件开发服务可以帮你把架构变成产品优势。

    需要专业服务?

    联系我们获取定制化解决方案和报价