记录一个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,而是:
- 在内存里开一块显存数组(比如
uint8_t OLED_GRAM[1024]) - 所有画点、写字的操作都在这个数组里修改
- 需要刷新屏幕时,一次性把整个显存用 DMA 发出去
- 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

浙公网安备 33010602011771号