C语言 如何在不滥用预处理器的情况下处理嵌入式系统示例(每个客户端)之间可能不同的函数和数组?

bfnvny8b  于 2023-06-21  发布在  其他
关注(0)|答案(1)|浏览(72)

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

例如,由所有可能的警报主题的枚举馈送的 * 警报模块 * 接收所有相关数据点上的更新,处理所有可能的警报主题触发器,跟踪每个警报主题已经活动了多长时间,并且每个警报主题具有其自己的可听蜂鸣器模式。
然而现在,我们开始为不同的客户开发这个嵌入式系统的示例,每个客户都有自己的功能子集。在报警模块的范围内,这可能意味着他们说“我们不关心ALARM 2,但我们关心ALARM 1和ALARM 3”。
在理想情况下,例如具有更多RAM和非易失性存储器的非嵌入式系统,超集模块将保持原样,并且我们只是在存储器中拥有我们从未触及的数据,函数和路径,或者可能在运行时系统将读取配置并分配适当的内存。然而,这是一个受限的嵌入式系统,并且使用malloc、具有不必要的RAM分配和死代码路径是不可接受的。
我的目标是引入模块化,以便有核心应用程序模块(alarm-module)链接到模块库(每个客户一个)。这意味着我们不必修改应用程序模块,而只需将其链接到适合客户的适当库。

超集报警模块具体示例

#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-module 有使用枚举的特定成员作为数组指示符[ALARM_TOPIC_PROBE3_FAULT] = BUZZER_PATTERN2的数组和直接引用枚举成员case ALARM_TOPIC_PROBE3_FAULT: {/*Do something5*/}的方法。
如果客户决定他们不需要第三个探测器,我可以在 customer-config 模块中从Alarm_Topics_E枚举中删除ALARM_TOPIC_PROBE3_FAULT,并让 alarm-module 正确确认该更改吗?
如果我只是简单地删除枚举成员,代码显然无法编译。
如果我在 customer-config 中使用预处理器定义/编译标志,例如#define USE_PROBE3,那么 alarm-module 将充满

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

如果我将受此更改影响的方法移动到 customer-config 模块中,并单独维护它们,我还必须将 alarm-module 的大量静态成员移动到 customer-config 模块中,这些静态成员感觉不属于那里。此外,我还必须将 customer-config 模块与“Buzzer.h”链接起来,这也感觉不对。
我可以在启动时解析配置结构,但我仍然必须动态分配数组内存(巨大的NO-NO),我仍然会有死代码路径(轻微的NO-NO,但我可以在必要时解决)。
我有什么选择?我真的必须求助于预处理器滥用,并将每个受影响的部分都 Package 在 alarm-module 中吗?

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

这似乎是我唯一的选择
我是否已经搞砸了我的项目的组织/结构?是否有任何开源项目处理类似的问题?感谢您的评分

wxclj1h5

wxclj1h51#

一个选项是在客户配置文件中定义一个事件处理程序函数指针数组,并为未使用的报警处理程序使用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

相关问题