2、SPI接口基础:四线制、时钟极性与相位、多线模式

SPI接口,说白了就是显示驱动芯片最常用的“门铃”。

我刚开始做驱动芯片开发那会儿,总觉得SPI太简单,不就是四根线嘛。结果第一次调屏,画面死活刷不出来,折腾了两天才发现是时钟相位配错了。嗯,从那以后我再也不敢小看这个接口了。

2.1 四线制:SCLK、MOSI、MISO、CS

标准的SPI有四根信号线,每一根都有自己的活要干:

  • SCLK(串行时钟):由主设备产生,决定数据传输的节奏。我习惯叫它“心跳线”。
  • MOSI(主出从入):主设备发送数据给从设备。在显示驱动芯片里,大部分指令和数据都走这根线。
  • MISO(主入从出):从设备回传数据给主设备。读寄存器状态、读帧缓冲都用它。
  • CS(片选):低电平有效。拉低时,从设备才“听”得见时钟和数据。

关键点:显示驱动芯片通常只写不读,所以很多方案直接把MISO省掉了。但如果你要做参数回读或者调试,MISO就是救命线。

我曾经在一个项目里,为了省一根IO,把MISO砍了。结果量产时发现有一批屏的gamma值偏了,没法回读校准,只能拆机换屏。教训啊。

2.2 时钟极性与相位(CPOL、CPHA)

这两个参数,是SPI最容易踩坑的地方。

  • CPOL(时钟极性):决定空闲时SCLK是高电平还是低电平。
  • CPHA(时钟相位):决定数据是在时钟的上升沿还是下降沿被采样。

组合起来有四种模式:

模式 CPOL CPHA 采样沿 空闲电平
模式0 0 0 上升沿
模式1 0 1 下降沿
模式2 1 0 上升沿
模式3 1 1 下降沿

注意:显示驱动芯片最常用的是模式0和模式3。我建议你拿到芯片手册后,第一件事就是确认CPOL和CPHA。别问我怎么知道的——我烧过三块屏才记住这个教训。

为什么会这样?因为很多国产屏厂的手册写得含糊,只写“SPI接口”,不写具体模式。你只能看时序图,数数采样沿在哪。

2.3 单线/双线/四线模式

标准SPI是单线模式,也就是MOSI发、MISO收。但显示驱动芯片为了刷屏速度,搞出了多线模式。

  • 单线模式(Standard SPI):一根数据线发,一根数据线收。速度一般,但兼容性最好。
  • 双线模式(Dual SPI):把MOSI和MISO都变成双向数据线。一次传2个bit,速度翻倍。
  • 四线模式(Quad SPI):再加两根数据线(通常叫IO2、IO3)。一次传4个bit,刷屏速度起飞。

我做过一个手表项目,用单线SPI刷128x128的屏,帧率只有15fps,拖影严重。换成四线模式后,直接干到60fps。你想想看,同样的晶振频率,数据吞吐量差了4倍。

实战技巧:多线模式通常需要先发一个命令字节,告诉芯片“我要切到双线/四线模式了”。这个命令只能用单线模式发。我习惯在初始化代码里先配好模式,后面全速跑数据。

2.4 知识体系总览

下面这张图,是我自己画的一个SPI接口知识结构。你可以把它当成一个快速索引:

SPI接口基础 四线制 时钟极性与相位 多线模式 SCLK - 时钟线 MOSI - 主出从入 MISO - 主入从出 CS - 片选 CPOL = 0 / 1 CPHA = 0 / 1 四种模式组合 模式0 ~ 模式3 单线:1bit/时钟 双线:2bit/时钟 四线:4bit/时钟 速度逐级翻倍 核心原则:主设备控制时钟,从设备响应片选 显示驱动芯片通常作为从设备,接收主控发来的指令和像素数据 避坑指南:先看手册时序图,再配CPOL/CPHA 多线模式下,命令头必须用单线模式发送

2.5 代码示例:初始化SPI为模式0

下面是我在一个LCD驱动项目里实际用过的初始化代码。注意看CPOL和CPHA的配置:

// SPI初始化 - 模式0 (CPOL=0, CPHA=0)
void spi_init_mode0(void) {
    // 配置GPIO
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = SCLK_PIN | MOSI_PIN | CS_PIN;
    gpio.Mode = GPIO_MODE_AF_PP;
    gpio.Pull = GPIO_PULLUP;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &gpio);
    
    // 配置SPI外设
    SPI_HandleTypeDef hspi = {0};
    hspi.Instance = SPI1;
    hspi.Init.Mode = SPI_MODE_MASTER;
    hspi.Init.Direction = SPI_DIRECTION_2LINES;
    hspi.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi.Init.CLKPolarity = SPI_POLARITY_LOW;   // CPOL = 0
    hspi.Init.CLKPhase = SPI_PHASE_1EDGE;       // CPHA = 0
    hspi.Init.NSS = SPI_NSS_SOFT;
    hspi.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
    hspi.Init.FirstBit = SPI_FIRSTBIT_MSB;
    HAL_SPI_Init(&hspi);
    
    // 拉高CS,进入空闲状态
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);
}

个人习惯:我一般把CPOL和CPHA的配置单独写成宏定义,方便不同屏之间切换。比如:

#define LCD_SPI_CPOL 0
#define LCD_SPI_CPHA 0

换屏时只改这两个宏,不用动初始化函数。

2.6 多线模式切换示例

四线模式刷屏时,命令和数据的发送方式不一样。看这段代码:

// 发送命令(必须用单线模式)
void lcd_send_cmd(uint8_t cmd) {
    HAL_GPIO_WritePin(DC_PORT, DC_PIN, GPIO_PIN_RESET); // 命令模式
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); // 拉低片选
    HAL_SPI_Transmit(&hspi, &cmd, 1, 100);
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);   // 拉高片选
}

// 发送像素数据(四线模式)
void lcd_send_pixels_quad(uint8_t *data, uint32_t len) {
    HAL_GPIO_WritePin(DC_PORT, DC_PIN, GPIO_PIN_SET);   // 数据模式
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);
    // 切换到四线模式(具体命令看芯片手册)
    lcd_send_cmd(0x3A); // 四线模式使能命令
    // 用四线方式发送数据
    HAL_SPI_Transmit(&hspi, data, len, 1000);
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);
}

我曾经踩过的坑:四线模式下,数据线的方向会变化。如果你用的是GPIO模拟SPI,记得在发送和接收之间切换IO方向。硬件SPI外设会自动处理,但软件模拟的话,这一步很容易忘。

好了,SPI接口的基础就这些。说白了就是四根线、四种模式、三种速度。实际项目中,你只要把CPOL和CPHA配对了,剩下的就是调时钟频率和看波形了。


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