Skip to content

备忘录:Cortex-M7 OLED eDMA 双缓冲重构踩坑录

2026-08-06

关键词: NXP SDKLPSPIeDMA双缓冲D-Cache 一致性NVIC 优先级FreeRTOS 临界区


为什么要做这次重构?

当初系统被极其恶心的隐性 Bug 折磨:

音频爆音/断流: SAI 音频 DMA 也是硬实时,但 OLED 刷屏用的是阻塞式 SPI 发送,疯狂霸占 CPU,导致音频中断响应不及时。

系统假死: 配合 Flash 擦写时,如果在全局临界区里呆太久,FreeRTOS 的心跳直接停跳,系统休克。

最终目标: 让 CPU 只管往内存里画图,画完瞬间切指针,剩下的 SPI 搬运工作全部丢给 eDMA 在后台完成。


避坑指南 1:NXP SDK 的“全双工强迫症”

SDK 里的 LPSPI_MasterTransferEDMA 非常死板,就算 OLED 只需要 TX 疯狂发数据,底层状态机还是会傻傻地等 RX。如果不处理,就会喜提满屏的 400 (Busy) 连环报错和总线死锁。

解法:强行洗胃 + 喂垃圾桶

必须物理开辟一个跟显存一样大的 OLED_DUMMY_RX,塞给 rxData 堵住它的嘴。并且在每次发数据前,暴力清空 FIFO 和状态标志

/* 扩大废件箱到 1024 字节,装下整屏的 RX 垃圾。必须 32 字节对齐! */
AT_NONCACHEABLE_SECTION_ALIGN(static uint8_t OLED_DUMMY_RX[OLED_VBYTE * OLED_WIDTH], 32);

/* 发送前的暴力洗肠子操作 */
LPSPI_FlushFifo(LPSPI3_PERIPHERAL, true, true);
LPSPI_ClearStatusFlags(LPSPI3_PERIPHERAL, kLPSPI_AllStatusFlag);

txXfer.rxData = OLED_DUMMY_RX; // 必须喂给它,不然 SDK 状态机卡死
txXfer.configFlags = kLPSPI_MasterPcs0;

避坑指南 2:Cortex-M7 的隐形杀手 —— D-Cache

把数据丢给 eDMA 后,屏幕居然撕裂、花屏了。

原因: CPU 画完的图像只留在了 D-Cache(高速缓存)里,还没来得及写回物理 RAM。而 eDMA 是个瞎子,它直接去物理 RAM 搬数据,搬出来的全是上一帧的过期垃圾。

解法:在触发 DMA 前,手动刷 Cache

只要用到 eDMA 读内存,永远别忘了 SCB_CleanDCache_by_Addr

/* 瞬间交换双缓冲指针 */
uint8_t (*temp)[OLED_WIDTH] = DrawBuffer;
DrawBuffer = DisplayBuffer;
DisplayBuffer = temp;

/* 致命遗漏点:确保 CPU 刚画好的 DisplayBuffer 数据真正落入物理内存 */
SCB_CleanDCache_by_Addr((uint32_t *)DisplayBuffer, sizeof(GRAM_Pool[0]));

/* 然后再触发 eDMA 发送 DisplayBuffer... */

避坑指南 3:NVIC 优先级

OLED 的 eDMA 搬运极快,中断触发极其频繁。如果它和 SAI(音频)的优先级一样高,依然会在微观上干扰音频输出。

记住系统资源的食物链: 音频是不可抢占的硬实时生命线,显示只是个后台苦力。

解法:拉开绝对的阶级差距。

/* SAI 音频 DMA 通道:必须给予高优先级特权! */
NVIC_SetPriority(DMA3_DMA19_IRQn, 5); // SAI1 TX
NVIC_SetPriority(DMA4_DMA20_IRQn, 5); // SAI2 RX

/* LPSPI3 OLED DMA 通道:允许被音频无情抢占 */
NVIC_SetPriority(DMA6_DMA22_IRQn, 10);

写给以后的自己:

底层驱动开发没有魔法。当觉得寄存器在“闹鬼”时,多想一想 Cache、DMA、中断嵌套和状态机缓存