我该如何可视化AVR程序的内存(SRAM)使用情况?

25

我在一个运行在AVR微控制器(ATMega328P)上的C程序中遇到了问题。我认为这是由于堆栈碰撞引起的,但我想确认一下。

是否有任何方法可以可视化堆栈和堆对SRAM使用情况?

注意:该程序使用avr-gcc编译,并使用avr-libc。

更新:我遇到的实际问题是malloc实现失败(返回NULL)。所有malloc操作都发生在启动时,所有free操作都发生在应用程序结束时(实际上从不结束,因为应用程序的主要部分位于无限循环中)。所以我确定碎片不是问题。


5
哇,你一定是唯一一个在Atmega上使用malloc的人。我很惊讶它们居然还能工作!以前它们甚至都没有被包含在内。 - Myforwik
1
这里涵盖了一些有用的AVR特定的malloc内容:https://www.nongnu.org/avr-libc/user-manual/malloc.html - Cameron Tacklind
8个回答

26
你可以使用avr-size工具来检查RAM的静态使用情况,如下所述:
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=62968,
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=82536,
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=95638,
http://letsmakerobots.com/node/27115

avr-size -C -x Filename.elf

(avr-size文档: http://ccrma.stanford.edu/planetccrma/man/man1/avr-size.1.html )

以下是如何在IDE上设置的示例: 在Code::Blocks中,选择项目 -> 构建选项 -> 预/后构建步骤 -> 后构建步骤,并包括:

avr-size -C $(TARGET_OUTPUT_FILE)
avr-size -C --mcu=atmega328p $(TARGET_OUTPUT_FILE)

构建结束后的示例输出:

AVR Memory Usage
----------------
Device: atmega16

Program:    7376 bytes (45.0% Full)
(.text + .data + .bootloader)

Data:         81 bytes (7.9% Full)
(.data + .bss + .noinit)

EEPROM:       63 bytes (12.3% Full)
(.eeprom) 

数据是您的SRAM使用情况,这仅是编译器在编译时知道的数量。您还需要为运行时创建的内容腾出空间(特别是堆栈使用情况)。
要检查堆栈使用情况(动态RAM),请访问http://jeelabs.org/2011/05/22/atmega-memory-use/
以下是一个小型实用程序函数,可以确定当前未使用多少RAM:
int freeRam () {
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

这里是使用该代码的草图:

void setup () {
    Serial.begin(57600);
    Serial.println("\n[memCheck]");
    Serial.println(freeRam());
}

freeRam()函数返回堆的末尾和栈上最后一个分配内存之间存在多少字节,因此它实际上表示堆/栈在碰撞之前可以增长多少。

您可以在怀疑可能导致堆栈/堆碰撞的代码周围检查此函数的返回值。


2
+1 - 感谢提供链接!请注意,此处定义的 freeRam() 函数在释放的内存仍被 malloc 的“空闲列表”所持有的情况下会失败。这只有在频繁动态分配和释放内存的情况下才可能成为实际问题——在 AVR 上可能是一个罕见的场景(例如,在我的情况下,我仅在程序初始化期间动态分配内存,因此 freeRam() 代码对我有效)。 - Matthew Murdoch
你有关于freeRam函数的任何提示吗?如果我使用O3编译,它就不能正常工作了。我在itoa之后显示该值。(调试在O1下正常工作) - bemeyer
@BennX 我只在 O0 和 O1 上使用过它,而且它运行得很好。但是你可能知道,在 O3 上进行调试更加困难,所以我建议在你的程序还不够稳定时改为较低级别的优化 -- 尽管如果你正在使用小型微控制器,则可能需要注释掉一些部分。 - mMontu

14

你说malloc失败并返回NULL:

首先要看的明显原因是你的堆栈“满了” - 也就是说,你要求malloc分配的内存无法得到分配,因为它不可用。

需要注意两种情况:

a: 你有一个16K的堆栈,你已经使用了10K并尝试再分配10K。你的堆栈太小了。

b: 更常见的情况是,你有一个16K的堆栈,你一直在进行malloc/free/realloc调用,你的堆栈空间不到50%是“满”的状态:你调用了1K的malloc并且失败了-这是怎么回事?答案是-堆栈空闲空间被碎片化了-没有连续的1K空闲内存可以返回。当发生这种情况时,C堆栈管理器不能压缩堆栈,所以你通常处于糟糕的状态。有技术可以避免碎片化,但很难知道这是否真正是问题所在。你需要在malloc和free中添加日志记录装置,以便了解正在执行哪些动态内存操作。

编辑:

你说所有的malloc都发生在启动时,所以碎片化不是问题。

在这种情况下,应该很容易用静态内存替换动态分配。

旧代码示例:

char *buffer;

void init()
{
  buffer = malloc(BUFFSIZE);
}

新代码:

char buffer[BUFFSIZE];

在你到处都这样做后,链接器应该会提醒你如果所有内容都无法适应可用的内存。不要忘记减小堆的大小 - 但要注意有些运行时IO系统函数可能仍然使用堆,因此您可能无法完全删除它。


+1 很可能是a)。所有的malloc操作都发生在启动时,而所有的free操作都发生在应用程序结束时(实际上永远不会结束,因为应用程序的主要部分在一个无限循环中)。所以我确定碎片化不是问题。 - Matthew Murdoch
1
这是非常重要的一点!请不要在评论中留下这些小提示,而是尽可能地向问题本身添加更多的细节! - Artelius
@Artelius - 已将此添加到问题中。谢谢。 - Matthew Murdoch

4
不要在较小的嵌入式目标上使用堆/动态分配。特别是在资源有限的处理器上。相反,重新设计您的应用程序,因为随着程序的增长,问题会再次发生。

3
通常的方法是用已知模式填充内存,然后检查哪些区域被覆盖。

这是一个非常粗糙的做法。我个人建议使用avr-size命令+freeRam()方法,而不是采取如此激烈的措施... - Matthias Hryniszak
@MatthiasHryniszak 我理解你为什么认为这是“hacky”,但实际上这是一种非常有用的技术。它并不是万无一失的,但有可能捕捉到freeRam()方法无法捕捉到的高堆栈使用情况(特别是当涉及中断时)。 - Cameron Tacklind

2
如果同时使用堆和栈,那么可能会更加棘手。我将解释在不使用堆的情况下我所做的事情。一般来说,在我曾经工作过的所有嵌入式C软件领域的公司都避免在小型嵌入式项目中使用堆,以避免堆内存可用性的不确定性。我们使用静态声明的变量代替。
一种方法是在启动时用已知模式(例如0x55)填充大部分堆栈区域。这通常是由一个小代码片段在软件执行早期完成的,要么就在main()开始之前,要么就在main()开始时。当然,要注意不要覆盖正在使用的少量堆栈。然后,在运行软件一段时间后,检查堆栈空间的内容,并查看哪些位置的0x55仍然完好无损。如何“检查”取决于您的目标硬件。假设您连接了调试器,那么您可以简单地停止微控制器的运行并读取内存。
如果您有一个可以进行内存访问断点的调试器(比通常的执行断点更高级),那么您可以在特定的堆栈位置上设置断点,例如堆栈空间的最远限制。这非常有用,因为它还可以显示达到堆栈使用极限时正在运行的代码。但是,它需要您的调试器支持内存访问断点功能,而且通常不会在“低端”调试器中找到。
如果您还要使用堆,则可能会更加复杂,因为无法预测堆和栈将在哪里发生冲突。

1
假设您只使用一个堆栈(不是实时操作系统或其他任何东西),并且堆栈位于内存末尾,向下增长,而堆从BSS/DATA区域之后开始,向上增长。我看到过一些malloc实现实际上会检查堆栈指针并在冲突时失败。您可以尝试这样做。
如果您无法适应malloc代码,您可以选择将堆栈放置在内存开头(使用链接器文件)。通常最好知道/定义堆栈的最大大小。如果将其放在开头,当RAM读取超出范围时,您将收到错误。堆将位于末尾,并且如果是合理的实现,则可能无法超过末尾(将返回NULL)。好处是您知道有2个单独的错误情况,对应2个单独的问题。
要找出最大堆栈大小,您可以用模式填充内存,运行应用程序,然后查看它走了多远,也可以参考Craig的回复。

malloc实现失败(返回NULL)。我遇到的问题是,我不确定是否是碰撞导致了这个问题... - Matthew Murdoch
碰撞通常会导致非常奇怪的事情,比如返回到错误的函数或数据更改或突然在RAM/FLASH之外执行并出现寻址错误。如果您的应用程序看起来“正常”,那么很可能没有发生碰撞。为了调试此问题,请在返回NULL的位置设置断点(或者如果您可以调试malloc函数本身,则在那里设置断点更好)。在那一点上,检查堆栈指针并查看是否出现了碰撞。另外,您的堆是定义为“未使用的任何内存”还是可以设置堆大小? - Ron
你能否将信息打印到串行线路上?如果可以,当信息出现时,你仍然可以将其打印到串行线路上。为了使它更容易,你可以编写一个malloc函数的包装器,并从你的代码中调用它,而不是直接调用malloc。在包装器中,检查NULL返回值并打印堆栈指针。如果你没有访问打印函数,那么你目前是如何进行调试的呢? - Ron
@Ron - 我可以往串行线打印。你如何建议我打印堆栈指针? - Matthew Murdoch
我以前用过的一个丑陋的技巧是创建一个本地变量数组。例如,在“printStack”函数的作用域中创建“char buffer [8]”。该缓冲区在堆栈上分配。然后使用for循环打印此缓冲区,但不仅仅读取8个字节,您可以读取256个字节或任意数量。超出“buffer”的边界读取将导致向上读取堆栈。要知道堆栈指针的地址,请打印缓冲区的地址。这并不是100%正确的,但它会给您一个想法。我知道,这很丑陋,但它有效。 - Ron

0
如果您可以编辑堆的代码,您可以在每个内存块上填充几个额外字节(在这样低的资源上有些棘手)。这些字节可以包含与堆栈不同的已知模式。如果它与堆栈发生冲突,您可能会通过在堆栈内部或反之亦然看到它出现来得到线索。

模式可以在自由函数中检查,但仍然很难找出错误发生的时间。另外请注意,有时会为可能未使用的局部变量保留额外的堆栈空间(取决于编译器/代码)。在这种情况下,模式可能保持不变,而堆仍然被破坏。 - Ron

0
在类Unix操作系统中,一个名为sbrk()的库函数,它带有一个参数0,允许您访问动态分配堆内存的最高地址。返回值是一个void *指针,并且可以与任意堆栈分配变量的地址进行比较。
使用这种比较的结果应该谨慎使用。根据CPU和系统架构,堆栈可能从任意高地址向下增长,而分配的堆将从低限制内存上移。
有时操作系统有其他的内存管理概念(例如OS/9),它将堆和栈放置在不同的内存段中的空闲内存中。在这些操作系统中 - 特别是对于嵌入式系统 - 您需要预先定义您应用程序的最大内存需求,以便系统能够分配相匹配大小的内存段。

2
ATMega328P不支持Linux/BSD(它只有2K的RAM,我根本没有在上面运行操作系统),是的,堆栈向下增长,堆向上增长。 - Matthew Murdoch
AVR系列没有内存保护块的概念,因此来自“大型”CPU的安全解决方案并不总是可行的。例如,在x86系列中,286部分实现了受保护的内存区域,而386则完全实现了。 386年代迎来了新一代操作系统的“复兴”,包括完整的Windows(非DOS)、Linux、Minix等......可能最受保护的是OS/2上的286,但在AVR上完全安全的操作系统(内核)是不可能的。 - Jacek Cz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接