我正在用 C 语言为带有 RTOS 的 RAM 受限嵌入式微控制器编程。
我经常将我的代码分解为短函数,但每个函数调用都需要更多的堆栈内存。每个任务都需要他的堆栈,这是项目中重要的内存消耗者之一。
是否有替代方法可以使代码保持良好的组织性和可读性,同时保留内存?
尝试使调用堆栈更平坦,而不是a()
调用b()
which calls c()
which calls d()
, have a()
call b()
,c()
和d()
它本身。
如果一个函数只被引用一次,标记它inline
(假设你的编译器支持这个)。
您的堆栈使用有 3 个组件:
最小化堆栈使用的关键是最小化参数传递和自动变量。实际函数调用本身的空间消耗相当小。
参数
解决参数问题的一种方法是传递结构(通过指针)而不是大量参数。
foo(int a, int b, int c, int d)
{
...
bar(int a, int b);
}
改为这样做:
struct my_params {
int a;
int b;
int c;
int d;
};
foo(struct my_params* p)
{
...
bar(p);
};
如果您传递大量参数,则此策略很好。如果参数都不同,那么它可能不适合您。您最终会得到一个包含许多不同参数的大型结构。
自动变量(局部变量)
这往往是堆栈空间的最大消耗者。
请记住,如果您只是将所有局部变量从局部范围移动到模块范围,您并没有节省任何空间。您用堆栈空间换取了数据段空间。
一些 RTOS 支持线程本地存储,它在每个线程的基础上分配“全局”存储。这可能允许您在每个任务的基础上拥有多个独立的全局变量,但这会使您的代码不那么简单。
如果您可以节省大量主内存但只有一小部分堆栈,我建议评估静态分配。
在 C 中,函数内声明的所有变量都是“自动管理的”,这意味着它们是在堆栈上分配的。
将声明限定为“静态”将它们存储在主内存中而不是堆栈中。它们基本上表现得像全局变量,但仍然允许您避免过度使用全局变量带来的坏习惯。您可以为将大的、长期存在的缓冲区/变量声明为静态的,以减少堆栈压力。
请注意,如果您的应用程序是多线程的或使用递归,这将无法正常工作/根本无法正常工作。
打开优化,特别是激进的内联。编译器应该能够内联方法以最小化调用。根据您使用的编译器和优化开关,将某些方法标记为inline
可能有帮助(或者可能会被忽略)。
使用 GCC,尝试添加“-finline-functions”(或 -O3)标志和可能的“-finline-limit=n”标志。
为了评估嵌入式设置中代码的堆栈要求,我在某处读到的一个技巧是在开始时用已知模式填充堆栈空间(十六进制的 DEAD 是我最喜欢的)并让系统运行一段时间。
正常运行后,读取堆栈空间,看看在运行过程中有多少堆栈空间没有被替换。设计为至少保留 150% 的空间,以解决所有可能未执行的模糊代码路径。
你可以用全局变量替换一些局部变量吗?特别是数组可以吃掉堆栈。
如果情况允许您在函数之间的某些全局变量之间共享一些全局变量,那么您就有可能减少内存占用。
权衡成本是增加了复杂性,以及功能之间不必要的副作用的更大风险与可能更小的内存占用空间。
您的函数中有哪些类型的变量?我们在谈论什么尺寸和限制?
根据您的编译器以及您的优化选项的积极程度,您将在每次调用函数时使用堆栈。因此,首先您可能需要限制函数调用的深度。一些编译器确实对简单的函数使用跳转而不是分支,这将减少堆栈的使用。显然,您可以通过使用汇编程序宏来跳转到您的函数而不是直接函数调用来做同样的事情。
正如其他答案中提到的,内联是一种可用的选项,尽管这确实是以更大的代码大小为代价的。
吃堆栈的另一个区域是本地参数。您确实可以控制这个区域。使用(文件级)静态将避免以静态内存分配为代价的堆栈分配。全球人也一样。
在(真正的)极端情况下,您可以为使用固定数量的全局变量作为临时存储来代替堆栈上的局部变量的函数提出约定。棘手的一点是确保不会同时调用任何使用相同全局变量的函数。(因此公约)
如果您需要开始保留堆栈空间,您应该获得更好的编译器或更多内存。
您的软件通常会增长(新功能,...),因此如果您必须通过考虑如何保留堆栈空间来启动项目,那么它从一开始就注定要失败。
是的,RTOS 确实会占用 RAM 以供任务堆栈使用。我的经验是,作为 RTOS 的新用户,往往会使用不必要的任务。
对于使用 RTOS 的嵌入式系统,RAM 可能是一种珍贵的商品。为了保留 RAM,对于简单的功能,在一个任务中实现多个功能仍然是有效的,以循环方式运行,并采用协作多任务设计。从而减少任务总数。
我想你可能在想象一个这里不存在的问题。大多数编译器在堆栈上“分配”自动变量时实际上并没有做任何事情。
在执行“main()”之前分配堆栈。当您从函数 a() 调用函数 b() 时,紧接 a 使用的最后一个变量之后的存储区地址将传递给 b()。如果 b() 然后调用函数 c() 然后 c 的堆栈在 b() 定义的最后一个自动变量之后开始,这将成为 b() 的堆栈的开始。
请注意,堆栈内存已经存在并已分配,没有进行初始化,唯一涉及的处理是传递堆栈指针。
这成为问题的唯一一次是所有三个函数都使用大量存储空间,然后堆栈必须容纳所有三个函数的内存。尝试将分配大量存储的函数保留在调用堆栈的底部,即不要从它们调用另一个函数。
内存约束系统的另一个技巧是将函数的内存占用部分拆分为单独的自包含函数。