下面给出一些在 ESP32-C3 上优化该眼睛渲染示例的思路和方法,帮助减少 RAM 占用并提升可用内存空间。主要思路是:
- 将大数组常量放在 Flash 中,而不占用宝贵的内部 RAM
- 缩减或分批处理需要动态分配的大块内存
- 剔除不必要的功能或数据
- 适当压缩或裁剪贴图
以下是一些具体可行的方向和操作示例:
1. 将大数组/查表存储在 Flash(const
修饰)
目前代码中有很多大的查表或贴图数据(如 sclera_xxx
, iris_xxx
, polar_xxx
, upper_xxx
, lower_xxx
等),如果只是只读数据,可以用以下几种方式让编译器将它们放到 Flash(.rodata 段),而不是在启动时拷贝到 RAM:
-
在数组声明时使用
const
关键字,并确保没有对其进行写操作:// eyes_data.h 等 // 原先可能是: // uint16_t sclera_default[SCLERA_SIZE] = { ... }; // // 改为: const uint16_t sclera_default[SCLERA_SIZE] = { ... } __attribute__((aligned(4)));
-
如果需要进一步确保数据不会挪到 IRAM,可显式使用宏
ROM_ATTR
(ESP-IDF 提供)或__attribute__((section(".rodata")))
:// 常量数据放在 .rodata 段 const uint16_t sclera_default[SCLERA_SIZE] ROM_ATTR = { ... };
-
对于一些辅助查表(例如
ease
数组),同理声明为static const uint8_t ease[] ROM_ATTR = { ... };
,以免它占用宝贵的 DRAM。 -
注意:如果你的数据非常大,检查一下链接脚本是否有分段大小限制;但一般只要是
const
并且不进行写操作,编译器会自动将其放进 Flash,而不会耗费 RAM。
这样做可以显著减少 ESP32-C3 在启动时的静态 RAM 占用。
2. 减少或拆分一次性分配的大块内存
在函数 drawEye()
中,出现了如下批量绘制的逻辑:
#define LINES_PER_BATCH 20
// 分配双缓冲区
lineBuf[0] = (uint16_t*)malloc(LINES_PER_BATCH * SCREEN_WIDTH * sizeof(uint16_t));
lineBuf[1] = (uint16_t*)malloc(LINES_PER_BATCH * SCREEN_WIDTH * sizeof(uint16_t));
-
问题:一次性分配了 2 块缓存,每块大小约为:
[
20 \ (\text{行}) \times 240 \ (\text{宽}) \times 2 \ (\text{字节/像素}) = 9600 \ \text{字节/块}
]
两块就是近 19 KB,对 ESP32-C3 来说较大。 -
改进:
- 减少
LINES_PER_BATCH
,例如调到8
、5
、甚至1
(单行刷新),可以显著降低一次性分配的内存。但同时要接受渲染效率的下降。 - 只用一个缓冲区:如果帧率需求不高,可以只用
lineBuf[0]
,每次处理一批行后立即发送到 LCD,然后继续下一批行,不一定非要双缓冲。这样可再减少一半的内存消耗。 - 如果你的显示驱动支持 DMA 传输,而且必须用双缓冲,可以保留双缓冲但继续减小
LINES_PER_BATCH
。
- 减少
示例——改为 单缓冲 + 行刷:
#define LINES_PER_BATCH 1 // 每次只刷 1 行
uint16_t* lineBuf = (uint16_t*)malloc(LINES_PER_BATCH * SCREEN_WIDTH * sizeof(uint16_t));
if (lineBuf == NULL) {
ESP_LOGE(TAG, "drawEye() no enough memory for lineBuf");
return;
}
for (uint16_t screenY = 0; screenY < SCREEN_HEIGHT; screenY += LINES_PER_BATCH) {
// 填充 lineBuf[0 ... SCREEN_WIDTH-1]
// ...
esp_lcd_safe_draw_bitmap(lcd_panel,
0,
screenY,
SCREEN_WIDTH,
screenY + 1, // 只绘制这一行
lineBuf);
}
free(lineBuf);
虽然速度会比大批量刷屏慢一些,但内存占用可以减少到 240×2=480 字节(忽略少许循环变量开销),极大节省了 DRAM。
3. 移除不必要的数据/功能
- 如果你的应用实际只使用 1~2 种虹膜或巩膜,不必把所有
eyes_data_*.h
都编译进来。可以通过#if
/#ifdef
判断,只保留必要资源。 - 有些场景下,
upper_xxx
和lower_xxx
这些眼睑图片大小也不小,如果某些外观不需要眨眼或不需要复杂眼睑贴图,可考虑用更小分辨率的盖板或简化算法。 - 如果不使用自动眨眼、跟踪等功能,可注释掉对应的结构体、全局变量、或者把 blink 逻辑放在
#ifdef BLINK_FEATURE
中进行编译控制。 - 依赖队列
file_queue
、任务间通信等,如果暂时用不到也可以精简掉。
4. 对贴图进行压缩或降分辨率
目前的 SCLERA_WIDTH=375
、SCLERA_HEIGHT=375
,而实际显示是 240×240
。如果在外部资源中可行,建议将原图降分辨率到与屏幕等大或稍大即可,比如 256×256 或者直接 240×240。这样能显著缩减巩膜数组和虹膜数组的大小。
同时,若能接受一定失真或多次实时解码,也可以把这些数组改为 RLE、Run-length、或其他轻量级压缩方式存放到 Flash,运行时用小的解码缓冲区进行动态解压再渲染。但实现逻辑会复杂一些。
5. 其它可选优化
-
将
ease[]
等查表数据改为放在 Flashstatic const uint8_t ease[] ROM_ATTR = { /* ... */ };
-
在 JPEG/GIF 解码时,使用更小的中间缓冲
- 某些 JPEG 解码库支持“区域解码”或回调模式,可以边解码边输出到 LCD,免得在内存里维护整张图片。
- 如果仅需要 240×240 的显示,也可以限制最大解码尺寸,减小解码时所需缓冲。
-
启用 ESP-IDF 的内存堆统计和分析
- 在
menuconfig
里打开Heap Memory Debugging
等特性,可以帮助定位最大内存占用点,针对性优化。
- 在
-
减小任务栈
- 如果默认任务栈较大(比如 4KB 或更多),在
menuconfig
里也可以酌情减小,但要确保不会出现栈溢出。
- 如果默认任务栈较大(比如 4KB 或更多),在
-
充分利用
CONFIG_ESP_SYSTEM_PSRAM_SUPPORT
(若有)- 如果你的硬件使用了支持 PSRAM 的 ESP32(非C3),可使用
heap_caps_malloc(MALLOC_CAP_SPIRAM, size)
在 PSRAM 分配大缓冲。 - 但 ESP32-C3 大多数模块没有 PSRAM,所以这一条可能不适用。
- 如果你的硬件使用了支持 PSRAM 的 ESP32(非C3),可使用
总结
- 将大数组声明为
const
并放到 Flash:这是最关键的一点,可最大限度缩小静态 RAM 占用。 - 减小或拆分动态分配缓冲:使用单行刷屏或更小的
LINES_PER_BATCH
。 - 减少/精简不需要的资源:只保留真正用到的贴图或功能。
- 根据需求对图像做降分辨率或轻量压缩:减少静态数据体积。
按照以上思路改进后,基本可以在仅 400 KB ~ 512 KB(具体看芯片和分区)的可用内存空间内运行出较流畅的眼睛动画。希望这些建议能帮助你在 ESP32-C3 上节省内存、稳定运行。祝开发顺利!