9

在许多嵌入式应用程序中,需要在使代码非常高效或将代码与特定系统配置隔离以不受不断变化的需求影响之间进行权衡。

您通常采用哪种 C 结构来实现两全其美(灵活性和可重构性而不损失效率)?

如果您有时间,请继续阅读以了解我在说什么。

当我为安全气囊控制器开发嵌入式软件时,我们遇到的问题是,每次客户改变对特定要求的想法时,我们都必须更改代码的某些部分。例如,在开发过程中,触发安全气囊展开的条件和事件的组合每隔几周就会发生变化。我们讨厌如此频繁地更改那段代码。

当时,我参加了嵌入式系统会议,并听到了 Stephen Mellor 的精彩演讲,题为“应对不断变化的需求”。你可以在这里阅读这篇论文(他们让你注册,但它是免费的)。

这样做的主要思想是在代码中实现核心行为,但以数据的形式配置具体细节。您可以轻松更改数据,甚至可以在 EEPROM 或闪存的不同部分进行编程。

这个想法听起来很棒,可以解决我们的问题。我与我的同事分享了这一点,我们立即开始重新设计一些软件模块。

在我们的编码中尝试使用这个想法时,我们在实际实现中遇到了一些困难。对于受限的嵌入式系统,我们的代码结构变得非常繁重和复杂。

为了说明这一点,我将详细说明我上面提到的示例。我们没有使用一堆 if 语句来确定输入组合是否处于需要展开安全气囊的状态,而是改为使用一个大表。一些条件不是微不足道的,所以我们使用了很多函数指针来调用很多小辅助函数,这些函数以某种方式解决了一些条件。我们有几个间接级别,一切都变得难以理解。长话短说,我们最终使用了大量的内存、运行时和代码复杂性。调试这个东西也不是很简单。老板让我们把一些东西改回来,因为模块太重了(他也许是对的!)。

PS:SO中有一个类似的问题,但看起来重点不同。适应不断变化的业务需求?

4

7 回答 7

3

作为关于不断变化的需求的另一种观点......需求进入构建代码。那么为什么不采取元方法呢:

  1. 分离出程序中可能发生变化的部分
  2. 创建一个脚本,将部分源代码粘合在一起

这样,您就可以在 C 中维护兼容的逻辑构建块......然后在最后将这些兼容的部分粘在一起:

/* {conditions_for_airbag_placeholder} */
if( require_deployment)
     trigger_gas_release()

然后保持独立条件:

/* VAG Condition */
if( poll_vag_collision_event() )
    require_deployment=1

和另一个

/* Ford Conditions */
if( ford_interrupt( FRONT_NEARSIDE_COLLISION )) 
    require_deploymen=1

您的构建脚本可能如下所示:

BUILD airbag_deployment_logic.c WITH vag_events
TEST airbag_deployment_blob WITH vag_event_emitter

真的想出声来。这样,您无需读取配置文件即可获得紧凑的二进制 blob。这有点像使用覆盖 http://en.wikipedia.org/wiki/Overlay_(programming)但在编译时进行。

于 2009-06-11T21:03:27.070 回答
2

我们的系统被细分为许多组件,具有公开的配置和测试点。有一个在启动时读取的配置文件实际上可以帮助我们实例化组件,将它们相互连接,并配置它们的行为。

在 C 语言中,它非常类似于 OO,偶尔会通过 hack 来实现类似继承之类的东西。

在国防/航空电子世界中,软件升级受到非常严格的控制,您不能只升级软件来解决问题……但是,出于某种奇怪的原因,您可以在不费吹灰之力的情况下更新配置文件。因此,能够在这些配置文件中指定很多我们的实现对我们来说非常有用。

没有魔法,只是在设计系统时很好地分离了关注点,以及开发人员的一些远见。

于 2009-06-11T20:57:34.540 回答
2

你到底想保存什么?代码返工的努力?软件版本发布的繁文缛节?

更改代码可能相当简单,而且很可能比更改表中的数据更容易。仅当出于某种原因修改数据而不是代码所需的工作量更少时,将经常更改的逻辑从代码迁移到数据才会有帮助。如果更改以数据形式更好地表达(例如,存储在 EEPROM 中的数字参数),这可能是正确的。或者,如果客户的要求使得有必要发布新版本的软件,并且新的软件版本是一个昂贵的构建过程(大量的文书工作,或者可能是芯片制造商烧毁的 OTP 芯片),这可能是真的。

对于这类事情,模块化是非常好的原则。听起来好像你已经在某种程度上做到了。最好将经常更改的代码隔离到尽可能小的区域,并尝试将其余代码(“帮助”函数)分开(模块化)并尽可能稳定。

于 2009-06-12T04:39:17.053 回答
2

我不会让代码本身不受需求变化的影响,但我总是通过在注释中放置一个唯一的字符串来标记一段实现需求的代码。有了需求标签,我可以在需求需要更改时轻松搜索该代码。这种做法也满足 CMMI 过程。

例如,在需求文档中:

以下是与 RST 相关的要求列表:

  • [RST001] Juliet 应在点火开关关闭时延迟 5 分钟启动 RST。

在代码中:

/* Delay for RST when ignition is turned off [RST001] */
#define IGN_OFF_RST_DELAY 5

...snip...

                        /* Start RST with designated delay [RST001] */
                        if (IS_ROMEO_ON())
                        {
                            rst_set_timer(IGN_OFF_RST_DELAY);
                        }
于 2009-06-25T17:50:55.413 回答
1

我想您可以做的是根据可以从 EEPROM 或 I/O 端口(如有必要)获取的数据字节或字指定几种有效行为,然后创建通用代码来处理这些字节描述的所有可能事件。

例如,如果您有一个字节指定释放安全气囊的要求,它可能类似于:

位 0:后碰撞

位 1:速度超过 55 英里/小时(推广速度值的奖励积分!)

位 2:车上的乘客

...

ETC

然后你拉入另一个字节,说明发生了什么事件并比较两者。如果它们相同,请执行您的命令,否则不要执行。

于 2009-06-11T23:52:27.680 回答
1

为了适应不断变化的需求,我将专注于使代码模块化并易于更改,例如通过对可能更改的参数使用宏或内联函数。

编写一个可以独立于代码更改的配置,我希望在需求中也指定可重新配置的参数。特别是对于安全气囊控制器等安全关键的东西。

于 2009-06-12T06:45:01.243 回答
0

如果你有足够的内存和处理器能力,挂在动态语言中可以成为救命稻草。

让 C 与硬件对话,然后将一组已知事件传递给像 Lua 这样的语言。让 Lua 脚本解析事件并回调到适当的 C 函数。

一旦你的 C 代码运行良好,除非硬件发生变化,否则你将不必再次接触它。所有的业务逻辑都变成了脚本的一部分,在我看来更容易创建、修改和维护。

于 2009-06-11T21:31:36.240 回答