几个月前,作者开始为使用 RP2040 微控制器的树莓派 Pico 板更新 TensorFlow Lite Micro。遇到了一些令人困惑的 bug 导致进度受阻,最终追踪到是对内存布局理解不足。
RP2040 的物理地址布局如下:闪存位置可能因板而异,Pico 板上始于 0x10000000 且为 2MB 长;RAM 更复杂,有内置 SRAM,由 4 个 64KB 银行和 2 个 4KB 银行组成,关于这些银行的特性文档较少,但芯片上的两个 Cortex M0 核心可同时访问不同银行,若同一银行被两个核心访问,其中一个核心至少会停滞一个周期。
物理布局由硬件固定和控制,编译器和链接器决定软件如何使用可用地址空间。Pico SDK 中的[src/rp2_common/pico_standard_link/memmap_default.ld]定义了默认 RAM 布局,向量表是系统例程函数指针的 256 字节数组,通常在 RAM 起始处,.data 存储初始值的全局和静态变量,.bss 存储无需初始化的变量,堆来自 malloc 分配的内存,两个栈存储函数的局部变量。
要注意的是,RP2040 有两个栈,分别对应两个 Cortex M0 核心,除非程序显式调用第二个核心,否则只使用核心 0,核心 1 栈常未使用,栈定义为 2KB 大小且向下增长,每个核心的栈定义在不同银行以提高性能。堆大小是分配完其他固定大小部分后剩余的内存,从.bss 顶部到核心 1 栈底部,理论上 malloc 分配区域无强制规定,但实际从堆底部开始向上分配。
总结来说,栈从内存最高地址向下增长,堆的已分配部分向上增长,栈下方区域除非大量从堆分配内存否则不太可能使用,堆顶部也不太可能使用,直到栈到达已分配的堆部分才可能出现问题。RP2040 栈的名义限制是 2KB,但实际可用 4KB,很多程序使用更多栈空间也能正常工作。
作者的探索始于启用双核优化时特定卷积测试失败,发现测试函数在栈上分配了几个多千字节数组,超出栈的限制但未出现可见问题,因为堆未大量使用,而核心 0 栈下方是核心 1 栈,核心 0 栈溢出会使用核心 1 栈内存,导致奇怪的竞态条件。通过在 memmap_default.ld 文件中调整栈位置,将核心 0 栈放在核心 1 栈下方,解决了问题,还从论坛学到了一些技巧,如运行 -fstack-usage 获取函数栈大小和‘USE_STACK_GUARDS’宏检查溢出,但尚未在 cmake 中指定自定义.ld 文件。希望此关于 RP2040 内存布局和潜在静默栈溢出的内容能帮助他人,这是作者追踪过的最难以捉摸的 bug 之一,最终理解后很有满足感。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。