深入解析 F´ 框架的被动速率组组件 Svc::PassiveRateGroup:端口架构、上下文配置与执行时间统计
【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime
导读
Svc::PassiveRateGroup是 F´(F Prime)飞行软件与嵌入式系统框架中的被动速率组组件:它自身不发起任何调用,而是通过一个同步Svc::Cycle输入端口接收来自周期驱动器(如RateGroupDriver、LinuxTimer)的驱动信号,然后按顺序依次调用一组连接在Svc::Sched输出端口上的"速率组成员",并为每个成员传入配置好的上下文(context)参数。本文以该组件的软件设计文档(Svc/PassiveRateGroup/docs/sdd.md)为核心骨架,结合 FPP 模型、C++ 实现、配置文件与单元测试,完整讲解其端口定义、configure()配置方式、周期执行流程、遥测通道、CLEAR_STATISTICS命令以及无锁原子统计的实现原理。读完本文,你将掌握如何在 F´ 部署中实例化、配置并验证一个被动速率组,并能正确理解其计时统计与RawTimeSource的使用限制。
1. 组件定位与设计需求
1.1 什么是被动速率组
在 F´ 中,速率组(Rate Group)是周期性任务调度的基本单元:一个"调度器"组件(如Svc::RateGroupDriver、Svc::LinuxTimer)在固定的周期到达时发出Svc::Cycle信号,速率组组件收到信号后依次调用其下游的Svc::Sched端口,从而驱动各任务组件(如遥测打包器、健康检查组件等)执行周期性工作。
Svc::PassiveRateGroup是其中的"被动"形态:它没有自己的线程,完全由外部输入端口CycleIn的同步调用驱动,调用返回即处理结束。与之相对的Svc::ActiveRateGroup则是主动形态,自带消息队列与任务线程(参见 Svc/ActiveRateGroup/docs)。
1.2 需求列表
设计文档(sdd.md)为组件定义了以下可追溯需求,均通过代码审查(Inspection)与单元测试(Unit test)验证:
| 需求编号 | 描述 | 验证方式 |
|---|---|---|
| FPRIME-PRG-001 | Svc::PassiveRateGroup组件应为被动组件,由同步输入端口调用驱动 | 审查、单元测试 |
| FPRIME-PRG-002 | 组件应按顺序调用其输出端口,并传递基于端口号的表(context 数组)中的值 | 单元测试 |
| FPRIME-PRG-003 | 组件应跟踪速率组周期的执行时间并作为遥测上报 | 单元测试 |
| FPRIME-PRG-004 | 当配置开启时,组件应跟踪每个端口的执行时间及高水位(high water mark) | 单元测试 |
| FPRIME-PRG-005 | 组件应提供清除统计信息与高水位的命令 | 单元测试 |
这些需求在单元测试中被显式引用:例如PassiveRateGroupTester::runNominal()中REQUIREMENT("FPRIME-PRG-001")、REQUIREMENT("FPRIME-PRG-002")等(见 Svc/PassiveRateGroup/test/ut/PassiveRateGroupTester.cpp)。
2. 架构与端口定义
2.1 组件框图
组件的行为图(BDD)如下,清晰展示了被动组件PassiveRateGroup的输入、输出端口与信号流向:
PassiveRateGroup 组件行为图,展示 CycleIn 输入端口与 RateGroupMemberOut Sched 输出端口等接口
2.2 端口清单
组件使用的端口类型由设计文档给出,并与 FPP 模型(Svc/PassiveRateGroup/PassiveRateGroup.fpp)完全一致:
| 端口数据类型 | 名称 | 方向 | Kind | 用途 |
|---|---|---|---|---|
| Svc::Cycle | CycleIn | 输入 | 同步 | 接收一次速率组周期运行的调用 |
| Svc::Sched | RateGroupMemberOut | 输出 | n/a | 速率组端口(数组端口) |
| Fw::Cmd | CmdDisp | 输入 | 同步 | 命令接收端口 |
| Fw::CmdResponse | CmdStatus | 输出 | n/a | 命令响应端口 |
| Fw::CmdReg | CmdReg | 输出 | n/a | 命令注册端口 |
| Fw::Tlm | Tlm | 输出 | n/a | 遥测端口 |
| Fw::Time | Time | 输出 | n/a | 时间获取端口 |
其中RateGroupMemberOut是数组端口,端口数量由 FPP 常量PassiveRateGroupOutputPorts决定(默认部署配置中为 5 个,即上图中Sched [5])。FPP 模型中对应声明如下:
sync input port CycleIn: Cycle output port RateGroupMemberOut: [PassiveRateGroupOutputPorts] Sched其余Time、Tlm、CmdDisp、CmdStatus、CmdReg为 F´ 组件的标准端口:组件通过这些端口向时间服务取时间、上报遥测、接收命令、回复命令状态并注册命令。
3. 配置接口 configure():上下文数组与计时源
3.1 两种配置重载
组件在运行前必须通过configure()完成配置(见 Svc/PassiveRateGroup/PassiveRateGroup.cpp 与头文件 Svc/PassiveRateGroup/PassiveRateGroup.hpp):
推荐形式(新接口):
void configure(const ContextArray& contexts, const Os::RawTimeSource rawTimeSource = Os::RAWTIME_DEFAULT);其中ContextArray是Fw::Array<U32, CONNECTION_COUNT_MAX>,CONNECTION_COUNT_MAX等于输出端口总数NUM_RATEGROUPMEMBEROUT_OUTPUT_PORTS(见 PassiveRateGroup.hpp)。每个元素对应一个输出端口,周期执行时该值会作为Sched端口调用的参数传给对应的速率组成员。
已弃用形式(旧接口):
DEPRECATED(void configure(const U32 contexts[], const FwIndexType numContexts), ...)该重载接收裸数组指针与元素个数,内部断言numContexts必须等于RateGroupMemberOut输出端口数,否则触发FW_ASSERT,随后转换为ContextArray再委托给新接口。源码中明确标记为 deprecated,建议新代码直接使用ContextArray版本(PassiveRateGroup.hpp)。
3.2 配置的内部处理
configure()实现的关键点(PassiveRateGroup.cpp):
void PassiveRateGroup::configure(const ContextArray& contexts, const Os::RawTimeSource rawTimeSource) { static_assert(FW_NUM_ARRAY_ELEMENTS(m_contexts) == NUM_RATEGROUPMEMBEROUT_OUTPUT_PORTS, "Context table size must match the number of rate group member output ports"); this->m_numContexts = CONNECTION_COUNT_MAX; for (FwIndexType entry = 0; entry < this->m_numContexts; entry++) { this->m_contexts[entry] = contexts[static_cast<FwSizeType>(entry)]; } this->m_rawTimeSource = rawTimeSource; }- 通过
static_assert在编译期强制上下文表大小与输出端口数一致; - 将上下文数组逐元素拷贝到私有存储
m_contexts[],索引即端口号; - 保存
m_rawTimeSource,供后续创建Os::RawTime计时对象使用。
构造函数中所有统计量被初始化为零:m_cycles(0)、m_maxTime(0)、m_portCycleTimeHWMUsec{}(原子数组零初始化),m_rawTimeSource默认Os::RAWTIME_DEFAULT(PassiveRateGroup.cpp)。
3.3 ⚠️ RawTimeSource 使用限制(重要)
设计文档特别强调了一个容易踩坑的限制:configure()中的RawTimeSource参数只影响每个周期的"结束"时间戳(end timestamp)。而"开始"时间戳(cycleStart)来自周期驱动器(如RateGroupDriver、LinuxTimer),驱动器构造其Os::RawTime时使用的是RAWTIME_DEFAULT。
因此:
- 如果为
PassiveRateGroup配置了与周期驱动器不同的计时源,Os::RawTime会拒绝计算该时间区间(POSIX 实现返回INVALID_PARAMS),导致CycleTime被上报为零; - 该特性仅在以下两种情况正确:
- 使用默认值
RAWTIME_DEFAULT; - 周期驱动器使用了相同的非默认计时源;
- 使用默认值
- 若希望在整个部署范围内更换时钟(例如改用
CLOCK_MONOTONIC),正确做法是覆盖config/Os/RawTimeSource.hpp,直接修改RAWTIME_DEFAULT的定义,而不是在此处传入非默认源。
头文件注释中也给出了同样的警告(PassiveRateGroup.hpp),实现中源码注释明确"a mismatched RawTimeSource (see sdd.md) leaves cycleTime at zero"(PassiveRateGroup.cpp)。单元测试runRawTimeSourceTest()验证了默认源下复制构造与赋值能保持 source,且默认源下周期时间非零(PassiveRateGroupTester.cpp)。
4. 周期执行流程
4.1 调用序列
组件在CycleIn输入端口上收到一次调用后,遍历所有RateGroupMemberOut输出端口,对已连接的端口依次调用并传入对应的上下文值。设计文档给出的序列图如下:
4.2 CycleIn_handler 实现剖析
核心实现位于CycleIn_handler(PassiveRateGroup.cpp),流程如下:
- 预创建计时对象:在循环外预先分配
Os::RawTime对象(endTime、portStart、portEnd),避免循环内重复构造的开销; - 依次调用成员端口:
for (FwIndexType port = 0; port < this->getNum_RateGroupMemberOut_OutputPorts(); port++) { if (this->isConnected_RateGroupMemberOut_OutputPort(port)) { if (Svc::PassiveRateGroupCfg::PortCycleTime) { (void)portStart.now(); } this->RateGroupMemberOut_out(port, this->m_contexts[port]); ... } }- 未连接的端口会被跳过(
isConnected_RateGroupMemberOut_OutputPort); - 若
PassiveRateGroupCfg::PortCycleTime开启,则在该端口调用前后分别取时间,计算该端口的执行时间并更新对应高水位;
- 计算周期总耗时:调用结束后取
endTime.now(),用endTime.getDiffUsec(cycleStart, cycleTime)计算与开始时间戳的差值(微秒); - 无锁更新最大时间:使用
std::atomic<U32>+compare_exchange_weak循环,仅在当前周期时间超过记录值时更新m_maxTime; - 周期计数自增:
U32 cycles = ++this->m_cycles;(单写者,无需原子保护); - 发送遥测:按配置发送
PortCycleTime、PortCycleTimeHWM、MaxCycleTime、CycleTime、CycleCount五个通道。
4.3 无锁原子统计(ISR 安全)
设计文档特别说明:所有统计量均由无锁原子操作保护,可在中断上下文(ISR)中使用,且消除了互斥锁开销。源码印证如下:
m_maxTime与m_portCycleTimeHWMUsec[]均为std::atomic<U32>(PassiveRateGroup.hpp);- 最大值更新采用标准的 CAS 乐观重试模式,
memory_order_relaxed即可满足需求(PassiveRateGroup.cpp):
U32 currentMax = this->m_maxTime.load(std::memory_order_relaxed); while (cycleTime > currentMax) { if (this->m_maxTime.compare_exchange_weak(currentMax, cycleTime, std::memory_order_relaxed)) { break; } }m_cycles为普通U32,因为在整个速率组中只有一个写者(组件自身的CycleIn调用上下文),无需原子保护;- 高水位数组在发送遥测前整体拷贝到普通数组,再调用
tlmWrite_PortCycleTimeHWM。
5. 遥测通道详解
组件提供 5 个遥测通道(FPP 模型定义见 PassiveRateGroup.fpp,设计文档表格):
| 通道 | 类型 | 描述 | 发送语义 |
|---|---|---|---|
| MaxCycleTime | U32 | 速率组周期最大执行时间(微秒) | update on change,仅当最大值增大时发送;可由CLEAR_STATISTICS清零 |
| CycleTime | U32 | 当前周期执行时间(微秒) | 每个周期都发送 |
| CycleCount | U32 | 累计执行周期数 | 每个周期都发送,不会被CLEAR_STATISTICS清零 |
| PortCycleTime | U32[PassiveRateGroupOutputPorts] | 最近一个周期内每个端口的执行时间(微秒) | 每个周期都发送(仅当PassiveRateGroupCfg::PortCycleTime开启) |
| PortCycleTimeHWM | U32[PassiveRateGroupOutputPorts] | 每个端口执行时间的高水位(微秒) | update on change,仅当任一高水位增大时发送;可由CLEAR_STATISTICS清零(仅当PassiveRateGroupCfg::PortCycleTime开启) |
FPP 模型中的对应声明:
telemetry MaxCycleTime: U32 update on change format "{} us" telemetry CycleTime: U32 format "{} us" telemetry CycleCount: U32 array CycleTime = [PassiveRateGroupOutputPorts] U32 default 0 telemetry PortCycleTime: CycleTime telemetry PortCycleTimeHWM: CycleTime update on change注意PortCycleTime与PortCycleTimeHWM的发送语义差异:前者无update on change修饰、每周期必发;后者有该修饰,未变化时会被遥测采集框架去重。单元测试runPortCycleTimeTest()对此做了专门验证:连续跑多个周期后,PortCycleTime每次发送,而PortCycleTimeHWM只有在高水位增大时才发送(将高水位手动置为 999999 后,后续周期 HWM 不再发送,见 PassiveRateGroupTester.cpp)。
6. CLEAR_STATISTICS 命令
组件支持唯一的同步命令CLEAR_STATISTICS(PassiveRateGroup.fpp),语义如下:
- 将
m_maxTime(最大周期时间)清零; - 将每个端口的
m_portCycleTimeHWMUsec(高水位)清零; - 不会重置
m_cycles(周期计数),因为它是一个运行累计值; - 清零操作全部使用原子
store,保证 ISR 安全; - 命令处理完成后通过
cmdResponse_out回发Fw::CmdResponse::OK。
实现(PassiveRateGroup.cpp):
void PassiveRateGroup::CLEAR_STATISTICS_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { this->m_maxTime.store(0, std::memory_order_relaxed); // Note: m_cycles is intentionally NOT cleared - it's a running total for (FwIndexType port = 0; port < this->getNum_RateGroupMemberOut_OutputPorts(); port++) { this->m_portCycleTimeHWMUsec[static_cast<FwSizeType>(port)].store(0, std::memory_order_relaxed); } this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); }单元测试runClearStatisticsTest()(PassiveRateGroupTester.cpp)完整验证了该命令:先跑 5 个周期积累统计,发送CLEAR_STATISTICS后确认收到OK响应,再跑 1 个周期,验证最大时间被重置、高水位从零重新开始(HWM == 当前周期时间),而CycleCount变为 6(5 + 1,未被清零)。
7. 配置开关:PassiveRateGroupCfg::PortCycleTime
是否逐端口测量执行时间由部署级配置头文件控制:default/config/PassiveRateGroupCfg.hpp。
namespace Svc { namespace PassiveRateGroupCfg { // Enable runtime measurement of per-output port execution time on the Svc.PassiveRateGroup component // This will take an Os::RawTime measurement before and after each RateGroupMemberOut port is invoked // and may incure some overall overhead on the execution time of the rate group. // Set to true by default to ensure FPRIME-PRG-004 per-port telemetry is tested in CI constexpr bool PortCycleTime = true; } // namespace PassiveRateGroupCfg } // namespace Svc要点:
- 默认值为
true,保证 CI 中 FPRIME-PRG-004 对应的逐端口遥测始终被测试; - 开启后,每个
RateGroupMemberOut端口调用前后各取一次Os::RawTime时间戳,这会带来额外的执行开销(配置注释明确说明 "may incur some overall overhead"); - 若对周期时间非常敏感、不需要逐端口统计,可将其改为
false,此时CycleIn_handler跳过逐端口计时,且不发送PortCycleTime/PortCycleTimeHWM遥测(单元测试的 else 分支验证了禁用时这两个通道发送次数为 0)。
8. 单元测试与验证
组件的单元测试位于 Svc/PassiveRateGroup/test/ut,由PassiveRateGroupTestMain.cpp(测试入口)与PassiveRateGroupTester.cpp/PassiveRateGroupTester.hpp(GTest 测试组件)组成,注册方式见 Svc/PassiveRateGroup/CMakeLists.txt。共 4 个测试用例(PassiveRateGroupTestMain.cpp):
| 测试用例 | 验证内容 |
|---|---|
| NominalSchedule | 3 个组件实例各跑一轮名义周期:所有已连接端口被按顺序调用(order == portNum)、上下文值正确传递(contextVal == contexts[portNum])、三类遥测各发送一次且值非零 |
| PortCycleTimes | 逐端口计时与高水位语义:首周期 HWM == CycleTime,后续 HWM 只增不减、与update on change去重行为一致 |
| ClearStatistics | CLEAR_STATISTICS命令:重置最大时间与 HWM、保留 CycleCount、返回 OK |
| RawTimeSourceConfiguration | RawTimeSource配置 API:默认源下 RawTime 复制/赋值保持 source,周期时间非零 |
这些测试在测试驱动中通过usleep(1)制造最小延时,确保周期时间大于 0 微秒;同时通过REQUIREMENT(...)宏将每个断言与设计文档中的需求编号(FPRIME-PRG-001 ~ 005)建立可追溯关联。
9. 状态与算法
- 状态:
Svc::PassiveRateGroup不包含任何状态机,行为完全由CycleIn的同步调用驱动; - 算法:组件没有复杂的调度算法,核心逻辑即"顺序遍历 + 上下文传递 + 计时统计",计时与最大值更新均通过
Os::RawTime与无锁原子操作完成。
10. 小结与变更记录
Svc::PassiveRateGroup是 F´ 中结构最精简、行为最可预期的周期调度组件之一:一个同步输入、一组Sched输出、一份上下文表、一套微秒级计时统计与一条清零命令。其设计要点可归纳为:
- 被动驱动:无线程、无队列,完全由外部
CycleIn同步调用驱动(FPRIME-PRG-001); - 按序分发:按端口号顺序调用成员,传递上下文表中的值(FPRIME-PRG-002);
- 计时统计:周期总耗时与逐端口耗时(可选)、高水位、周期计数(FPRIME-PRG-003/004);
- ISR 安全:统计量全部采用无锁原子操作,可在中断上下文并发访问;
- 运维便利:
CLEAR_STATISTICS命令一键重置最大时间与高水位,且不丢失运行累计周期数(FPRIME-PRG-005); - 配置纪律:如需更换计时时钟,应通过
config/Os/RawTimeSource.hpp统一修改RAWTIME_DEFAULT,而不是在configure()中传非默认源。
设计文档变更记录显示,该文档自 2017 年 2 月 9 日首次成稿,2026 年 8 月 8 日更新了CLEAR_STATISTICS命令、遥测通道、标准端口说明及无锁原子操作实现(sdd.md)。对于需要在 F´ 部署中实现高确定性周期调度与执行时间观测的场景,PassiveRateGroup是一个轻量且值得优先考虑的选择。
【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考