使用DMA驱动SPI显示屏

背景

在嵌入式图形界面开发中,STM32微控制器常用来驱动各类显示屏(如 TFT LCD、OLED),实现丰富的用户交互。随着用户体验要求提升,显示界面的刷新帧率成为评价系统性能的重要指标之一。
特别是在使用LVGL这类高性能 GUI 库时,全屏或局部刷新数据量大,对数据搬运速度提出了更高的要求。

最简单的SPI屏幕驱动方式是:CPU逐个将像素将数据通过 SPI 总线写入屏幕。这种方式在低分辨率或静态界面下尚可满足需求,但在高分辨率、频繁刷新的场景下,CPU负载会迅速升高,导致帧率下降,界面卡顿,影响用户体验。

所以我们就需要在原有通讯协议(I2C/SPI)协议的DMA来帮助我们搬运数据

为什么使用DMA?

不使用DMA:MCU在整个数据搬运过程中都需要亲自参与,CPU会被大量堵塞在像素数据的发送上,导致刷新期间其它任务处理变慢,严重影响系统的实时性和整体性能。

使用DMA:由于CPU和DMA是一个并行的关系,CPU发起DMA请求后,DMA硬件能批量高速搬运数据到外设(SPI)。

在DMA传输期间,CPU可以专注于准备下一帧的像素数据,或处理其他任务。等DMA搬运完成后,CPU只需在中断中进行收尾操作,然后可以立即开启下一次数据搬运。这样不仅大幅提升了刷屏效率,也显著提高了系统的帧率和响应速度。

传统SPI(轮询) DMA驱动SPI
MCU逐像素搬运数据 DMA硬件整体搬运数据
CPU全程忙于刷屏 CPU只发起DMA,刷屏时可做其它事
刷屏速度受CPU限制 刷屏速度受DMA+SPI总线限制(更快)
刷新越大CPU越慢 刷新越大DMA优势越明显
帧率低 帧率高

SPI DMA传输完成中断

原本是在lvgl中逐个发送像素后,发送完了才会执行lv_display_flush_ready通知可以开始下一次刷新了

image-20250803192158407

现在我们使用了DMA,数据传输被放在了后台执行,LVGL需要知道何时数据传输完成了,因此我们需要在DMA传输完成中断中调用lv_disp_flush_ready(&disp_drv)进行通知LVGL,LVGL才会进行下一次刷新

由于我们使用的是SPI的DMA模式,DMA搬运完成,SPI发送完成后,会调用SPI的发送完成中断回调函数,所以就是对HAL_SPI_TxCpltCallback进行一个重写,我们在其中进行通知LVGL即可,最终中断回调函数如下:

image-20250803192758167

需要注意的是disp_drv本来是在lv_port_disp_init()中的局部变量,我们需要将其变为全局变量才能在中断回调函数中使用

实际使用

下面是实际使用时的具体步骤:

disp变量全局化

如图:将在lv_port_disp_init定义的disp变量从局部变量变为全局变量

image-20250803210855053

disp_flush修改

  1. 将disp_flush中的LCD输出部分修改为以SPI DMA方式发送数据

  2. 注释掉lv_display_flush_ready(disp_drv),后续将在中断回调中调用

image-20250803211354587

LCD_Fill_DMA实现逻辑:

image-20250803212352501

重写中断回调函数

  1. 将之前的disp全局变量extern到实现SPI中断回调函数的位置
  2. 然后在SPI中断回调函数中调用lv_display_flush_ready(disp),实现DMA传输完成后通知lvgl开始下一次刷新的功能

image-20250803212704523

image-20250803192758167

注意事项

size大小问题

由于HAL_SPI_Transmit()HAL_SPI_Transmit_DMA()Size参数均是uint16_t类型的,因此最大传输数据量为65535,在显示屏中很容易超,需要非常注意

DMA与SPI宽度问题

注意在使用SPI的DMA模式时要注意,一定确保SPI和DMA的数据宽度需要匹配,不能出现SPI的数据宽度为16bit,DMA的数据宽度为8bit的情况,否则会出现DMA将`分两次从同一个地址传输两次8bit数据给16bit SPI寄存器。造成数据传输出错,LCD显示异常问题

DMA配置数据宽度:半字(16bit)

image-20250803224105377

image-20250803224031593

SPI数据宽度切换问题

在使用SPI通信的过程中,因为SPI传输存在8bit和16bit的情况,所以我们需要在传输像素数据(RGB565)时,将SPI数据宽度切换为16bit。

但是我在使用的过程中,发现使用HAL库切换数据宽度的方式存在问题,最稳妥的方式是直接操作SPI控制寄存器切换数据宽度为16bit。

方式一(异常):使用HAL库的HAL_SPI_Init重新设置SPI发送的数据宽度为16bit,最开始使用这种方式切换数据宽度,这样就会出问题,导致LCD只有部分行数能正常刷新,且对应LCD像素颜色异常

image-20250803230829730

该异常我暂不知是什么原因,具体应该抓抓对应的波形看看数据是什么?

AI的解答:

1.HAL库有复杂的状态机和句柄操作,并且DMA和SPI重新关联可能丢失,有时不稳定

2.可能是HAL库的bug

方式二(正常):使用操作寄存器的方式直接修改spi对应控制寄存器的DFF位,将其修改为1,控制SPI数据宽度为16bit。这样修改过后LCD显示就正常了

image-20250803225041285

image-20250803225107175

image-20250803230803862