基于事件或轮询的嵌入式MCU系统架构?

9
我有编写基于事件和轮询的嵌入式系统的经验(用于没有抢占式操作系统的微型MCU)。
在事件驱动系统中,任务通常在队列上接收事件(消息)并依次处理它们。
在轮询驱动系统中,任务以一定的间隔轮询状态并响应变化。
你更喜欢哪种架构?两者可以共存吗?
更新:提出的观点
轮询驱动 - 与时间相关的紧密耦合 (@Lundin) * 可以与使用队列的事件系统共存 (@embedded.kyle) * 对于较小的程序来说非常适用 (@Lundin)
事件驱动 + 长期来看更灵活的系统 (@embedded.kyle) - RTOS版本增加了复杂性 (@Lundin) * 小程序 = 状态机控制 (@Lundin) * 可以使用队列和“超级循环”(控制器/主内部)实现 (@embedded.kyle) * 真正的“事件”只是硬件中断事件 (@Lundin)

相关问题
* 寻找有限状态机不同调度算法的比较 (@embedded.kyle)

相关信息
* "使用Active Objects而非裸线程" (@Miro)
http://www.drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/225700095
* "正确使用线程=隔离+异步消息" (@Miro) http://www.drdobbs.com/parallel/use-threads-correctly-isolation-asynch/215900465


我无法访问也找不到“正确使用线程=隔离+异步消息”文章的任何地方。你本地有这个PDF吗?能否提供另一个镜像站点? - K. Koovalsky
这是链接:https://web.archive.org/web/20170521024735/http://www.drdobbs.com/parallel/use-threads-correctly-isolation-asynch/215900465 - K. Koovalsky
3个回答

6
在裸机MCU平台上真正没有所谓的“事件驱动”,尽管热门术语的使用者试图向你传达这种观点。你能收到的唯一类型的真实事件就是硬件中断。
根据应用程序的性质及其实时要求,中断可能或可能不适用。通常,使用轮询系统更容易实现确定性实时性。然而,仅依赖于轮询的系统很难维护,因为所有时间方面都存在紧密耦合。
假设您尝试启动一个速度较慢的LCD。您可以决定在此期间通过总线接收一些数据,而不是在空循环中反复轮询某个定时器并烧毁CPU周期。然后,您想打印在LCD上接收到的数据。这样的设计在LCD启动时间和串行总线之间创建了紧密的耦合,并在串行总线与数据打印之间创建了另一个紧密的耦合。从面向对象的角度来看,这些事情根本没有关联。如果将来某个时刻加速串行总线,则突然间您可能会遇到LCD打印错误,因为当您尝试在其上打印时,它还没有完成启动。
在小型程序中,像上述示例中那样使用轮询是完全可以的。但是,如果程序有扩展潜力,轮询将使它变得非常复杂,并且紧密耦合最终将导致许多奇怪和致命的错误。
另一方面,多线程和RTOS会增加相当多的额外复杂度,反过来也可能导致错误。在哪里划界并不容易确定。
从个人经验来看,我认为任何小于20-30k LOC的程序都不会从调度和多任务处理中受益,除了简单的状态机。如果程序大于这个规模,我会考虑使用多任务处理的RTOS。
此外,低端MCU(8位和16位)远非运行操作系统的理想选择。如果发现您需要一个OS来处理8位或16位平台上的复杂性,则可能选择的MCU就是错误的。对于任何小于32位的东西,我对引入操作系统的任何尝试持怀疑态度。

这些都是很好的评论,尽管我认为“事件驱动”系统有许多不同的类型,其中只有一个... - mhoff
抱歉,我被打断了...这些都是很好的评论,尽管我会认为“事件驱动”系统有许多不同的类型。例如,观察者(发布/订阅)设计确保解耦组件,但更直接的“发送到<收件人>”则不是。因此,事件并不能保证低耦合。我相信你也同意。 - mhoff
2
事件驱动编程在裸机微控制器上是完全可行的,也非常自然。请谷歌一下这个术语的定义。事件驱动编程需要事件对象,它们是通信的显式工件。从这个意义上讲,中断不是事件,而是中断可以生成事件对象并将其发布到事件队列中以供后续处理(异步通信)。 - Miro Samek
@Miro,那不是“裸机”微控制器。你所说的是某种形式的操作系统,或者至少是在你的应用程序之上的某种抽象层开销代码。 - Lundin

4
实际上,事件驱动编程和线程可以结合使用,这种模式被广泛称为“活动对象”或“actors”。
活动对象(actors)是封装的、事件驱动的状态机,它们通过向彼此发布事件来异步通信。活动对象在自己的执行线程中处理所有事件(至少在概念上,如果使用协作调度程序),因此它们按设计避免了大多数并发问题。
Actors和active objects在通用计算领域(你可以搜索Erlang、Scala、Akka)很受欢迎。Herb Sutter撰写了一些好的文章,解释了“活动对象”模式:“Prefer Using Active Objects Instead of Naked Threads”(http://www.drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/225700095)和“Use Threads Correctly = Isolation + Asynchronous Messages”(http://www.drdobbs.com/parallel/use-threads-correctly-isolation-asynch/215900465)。
以下是Herb在这些文章中提到的内容:
"直接使用原始线程存在很多问题...活动对象通过提供更高级的抽象和习语,显著改善了我们对线程代码和操作的理解能力,这提高了程序的语义水平,让我们更直接地表达意图。与所有良好的模式一样,我们也获得了更好的词汇来谈论我们的设计。请注意,活动对象并不是一项新颖的技术:UML和各种库已经为活动类提供了支持。"
"因此,所有这些并不是新的。但也许在嵌入式系统社区中尤其不为人知的是,活动对象不仅完全适用于嵌入式系统,而且它们实际上比传统的RTOS更加轻量级,是嵌入式系统的完美匹配。"
"我已经使用基于事件驱动的活动对象超过十年,并为嵌入式系统创建了QP系列活动对象框架(参见http://www.state-machine.com/)。我永远不会回到轮询“超级循环”或原始RTOS。"

1
你有没有调整每个活动对象允许处理的消息数量,然后将控制权返回给下一个对象的经验?调度是如何工作的? - mhoff

2
我更偏向于选择最适合当前应用的架构。
在多级队列架构中,两种方式可以共存。一个队列基于轮询方式在主循环中运行,而另一个队列则通过使用基于中断的抢占方式处理优先级更高的事件。
有关不同调度算法的详细解释和比较,请参见我对此SO问题的回答。

问题在于,我们必须致力于一种共同的架构,以便我们可以开发可重用的组件。同时进行这两种风格似乎是多余的工作,而且可能会产生冲突?感谢提供链接。 - mhoff
已经编辑了问题,以反映它是针对非抢占式操作系统的。 - mhoff
1
如果你必须致力于一个共同的架构,那么在前期开发更灵活的系统所需的额外工作将会在长期内得到回报。只要你设计过程时记住它们将从哪个队列运行,冲突就不应该成为问题。 - embedded.kyle
1
@mhoff,我实际上假设没有操作系统,这在嵌入式MCU设计中很常见。main()内部的超级循环进行轮询,事件处理发生在MCU的优先级中断控制器中。因此,抢占是控制器固有的,并且与任何操作系统无关。 - embedded.kyle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接