[PICO][Adv]动态内存

Raspberry Pi Pico 2 动态内存

在嵌入式编程中,内存管理是一个重要的主题,尤其是在处理复杂项目时。动态内存允许我们在程序运行时分配和释放内存,这为我们提供了更大的灵活性。Raspberry Pi Pico 2 虽然拥有 520KB 的 SRAM,比传统 Arduino 宽裕很多,但动态内存管理仍然需要谨慎处理,以避免内存泄漏、碎片化以及堆栈冲突等问题。

本文将分别使用 Arduino 风格(基于 Arduino-Pico 核心)和 C/C++ 风格(基于官方 Pico SDK)介绍动态内存的概念、基本操作以及实际应用案例。


1. 什么是动态内存?

动态内存 是指在程序运行时分配的内存。与静态内存(在编译时分配)不同,动态内存的大小和生命周期可以在运行时决定。

1.1 静态内存 vs 动态内存

特性 静态内存 动态内存
分配时机 编译时 运行时
大小 固定 可变
生命周期 程序全程或函数作用域 由程序员控制(分配直到释放)
存储位置 数据段(全局)或栈(局部) 堆(Heap)
示例 全局变量、static 局部变量、局部数组 malloc()new 分配的内存

1.2 为什么需要动态内存?

  • 处理大小不确定的数据(如从串口接收的命令、动态增长的数组)
  • 构建复杂数据结构(如链表、树)
  • 节省内存:只在需要时分配,不用时可以释放

提示:Pico 2 的 SRAM 较大,但动态内存的不确定性(碎片、分配耗时)仍可能影响实时性。对于固定大小的需求,优先使用静态分配。


2. 动态内存的基本操作

在 Pico 2 编程中,动态内存的分配和释放主要使用 C 标准库函数:malloccallocreallocfree

2.1 分配内存

  • malloc(size):分配 size 字节的未初始化内存。
  • calloc(count, size):分配 count * size 字节的内存,并将所有位初始化为 0
  • realloc(ptr, new_size):调整之前分配的内存块大小,可能会移动内存块。
1
2
3
4
5
// 分配 10 个整数的内存(未初始化)
int* ptr = (int*)malloc(10 * sizeof(int));

// 分配并清零 10 个整数
int* ptr2 = (int*)calloc(10, sizeof(int));

2.2 释放内存

使用 free 函数释放之前分配的内存:

1
2
free(ptr);
ptr = NULL; // 重要:避免悬空指针

警告:释放内存后,指针 ptr 仍然指向原来的内存地址,但它不再有效。为了避免悬空指针,建议在释放后将指针设置为 NULL


3. Arduino 风格:动态内存分配

在 Arduino-Pico 核心中,可以直接使用 mallocfree,需要包含 stdlib.h

3.1 基础示例:动态数组

以下示例分配一个动态整数数组,填充数据并打印,最后释放内存。

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
#include <stdlib.h>

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

int size = 5;
int* arr = (int*)malloc(size * sizeof(int));

// 始终检查分配是否成功
if (arr == NULL) {
Serial.println("内存分配失败!");
return;
}

// 使用数组
for (int i = 0; i < size; i++) {
arr[i] = i * 2;
}

for (int i = 0; i < size; i++) {
Serial.println(arr[i]);
}

// 释放内存
free(arr);
arr = NULL; // 避免悬空指针
}

void loop() {
// 主循环代码
}

3.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
#include <stdlib.h>

char* lastCommand = NULL;

void setup() {
Serial.begin(115200);
Serial.println("Enter commands (end with newline):");
}

void loop() {
static String input = "";

while (Serial.available()) {
char ch = Serial.read();
if (ch == '\n') {
// 分配刚好容纳字符串的内存
char* cmd = (char*)malloc(input.length() + 1);
if (cmd != NULL) {
input.toCharArray(cmd, input.length() + 1);
Serial.print("Command: ");
Serial.println(cmd);

// 释放之前的命令
if (lastCommand != NULL) {
free(lastCommand);
}
lastCommand = cmd;
} else {
Serial.println("Out of memory!");
}
input = "";
} else if (ch != '\r') {
input += ch;
}
}
}

提示:频繁的 malloc/free 会导致内存碎片。对于长期运行的程序,考虑使用静态缓冲区或内存池。


4. C/C++ 风格:动态内存分配

在 Pico SDK 环境中,使用标准 C 函数,代码通常在 main() 中运行。

4.1 基础示例:使用 calloc 分配并清零

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
#include "pico/stdlib.h"
#include <stdlib.h>
#include <stdio.h>

int main() {
stdio_init_all();
sleep_ms(2000);

// 分配并清零 5 个浮点数
float* readings = (float*)calloc(5, sizeof(float));

if (readings == NULL) {
printf("Memory allocation failed!\n");
return -1;
}

// calloc 已将所有元素初始化为 0.0
for (int i = 0; i < 5; i++) {
printf("readings[%d] = %.2f\n", i, readings[i]);
}

// 模拟赋值
for (int i = 0; i < 5; i++) {
readings[i] = (i + 1) * 1.5f;
}

free(readings);
readings = NULL;

while (true) {
tight_loop_contents();
}
return 0;
}

4.2 使用 realloc 扩展动态数组

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 "pico/stdlib.h"
#include <stdlib.h>
#include <stdio.h>

int main() {
stdio_init_all();

int* data = NULL;
int capacity = 0;
int count = 0;

for (int i = 0; i < 10; i++) {
if (count >= capacity) {
int new_cap = (capacity == 0) ? 4 : capacity * 2;
// 使用临时指针接收 realloc 结果
int* new_data = (int*)realloc(data, new_cap * sizeof(int));
if (new_data == NULL) {
printf("Reallocation failed at %d elements\n", count);
free(data);
return -1;
}
data = new_data;
capacity = new_cap;
printf("Resized to capacity %d\n", capacity);
}
data[count] = i * 10;
count++;
}

printf("Final array: ");
for (int i = 0; i < count; i++) {
printf("%d ", data[i]);
}
printf("\n");

free(data);
return 0;
}

关键点realloc 失败时返回 NULL,但原内存块仍然有效。绝不能直接使用 data = realloc(data, new_size),否则失败会导致原指针丢失。


5. 避免内存泄漏

内存泄漏 是指程序分配了内存但没有释放它,导致可用内存逐渐减少。在长时间运行的项目中,泄漏最终会使程序崩溃。

5.1 错误示例:导致泄漏

1
2
3
4
5
void leakyFunction() {
int* ptr = (int*)malloc(100 * sizeof(int));
// 使用 ptr ...
// 忘记调用 free(ptr) → 每次调用都泄漏 400 字节
}

5.2 正确做法:配对 mallocfree

1
2
3
4
5
6
7
8
void goodFunction() {
int* ptr = (int*)malloc(100 * sizeof(int));
if (ptr != NULL) {
// 使用 ptr ...
free(ptr);
ptr = NULL;
}
}

警告:内存泄漏会导致系统资源耗尽,最终使 Pico 2 程序崩溃或行为异常。务必确保每次 malloc/calloc 都有对应的 free


6. 动态内存的风险与注意事项

风险 描述 解决方案
内存碎片 频繁分配/释放不同大小的内存,导致堆中出现许多小空闲块,无法分配大块内存 使用内存池;尽量分配固定大小;减少分配次数
分配失败 堆空间耗尽时 malloc 返回 NULL 始终检查返回值,并优雅处理(如重启、报错)
堆栈冲突 堆向上增长,栈向下增长,两者相遇时程序崩溃 监控内存使用;避免深度递归或巨大局部变量
非确定性 malloc/free 的执行时间不固定,可能影响实时任务 禁止在中断服务函数(ISR)中使用动态内存
悬空指针 释放后未置 NULL,再次访问导致未定义行为 释放后立即将指针设为 NULL

7. 内存调试技巧

7.1 统计分配的内存总量(Arduino 风格)

可以手动封装 malloc/free 来统计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdlib.h>

size_t totalAllocated = 0;

void* debug_malloc(size_t size) {
void* ptr = malloc(size);
if (ptr) totalAllocated += size;
return ptr;
}

void debug_free(void* ptr, size_t size) {
if (ptr) {
free(ptr);
totalAllocated -= size;
}
}

// 使用 debug_malloc 和 debug_free 代替原函数

7.2 查看剩余堆空间(C/C++ 风格)

Pico SDK 提供了链接器符号,可以估算空闲堆大小。

1
2
3
4
5
6
7
8
9
10
11
#include "pico/stdlib.h"
#include <stdlib.h>

extern char __heap_end;
extern char __StackLimit;

size_t get_free_heap() {
// 简单估算:栈底(堆上限)减去当前堆顶(__brk)
// 注意:这只是一个近似值,实际可用内存还包含未分配的碎片
return (size_t)&__StackLimit - (size_t)sbrk(0);
}

8. 总结

动态内存管理是 Raspberry Pi Pico 2 编程中的一个重要概念,尤其是在处理不确定大小的数据或构建复杂数据结构时。通过合理使用 malloccallocreallocfree 函数,你可以灵活地管理内存,但也要小心避免内存泄漏、碎片和分配失败。

要点 说明
核心函数 malloc, calloc, realloc, free
适用场景 数据结构大小在编译时未知(如动态协议、链表)
主要风险 内存泄漏、碎片、分配失败、非确定性
最佳实践 检查返回值;释放后指针置 NULL;避免频繁分配;不在 ISR 中使用
替代方案 静态全局数组、内存池、栈上的变长数组(谨慎使用)

9. 练习与附加资源

  • 练习 1:编写一个程序,动态分配一个字符数组,存储用户从串口输入的字符串,并在屏幕上显示。确保在程序结束时释放所有分配的内存。
  • 练习 2:实现一个简单的链表(例如存储传感器历史读数),使用 malloc 动态创建节点,并提供删除节点的功能。
  • 练习 3:修改上述动态数组示例,使用 realloc 实现一个可以自动扩容的“动态数组”库(类似 C++ 的 std::vector)。
  • 资源

通过练习这些内容,你将更好地理解 Pico 2 中的动态内存管理,并能够编写出更健壮、更高效的嵌入式程序。


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