提问人:sole_developer_as_a_junior 提问时间:6/7/2023 最后编辑:sole_developer_as_a_junior 更新时间:6/10/2023 访问量:114
如何处理嵌入式系统实例(每个客户端)之间可能不同的函数和数组,而不诉诸预处理器滥用?
How to deal with functions and arrays which might differ between embedded system instances (per client) without resorting to pre-processor abuse?
问:
描述我是一家控制公司嵌入式系统产品的唯一开发人员/设计者/维护者。我是一名大三学生,公司里没有其他工程师(任何学科)。语言是 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_PATTERN2
case ALARM_TOPIC_PROBE3_FAULT: {/*Do something5*/}
如果客户决定他们不需要第三个探测器,那么我离简单地从 customer-config 模块中的枚举中删除并让警报模块正确确认该更改有多接近?ALARM_TOPIC_PROBE3_FAULT
Alarm_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
这似乎是我唯一的选择。
我是否已经搞砸了项目的组织/结构? 有没有处理类似问题的开源项目? 先谢谢你!
答:
一种选择是在客户配置文件中定义事件处理程序函数指针数组,并将指针用于未使用的警报处理程序。您可以使用共享模块中的常见事件处理程序函数填充数组,或者在需要时在客户配置模块中定义特殊情况的事件处理程序。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
评论
-ffunction-sections -Wl,--gc-sections
评论
#ifdef CUSTOMER_USES_PROBE3
#ifdef ENABLE_PROBE3
#ifdef