3、SPI/I2C通信协议详解:计量芯片常用通信接口

做计量芯片驱动,说白了就是跟芯片「对话」。你得告诉它要干什么,它再把数据吐给你。这个对话靠的就是通信协议。计量芯片里最常见的三种接口——SPI、I2C、UART,我这些年都摸了个遍。今天咱们就一个一个拆开看。

3.1 SPI接口:速度与可靠性的首选

我个人最喜欢用SPI。为什么?简单、快、可靠。计量芯片对实时性要求高,SPI的全双工通信正好对路。

3.1.1 四根线,搞定一切

SPI用四根线:

  • SCLK(时钟)—— 主机提供,决定通信节奏
  • MOSI(主机输出,从机输入)—— 数据从MCU流向芯片
  • MISO(主机输入,从机输出)—— 数据从芯片流回MCU
  • CS(片选)—— 低电平有效,选中你要通信的芯片

嗯,这里要注意:片选信号必须拉低才能通信。我见过新手把CS一直拉低,结果多个设备打架,数据全乱套。

3.1.2 时序分析:别被模式搞晕

SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。计量芯片常用的是模式0和模式3。

模式 CPOL CPHA 特点
模式0 0 0 空闲时时钟低电平,第一个边沿采样
模式3 1 1 空闲时时钟高电平,第一个边沿采样

我在项目中遇到过一件事:某款计量芯片手册写的是模式0,结果实际调试时死活读不到数据。后来用示波器一看,芯片内部默认是模式3。所以啊,别全信手册,示波器才是真理。

3.1.3 寄存器读写驱动实现

以读取计量芯片的电压寄存器为例,SPI通信流程一般是:

  1. 拉低CS,选中芯片
  2. 发送读命令(通常是0x80 | 寄存器地址)
  3. 发送一个哑字节(0x00),同时接收芯片返回的数据
  4. 拉高CS,结束通信
// SPI读取16位寄存器示例
uint16_t spi_read_reg(uint8_t reg_addr) {
    uint8_t tx_buf[3];
    uint8_t rx_buf[3] = {0};
    uint16_t value = 0;

    tx_buf[0] = 0x80 | reg_addr;  // 读命令
    tx_buf[1] = 0x00;             // 哑字节
    tx_buf[2] = 0x00;             // 哑字节

    CS_LOW();                     // 拉低片选
    HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 3, 100);
    CS_HIGH();                    // 拉高片选

    value = (rx_buf[1] << 8) | rx_buf[2];
    return value;
}
避坑指南:我曾经在SPI时钟频率上吃过亏。某款芯片手册说支持10MHz,结果实际跑到5MHz以上数据就出错。后来发现是PCB走线太长,信号反射严重。建议量产前先降频测试,稳定了再往上提。

3.2 I2C接口:引脚少,但别小看它

I2C只用两根线——SDA(数据)和SCL(时钟)。引脚省了,但协议比SPI复杂。计量芯片用I2C的场景也不少,尤其是那些引脚受限的设计。

3.2.1 时序要点

I2C的时序有几个关键点:

  • 起始条件:SCL高电平时,SDA从高变低
  • 停止条件:SCL高电平时,SDA从低变高
  • 应答机制:每发送一个字节,接收方必须拉低SDA表示应答

你想想看,如果从机没应答,说明它没收到或者忙不过来。我调试时最怕看到NACK(非应答),那意味着要么地址错了,要么芯片没上电。

3.2.2 寄存器读写驱动实现

I2C读寄存器分两步:先写地址,再读数据。

// I2C读取16位寄存器示例
uint16_t i2c_read_reg(uint8_t dev_addr, uint8_t reg_addr) {
    uint8_t data[2] = {0};
    uint16_t value = 0;

    // 第一步:发送寄存器地址
    HAL_I2C_Master_Transmit(&hi2c1, dev_addr, ®_addr, 1, 100);

    // 第二步:读取数据
    HAL_I2C_Master_Receive(&hi2c1, dev_addr, data, 2, 100);

    value = (data[0] << 8) | data[1];
    return value;
}
注意:I2C的上拉电阻不能省。我见过有人为了省成本,把上拉电阻去掉,结果通信时好时坏。一般4.7kΩ是安全值,总线电容大的话可以降到2.2kΩ。

3.3 UART接口:简单但有限制

UART在计量芯片里用得少一些,但有些老款芯片还在用。它只需要TX和RX两根线,没有时钟线,所以通信双方必须约定好波特率。

3.3.1 时序特点

  • 空闲时,数据线为高电平
  • 起始位:一个低电平位
  • 数据位:5~8位,常用8位
  • 停止位:1位或2位高电平

UART的时序容错性比SPI和I2C差。波特率偏差超过2%就可能丢数据。我建议用UART时,先发一个固定数据包做回环测试,确认通信没问题再往下走。

3.4 通信故障排查:三板斧

不管用哪种接口,出问题时的排查思路是相通的。我总结了三板斧:

  1. 看波形:示波器是亲爹。抓一下SCLK、SDA、MISO这些关键信号,看看时序对不对。
  2. 查电平:用万用表量一下引脚电压。3.3V的芯片你给5V,不烧才怪。
  3. 读ID:大多数计量芯片都有芯片ID寄存器。如果能读到ID,说明通信基本通了。

我曾经遇到一个最诡异的故障:SPI通信偶尔失败,频率大概一小时一次。查了三天,最后发现是电源纹波太大,导致芯片内部逻辑偶尔复位。加了个100μF的电解电容就解决了。所以啊,通信问题不一定是通信本身的问题,电源也要查。

3.5 三种接口对比

特性 SPI I2C UART
引脚数 4 2 2
速度 最高(可达几十MHz) 中等(标准100kHz,快速400kHz) 较低(常用115200bps)
多设备支持 需额外片选 地址寻址,最多127个 点对点
全双工
抗干扰能力 较好 一般 较差

我个人建议:新设计优先选SPI,省心。如果引脚紧张,I2C也行。UART嘛,除非是老芯片兼容,否则尽量别碰。

好了,通信协议这块就聊到这儿。下一章咱们开始写真正的驱动代码,到时候我会把寄存器配置、校准流程这些实战内容都掏出来。你先把SPI和I2C的时序吃透,后面就顺了。