Hthsm:保持C应用程序整洁的分层有限状态机框架

2021-01-05 15:23:34

如果您正在研究Bluetooth LE固件应用程序,那么您可能会熟悉“很好,这很快就变得很复杂”。在这篇文章中,我建议您采取一些灵丹妙药:一种经过实践检验的方法,可以使BLE固件保持结构化,可维护和可维护。如果您要处理的固件项目不拘一格,请放心一些,并检查这对您的项目有什么帮助。

低功耗蓝牙固件应用程序具有其固有的复杂性。它们被构造为事件驱动的系统。这基本上意味着他们对随时可能发生的“事件”做出反应。这些物理事件源包括按钮按下,电池警报和传感器输入,还包括来自应用程序的传入蓝牙通信。根据我们的经验,即使像Nordic Semiconductors这样的顶级芯片供应商也提供了有关如何最佳地组织事件和反应系统的出色指南。任何合理的开发人员都可以通过正在开发的功能来开始将代码中的事件源链接到其相应的响应。这个补丁网络的复杂性很快成倍增长,并开始感觉到需要固件才能完全理解它们。随着项目的发展,事件反应映射的数量也在增加,并且可能需要在不同的操作模式下动态地重新映射。如果不加以治疗,这种痛苦可以使您获得最大的收益,传播的速度比添加功能要快。如果您像我们一样,那将不可避免地发生在面对面的音乐时刻,这种事件驱动的意大利面条式代码将不再被容忍。

这不是您用牙齿咬的糯米粉。这种类型可以编译为字节和字节以进行处理。如果您不了解意大利面条代码及其令人讨厌的效果,那么请简要回顾一下。 “意大利面条式代码”是具有复杂结构的代码的语,以至于难以调试和维护。

错误:最糟糕的错误。当您对应用程序的结构失去控制时,这些丑陋的本应避免的错误会不断出现。

代码重复。完全不必要的重复方式。当用于处理事件的系统的结构不良时,开发人员将难以在类似事件和操作状态之间重新利用代码。例如,“电池充电”和“充电完成”状态应该能够优雅地共享许多与充电相关的功能。

由于您无法维护的代码库,孤独感(也称为人员配置问题)。没有人喜欢独自工作,因此以一种让您的同事和未来的同事珍惜的方式重组您的应用程序。

我想推荐一个对我有用的系统-两步,让像我们这样的开发人员从一开始就免疫意大利面条。

使用原子事件队列。事件队列允许您将代码的那些部分合并到一个位置,而不是对整个不同的中断执行上下文中的事件做出反应。它是您将事件处理从许多上下文(例如中断)“推迟”到一个主要上下文的管道。通过避免在不同上下文中并发访问共享资源,它还解决了线程/中断安全性问题。例如,您不希望在两个上下文中使用SPI外设可能会相互中断。相反,两个上下文都可以将事件排入队列,以供主要功能按顺序执行SPI操作。

使用分层状态机(HSM)和框架来实现。 HSM是我们意粉敌人的淘汰赛。它提供了一种直观的确定性方案来定义在各种操作模式下如何处理事件。 HSM是“分层的”,从某种意义上说,每个状态都可以配置为从“超级状态”继承行为,“超级状态”是封装它以及可能包含其他状态的更通用的状态。 HSM的分层方面使它们在实际固件应用中比在教科书平面状态机上更加实用。 “电池充电”和“充电完成”状态可以从“充电器连接”状态继承并共享大部分代码。

事件队列是事件驱动架构的本质。如果您已经在使用它们并了解它们的优点,则可能需要跳到下一部分。

固件事件通常在硬件或软件驱动的中断上下文中到达。对于一些非常简单的事件,您可能会在中断中立即处理它们。但是,通常这是不好的做法。有一个常见的嵌入式开发诫命:

免疫步骤1是使用原子队列将事件处理推迟到主上下文中的一个位置。这使我们对嵌入的诫命有所抱怨,并为第二步奠定了基础。

事件队列范例已经存在了一段时间。没有它,许多开发人员将很难开发复杂的应用程序。您可以使用队列来处理所有事件,并将它们集中到一个上下文中,一次可以处理它们。立即获取线程/中断安全的温馨保证,因为事件处理不会散布在各种重叠的执行上下文中。

有礼貌的硅供应商通常会为开发人员提供某种形式的原子队列。 Nordic为nRF52开发人员提供了“应用程序计划程序”。德州仪器(TI)为CC2640开发人员提供了TI-RTOS随附的队列模块。就我们的意图和目的而言,这两个模块只是功能等效队列的不同包装。

// eventHandler始终在" main"中执行contextvoid eventHandler(Event event){switch(event){case TIMER_FIRED:perform_periodic_task(); start_timer();打破;情况BUTTON_PRESS:react_to_button_press();打破;案例BUTTON_RELEASE:react_to_button_release();打破;默认值:break; }} //中断处理程序void timerFired_interrupt(void){//将事件添加到事件队列// //将其处理推迟到主上下文queue_push(TIMER_FIRED);} void buttonPress_interrupt(void){queue_push(BUTTON_PRESS);} void buttonRelease_interrupt(void ){queue_push(BUTTON_RELEASE);} int main(void){//初始化和配置按钮中断initialize_buttons(); initialize_timer(); start_timer(); //(;;)的主循环{// idle()保持低功耗状态,直到发生事件idle();。 while(!queue_empty()){事件event = queue_dequeue(); eventHandler(event); }}}

至此,您的所有事件处理都已推迟并合并到一个主上下文中,例如上面示例的“ eventHandler()”函数。但是,仍然需要系统地处理事件。与步骤1中的示例不同,大多数固件项目将包含更多事件,这些事件的处理方式会随时间而变化。此步骤建议使用分层状态机来干净地实现事件处理,以适应整个操作的不同模式/状态。

如果您正在开发产品(例如低功耗可穿戴设备),那么您的设备肯定具有状态。大多数状态是明确且易于定义的。我说的是“开”,“关”,低功耗运输模式,已连接,已断开等。在此基础上,状态机用于封装每种不同操作状态的事件处理行为。

此外,您可能会发现这些州(包括其他州在内的州)之间的共性或“层次结构”。从状态继承包含状态的状态(超状态)的意义上讲,HSM是“分层的”。这种层次结构使您的代码保持简洁明了,在相似状态之间共享事件处理子例程。

在状态机对状态行为的合并/封装与层次结构方面对复杂共享状态行为的管理之间,我们现在有了打击意大利面条式代码的方法。

如果您发现本节令人生畏,请跳至底部的好内容:指向我们称为HTHSM的开源HSM实现的链接。 HTHSM的Github文档提供了实施HSM的实际示例。

如果您想了解更多,我建议您研究Miro Samek。他是所有事件驱动固件的专家,并在“事件驱动系统的状态机”中清楚地说明了状态机的情况。像我们一样,他在“分层状态机简介”中以其实用性来拥护HSM。

但是,状态机的所有实现都不完全相同。如果您准备实施一台有价值的状态机,则下面是我经过反复考验的建议。

对于每个状态,请使用一个函数来处理其事件。使用函数指针来引用活动状态的事件处理程序。当事件发生时,此“活动状态”指针用于调用适当的处理程序。在状态之间转换时,您可以简单地将该指针的值更改为所需的目标状态。

实施自动退出和输入状态转换事件。他们是天赐之物。当您转换状态时,活动状态会自动清除,新状态(目标)将有机会解决。这不是一个大主意,实际上只是基本的礼貌。如果您的开启状态通过天哪打开了LED,那么完成后应该适当关闭它。定义明确的退出&使用Enter键可以轻松编写在整个执行过程中保持确定性行为的固件。

无政府主义者该死,状态机是有效的。在概念上与OOP中的“覆盖”类似,事件分派从活动状态开始,并通过其上层状态向上进行。子状态有一个选项可以抑制其超级状态的事件处理。出口与出入境输入事件应该足够聪明,以确定在超级状态链中的哪些位置需要执行它们(在最不常见的祖先之下)。我反映了米罗·萨梅克(Miro Samek)的观点,即没有等级制,状态机将失去很多实用性。

想知道我是否要让您从头开始实施自己的HSM吗?绝对不。卑微晶体管制造了一个称为HTHSM的开源HSM框架。我们在内部使用它,很高兴与您分享。您可以在Github上的HTHSM存储库中访问示例和源代码。

我希望您发现这篇文章有益,甚至可能有所帮助。如果您在项目中使用HSM,对本文有任何反馈或有任何疑问,请随时通过[email protected]与我联系。我很高兴收到您的来信。