4. 驱动适配核心概念

SPI接口的显示屏,在嵌入式GUI里太常见了。小到智能手表,大到工业控制面板,很多都用它。说实话,我最早接触SPI屏的时候,觉得不就是发几个字节嘛,能有多难?结果第一次点亮屏幕,花了我整整两天——不是协议不对,是初始化序列里一个延时参数没给够。

好,咱们今天就把这块彻底讲透。从SPI协议最核心的点开始,到写命令/写数据的封装,再到DMA传输优化,一条线串下来。

4.1 SPI协议快速回顾

SPI是同步、全双工的串行通信。主设备产生时钟,同时收发数据。对于显示屏驱动,我们通常只关心主设备发往从设备的数据方向,也就是MOSI线上的内容。

SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。我见过不少新手在这上面栽跟头——屏不亮,查了半天,结果是模式配错了。

模式 CPOL CPHA 数据采样边沿 常见屏类型
模式0 0 0 上升沿采样 ILI9341, ST7789
模式1 0 1 下降沿采样 较少见
模式2 1 0 上升沿采样 部分OLED
模式3 1 1 下降沿采样 部分OLED

我个人习惯,拿到一块新屏,第一件事就是看数据手册里的时序图。手册里通常会画出来,照着配就行。别猜,猜容易出事。

小技巧: 大多数彩色TFT-LCD屏使用SPI模式0或模式3。如果你不确定,先试模式0,不行再切模式3。这两个模式时钟极性相反,但采样边沿都是上升沿,很多屏都能兼容。

4.2 初始化序列——点亮屏幕的第一步

初始化序列,说白了就是给屏发一串配置命令,让它进入正常工作状态。每个屏的初始化序列都不一样,但套路是通用的。

典型的初始化流程是这样的:

  1. 硬件复位:拉低RST引脚至少10ms,再拉高。确保屏内部状态机复位。
  2. 退出睡眠模式:发SLPOUT命令。之后要等120ms以上,让屏内部DC-DC稳定。
  3. 设置像素格式:比如COLMOD命令,设置16位色或18位色。
  4. 设置显示方向:MADCTL命令,控制横竖屏、BGR顺序等。
  5. 设置显示窗口:CASET和RASET,定义可写入的像素区域。
  6. 开启显示:DISPON命令。

嗯,这里要注意。不同厂商的屏,初始化序列长度差别很大。有的屏只需要十几条命令,有的需要四五十条。我曾经遇到过一块屏,手册上给了60多条初始化命令,少发一条,颜色就偏得离谱。

避坑指南: 初始化序列里的延时参数,不要随意缩短。我曾经为了加快启动速度,把SLPOUT后的延时从120ms砍到50ms,结果屏偶尔花屏。查了两天才发现是延时不够。老老实实按手册来,别耍小聪明。

4.3 写命令/写数据函数封装

SPI屏驱动里,最核心的两个函数就是写命令和写数据。它们的区别在于DC引脚的电平——命令时DC拉低,数据时DC拉高。

我习惯这样封装:

// 底层SPI发送一个字节
static void spi_write_byte(uint8_t data)
{
    // 等待发送缓冲区空
    while (!(SPI->SR & SPI_SR_TXE));
    // 发送数据
    SPI->DR = data;
    // 等待接收完成(全双工必须等)
    while (!(SPI->SR & SPI_SR_RXNE));
    // 读DR清标志
    (void)SPI->DR;
}

// 写命令
void lcd_write_cmd(uint8_t cmd)
{
    LCD_DC_LOW();      // DC拉低,表示命令
    LCD_CS_LOW();      // 片选拉低
    spi_write_byte(cmd);
    LCD_CS_HIGH();     // 片选拉高
    LCD_DC_HIGH();     // DC恢复高电平
}

// 写数据
void lcd_write_data(uint8_t data)
{
    LCD_DC_HIGH();     // DC拉高,表示数据
    LCD_CS_LOW();
    spi_write_byte(data);
    LCD_CS_HIGH();
}

// 批量写数据(用于填充显存)
void lcd_write_data_bulk(uint8_t *buf, uint32_t len)
{
    LCD_DC_HIGH();
    LCD_CS_LOW();
    for (uint32_t i = 0; i < len; i++) {
        spi_write_byte(buf[i]);
    }
    LCD_CS_HIGH();
}

你看,逻辑很简单。但实际项目中,批量写数据这个函数往往是性能瓶颈。因为每发一个字节都要轮询等待,CPU被死死卡住。这时候就需要DMA出场了。

核心要点: 写命令和写数据的本质区别就是DC引脚电平。封装时一定要保证DC状态在CS片选之前设置好,否则屏可能把数据当成命令,或者反过来。这个顺序问题,我见过不止一个人踩坑。

4.4 DMA传输优化——让CPU喘口气

为什么需要DMA?你想想看,一块320x240的16位色屏,一帧数据是320×240×2 = 153600字节。如果SPI时钟20MHz,纯轮询发送一帧大约需要7.68ms。这7.68ms里CPU什么都干不了,只能在那等。

用DMA就不一样了。CPU把数据地址和长度配好,启动DMA,然后就可以去干别的事。DMA传输完成后触发中断,通知CPU。

DMA传输的典型流程:

  1. 配置DMA通道:源地址(内存)、目的地址(SPI数据寄存器)、传输长度。
  2. 设置SPI为DMA请求模式:使能SPI的TX DMA请求。
  3. 启动DMA传输。
  4. 等待DMA传输完成中断。
  5. 在中断里关闭DMA,处理后续。

代码实现大致如下:

// 使用DMA发送数据(非阻塞)
void lcd_write_data_dma(uint8_t *buf, uint32_t len)
{
    // 等待上一次DMA传输完成
    while (dma_busy);
    
    LCD_DC_HIGH();
    LCD_CS_LOW();
    
    // 配置DMA
    DMA->CCR = 0;  // 复位配置
    DMA->CPAR = (uint32_t)&(SPI->DR);  // 外设地址
    DMA->CMAR = (uint32_t)buf;          // 内存地址
    DMA->CNDTR = len;                   // 传输长度
    DMA->CCR = DMA_CCR_MINC |           // 内存地址递增
               DMA_CCR_DIR |            // 内存到外设
               DMA_CCR_TCIE;            // 传输完成中断使能
    
    dma_busy = 1;
    DMA->CCR |= DMA_CCR_EN;            // 使能DMA
    
    // 使能SPI的TX DMA请求
    SPI->CR2 |= SPI_CR2_TXDMAEN;
}

// DMA传输完成中断
void DMA_IRQHandler(void)
{
    if (DMA->ISR & DMA_ISR_TCIF) {
        DMA->IFCR = DMA_IFCR_CTCIF;    // 清除标志
        DMA->CCR &= ~DMA_CCR_EN;       // 关闭DMA
        SPI->CR2 &= ~SPI_CR2_TXDMAEN;  // 关闭SPI DMA请求
        LCD_CS_HIGH();
        dma_busy = 0;
    }
}
经验之谈: DMA传输完成后,一定要等SPI移位寄存器里的最后一个字节发送完毕,再拉高CS。否则最后一个字节会被截断。我习惯在DMA中断里加一个小延时,或者检查SPI的忙标志。这个细节,手册上通常不会写,但实际中很容易出问题。

4.5 整体驱动架构

把上面这些串起来,一个完整的SPI屏驱动架构就出来了。我画了张图,帮你理清关系。

SPI显示屏驱动架构 应用层(GUI框架) 驱动接口层(lcd_init / lcd_set_window / lcd_fill) 命令/数据封装层(lcd_write_cmd / lcd_write_data / lcd_write_data_bulk) 传输层(SPI轮询发送 / SPI+DMA发送 / 中断处理) 硬件层(SPI外设 / DMA控制器 / GPIO) 说明:上层调用下层接口,每层职责清晰。DMA优化主要在传输层实现,对上层透明。 驱动接口层通过条件编译选择轮询或DMA模式,不改上层代码即可切换。

这个架构的好处是分层清晰。如果以后要换MCU,只需要改传输层和硬件层,上面的接口层基本不用动。我在多个项目里复用这套结构,移植效率很高。

4.6 性能对比:轮询 vs DMA

最后,咱们用数据说话。我实际测过一块320x240的屏,SPI时钟20MHz,刷一帧全屏数据:

传输方式 CPU占用率 传输时间 适用场景
轮询发送 100%(全程等待) 约7.7ms 简单测试、低帧率应用
DMA发送 约5%(仅启动和中断) 约7.7ms 高帧率、多任务系统
DMA+双缓冲 约3%(几乎不占用) 约7.7ms(无额外开销) 动画、视频播放

你看,传输时间几乎一样,但CPU占用率天差地别。DMA方式下,CPU可以在传输期间处理其他任务——比如触摸扫描、通信协议、GUI计算。这就是优化的价值。

总结一下: SPI屏驱动,核心就三件事——初始化序列配对、命令/数据封装做对、DMA传输用好。这三件事做好了,驱动就稳了。剩下的就是根据具体屏的参数微调。嗯,今天就到这,你把这些代码跑一遍,遇到问题再回来翻翻这张图。