记录一个SPI+DMA驱动OLED的小问题

记录一个 SPI + DMA 驱动 OLED 的小问题

想给 OLED 屏加上 DMA 刷新,省的 CPU 每帧都傻等 SPI 传完。翻了一些资料,发现有两种说法不太一样,搞清楚之后记录一下。

看了一篇教程,其中只改了一个函数,就是把原来的OLED写字节函数小修了一下:

void OLED_WR_Byte(u8 dat, u8 cmd)
{	
    u8 i;			  
    if(cmd)
        OLED_DC_Set();
    else 
        OLED_DC_Clr();		  
    OLED_CS_Clr();
    for(i=0;i<8;i++)
    {			  
        OLED_SCLK_Clr();
        if(dat&0x80)
            OLED_SDIN_Set();
        else 
            OLED_SDIN_Clr();
        OLED_SCLK_Set();
        dat<<=1;   
    }				 		  
    OLED_CS_Set();
    OLED_DC_Set();   	  
} 

然后把其中的位翻转部分替换成了 HAL_SPI_Transmit_DMA,改成了这样:

void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{ 
    if(cmd)
        OLED_DC_Set();  
    else 
        OLED_DC_Clr();		   	
    OLED_CS_Clr();
    HAL_SPI_Transmit_DMA(&hspi1,&dat,1);
    OLED_CS_Set();	  
    OLED_DC_Set();	 
} 	    	

但是我感觉很奇怪,所以又搜了一点资料

后续又找到了一篇文章,讲的是如何用 SPI + DMA 极限驱动 OLED,帧率跑到 60FPS。

这篇把 DMA 的正确用法讲清楚了。核心思路不是去改那个逐字节的 OLED_WR_Byte,而是:

  1. 在内存里开一块显存数组(比如 uint8_t OLED_GRAM[1024]
  2. 所有画点、写字的操作都在这个数组里修改
  3. 需要刷新屏幕时,一次性把整个显存用 DMA 发出去
  4. CS 不能立刻释放,要在 DMA 传输完成回调里释放

把两篇一比,前面那篇文章的问题就很清楚了,而且不止一个。

分析出的几个问题

问题一:DMA 是非阻塞的,CS 被提前拉高了

HAL_SPI_Transmit_DMA 调用后立即返回,CPU 不等传输完成就执行下一行。那行代码里紧跟着 OLED_CS_Set(),CS 被拉高了——但 DMA 还在后台往 SPI 里送数据。CS 拉高意味着传输终止,OLED 收不到完整字节。

原来的软件模拟里,for 循环执行完 8 个位翻转才释放 CS,时序完全在 CPU 掌控下。改成 DMA 后还是按原样写代码,结果就是 CS 时序崩了。

问题二:局部变量的地址传给了 DMA

dat 是函数参数,存在栈上。&dat 给了 DMA,但函数执行完 dat 就被释放了。DMA 读到这块内存时可能已经被别的函数覆盖了,发出去的都是随机数据。实际上就是悬空指针。

问题三:每次发 1 个字节就启动一次 DMA,比不用还慢

启动一次 DMA 的开销(配置通道、总线仲裁、中断响应)是固定的,比 SPI 发一个字节还大。

正确的做法

方案一: 换成阻塞式发送,CPU 等传完再释放 CS。

void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{
    if (cmd) OLED_DC_Set(); else OLED_DC_Clr();
    OLED_CS_Clr();
    HAL_SPI_Transmit(&hspi1, &dat, 1, HAL_MAX_DELAY);
    OLED_CS_Set();
    OLED_DC_Set();
}

但这和 DMA 没关系,只是把位翻转换成了硬件 SPI,速度提升有限。

方案二:显存 + 整屏批量刷新

// 全局显存数组
uint8_t OLED_GRAM[1024];

// 刷新整屏
void OLED_Refresh(void)
{
    OLED_DC_Set();   // 数据模式
    OLED_CS_Clr();   // 片选拉低
    HAL_SPI_Transmit_DMA(&hspi1, OLED_GRAM, 1024);
    // 不能在这里拉高 CS!等 DMA 传完
}

// DMA 传输完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi->Instance == SPI1)
    {
        OLED_CS_Set();  // 在这里释放片选
    }
}

利用这种思路,DMA 一次搬运 1024 字节,启动开销只摊了一次。CPU 在传输期间可以去做别的事(比如计算下一帧、处理按键),传输完才进一次中断。中断频率从 1024 次/帧降到了 1 次/帧。

小结

  • 时序要跟着变:DMA 是非阻塞的,原来软件模拟里那种"发完再释放 CS"的逻辑,要改成在回调里释放,不能紧跟在启动函数后面
  • 地址得稳定存在:传给 DMA 的内存地址必须是全局或 static 变量,不能是局部变量的地址,否则函数退出后 DMA 读到的全是垃圾
  • DMA 只适合批量:单字节用 DMA 反而更慢,要传就传一批数据

参考文章
https://blog.csdn.net/qq_53000374/article/details/127796468
https://zhuanlan.zhihu.com/p/1999141625302122684

posted @ 2026-07-29 16:33  Javenwww  阅读(10)  评论(0)    收藏  举报