[PICO][Adv]内存分配

Raspberry Pi Pico 2 内存分配

在嵌入式编程中,内存分配是一个关键概念,尤其是在处理复杂项目或资源受限的设备时。Raspberry Pi Pico 2 搭载的 RP2350 芯片拥有比传统 Arduino 更充裕的内存资源,但合理管理内存仍然是编写高效、稳定程序的核心技能。本文将详细介绍 Pico 2 的内存结构、分配机制以及优化技巧,并分别使用 Arduino 风格(基于 Arduino-Pico 核心)和 C/C++ 风格(基于官方 Pico SDK)提供实际案例。


1. Pico 2 的内存结构

RP2350 芯片的内存架构与传统的 AVR 架构(如 Arduino Uno)有所不同,主要包括以下几部分:

内存类型 大小(典型) 描述
Flash 存储器 2MB / 16MB(外部 QSPI Flash) 存储程序代码和只读数据(如 const 变量)。程序运行时,代码通过 XIP(就地执行)直接从 Flash 运行。
SRAM 520KB(芯片内部) 存储程序运行时的变量、堆栈和动态分配的内存。分为 8 个独立的 bank,支持并行访问,提高性能。
OTP(一次性可编程) 8KB 存储芯片唯一 ID、安全密钥等只读数据,用户通常不直接使用。
无内置 EEPROM Pico 2 没有专门的 EEPROM,但可以使用 Flash 中的剩余空间模拟 EEPROM 功能。

提示:与传统 Arduino(如 Uno 仅有 2KB SRAM)相比,Pico 2 的 520KB SRAM 提供了极大的内存空间,足以应对绝大多数嵌入式应用。但合理的分配依然重要。


2. SRAM 的管理与优化

SRAM 是程序运行时的“工作内存”,存储:

  • 全局变量(静态数据)
  • 堆栈(局部变量、函数调用上下文)
  • (动态分配的内存)

2.1 变量类型与存储位置

变量类型 存储位置 生命周期 示例
全局变量 SRAM 的数据段 程序全程 int globalVar = 10;
静态局部变量 SRAM 的数据段 程序全程 static int count = 0;
局部变量 函数执行期间 int localVar = 5;
动态分配 malloc()free() int* p = (int*)malloc(100);
常量(const Flash(XIP) 程序全程 const int TABLE[] = {1,2,3};

2.2 Arduino 风格:避免栈溢出

在 Arduino 编程中,局部变量占用栈空间。深度函数调用或大型局部数组可能导致栈溢出(Stack Overflow),覆盖相邻内存区域,引发奇怪错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 危险:在栈上分配大型数组
void dangerousFunction() {
char buffer[4096]; // 4KB 局部数组,可能超过栈空间
// 使用 buffer...
}

// 安全:使用全局或静态分配,或动态分配
char globalBuffer[4096]; // 全局变量,位于数据段

void safeFunction() {
static char staticBuffer[4096]; // 静态变量,也在数据段
// 使用 staticBuffer...
}

2.3 C/C++ 风格:查看内存使用情况

Pico SDK 提供了 pico_get_memory_size() 等函数来查看内存布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "pico/stdlib.h"
#include "pico/multicore.h"

int main() {
stdio_init_all();

// 打印内存信息
printf("SRAM end address: %p\n", &__ram_end__);
printf("Heap end address: %p\n", &__heap_end__);

// 获取当前栈指针(近似栈使用)
uint32_t* sp = (uint32_t*)__get_MSP();
printf("Current stack pointer: %p\n", sp);

while (true) {
tight_loop_contents();
}
}

注意:在 Pico SDK 中,链接脚本定义了 __StackLimit__StackTop__bss_end__ 等符号,可用于运行时内存监控。

2.4 动态内存分配(malloc / free

Pico 2 支持标准的 C 动态内存分配。但动态分配可能导致内存碎片(Memory Fragmentation),尤其是在频繁分配释放不同大小内存时。

Arduino 风格示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void setup() {
Serial.begin(115200);

// 分配 10 个整数的内存
int* ptr = (int*)malloc(10 * sizeof(int));

if (ptr == NULL) {
Serial.println("内存分配失败!");
return;
}

for (int i = 0; i < 10; i++) {
ptr[i] = i * 2;
Serial.println(ptr[i]);
}

free(ptr); // 释放内存
ptr = NULL; // 避免野指针
}

void loop() {}

C/C++ 风格示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "pico/stdlib.h"
#include <stdlib.h>
#include <stdio.h>

int main() {
stdio_init_all();

int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr == NULL) {
printf("malloc failed\n");
return -1;
}

for (int i = 0; i < 10; i++) {
ptr[i] = i * 2;
printf("%d\n", ptr[i]);
}

free(ptr);
ptr = NULL;

while (true) {
tight_loop_contents();
}
}

动态内存使用建议

  • 在嵌入式系统中,尽量避免频繁动态分配。对于固定大小的数据结构,使用全局或静态数组。
  • 如果必须使用,考虑在初始化阶段一次性分配所有需要的内存,并始终使用相同大小。
  • 始终检查 malloc 返回值是否为 NULL

3. Flash 存储与 XIP 机制

Pico 2 的程序代码存储在外部 QSPI Flash 中。RP2350 支持 XIP(Execute in Place),即 CPU 直接在 Flash 地址空间执行代码,无需将代码复制到 SRAM 中。这极大地节省了 SRAM 空间。

3.1 存储常量数据到 Flash

在 Pico 2 中,使用 const 修饰的全局数据会默认存储在 Flash 中,通过 XIP 读取,不占用 SRAM。

1
2
3
4
5
6
7
8
// 存储在 Flash 中,不占用 SRAM
const uint16_t sineTable[] = {
0, 159, 318, 477, 636, 795, 954, 1113,
1272, 1431, 1590, 1749, 1908, 2067, 2226, 2385
};

// 普通数组存储在 SRAM 中
uint16_t buffer[256];

3.2 将特定数据强制放入 Flash(C/C++ 风格)

使用 __attribute__((section(".flashdata"))) 可以将变量放置在 Flash 中(但注意 Flash 不可写,需确保数据只读)。

1
2
__attribute__((section(".flashdata"))) 
const char welcomeMsg[] = "Hello from Flash!";

3.3 Arduino 风格:使用 PROGMEM(兼容层)

在 Arduino-Pico 核心中,PROGMEM 宏被映射为 const 属性,可以方便地将数据放入 Flash。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <avr/pgmspace.h>  // 或直接使用 PROGMEM

const char myString[] PROGMEM = "This string is in Flash";
const int myNumbers[] PROGMEM = {1, 2, 3, 4, 5};

void setup() {
Serial.begin(115200);
// 读取 Flash 中的字符串(需使用 pgm_read 函数)
char buffer[30];
strcpy_P(buffer, myString);
Serial.println(buffer);
}

void loop() {}

4. 模拟 EEPROM 存储

Pico 2 没有硬件 EEPROM,但可以使用 Flash 中的剩余空间模拟 EEPROM 功能。Pico SDK 提供了 hardware/flash.hhardware/sync.h 来实现 Flash 的擦写。

重要:Flash 的擦写寿命约为 10 万次,且必须按扇区(256 字节) 擦除,写入前必须先擦除。写入时需禁用中断,且程序会短暂暂停。

4.1 Arduino 风格:使用 EEPROM

Arduino-Pico 核心提供了兼容的 EEPROM 库,模拟 EEPROM 功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <EEPROM.h>

#define EEPROM_SIZE 512 // 使用 512 字节 Flash 模拟 EEPROM

void setup() {
Serial.begin(115200);

EEPROM.begin(EEPROM_SIZE);

// 写入数据
int address = 0;
int value = 12345;
EEPROM.put(address, value); // 存储整数

// 提交更改(实际写入 Flash)
EEPROM.commit();

// 读取数据
int readValue;
EEPROM.get(address, readValue);
Serial.print("Read from EEPROM: ");
Serial.println(readValue);
}

void loop() {}

4.2 C/C++ 风格:直接操作 Flash

使用 SDK 的 Flash 函数进行扇区擦写(以存储配置数据为例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include "pico/stdlib.h"
#include "hardware/flash.h"
#include "hardware/sync.h"

#define FLASH_TARGET_OFFSET (256 * 1024) // 从 Flash 256KB 处开始存储
#define SECTOR_SIZE 4096 // 扇区大小 4KB

// 待存储的数据结构
typedef struct {
uint32_t magic; // 魔数,验证数据有效性
int sensorThreshold;
char deviceName[32];
} ConfigData;

void save_config(const ConfigData* config) {
// 禁用中断
uint32_t ints = save_and_disable_interrupts();

// 擦除扇区
flash_range_erase(FLASH_TARGET_OFFSET, SECTOR_SIZE);

// 写入数据(注意:flash_range_program 以 256 字节为单位)
flash_range_program(FLASH_TARGET_OFFSET, (const uint8_t*)config, sizeof(ConfigData));

// 恢复中断
restore_interrupts(ints);
}

void load_config(ConfigData* config) {
// 直接从 Flash 地址读取(XIP 区域)
const ConfigData* flash_config = (const ConfigData*)(XIP_BASE + FLASH_TARGET_OFFSET);
memcpy(config, flash_config, sizeof(ConfigData));
}

int main() {
stdio_init_all();

ConfigData myConfig;

// 尝试加载配置
load_config(&myConfig);
if (myConfig.magic != 0xDEADBEEF) {
// 未初始化,写入默认值
printf("Config not valid, initializing...\n");
myConfig.magic = 0xDEADBEEF;
myConfig.sensorThreshold = 500;
strcpy(myConfig.deviceName, "Pico2 Sensor");
save_config(&myConfig);
} else {
printf("Loaded config:\n");
printf(" Threshold: %d\n", myConfig.sensorThreshold);
printf(" Name: %s\n", myConfig.deviceName);
}

while (true) {
tight_loop_contents();
}
}

警告:直接操作 Flash 时,务必确保目标地址不覆盖程序代码。建议使用链接脚本中保留的区域,或选择 Flash 末尾的扇区。


5. 实际案例:内存优化与数据持久化

5.1 案例:大型传感器数据缓冲区

假设你需要存储 2000 个传感器读数(每个为 uint16_t),共 4KB 数据。以下演示如何合理分配内存。

方案一:静态全局数组(推荐)

1
2
3
4
5
6
// 全局数组,位于 SRAM 数据段
uint16_t sensorBuffer[2000]; // 4KB

void addSample(uint16_t value, int index) {
if (index < 2000) sensorBuffer[index] = value;
}

方案二:动态分配(不推荐)

1
2
3
4
5
6
7
8
9
uint16_t* sensorBuffer = nullptr;

void setup() {
sensorBuffer = (uint16_t*)malloc(2000 * sizeof(uint16_t));
if (!sensorBuffer) {
Serial.println("Out of memory!");
while(1);
}
}

方案三:使用 Flash 存储历史数据(适合长期记录)

1
2
3
4
5
6
#define HISTORY_SIZE 10000
uint16_t history[HISTORY_SIZE]; // 如果 SRAM 不足,考虑分页写入 Flash

void logToFlash(uint16_t value, uint32_t index) {
// 按页写入 Flash(需实现循环缓冲区)
}

5.2 案例:系统配置持久化

结合之前的状态机或任务调度器,在系统启动时加载配置,运行时修改并保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <EEPROM.h>

struct SystemConfig {
char wifiSSID[32];
char wifiPassword[64];
uint16_t updateInterval; // 秒
uint8_t ledBrightness;
};

SystemConfig config;

void loadConfig() {
EEPROM.get(0, config);
// 校验魔数(假设结构体第一个字段为 magic)
if (config.wifiSSID[0] == 0xFF) { // 未初始化
strcpy(config.wifiSSID, "default");
strcpy(config.wifiPassword, "");
config.updateInterval = 60;
config.ledBrightness = 128;
saveConfig();
}
}

void saveConfig() {
EEPROM.put(0, config);
EEPROM.commit();
}

void setup() {
Serial.begin(115200);
EEPROM.begin(sizeof(SystemConfig) + 4); // 多申请一点空间
loadConfig();
Serial.print("SSID: "); Serial.println(config.wifiSSID);
}

void loop() {
// 主逻辑
}

6. 内存分配最佳实践

实践 说明
优先使用静态分配 全局变量或静态局部变量,避免堆碎片
常量数据放入 Flash 使用 constPROGMEM 存储只读数据
避免大型栈变量 大型数组、结构体应声明为 static 或全局
谨慎使用 malloc 若非必要,避免动态分配;如必须,确保成对 free
监控内存使用 定期检查栈指针和堆边界,预防溢出
利用 XIP 特性 代码和常量自动在 Flash 中执行/读取,节省 SRAM
EEPROM 模拟 优先使用 EEPROM 库,避免直接操作 Flash 出错

7. 总结

Raspberry Pi Pico 2 的 RP2350 芯片提供了充裕的内存资源,但合理管理仍然是高质量嵌入式程序的基础。通过本文,你应该已经掌握了:

  • Pico 2 的内存架构:Flash(XIP)、SRAM、无内置 EEPROM 但可模拟
  • SRAM 管理:变量存储位置、避免栈溢出、动态分配注意事项
  • Flash 使用:常量数据存储、XIP 机制
  • EEPROM 模拟:Arduino 风格的 EEPROM 库和 SDK 原生 Flash 操作
  • 实际案例:数据缓冲、配置持久化
内存类型 容量 主要用途 管理建议
Flash 2MB+ 代码、只读常量 使用 XIP,存储表格、字符串
SRAM 520KB 变量、堆、栈 静态分配优先,监控栈使用
模拟 EEPROM 自定义 持久化配置 写入前擦除扇区,注意寿命

8. 练习与拓展

  • 练习 1:编写程序,使用 malloc 动态分配一个大型数组,并用 free 释放。在串口打印分配前后的内存状态(可参考 mallinfo() 函数)。
  • 练习 2:将一组正弦波查表数据(例如 256 个 16 位值)存储在 Flash 中,并在主循环中快速查询,输出到 PWM 产生正弦波。
  • 练习 3:实现一个简单的配置管理系统,允许通过串口修改设备 ID、采样间隔等参数,并保存到模拟 EEPROM 中,重启后自动加载。
  • 练习 4:使用 pico_get_memory_size 和链接脚本符号,编写一个内存监控任务,定期打印剩余栈空间和堆使用情况。

通过深入理解内存分配机制,你将能够更高效地利用 Pico 2 的资源,编写出稳定、可靠的嵌入式应用。


[PICO][Adv]内存分配
https://ka5fxt.cn/2026/03/30/PICO-Adv-内存分配/
发布于
2026年3月30日
许可协议