如何处理嵌入式系统实例(每个客户端)之间可能不同的函数和数组,而不诉诸预处理器滥用?

How to deal with functions and arrays which might differ between embedded system instances (per client) without resorting to pre-processor abuse?

提问人:sole_developer_as_a_junior 提问时间:6/7/2023 最后编辑:sole_developer_as_a_junior 更新时间:6/10/2023 访问量:114

问:

描述我是一家控制公司嵌入式系统产品的唯一开发人员/设计者/维护者。我是一名大三学生,公司里没有其他工程师(任何学科)。语言是 C(C99 标准),构建系统是 CMake。嵌入式系统是裸机STM32 MCU,具有8k RAM和64k FLASH。 到目前为止,该应用程序只有一个“实例”,即所有功能的超集。

例如,由所有可能的警报主题的枚举馈送的警报模块接收所有相关数据点的更新,处理所有可能的警报主题触发器,跟踪每个警报主题的活动时间,并且每个警报主题都有自己的声音蜂鸣器模式。

然而,现在,我们开始为不同的客户端开发这个嵌入式系统的实例,每个客户端都有自己的功能子集。在警报模块的范围内,这可能意味着他们说“我们不关心 ALARM2,但我们确实关心 ALARM1 和 ALARM3”。

在理想情况下,例如具有更多RAM和非易失性存储器的非嵌入式系统,超集模块将保持原样,我们只是在内存中拥有我们从未接触过的数据,或者在运行时系统将读取配置并分配适当的内存。但是,这是一个受限制的嵌入式系统,使用 malloc、不必要的 RAM 分配和死代码路径是不可接受的。

我的目标是引入模块化,以便有核心应用程序模块(报警模块)链接到模块库(每个客户一个)。这意味着我们不必修改应用程序模块,只需将其链接到相应客户的相应库即可。

“Super-Set”报警模块的具体示例

#include "Counter.h" //Count time
#include "Buzzer.h" //Produce audible alarm patterns

typedef enum  
{
ALARM_TOPIC_NONE = 0U,
ALARM_TOPIC_TEMPERATURE_RANGE,
ALARM_TOPIC_PROBE1_FAULT,
ALARM_TOPIC_PROBE2_FAULT,
ALARM_TOPIC_PROBE3_FAULT,
ALARM_TOPIC_EXTERNAL,
ALARM_TOPIC_EXT_SERIOUS,
ALARM_TOPIC_MAX, //Used to set array-lengths and loop conditions
} Alarm_Topics_E;

//Holds the current "signal state" of an alarm topic.
//Separate from "raised state" of an alarm topic
static bool alarm_conditions[ALARM_TOPIC_MAX] = {
[ALARM_TOPIC_NONE]              = true,
[ALARM_TOPIC_TEMPERATURE_RANGE] = false,
[ALARM_TOPIC_PROBE1_FAULT]      = false,
[ALARM_TOPIC_PROBE2_FAULT]      = false,
[ALARM_TOPIC_PROBE3_FAULT]      = false,
[ALARM_TOPIC_EXTERNAL]          = false,
[ALARM_TOPIC_EXT_SERIOUS]       = false,
};
static bool raised_alarms[ALARM_TOPIC_MAX] = {0};

//Holds all counter structs for each alarm topic
static Counter_T alarm_counters[ALARM_TOPIC_MAX] = {0};

static const Buzzer_Pattern_E alarm_pattern_table[ALARM_TOPIC_MAX] = {
[ALARM_TOPIC_NONE]              = BUZZER_PATTERN_QUIET,
[ALARM_TOPIC_TEMPERATURE_RANGE] = BUZZER_PATTERN1,
[ALARM_TOPIC_PROBE1_FAULT]  = BUZZER_PATTERN2,
[ALARM_TOPIC_PROBE2_FAULT]      = BUZZER_PATTERN3,
[ALARM_TOPIC_PROBE3_FAULT]      = BUZZER_PATTERN4,
[ALARM_TOPIC_EXTERNAL]          = BUZZER_PATTERN3,
[ALARM_TOPIC_EXT_SERIOUS]       = BUZZER_PATTERN5,
};

void alarm_event_handler(const Args_Generic_T *generic_args) {
//Extract information from arguments, such as alarm_topic
    switch(alarm_topic)
    {
        case ALARM_TOPIC_NONE:         {/*Do something1*/}
        case ALARM_TOPIC_TEMPERATURE_RANGE: {/*Do something2*/}
        case ALARM_TOPIC_PROBE1_FAULT: {/*Do something3*/}
        case ALARM_TOPIC_PROBE2_FAULT: {/*Do something4*/}
        case ALARM_TOPIC_PROBE3_FAULT: {/*Do something5*/}
        case ALARM_TOPIC_EXTERNAL:     {/*Do something6*/}
        case ALARM_TOPIC_EXT_SERIOUS:  {/*Do something7*/}
    }
}

/* Below are other methods which handle updating alarm_condition, raised_alarm, and alarm_counters.
These methods are agnostic to specifics about the Alarm_Topics enum. 
For example, the method which handles alarm counters might look like such */
static void alarm_handle_condition(Alarm_Topics_E topic, bool alarm_condition) {
    /* business logic */
    if (topic < ALARM_TOPIC_MAX) {
        update_counter(&alarm_counters[topic]);
    }
    /* what do ya know more business logic */
}

现在,由于警报主题列表可能会因客户而异,因此我将Alarm_Topics_E枚举放入其自己的模块中,该模块可以按客户进行维护。我们称这个模块为 customer-config

但是,超集报警模块具有使用枚举的特定成员作为数组指示符的数组和直接引用枚举成员的方法的数组。[ALARM_TOPIC_PROBE3_FAULT] = BUZZER_PATTERN2case ALARM_TOPIC_PROBE3_FAULT: {/*Do something5*/}

如果客户决定他们不需要第三个探测器,那么我离简单地从 customer-config 模块中的枚举中删除并让警报模块正确确认该更改有多接近?ALARM_TOPIC_PROBE3_FAULTAlarm_Topics_E

如果我只是删除了枚举成员,则代码显然不会编译。

如果我在我的客户配置中使用了预处理器定义/编译标志,例如,则警报模块将充满#define USE_PROBE3

#ifdef USE_PROBE3
/*activity related to probe3*/
#endif

如果我将受此更改影响的方法移动到 customer-config 模块中,并单独维护它们,我还必须将 alarm-module 的大量静态成员移动到 customer-config 模块中,这些成员感觉不属于那里。此外,我必须将 customer-config 模块链接到“Buzzer.h”,这也感觉不对。

我可以在启动时解析配置结构,但我仍然必须动态分配阵列内存(HUGE NO-NO),并且我仍然会有死代码路径(MINOR NO-NO,但如有必要,我可以解决)。

我在这里有什么选择?我真的必须求助于预处理器滥用,并用

#ifdef USE_PROBE3
/*activity related to probe3*/
#endif

这似乎是我唯一的选择。

我是否已经搞砸了项目的组织/结构? 有没有处理类似问题的开源项目? 先谢谢你!

嵌入式 C 预处理器 组织

评论

2赞 Andrew Henle 6/7/2023
您还考虑过哪些其他选择?关于预处理器滥用,请将 pr similar 替换为 or similar。不要将任何一个功能的实现与任何客户绑定 - 仅将他们绑定到他们启用的功能。并保持每个功能完全独立。如果您将客户与功能混合在一起和/或使功能相互依赖,您将迅速达到无法维持的复杂程度。#ifdef CUSTOMER_USES_PROBE3#ifdef ENABLE_PROBE3
3赞 pmacfarlane 6/7/2023
撇开一点不谈,但你可能不需要将函数包装在 s 中,你可以告诉链接器丢弃未使用的函数。这是STM32CubeIDE项目的默认配置,也可以添加到您使用的任何工具中。不是你问题的答案,但也许少了一件需要担心的事情。#ifdef
2赞 pmacfarlane 6/7/2023
@AndrewHenle我的脑海中有一个答案的草图,但现在是凌晨 1 点,我需要睡觉。如果明天晚上之前没有其他人回答,我会尝试回答。
1赞 user3386109 6/7/2023
另一种选择是编写代码生成器。生成器的输入是超集模块,以及不需要的功能列表。生成器输出一个 C 文件,其中删除了不需要的功能。当您独自工作时,代码生成器很好。但它们可以为新团队成员创造一个陡峭的学习曲线。因此,请务必创建培训手册,并在添加新功能时保持最新状态。
1赞 Lundin 6/7/2023
这听起来像是应该由版本控制而不是源代码处理的事情。每个配置使用不同的分支,尽可能少地更改源文件。应该避免预处理器 #ifdef 混乱,它们根本无法很好地扩展,如果您继续添加它们,那么最终一切都变得无法阅读。

答:

0赞 remcycles 6/7/2023 #1

一种选择是在客户配置文件中定义事件处理程序函数指针数组,并将指针用于未使用的警报处理程序。您可以使用共享模块中的常见事件处理程序函数填充数组,或者在需要时在客户配置模块中定义特殊情况的事件处理程序。NULL

然后,可以编写“Super-Set”警报模块,通过将语句替换为类似 .这会将逻辑从编译时移动到运行时,但对于此应用程序来说,这可能不是严重的性能损失。“业务逻辑”可以用类似的方式处理,使用另一个函数指针数组。switch(alarm_topic)if (alarm_event_handlers[alarm_topic]) { alarm_event_handlers[alarm_topic](args); }

作为参考,本文介绍了函数指针数组在嵌入式系统中的其他用途,包括语法示例: https://barrgroup.com/embedded-systems/how-to/c-function-pointers

评论

1赞 Lundin 6/7/2023
“这可能不适合将链接器配置为丢弃未使用的函数,因为它可能不会注意到你是否在数组中包含函数指针。这应该不是问题。只要你通过获取函数的地址来引用它,它就会被链接。
0赞 remcycles 6/9/2023
好点子。我尝试使用 GCC () 并且奏效了。我建议使用您使用的任何工具运行简单的测试。-ffunction-sections -Wl,--gc-sections