第3章:接口协议详解(上):SPI接口协议、I2C接口协议、配置寄存器读写实战

各位同学,今天咱们来聊聊显示驱动芯片的“嘴巴”——接口协议。说白了,芯片再聪明,也得有人告诉它怎么干活。SPI和I2C就是最常用的两种“传话筒”。

我个人习惯把接口协议比作“沟通的规矩”。你想想看,两个人说话,总得有个先来后到、谁先说谁后听吧?芯片之间也一样。SPI和I2C就是两套不同的规矩,各有各的脾气。

3.1 SPI接口协议:简单粗暴,但很可靠

SPI,全称Serial Peripheral Interface,串行外设接口。这玩意儿是Motorola公司发明的,年代久远但生命力顽强。我在项目中遇到过不少次,SPI的调试往往是最省心的,只要线接对了,时序别太离谱,基本一次过。

SPI的物理层很简单,就4根线:

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

嗯,这里要注意:SPI是全双工的。什么意思?就是主机发数据的同时,从机也能回数据。这一点比I2C强,I2C是半双工,同一时间只能一个方向说话。

核心要点:SPI的通信由时钟边沿决定。主机在时钟的上升沿或下降沿发送数据,从机在相反的边沿采样。这个“极性”和“相位”可以配置,也就是我们常说的CPOL和CPHA。

我曾经踩过一个坑:某款驱动芯片的SPI时序要求CPOL=1、CPHA=0,我默认用了模式0,结果读回来的寄存器全是0xFF。查了半天,才发现是时序不匹配。从那以后,我每次拿到新芯片,第一件事就是看数据手册里的时序图。

3.2 I2C接口协议:两根线,搞定一切

I2C,Inter-Integrated Circuit,由Philips公司发明。它只用两根线:SDA(数据线)和SCL(时钟线)。但别小看这两根线,它能挂载多个设备,每个设备有唯一的地址。

I2C的通信流程是这样的:

  1. 主机发送起始条件:SCL高电平时,SDA从高变低
  2. 主机发送7位从机地址 + 1位读写位
  3. 从机回复ACK(应答位)
  4. 数据传输:每8位数据后跟一个ACK
  5. 主机发送停止条件:SCL高电平时,SDA从低变高

调试技巧:I2C调试时,我最常用的工具就是逻辑分析仪。把SDA和SCL两根线接上,抓一段波形,一眼就能看出问题。比如地址对不对、ACK有没有收到、数据有没有错位。

我记得有一次,客户反馈说驱动芯片偶尔不响应。我抓了波形才发现,是上拉电阻选得太大了,导致SDA信号上升沿太慢,超过了I2C的时序要求。换成4.7kΩ的电阻后,问题解决。

3.3 配置寄存器读写实战

好了,理论说完了,咱们来点实际的。显示驱动芯片的配置,说白了就是读写寄存器。每个寄存器有地址,有数据位,你往里面写值,芯片就按你的意思干活。

以SPI为例,写寄存器的流程:

// SPI写寄存器示例(伪代码)
void spi_write_reg(uint8_t addr, uint8_t data) {
    CS_LOW();                    // 拉低片选
    spi_transfer(0x02);          // 写命令(0x02表示写操作)
    spi_transfer(addr);          // 寄存器地址
    spi_transfer(data);          // 要写入的数据
    CS_HIGH();                   // 拉高片选
}

读寄存器的流程:

// SPI读寄存器示例(伪代码)
uint8_t spi_read_reg(uint8_t addr) {
    uint8_t data;
    CS_LOW();                    // 拉低片选
    spi_transfer(0x03);          // 读命令(0x03表示读操作)
    spi_transfer(addr);          // 寄存器地址
    data = spi_transfer(0x00);   // 发送哑数据,接收返回值
    CS_HIGH();                   // 拉高片选
    return data;
}

I2C的读写稍微复杂一点,因为要处理从机地址和ACK:

// I2C写寄存器示例(伪代码)
void i2c_write_reg(uint8_t slave_addr, uint8_t reg_addr, uint8_t data) {
    i2c_start();                          // 起始条件
    i2c_send_byte(slave_addr << 1 | 0);  // 从机地址 + 写位
    i2c_wait_ack();                       // 等待ACK
    i2c_send_byte(reg_addr);              // 寄存器地址
    i2c_wait_ack();
    i2c_send_byte(data);                  // 要写入的数据
    i2c_wait_ack();
    i2c_stop();                           // 停止条件
}

避坑指南:我曾经在I2C读寄存器时犯过一个低级错误——忘了发送“重复起始条件”。I2C读寄存器的标准流程是:先写寄存器地址,然后发重复起始条件,再发读命令。如果直接发停止条件再发起始条件,有些芯片会不认账。

3.4 知识体系结构图

下面这张图,是我自己总结的接口协议知识体系。你看一眼,心里就有数了:

接口协议知识体系 SPI接口协议 I2C接口协议 4线制:SCLK/MOSI/MISO/CS 全双工通信 CPOL/CPHA配置 片选控制 2线制:SDA/SCL 半双工通信 7位/10位地址 ACK/NACK机制 寄存器读写实战 写操作:命令+地址+数据 读操作:命令+地址+哑数据 调试工具:逻辑分析仪

这张图把SPI和I2C的核心要素都列出来了。你调试的时候,对照着这张图,一条一条检查,基本不会漏掉什么。

3.5 实战中的小技巧

最后,分享几个我这些年积累的实战技巧:

  • 先读后写:修改寄存器之前,先读一下当前值。很多寄存器是“读-改-写”的,直接写会覆盖掉其他位。
  • 批量初始化:显示驱动芯片的寄存器动辄几十上百个,我习惯把初始化序列做成数组,一次性写入,省时省力。
  • 验证回读:写完寄存器后,再读一遍确认。别问我为什么,问就是吃过亏。

个人习惯:我每次调试新芯片,都会先写一个“寄存器dump”函数,把芯片所有寄存器的值读出来打印到串口。这样一眼就能看出芯片的初始状态对不对,省去了很多猜疑的时间。

好了,这一章的内容就到这里。SPI和I2C是基本功,搞懂了它们,后面的MIPI DSI、LVDS这些高速接口学起来会轻松很多。下一章咱们继续聊接口协议的下半部分,到时候见。


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