在 C 中为模块化固件设计分配内存的可能性

电器工程 C 设计 固件
2022-01-13 10:51:57

模块化方法通常非常方便(便携且干净),因此我尝试将模块编程为尽可能独立于任何其他模块。我的大多数方法都基于描述模块本身的结构。初始化函数设置主要参数,然后将处理程序(指向描述结构的指针)传递给模块内被调用的任何函数。

现在,我想知道为描述模块的结构分配内存的最佳方法可能是什么。如果可能的话,我想要以下内容:

  • 不透明结构,因此只能通过使用提供的接口函数来更改结构
  • 多个实例
  • 链接器分配的内存

我看到以下可能性,所有这些都与我的目标之一相冲突:

全球宣言

多个实例,由链接器分配,但结构不是不透明的

(#includes)
module_struct module;

void main(){
   module_init(&module);
}

malloc

不透明的结构,多个实例,但堆上的 allcotion

在 module.h 中:

typedef module_struct Module;

在 module.c 初始化函数中,malloc 并返回指向分配内存的指针

module_mem = malloc(sizeof(module_struct ));
/* initialize values here */
return module_mem;

在 main.c

(#includes)
Module *module;

void main(){
    module = module_init();
}

模块中的声明

由链接器分配的不透明结构,仅预定义数量的实例

将整个结构和内存保留在模块内部,并且永远不要暴露处理程序或结构。

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

是否可以选择以某种方式将这些组合起来用于不透明的结构、链接器而不是堆分配和多个/任意数量的实例?

解决方案

正如下面一些答案中所建议的那样,我认为最好的方法是:

  1. 在模块源文件中为 MODULE_MAX_INSTANCE_COUNT 个模块保留空间
  2. 不要在模块本身中定义 MODULE_MAX_INSTANCE_COUNT
  3. 在模块头文件中添加 #ifndef MODULE_MAX_INSTANCE_COUNT #error 以确保模块用户了解此限制并定义应用程序所需的最大实例数
  4. 在初始化实例时,返回描述性结构的内存地址 (*void) 或模块索引(无论您更喜欢什么)
4个回答

我用 C++ 编写小型微控制器,这正是你想要的。

您所说的模块是一个 C++ 类,它可以包含数据(外部可访问或不可访问)和函数(同样)。构造函数(专用函数)对其进行初始化。构造函数可以采用运行时参数或(我最喜欢的)编译时(模板)参数。类中的函数隐式地将类变量作为第一个参数。(或者,通常是我的偏好,该类可以充当隐藏的单例,因此可以访问所有数据而没有这种开销)。

类对象可以是全局的(因此您在链接时知道一切都适合),也可以是堆栈本地的,大概在主对象中。(由于未定义的全局初始化顺序,我不喜欢 C++ 全局变量,所以我更喜欢堆栈本地)。

我首选的编程风格是模块是静态类,它们的(静态)配置是通过模板参数。这避免了几乎所有的负担并实现了优化。将其与计算堆栈大小的工具结合使用,您就可以无忧无虑地睡觉了 :)

我对这种 C++ 编码方式的演讲:对象?不用了,谢谢!

许多嵌入式/微控制器程序员似乎不喜欢 C++,因为他们认为这会迫使他们使用所有C++。这绝对没有必要,而且将是一个非常糟糕的主意。(你可能也不使用所有的 C 语言!想想堆、浮点、setjmp/longjmp、printf 等)


在评论中,Adam Haun 提到了 RAII 和初始化。IMO RAII 更多地与解构有关,但他的观点是正确的:全局对象将在您的 main 开始之前构建,因此它们可能会在无效的假设下工作(例如稍后将更改的主时钟速度)。这是不使用全局代码初始化对象的另一个原因。(我使用的链接器脚本在我拥有全局代码初始化对象时会失败。)IMO 此类“对象”应显式创建并传递。这包括提供 wait() 函数的“等待”设施“对象”。在我的设置中,这是设置芯片时钟速度的“对象”。

谈论 RAII:这是另一个在小型嵌入式系统中非常有用的 C++ 功能,尽管不是因为它最常用于大型系统(小型嵌入式系统大多不使用动态内存释放)的原因(内存释放)。考虑锁定资源:您可以使锁定的资源成为包装器对象,并将对资源的访问限制为只能通过锁定包装器进行。当包装器超出范围时,资源被解锁。这可以防止在没有锁定的情况下进行访问,并且更不可能忘记解锁。使用一些(模板)魔法,它可以是零开销。


最初的问题没有提到 C,因此我以 C++ 为中心的答案。如果它真的必须是C....

您可以使用宏技巧:公开声明您的结构,因此它们具有类型并且可以全局分配,但是会破坏其组件的名称以超出可用性,除非某些宏的定义不同,例如模块的 .c 文件中的情况。为了获得额外的安全性,您可以在修改中使用编译时间。

或者有一个公共版本的结构,其中没有任何用处,而私有版本(带有有用的数据)仅在您的 .c 文件中,并断言它们的大小相同。一些制作文件的技巧可以自动执行此操作。


@Lundins 对糟糕(嵌入式)程序员的评论:

  • 您描述的程序员类型可能会在任何语言中造成混乱。宏(存在于 C 和 C++ 中)是一种明显的方式。

  • 工具可以在一定程度上有所帮助。对于我的学生,我要求构建一个脚本,该脚本指定无异常、无 rtti,并在使用堆或存在代码初始化的全局变量时给出链接器错误。它指定警告=错误并启用几乎所有警告。

  • 我鼓励使用模板,但是对于 constexpr 和概念,元编程的要求越来越少。

  • “困惑的 Arduino 程序员” 我非常想用现代 C++ 方法替换 Arduino(布线、库中的代码复制)编程风格,这样可以更容易、更安全,并生成更快、更小的代码。要是我有时间和力量就好了……

我相信 FreeRTOS(可能是另一个操作系统?)通过定义 2 个不同版本的结构来完成您正在寻找的事情。
由操作系统功能在内部使用的“真实”一个,以及一个与“真实”相同大小的“假”,但内部没有任何有用的成员(只是一堆int dummy1和类似的)。
只有“假”结构暴露在操作系统代码之外,这用于为结构的静态实例分配内存。
在内部,当调用 OS 中的函数时,它们被传递外部“假”结构的地址作为句柄,然后将其作为指向“真实”结构的指针进行类型转换,因此 OS 函数可以做他们需要做的事情做。

是否可以选择以某种方式将这些组合用于匿名结构、链接器而不是堆分配和多个/任意数量的实例?

当然有。然而,首先要认识到,“任意数量”的实例必须在编译时固定,或者至少建立一个上限。这是静态分配实例的先决条件(您称之为“链接器分配”)。您可以通过声明一个指定它的宏来使数字可调而无需修改源代码。

然后包含实际结构声明及其所有相关函数的源文件也声明了一个具有内部链接的实例数组。它提供了一个具有外部链接的指向实例的指针数组,或者提供了一个通过索引访问各种指针的函数。函数变体更加模块化:

模块.c

#include <module.h>

// 4 instances by default; can be overridden at compile time
#ifndef NUM_MODULE_INSTANCES
#define NUM_MODULE_INSTANCES 4
#endif

struct module {
    int demo;
};

// has internal linkage, so is not directly visible from other files:
static struct module instances[NUM_MODULE_INSTANCES];

// module functions

struct module *module_init(unsigned index) {
    instances[index].demo = 42;
    return &instances[index];
}

我想您已经熟悉头文件如何将结构声明为不完整类型并声明所有函数(根据指向该类型的指针编写)。例如:

模块.h

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

现在struct module在除module.c, *之外的翻译单元中是不透明的,您可以访问和使用最多在编译时定义的实例数,而无需任何动态分配。


*除非你复制它的定义,当然。关键是它module.h不会那样做。

匿名结构,因此只能通过使用提供的接口函数来更改结构

在我看来,这是没有意义的。您可以在那里发表评论,但没有必要进一步隐藏它。

C 永远不会提供如此高的隔离性,即使结构没有声明,也很容易被错误的 memcpy() 或缓冲区溢出等意外覆盖。

相反,只需为结构命名并相信其他人也能编写出好的代码。当结构具有可用于引用它的名称时,它还将使调试更容易。