coact:让嵌入式事件处理更可控

coact:让嵌入式事件处理更可控

GitHub:https://github.com/DeguiLiu/coact

1. 概述

嵌入式设备经常需要同时处理按键、通信、传感器、定时器和故障通知。传统做法通常是为每类功能创建任务,再用队列、回调和锁连接这些任务。功能增加后,系统还要处理几个问题:事件按什么顺序执行,队列最多能积压多少,提交失败后如何处理,以及系统过载后能否恢复。

coact 是一个面向单核微控制器的 C++17 事件框架。它让多个功能模块共享一个事件分发机制,并用固定容量的队列、事件池和状态机组织执行过程。框架支持 RT-Thread 和 FreeRTOS,也可以在 Linux 和 Windows 主机上运行测试。

coact 的主要价值在于统一这些规则:资源有明确上限,失败有可检查的结果,过载有受控的退化和恢复过程。选择框架时,需要同时考虑这些机制带来的收益,以及共享分发线程和静态配置带来的限制。

2. 它解决什么问题

事件传递只是事件驱动系统的一部分。多个模块协作时,还需要明确执行方式、资源归属和异常处理。例如,传感器持续上报数据时,普通采样事件能否占满故障通知需要的队列?通信模块处理变慢时,是否会拖延其他模块?这些问题需要在系统设计时给出答案。

需要明确的问题 coact 的处理方式
事件由谁执行 排队事件由分发线程处理,符合条件的任务提交也可以直接执行
队列能容纳多少事件 队列和事件池的容量在编译期确定
提交失败怎么办 提交接口返回明确结果,调用方可以区分拒绝、已满和其他失败
过载后如何处理 通过监控和熔断状态限制继续提交,并按条件恢复
同一模块能否被同时执行 通过执行租约保证同一主动对象不被两个执行路径同时推进

使用任务、队列和回调也能实现这些功能。随着提交点和状态机增加,各模块分别实现容量控制、错误处理和恢复逻辑,会增加检查与维护的工作量。coact 将这些公共机制集中在框架中,业务代码仍负责事件含义、状态变化和失败后的具体动作。

3. coact 如何组织一次处理

3.1 事件如何到达目标模块

任务或中断产生事件后,先由协调器检查目标、提交规则和过载状态。下面展示事件需要排队时的处理路径。
#mermaid-svg-uHgkekq8YDiPDb3n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uHgkekq8YDiPDb3n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uHgkekq8YDiPDb3n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uHgkekq8YDiPDb3n .error-icon{fill:#552222;}#mermaid-svg-uHgkekq8YDiPDb3n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uHgkekq8YDiPDb3n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uHgkekq8YDiPDb3n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uHgkekq8YDiPDb3n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uHgkekq8YDiPDb3n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uHgkekq8YDiPDb3n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uHgkekq8YDiPDb3n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uHgkekq8YDiPDb3n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uHgkekq8YDiPDb3n .marker.cross{stroke:#333333;}#mermaid-svg-uHgkekq8YDiPDb3n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uHgkekq8YDiPDb3n p{margin:0;}#mermaid-svg-uHgkekq8YDiPDb3n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uHgkekq8YDiPDb3n .cluster-label text{fill:#333;}#mermaid-svg-uHgkekq8YDiPDb3n .cluster-label span{color:#333;}#mermaid-svg-uHgkekq8YDiPDb3n .cluster-label span p{background-color:transparent;}#mermaid-svg-uHgkekq8YDiPDb3n .label text,#mermaid-svg-uHgkekq8YDiPDb3n span{fill:#333;color:#333;}#mermaid-svg-uHgkekq8YDiPDb3n .node rect,#mermaid-svg-uHgkekq8YDiPDb3n .node circle,#mermaid-svg-uHgkekq8YDiPDb3n .node ellipse,#mermaid-svg-uHgkekq8YDiPDb3n .node polygon,#mermaid-svg-uHgkekq8YDiPDb3n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uHgkekq8YDiPDb3n .rough-node .label text,#mermaid-svg-uHgkekq8YDiPDb3n .node .label text,#mermaid-svg-uHgkekq8YDiPDb3n .image-shape .label,#mermaid-svg-uHgkekq8YDiPDb3n .icon-shape .label{text-anchor:middle;}#mermaid-svg-uHgkekq8YDiPDb3n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uHgkekq8YDiPDb3n .rough-node .label,#mermaid-svg-uHgkekq8YDiPDb3n .node .label,#mermaid-svg-uHgkekq8YDiPDb3n .image-shape .label,#mermaid-svg-uHgkekq8YDiPDb3n .icon-shape .label{text-align:center;}#mermaid-svg-uHgkekq8YDiPDb3n .node.clickable{cursor:pointer;}#mermaid-svg-uHgkekq8YDiPDb3n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uHgkekq8YDiPDb3n .arrowheadPath{fill:#333333;}#mermaid-svg-uHgkekq8YDiPDb3n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uHgkekq8YDiPDb3n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uHgkekq8YDiPDb3n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uHgkekq8YDiPDb3n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uHgkekq8YDiPDb3n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uHgkekq8YDiPDb3n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uHgkekq8YDiPDb3n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uHgkekq8YDiPDb3n .cluster text{fill:#333;}#mermaid-svg-uHgkekq8YDiPDb3n .cluster span{color:#333;}#mermaid-svg-uHgkekq8YDiPDb3n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uHgkekq8YDiPDb3n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uHgkekq8YDiPDb3n rect.text{fill:none;stroke-width:0;}#mermaid-svg-uHgkekq8YDiPDb3n .icon-shape,#mermaid-svg-uHgkekq8YDiPDb3n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uHgkekq8YDiPDb3n .icon-shape p,#mermaid-svg-uHgkekq8YDiPDb3n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uHgkekq8YDiPDb3n .icon-shape .label rect,#mermaid-svg-uHgkekq8YDiPDb3n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uHgkekq8YDiPDb3n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uHgkekq8YDiPDb3n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uHgkekq8YDiPDb3n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 任务或中断

产生事件
协调器

检查并提交
固定容量队列

等待处理
分发线程

按顺序取出
主动对象

更新状态
事件回收

释放资源

排队事件进入 Staging,也就是固定容量的事件暂存区。它按 High、Normal、Low 划分优先级,分发线程再按策略取出事件,交给目标模块。配置关键事件预留容量后,普通流量不能占用相应预留槽位;预留容量默认关闭,需要按业务需求配置。低优先级事件等待达到配置阈值后,会被优先取出,避免持续被高优先级流量推迟。

框架还支持直发:当提交来自任务、目标允许直发且当前可以执行时,在提交任务的调用过程中完成处理,省去一次排队和唤醒。当前中断提交路径不执行直发。

直发和排队派发共用目标的执行租约。执行租约相当于该模块的独占执行权,只有取得它的路径才能推进模块状态。因此,同一个模块不会被两个路径同时执行,但不同模块可能在不同任务中执行。跨模块共享的可变数据仍需要单独设计同步方式。

3.2 状态机如何描述业务

主动对象(AO)是拥有自身业务数据和状态机的功能模块,例如通信模块或传感器模块。它通过事件改变状态,无须为每个模块单独创建线程。

coact 使用层次状态机(HSM)描述行为。"层次"表示状态之间可以存在父子关系。例如,通信模块的"连接中"和"已连接"可以共同属于"工作中",两个子状态都无法处理的停止事件,可以交给父状态统一处理。

状态和转移使用 C++ 的 constexpr 常量表定义。一条转移写明源状态、事件、目标状态、触发条件和执行动作,构建时不需要额外的建模工具或代码生成器。状态机引用这些表,不在运行时复制或扩展它们。评审和调试都可以直接对应源码中的定义。

共享分发线程也带来限制:某个处理函数长时间不返回,会延迟其他排队事件。状态机每一步都应在有限时间内完成;耗时计算和阻塞式设备操作应拆分,或交给独立任务,完成后再用事件通知状态机。

4. 如何控制资源和过载

4.1 容量在运行前确定

队列、事件池和模块数量都有配置上限。事件池预先保留一组存储块,事件使用完毕后归还;多个接收方共享池中事件时,通过引用计数决定何时回收。框架不在运行过程中申请堆内存,也不依靠动态注册模块扩展系统。

静态容量便于估算内存,但容量耗尽仍可能发生。事件池没有空闲块、队列已满时,调用方仍需根据业务选择重试、丢弃可替代数据,或执行故障处置。关键事件需要怎样的容量保障,也应在配置时明确。

coact 使用 C++17 的编译期检查约束部分配置和资源用法。例如,关键事件预留容量不能超过所在分区的总容量,事件池存储块必须满足事件类型的大小与对齐要求。协调器禁止复制,避免复制出两份独立的准入状态;部分关键返回值通过 [[nodiscard]] 提示调用方检查。这些机制让一部分错误在构建阶段暴露,业务层的失败处理仍需检查。

4.2 在接收事件前检查负载

队列总容量限制整个暂存区的占用,准入检查则可以进一步限制单个模块接收的负载。准入指事件正式接受前的检查,当前可配置两项约束:

约束 含义 作用
最小到达间隔 同一目标两次成功准入之间至少间隔指定时间 限制过密提交
每个 AO 的积压上限 限制已接受但尚未完成的排队事件数量,包含已出队但暂缓处理的事件 避免单个目标积压过多事件

最小到达间隔在提交处检查,不按事件出队的时间计算。事件积压后可能被连续取出,这不代表生产者提交过密。只有成功直发或成功入队才更新成功准入时间,提交失败会回滚预约。多个提交交错时,预约状态采用保守判定,可能额外拒绝请求,但不会因此放宽间隔限制。

任务和中断可能同时提交,因此检查和容量预约需要在单核有界临界区中同步,业务状态机不在该临界区中执行。准入策略在编译期选择,默认 NoAdmission 不启用这些附加检查,也不支持运行中切换策略。

4.3 拒绝和异常留下可检查的结果

提交接口返回 SubmitResult,用于区分接受、限流、过载拒绝等结果。监控模块记录队列水位、溢出、拒绝和处理耗时等信息,状态机未处理的事件也有单独计数。测试或上层监控可以据此判断事件是否按预期处理。

需要定位准入问题时,可以额外配置故障快照,记录目标、原因、时间等现场信息。快照默认不分配记录槽位,并且在并发竞争时允许丢失诊断记录,不能把它当作保证完整的事件日志。

4.4 过载后逐步限制并恢复

coact 为每个 AO 保存一份熔断状态机。熔断指负载或异常达到条件后,暂时限制继续执行的工作,避免问题持续扩大。

状态 主要行为
Normal 按正常规则提交和执行
BrokenL1 撤销该 AO 的直发能力
BrokenL2 限制非关键事件,减少继续积压
Safe 仅允许规定的安全事件,需要外部恢复请求
Recovering 进行受控探测,观察是否具备恢复条件

从降级状态恢复,需要满足冷却、水位持续降低、探测成功和连续健康窗口等条件。一次处理成功不足以恢复到 Normal。恢复所需的水位采样、健康信息和外部恢复请求,需要产品侧按接口提供。

这里的 Safe 是事件策略状态,不代表设备已经完成硬件安全动作。熔断器不能终止正在执行的函数,也不能直接切断电机输出或关闭危险外设,这些动作需要由产品定义并接入。

5. 故障处理能做到什么

coact 通过准入检查限制提交负载,通过耗时监测识别处理过慢的模块,再由熔断机制限制后续事件。这些机制用于控制过载,不提供内存隔离,也不保证内存损坏后的局部恢复。

处理耗时在函数调用前后测量,包含期间的中断和任务抢占时间。因此,它是经过时间监测,无法精确代表 CPU 执行时间,也不能在超时瞬间强制终止函数。函数陷入死循环时,需要独立硬件看门狗或经验证的平台机制执行安全动作或复位。

多个 AO 共用分发线程栈,发生栈损坏后不能默认只重启一个 AO。若无法证明栈和框架数据完整,应交由平台执行安全动作或复位。

6. 使用 coact 需要多少资源

固定容量使资源用量便于核算,但框架本身仍有代码和存储开销。仓库在 Cortex-M4、FreeRTOS、-Os 优化和零堆配置下,以裸 FreeRTOS 为基线运行一个 AO,得到以下增量:

配置 Flash 增量 静态 RAM 增量
默认配置 约 8.8 KiB 约 9.3 KiB
小配置并启用链接时优化(LTO) 约 7.9 KiB 约 6.3 KiB

默认配置分发线程栈为 4 KiB,三个优先级队列分别有 32、64、128 个槽位,每个槽位 12 字节。栈和队列占据主要 RAM 成本。缩小配置能够降低开销,同时也会降低可容纳的模块数或积压事件数。

这些数字来自仓库测试配置,不能直接当作产品的最低需求。实际项目还要计入业务数据、事件载荷和其他任务栈,并验证长调用链、抢占和故障路径下的栈用量。静态容量给出了存储上限,单个业务函数的最坏执行时间仍需另行分析。

7. 什么情况下适合使用

coact 更适合以下系统:

  • 有多个相互协作的功能模块;
  • 事件来源同时包括任务和中断;
  • 模块内部存在较多状态转换;
  • 需要说明队列上限、失败路径和恢复条件;
  • 目标平台希望避免运行时堆分配。

以下情况通常不需要引入 coact:

  • 系统只有一个生产者和一条线性处理流程;
  • 业务规模很小,队列和状态机可以直接写清楚;
  • Flash 或 RAM 预算无法承担固定的分发线程和队列开销;
  • 系统需要多核调度、完整用户态或现成的认证平台。

框架的价值取决于它减少了多少重复的约定和检查。如果系统本身很简单,直接使用任务、队列和状态机可能更合适。

8. 进一步了解

coact 的源码、示例、测试和完整接口说明见 GitHub:

https://github.com/DeguiLiu/coact

资源测量过程见固件与 RAM 占用实测,准入检查和故障快照的实现说明见可靠性增强设计第 4 节和第 8.1 节。

项目采用 MIT License。coact 不是经过功能安全认证的产品,使用者仍需根据目标设备完成风险分析、故障注入、覆盖率验证和硬件平台测试。

相关推荐
人邮异步社区1 天前
学习FreeRTOS推荐哪本书?
freertos·rtos·嵌入式固件工程师
La Pulga3 天前
【FreeRTOS源码学习笔记】空闲任务与阻塞延时
笔记·mcu·学习·嵌入式·freertos·实时操作系统
La Pulga4 天前
【FreeRTOS源码学习笔记】任务切换
单片机·mcu·学习·嵌入式·freertos·实时操作系统
La Pulga6 天前
【FreeRTOS源码学习笔记】链表的实现
数据结构·笔记·stm32·mcu·链表·嵌入式·freertos
La Pulga6 天前
【FreeRTOS源码学习笔记】任务创建
笔记·stm32·mcu·学习·嵌入式·freertos·实时操作系统
天吾cc11 天前
FreeRTOS 事件标志组
freertos
编码追梦人12 天前
基于 ESP32-S3 的便携式多参数环境监护仪设计:从硬件选型到 FreeRTOS 多任务落地
数据库·单片机·嵌入式硬件·mongodb·esp32·freertos·低功耗设计
天吾cc16 天前
FreeRTOS 队列集
freertos
硅基手札16 天前
操作系统设计思想系列 06 · FreeRTOS 深度 — 极简可移植范本
架构·操作系统·freertos·实时操作系统·嵌入式开发·设计思想