4、SPI与显示驱动:SPI控制命令与数据发送

SPI接口在显示驱动芯片里,可以说是最常用的通信方式之一。我刚开始做显示驱动时,用的就是ILI9341这颗经典芯片。说实话,SPI的时序看起来简单,但真正调通、调稳,还是有不少门道的。

4.1 SPI通信的基本概念

SPI全称是Serial Peripheral Interface,串行外设接口。它用四根线完成通信:

  • SCLK:时钟线,由主机产生
  • MOSI:主机输出,从机输入
  • MISO:主机输入,从机输出
  • CS:片选信号,低电平有效

显示驱动芯片通常只需要三根线——SCLK、MOSI和CS。MISO一般不用,因为数据是单向流向屏幕的。嗯,这里要注意:有些驱动IC支持读操作,比如读取寄存器状态,这时候MISO就有用了。

4.2 SPI控制命令与数据的区别

显示驱动芯片怎么区分你发的是命令还是数据?答案是通过DCX引脚(也叫D/CX、RS)。

DCX电平 含义 示例
低电平(0) 发送命令 0x36(设置扫描方向)
高电平(1) 发送数据 颜色值、参数等

我遇到过不少新手,上来就把命令和数据混着发,结果屏幕死活不亮。后来一查,DCX引脚没控制对。说白了,DCX就是告诉芯片:我现在要给你下指令,还是给你喂数据。

4.3 ILI9341的SPI时序要求

ILI9341这颗芯片,我用了不下五年。它的SPI时序有几个关键点:

  • 时钟极性(CPOL):通常设为0,即空闲时时钟为低电平
  • 时钟相位(CPHA):通常设为0,即第一个时钟沿采样数据
  • 最大时钟频率:一般不超过10MHz,实际建议跑6-8MHz比较稳
  • CS建立时间:CS拉低后,至少等待5ns再发时钟
  • CS保持时间:最后一个时钟结束后,CS至少保持15ns再拉高

核心要点:ILI9341的SPI模式是Mode 0(CPOL=0, CPHA=0)。数据在时钟上升沿被采样,下降沿切换。

4.4 初始化序列编写

显示驱动芯片上电后,必须按照数据手册的初始化序列进行配置。我习惯把初始化序列写成结构体数组,方便管理和调试。

// 初始化序列结构体
typedef struct {
    uint8_t cmd;      // 命令
    uint8_t len;      // 参数长度
    uint8_t *params;  // 参数指针
} lcd_init_cmd_t;

// ILI9341初始化序列示例
static const lcd_init_cmd_t ili9341_init[] = {
    // 软件复位
    {0x01, 0, NULL},
    // 延时120ms
    {0xFF, 0, NULL},  // 自定义延时命令
    
    // 电源控制A
    {0xCB, 5, (uint8_t[]){0x39, 0x2C, 0x00, 0x34, 0x02}},
    
    // 电源控制B
    {0xCF, 3, (uint8_t[]){0x00, 0xC1, 0x30}},
    
    // 驱动时序控制A
    {0xE8, 3, (uint8_t[]){0x85, 0x00, 0x78}},
    
    // 驱动时序控制B
    {0xEA, 2, (uint8_t[]){0x00, 0x00}},
    
    // 电源控制1
    {0xC0, 1, (uint8_t[]){0x23}},
    
    // 电源控制2
    {0xC1, 1, (uint8_t[]){0x10}},
    
    // VCOM控制1
    {0xC5, 2, (uint8_t[]){0x3E, 0x28}},
    
    // VCOM控制2
    {0xC7, 1, (uint8_t[]){0x86}},
    
    // 内存访问控制
    {0x36, 1, (uint8_t[]){0x48}},
    
    // 像素格式设置
    {0x3A, 1, (uint8_t[]){0x55}},  // 16位色
    
    // 帧率控制
    {0xB1, 2, (uint8_t[]){0x00, 0x18}},
    
    // 显示功能控制
    {0xB6, 3, (uint8_t[]){0x08, 0x82, 0x27}},
    
    // 伽马设置
    {0xF2, 1, (uint8_t[]){0x00}},
    
    // 正伽马校正
    {0x26, 1, (uint8_t[]){0x01}},
    
    // 退出睡眠
    {0x11, 0, NULL},
    // 延时120ms
    {0xFF, 0, NULL},
    
    // 显示开启
    {0x29, 0, NULL},
};

我的经验:初始化序列里,延时非常关键。ILI9341从睡眠模式唤醒后,需要至少120ms的稳定时间。我曾经因为延时不够,屏幕出现花屏,查了两天才发现是复位后等待时间太短。

4.5 SPI发送函数实现

写SPI发送函数时,我习惯把命令发送和数据发送分开,这样代码更清晰。

// SPI发送命令
void LCD_WriteCmd(uint8_t cmd) {
    DCX_LOW();          // DCX拉低,表示命令
    CS_LOW();           // 片选使能
    SPI_WriteByte(cmd); // 发送命令字节
    CS_HIGH();          // 片选禁止
}

// SPI发送数据
void LCD_WriteData(uint8_t data) {
    DCX_HIGH();         // DCX拉高,表示数据
    CS_LOW();           // 片选使能
    SPI_WriteByte(data);// 发送数据字节
    CS_HIGH();          // 片选禁止
}

// 批量发送数据(用于填充颜色)
void LCD_WriteDataBuf(uint8_t *buf, uint32_t len) {
    DCX_HIGH();
    CS_LOW();
    for(uint32_t i = 0; i < len; i++) {
        SPI_WriteByte(buf[i]);
    }
    CS_HIGH();
}

避坑指南:我曾经在批量发送数据时,忘记在每次发送后检查SPI忙标志。结果数据发送太快,芯片来不及处理,导致显示错位。建议在SPI_WriteByte函数里加入忙等待,或者用DMA方式发送。

4.6 初始化流程的执行

初始化序列的执行逻辑其实很简单:遍历数组,依次发送命令和参数。我一般这样写:

void LCD_Init(void) {
    uint32_t i;
    
    // 硬件复位
    RST_LOW();
    delay_ms(10);
    RST_HIGH();
    delay_ms(120);
    
    // 执行初始化序列
    for(i = 0; i < sizeof(ili9341_init)/sizeof(ili9341_init[0]); i++) {
        LCD_WriteCmd(ili9341_init[i].cmd);
        
        // 自定义延时命令
        if(ili9341_init[i].cmd == 0xFF) {
            delay_ms(120);
            continue;
        }
        
        // 发送参数
        for(uint8_t j = 0; j < ili9341_init[i].len; j++) {
            LCD_WriteData(ili9341_init[i].params[j]);
        }
    }
    
    // 清屏为黑色
    LCD_Clear(0x0000);
}

4.7 常见问题与调试技巧

做显示驱动这么多年,我总结了几条调试经验:

  • 屏幕不亮:先检查电源和复位,再用逻辑分析仪看SPI波形
  • 花屏:多半是初始化序列不对,或者时序参数没配好
  • 颜色不对:检查像素格式设置,ILI9341支持12位、16位、18位色
  • 显示偏移:检查内存访问控制寄存器(0x36)的设置

调试利器:逻辑分析仪是调试SPI时序的最佳工具。我每次调新屏,都会先抓一波波形,确认命令和数据发送正确,再去看显示效果。

4.8 本章知识体系

下面这张图,是我对SPI与显示驱动核心逻辑的总结:

SPI与显示驱动核心知识体系 SPI通信基础 SCLK / MOSI / MISO / CS 命令 vs 数据 DCX引脚控制 时序要求 CPOL / CPHA / 频率 初始化序列编写 结构体数组 / 命令参数 / 延时控制 SPI发送函数实现 WriteCmd / WriteData / WriteDataBuf / DMA优化 调试与验证:逻辑分析仪 / 波形分析

这张图把SPI驱动显示芯片的整个流程串起来了。从最底层的SPI协议,到命令数据的区分,再到时序要求、初始化序列、发送函数,最后是调试验证。每一步都环环相扣。

个人建议:刚开始学的时候,别急着写复杂的初始化序列。先点亮背光,再发一个清屏命令,看看屏幕能不能正常显示颜色。一步一步来,出问题了也好定位。

SPI驱动显示芯片,说白了就是控制好DCX引脚,按照时序要求把命令和数据发出去。ILI9341的初始化序列虽然长,但每个寄存器都有明确的用途。我建议你把数据手册打印出来,对照着看,理解每个寄存器的作用,而不是盲目复制代码。


公众号:蓝海资料掘金营,微信deep3321