使用 FPP 为 F Prime 组件定义与集成状态机【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime本文是一份基于 F PrimeF´框架的实战指南讲解如何用 F Prime 建模语言FPP为组件定义状态机并将其集成进queued/active组件的 C 实现中。文中以 IMU惯性测量单元驱动的启动与运行逻辑为完整示例覆盖状态机设计、FPP 建模、fprime-util impl代码生成、动作实现、信号发送与消息队列派发的全部流程读完本文你将掌握在 F Prime 项目中用状态机显式建模组件工作模式、并自动化生成实现代码的完整方法。[!NOTE] F Prime 与 F´ 是同一框架的两种写法。本文统一使用 F Prime并在文末提供术语表。前置条件开始之前建议先具备以下基础完成过 Hello World 教程至少构建并运行过一个组件。仓库内的入门资料可参考 docs/getting-started 与 docs/tutorials 目录下的文档。对 FPP 组件建模有基本了解FPP 中 Defining Components 一节的内容。有在 FPP 中创建命令command、事件event与遥测telemetry的经验。本机已有一份可用的 F Prime 构建环境fprime-util能正常执行。何时使用状态机F Prime 中的状态机就是计算机科学课程中讲授的有限状态机一组状态state、一组输入signal、以及描述收到某个输入时进入哪个状态的规则transition。如果你学过状态机那么这里的一切你都熟悉——FPP 只是提供了一种把它们写下来并自动生成实现的途径。当组件具有不同的模式或运行状态且各状态行为不同、状态之间存在明确的转移规则时状态机就非常有用。典型例子一台无线电设备具有OFF、IDLE、TRANSMITTING三个状态。一个传感器具有一系列启动状态。把这类行为建模为状态机可以让系统行为显式、可验证、更易测试。开发工作流为组件添加状态机遵循一条固定的流水线本文后续各节逐一讲解设计状态机确定状态、驱动它的信号、状态之间的转移以及每个状态下执行的动作。在 FPP 中定义状态机见 用 FPP 定义状态机。在queued或active组件中实例化状态机见 在组件中实例化状态机。使用fprime-util impl生成实现模板见 集成到 C。在 C 中实现动作处理器见 实现动作。从组件实现发送信号驱动状态机见 实现信号发送。派发组件的消息队列使信号得到处理见 派发状态机。示例状态机IMU 的启动与运行本文全程以 IMUInertial Measurement Unit惯性测量单元一种测量加速度与角速度的传感器的启动与运行逻辑为例。按 F Prime 命名规范类型与组件名使用 CamelCase因此缩写 IMU 在标识符中写作Imu例如ImuStateMachine与ImuManager。先看一个仅含两个状态的简化版本。设备必须先复位才能使用因此状态机从RESET状态开始复位成功后进入RUN读图约定对本文所有状态图均适用方框是状态。状态名下方以signal / action形式书写的文字表示该状态收到此信号时执行此动作。例如tick / doReset表示每次tick到达RESET状态时执行doReset动作。箭头是转移箭头标签为触发转移的信号。例如success使状态机从RESET转移到RUN。真实设备还需要更多步骤等待复位完成、使能数据流、配置设备。在简单两状态设计之上扩展就得到本文完整实现的状态机它具有如下性质初始状态为RESET其后依次为WAIT_RESET、ENABLE、CONFIGURE、RUN。每个状态中tick信号触发一个动作。动作可能产出success或error信号。success信号使流程线性推进到下一个状态。error信号使状态机回到RESET。当动作不产出任何信号时状态机停留在当前状态并重复执行该动作。该状态机的状态、动作与转移汇总如下状态tick上的动作success时进入error时进入RESETdoResetWAIT_RESET停留在RESETWAIT_RESETcheckResetENABLERESETENABLEdoEnableCONFIGURERESETCONFIGUREdoConfigureRUNRESETRUNdoRead停留在RUNRESET[!NOTE] 在原示例的完整实现fprime-sensors 社区的 MpuImu 组件中还额外实现了一个reconfigure信号允许状态机回到重新配置状态。本文聚焦基础机制仓库中的ImuManager组件由速率组rate group处理器驱动tick信号。用 FPP 定义状态机下面用 FPP 建模Imu状态机。本文将状态机放在与ImuManager组件同目录即同一 module的独立文件ImuStateMachine.fpp中你也可以把状态机直接内联在组件定义里。先定义基本模块与状态机module MpuImu { state machine ImuStateMachine }模块名、文件名等均与示例代码以及使用该状态机的ImuManager组件保持一致。下面将逐步构建同一个状态机定义每个代码片段中用hl_lines高亮标注了该步骤新增的行。定义状态机与初始状态第一步是为状态机命名并定义初始状态RESETmodule MpuImu { Define ImuStateMachine State Machine state machine ImuStateMachine { Initial state: reset the device initial enter RESET Reset the Imu state RESET { } } }initial enter RESET声明了状态机的初始转移状态机启动时首先进入RESET状态。定义信号并添加更多状态接下来定义success、error、tick三个信号以及其余状态WAIT_RESET、ENABLE、CONFIGURE、RUN。至此状态机的整体结构状态与信号已齐备但还没有任何转移逻辑module MpuImu { Define ImuStateMachine State Machine state machine ImuStateMachine { Initial state: reset the device initial enter RESET Rate-group driven tick signal signal tick Current state passed successfully signal success Current state erred signal error Reset the Imu state RESET Wait for the Imu to reset state WAIT_RESET Enable Imu data flows state ENABLE Configure Imu state CONFIGURE Run the Imu state RUN } }signal关键字声明状态机的输入事件。tick由速率组驱动success/error由动作即用户 C 代码产出用于驱动状态推进。定义转移下一步为状态之间添加转移。转移transition使状态机在收到某信号后从一个状态移动到另一个状态。这里使用on语法处理信号并指定要进入的下一个状态从而形成线性推进与回到RESET的行为module MpuImu { Define ImuStateMachine State Machine state machine ImuStateMachine { Initial state: reset the device initial enter RESET Rate-group driven tick signal signal tick Current state passed successfully signal success Current state erred signal error Reset the Imu state RESET { on success enter WAIT_RESET } Wait for the Imu to reset state WAIT_RESET { on success enter ENABLE on error enter RESET } Enable Imu data flows state ENABLE { on success enter CONFIGURE on error enter RESET } Configure Imu state CONFIGURE { on success enter RUN on error enter RESET } Run the Imu state RUN { on error enter RESET } } }[!NOTE]RESET状态没有定义error转移因为出错时状态机应停留在RESET同理RUN状态没有定义success转移成功时应停留在RUN。状态未处理的信号在该状态内被直接忽略。定义动作动作action是状态机执行的一段由用户提供的 C 代码它与改变当前状态的转移不同。动作可以出现在转移中、响应信号时以及状态进入/退出时并回调到组件的 C 实现中从而允许用户自定义行为例如通过 I2C 与 IMU 通信。本例使用tick信号触发动作。tick从速率组处理器rate-group handler调用把动作限定在tick信号上可以保证每次速率组调用只执行一个状态及其对应的单次I2C 通信。这样做是为了让速率组调用时长具有确定性同时避免 I2C 总线被争用。使用action关键字定义动作doReset、checkReset、doEnable、doConfigure、doRead。用on signal do { action }语法指定每次tick信号到达时执行的动作module MpuImu { Define ImuStateMachine State Machine state machine ImuStateMachine { Initial state: reset the device initial enter RESET Rate-group driven tick signal signal tick Current state passed successfully signal success Current state erred signal error Perform reset commands action doReset Check if reset completed action checkReset Perform enable commands action doEnable Perform configure commands action doConfigure Read the IMU action doRead Reset the Imu state RESET { on success enter WAIT_RESET on tick do { doReset } } Wait for the Imu to reset state WAIT_RESET { on success enter ENABLE on error enter RESET on tick do { checkReset } } Enable Imu data flows state ENABLE { on success enter CONFIGURE on error enter RESET on tick do { doEnable } } Configure Imu state CONFIGURE { on success enter RUN on error enter RESET on tick do { doConfigure } } Run the Imu state RUN { on error enter RESET on tick do { doRead } } } }[!NOTE] 关于书写顺序与信号处理顺序有几点值得注意状态内部on处理器的书写顺序无关紧要。每个状态对同一信号至多处理一次因此on success与on tick之间不存在歧义——执行哪个处理器由到达的信号决定而不是由它们在文件中的先后顺序决定。信号发送的顺序很重要。信号被放入组件的消息队列按发送顺序逐条处理。每个信号都被完整处理动作执行、转移完成之后才处理下一个信号。如果组件代码发送了多个信号例如某个动作发送success的同时已有一个tick在排队这些信号不会重叠每个信号按序排队并在它被派发时针对状态机当时的所处状态依次处理。到此状态机定义完成。但还需要把它绑定到ImuManager组件上。更丰富的建模能力仓库中的内部状态机本文示例使用的外部状态机behavior 由组件 C 实现提供只是 FPP 状态机能力的一部分。在仓库的测试工程 FppTestProject/FppTest/state_machine 中可以看到大量内部状态机behavior 直接用 FPP 表达的示例它们在基础语法之上补充了更多可用的建模元素entry / exit 动作与转移附带动作在 Basic.fppi 中状态S声明exit do { a }并在on s do { a, a } enter T中于转移同时执行动作状态T声明entry do { a, a, a }。可见动作不仅可由信号触发还可挂在状态的进入/退出以及转移上。嵌套层次状态机StateToChild.fppi 在状态S1内嵌套定义了子状态S2、S3各自拥有entry/exit动作展示了状态内再定义状态的层次化建模。内部转移Internal.fppi 在父状态S1中通过on S1_internal do { a }定义不改变当前状态的内部转移同时保留子状态间的on S2_to_S3 enter S3转移。选择choice与守卫guardBasic.fppi 声明guard g并用choice C { if g do { a } enter S2 else do { b } enter S3 }实现基于守卫条件的分支转移Choice.fppi 则展示了initial do { a } enter C的初始选择——初始转移也可先进入一个 choice 节点做条件分流。这些示例说明on signal [do { actions }] enter state、entry/exit、initial、guard、choice共同构成了 FPP 状态机的完整语法面足以表达线性的状态推进、条件分支与层次化模式。在组件中实例化状态机每个状态机都可以被实例化多次。要为ImuManager组件定义一个挂接其上的实例在组件定义中使用queued component ImuManager { Use the ImuStateMachine state machine instance imuStateMachine: ImuStateMachine }状态机信号通过组件的消息队列异步送达。这正是只有queued与active组件才能包含状态机的原因passive组件没有消息队列信号无处可去。这里选择了queued组件它要求组件自行派发队列。这让我们可以在速率组调用内部同步地处理tick信号进而执行 I2C 工作。active组件同样可行——其内部线程会自动派发队列——但此时信号会在速率组处理器返回之后、于组件自身线程上的某个时刻才被处理而不是作为速率组调用的一部分。换言之active组件同样能正确运行状态机只是不再保证工作发生在速率组调用期间——而本例恰恰依赖这一点来获得确定性时序。[!WARNING] 只有queued与active组件可以包含状态机。选择queued组件的用户必须像处理其他组件消息如命令、端口调用一样自行派发状态机消息active组件则由内部线程派发全部消息。多实例、优先级与队列溢出策略一个组件可以持有同一状态机的多个实例。仓库测试组件 SmTest.fpp 展示了这一点并为每个实例指定了不同的priority与队列处理策略state machine instance device1: DeviceSm priority 1 block state machine instance device2: DeviceSm priority 2 assert state machine instance device3: HackSm priority 3 drop state machine instance device4: HackSm priority 4 hook state machine instance device5: HackSm从源码结构看priority后接的关键字用于声明消息队列满时的处理策略block阻塞、assert断言失败、drop丢弃信号、hook调用溢出钩子回调。例如hook策略对应生成 SmTest.cpp 中的device4_stateMachineOverflowHook回调允许用户自定义溢出时的行为不显式指定策略的实例如device5则采用默认行为。在 SmTest.cpp 的schedIn_handler中每个实例通过生成的deviceN_stateMachineInvoke(Signal, data)函数被显式驱动这也是外部状态机实例的一种调用方式。集成到 C接下来把状态机集成进组件的 C 实现。与其他 FPP 构造一样运行fprime-util impl即可获得所需的原型与模板其中就包括待填充的动作处理器。实现动作动作必须由组件为状态机实现。fprime-util impl会在.template.hpp中提供如下原型该原型应放入组件的 HPP 文件HPP 中的函数原型//! Implementation for action doReset of state machine MpuImu_ImuStateMachine //! //! Perform reset commands void MpuImu_ImuStateMachine_action_doReset(SmId smId, //! The state machine id MpuImu_ImuStateMachine::Signal signal //! The signal ) override;随后在 CPP 文件中填充实现。下面调用一个辅助函数reset并根据返回值决定是否输出错误CPP 中的函数实现void ImuManager ::MpuImu_ImuStateMachine_action_doReset(SmId smId, MpuImu_ImuStateMachine::Signal signal) { Drv::I2cStatus status this-reset(); // Transition to RESET state on failure if (status ! Drv::I2cStatus::I2C_OK) { this-log_WARNING_HI_I2cError(DEVICE_ADDRESS, status); } else { // TODO: success } }[!WARNING] 必须实现组件中的所有动作方法。本文仅为简洁起见只展示doReset完整实现可参考原示例的ImuManager.cpp。关于生成的代码形态仓库中的外部状态机 DeviceSm.cpp 提供了一个直观参照状态机被编译为一个update(stateMachineId, signal, data)方法内部采用双层switch——外层按this-state分支到各状态内层按到达的信号分支匹配时依次调用用户实现的动作、转移动作并更新state。FW_ASSERT(0)落在默认分支上用于捕获非法状态/信号组合。实现信号发送下一步是添加信号发送。本例需要发送success与error信号同时要在速率组调用run_handler中发送tick。信号通过调用this-state_machine_instance_name_sendSignal_signal_name();发送。下面在doReset实现中加入 if 块根据状态检查结果发送相应信号void ImuManager ::MpuImu_ImuStateMachine_action_doReset(SmId smId, MpuImu_ImuStateMachine::Signal signal) { // This function is implemented only for the specific instance imuStateMachine FW_ASSERT(smId SmId::imuStateMachine); Drv::I2cStatus status this-reset(); // Transition to RESET state on failure if (status ! Drv::I2cStatus::I2C_OK) { this-log_WARNING_HI_I2cError(DEVICE_ADDRESS, status); this-imuStateMachine_sendSignal_error(); } else { this-imuStateMachine_sendSignal_success(); } }[!TIP] 动作函数是类型层面通用的即属于ImuStateMachine类型而信号通过实例发送即imuStateMachine。用断言锁定状态机 id可以防止把信号派发到错误的实例。当单个组件持有某状态机的多个实例时可以在动作函数内用switch-case分派到不同的信号发送函数switch (smId) { case SmId::imuStateMachine1: this-imStateMachine1_sendSignal_success(); break; case SmId::imuStateMachine2: this-imStateMachine2_sendSignal_success(); break; }发送tick信号发生在速率组调用run_handler中采用同样的结构void ImuManager ::run_handler(FwIndexType portNum, U32 context) { this-imuStateMachine_sendSignal_tick(); }派发状态机最后因为我们选择了queued组件需要派发状态机消息。这一步在run_handler中使用dispatchCurrentMessages()辅助函数完成void ImuManager ::run_handler(FwIndexType portNum, U32 context) { this-imuStateMachine_sendSignal_tick(); this-dispatchCurrentMessages(); }[!WARNING] 只有queued组件应以这种方式派发队列消息active组件使用自己的线程派发。this-dispatchCurrentMessages()会派发发给该组件的全部消息状态机信号、异步命令、异步端口调用等。到这一步状态机就可以运行了。信号数据的传递状态机信号不仅可以驱动转移还能携带数据。仓库在 Fw/Sm/SmSignalBuffer.hpp 中定义了Fw::SmSignalBufferusing SmSignalBuffer LinearBufferTemplateFW_SM_SIGNAL_BUFFER_MAX_SIZE;它基于Fw::LinearBufferTemplate为状态机信号提供定长缓冲区。在外部状态机场景中生成的调用接口stateMachineInvoke(signal, data)与状态机的update(stateMachineId, signal, data)方法都会携带一个const Fw::SmSignalBuffer data参数参见 DeviceSm.cpp 与 SmTest.cpp用户动作可以从中读取与信号一同到达的数据。仓库中的验证状态机测试FPP 状态机的语义在仓库测试工程中有系统性验证是理解各语法元素行为的最佳参考Basic.cpp 是Basic内部状态机的单元测试先断言初始化后处于S状态且动作历史为空发送一次s信号后断言进入T再次发送s断言仍停留在T因为T未处理s最终断言动作历史大小与内容验证on s do { a, a } enter T与entry do { a, a, a }的动作执行次数2 次转移动作 1 次 entry 动作 后续忽略 6 次。SmTest.fpp 与 SmTest.cpp 验证外部状态机在active组件中的多实例、多优先级策略block/assert/drop/hook与溢出钩子。state_machine 目录 下的internal内部状态机语法测试与internal_instance内部状态机挂入组件实例的测试子目录覆盖了 choice、guard、嵌套状态、内部转移、初始选择等全部扩展语法并附有 README 说明各自验证点。这些测试同时证明了信号发送的顺序决定处理顺序、未处理信号被忽略、动作按entry/on/exit的语义精确执行——与上文 FPP 建模部分描述的行为完全一致。结论FPP 中的状态机让你能够显式地捕获运行模式、强制合法转移、确保组件行为可预期。它尤其有助于减少处理状态变更的代码量、减少状态变量的数量并对高层行为进行建模。你可以结合本文示例与仓库中 state_machine 测试集 深入理解如何处理新的转移如reconfigure以及如何把状态机完整地集成进组件。术语表术语定义State machine状态机由有限状态、信号与转移构成的行为模型与计算机科学中的有限状态机同义。State状态状态机可以处于的不同模式之一如RESET、RUN。任意时刻状态机恰好处于一个状态。Signal信号发送给状态机的输入事件如tick、success、error。信号可触发动作与转移。Transition转移响应某信号从一种状态变化到另一种状态。Action动作由状态机执行的用户提供的 C 代码例如响应某信号或作为转移的一部分。Component组件F Prime 软件的基本单元带有类型化输入/输出端口、封装某种行为的模块。Passive / Queued / Active componentF Prime 的三种组件类型。passive组件没有消息队列或线程queued组件有消息队列由组件自身派发active组件有消息队列和自行派发它的独立线程。Port端口组件间通信的类型化连接点。Rate group速率组F Prime 中按固定周期如 1 Hz调用组件的机制。FPPF Prime 建模语言F Prime Prime用于定义组件、端口、拓扑与状态机并从中生成 C 代码。Autocoding自动编码由 FPP 模型自动生成 C 代码。IMUInertial Measurement Unit惯性测量单元——测量加速度与角速度的传感器按命名规范代码标识符中写作Imu。I2C常用于与 IMU 这类传感器通信的串行总线。Dispatch派发处理组件消息队列上等待的消息信号、命令、端口调用。参考资源本文源头文档docs/how-to/develop/define-state-machines.md仓库状态机测试与示例FppTestProject/FppTest/state_machine内部状态机语法示例Basic.fppi、StateToChild.fppi、Internal.fppichoice 与 guard 示例Basic.fppi、Choice.fppi外部状态机多实例示例SmTest.fpp、SmTest.cpp生成的状态机代码形态DeviceSm.cpp信号数据缓冲区Fw/Sm/SmSignalBuffer.hpp状态机单元测试Basic.cppF Prime 入门与教程docs/getting-started、docs/tutorials【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考