4、SPI与并行接口详解:SPI协议时序、4线/3线SPI驱动LCD、8080并行接口驱动、DMA加速传输

好,咱们今天来聊聊LCD驱动里最核心的两个接口——SPI和8080并行接口。说实话,这两个接口我几乎每天都在打交道。你想想看,不管是小尺寸的OLED屏,还是大尺寸的TFT彩屏,总得有个方式让单片机把数据送过去。SPI省引脚,并行接口快,各有各的脾气。

我个人习惯是:128x64以下的单色屏,用SPI就够了;320x240以上的彩色屏,必须上8080并行接口,否则刷新率会让你崩溃。嗯,这里要注意,选错了接口方案,后面调试起来真的很痛苦。

4.1 SPI协议时序——看似简单,坑不少

SPI,全称Serial Peripheral Interface,说白了就是四根线搞定通信。哪四根?SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。驱动LCD时,我们一般只用三根——因为LCD只需要接收数据,不需要回传,所以MISO可以省掉。

时序上,SPI有四种模式,由CPOL(时钟极性)和CPHA(时钟相位)决定。我刚开始学的时候,总觉得这俩参数很抽象。后来我总结了一个笨办法:

  • CPOL=0:空闲时时钟是低电平
  • CPOL=1:空闲时时钟是高电平
  • CPHA=0:在时钟的第一个边沿采样数据
  • CPHA=1:在时钟的第二个边沿采样数据

驱动LCD时,绝大多数芯片用的是模式0(CPOL=0, CPHA=0)模式3(CPOL=1, CPHA=1)。为什么?因为这两种模式都是在时钟的上升沿采样,下降沿变化,时序上最干净。

避坑指南:我曾经在一个项目里,ST7735S的LCD死活不显示。查了两天,最后发现是SPI模式配错了。芯片手册上写的是“Data is latched on the rising edge of SCLK”,我配成了模式1(上升沿变化,下降沿采样)。嗯,从此以后,我拿到任何LCD芯片,第一件事就是翻时序图,确认采样边沿。

4.2 4线SPI与3线SPI驱动LCD

先说说4线SPI。这里的“4线”指的是:CS、SCLK、MOSI、DC(数据/命令选择线)。DC线是LCD驱动特有的,用来区分发送的是命令还是数据。DC=0时发命令,DC=1时发数据。

代码示例(STM32 HAL库):

// 4线SPI写命令
void LCD_WriteCmd(uint8_t cmd) {
    LCD_DC_LOW();   // DC拉低,表示命令
    LCD_CS_LOW();   // 片选使能
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
    LCD_CS_HIGH();  // 片选释放
}

// 4线SPI写数据
void LCD_WriteData(uint8_t data) {
    LCD_DC_HIGH();  // DC拉高,表示数据
    LCD_CS_LOW();
    HAL_SPI_Transmit(&hspi1, &data, 1, 100);
    LCD_CS_HIGH();
}

那3线SPI呢?说白了就是把DC线省掉,通过协议来区分命令和数据。怎么区分?在发送每个字节之前,先发一个额外的bit:0表示命令,1表示数据。这样,原本8bit的数据变成了9bit传输。

我个人不太喜欢3线SPI,为什么?因为很多单片机的SPI硬件不支持9bit模式,你得用软件模拟,速度一下就掉下来了。我建议:只要引脚够用,就用4线SPI,别给自己找麻烦。

小技巧:如果你非要用3线SPI,可以用SPI的8bit模式配合一个额外的GPIO来模拟第9位。具体做法是:先发一个字节(高7位为0,最低位表示命令/数据),再发真正的数据字节。虽然效率低一点,但至少不用改硬件。

4.3 8080并行接口驱动——速度的王者

8080接口,也叫Intel Bus,是驱动彩色LCD最常用的并行接口。它需要多少根线?最少16根数据线(8位或16位),加上WR(写使能)、RD(读使能)、CS(片选)、DC(命令/数据选择)、RESET(复位)。

你想想看,SPI一次只能传1bit,8080一次能传8bit或16bit。同样是16MHz的时钟,SPI的吞吐量是2MB/s,而8080接口可以达到32MB/s以上。这就是为什么大尺寸彩屏必须用8080接口。

时序上,8080接口其实很简单:

  • 拉低CS,选中芯片
  • 设置DC电平(命令或数据)
  • 把数据放到数据线上
  • 拉低WR,产生一个写脉冲
  • 拉高WR,数据被锁存

代码示例(GPIO模拟8080时序):

// 8080接口写数据(16位)
void LCD_WriteData16(uint16_t data) {
    LCD_CS_LOW();
    LCD_DC_HIGH();  // 数据模式
    
    // 数据线赋值
    GPIO->ODR = (GPIO->ODR & 0xFFFF0000) | data;
    
    // 产生写脉冲
    LCD_WR_LOW();
    // 这里需要几个nop延时,根据LCD芯片速度调整
    __NOP(); __NOP();
    LCD_WR_HIGH();
    
    LCD_CS_HIGH();
}

注意:8080接口的时序对延时很敏感。我曾经遇到过一个问题:WR低电平保持时间不够,导致数据写不进去。后来用示波器一量,发现低电平只有10ns,而芯片要求至少20ns。加两个NOP指令就解决了。所以,如果你发现LCD显示花屏或颜色不对,先查WR脉冲宽度。

4.4 DMA加速传输——让CPU喘口气

好了,前面讲的都是怎么把数据发出去。但你想过没有,如果每次发数据都要CPU亲自操作,那CPU啥也别干了。尤其是刷全屏的时候,320x240的屏幕,16位色,一帧就是153600字节。用SPI发,就算2MB/s的速度,也要70多ms。这期间CPU一直在等SPI传输完成。

DMA(Direct Memory Access)就是来解决这个问题的。它可以让数据在内存和外设之间直接传输,不需要CPU干预。你只需要告诉DMA:数据从哪里来,要发到哪里去,发多少字节。然后DMA自己搞定,发完了给你一个中断。

代码示例(STM32 SPI + DMA发送):

// 使用DMA发送一帧数据
void LCD_DrawFrame(uint16_t *frameBuffer) {
    // 先设置窗口,准备刷屏
    LCD_SetWindow(0, 0, 239, 319);
    
    // 启动DMA传输
    // 源地址:frameBuffer,目标地址:SPI数据寄存器
    // 传输长度:320*240 = 76800个半字(16位)
    HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)frameBuffer, 320*240*2);
    
    // CPU可以去做其他事情了
    // DMA传输完成后,会调用回调函数
}

// DMA传输完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    if(hspi->Instance == SPI1) {
        // 刷屏完成,可以更新下一帧或做其他处理
        frameReady = 1;
    }
}

用了DMA之后,刷一帧的时间从70ms降到了不到20ms(假设SPI时钟18MHz)。而且这20ms里,CPU几乎不参与,可以处理触摸、传感器、通信等任务。

我的经验:DMA虽然好,但有个坑——缓存一致性问题。有些单片机(比如STM32F4)有Cache,DMA直接访问内存,而CPU可能还在Cache里操作数据。结果就是DMA发出去的是旧数据。解决办法很简单:在启动DMA之前,调用SCB_CleanDCache()刷新Cache。我因为这个bug调了整整一个下午,最后发现是Cache没刷。

4.5 接口选型建议

最后,我根据实际项目经验,给一个接口选型的参考表:

屏幕类型 推荐接口 引脚数 最大刷新率 适用场景
128x64 单色OLED 3线/4线SPI 4-5 30fps 手表、小仪表盘
160x128 彩屏 4线SPI 5 25fps MP3播放器、遥控器
320x240 彩屏 8080 8位 12 40fps 智能家居面板
480x320 彩屏 8080 16位 20 55fps 手持设备、工控屏
800x480 以上 RGB/MIPI 24+ 60fps HMI、车载

嗯,总结一下:SPI省引脚,适合小屏;8080接口速度快,适合中大屏;DMA是必须的,否则CPU会被刷屏任务拖死。下一章我们会讲LVGL的移植和配置,到时候这些接口知识都会用上。