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通信流程一般是:
- 拉低CS,选中芯片
- 发送读命令(通常是0x80 | 寄存器地址)
- 发送一个哑字节(0x00),同时接收芯片返回的数据
- 拉高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;
}
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;
}
3.3 UART接口:简单但有限制
UART在计量芯片里用得少一些,但有些老款芯片还在用。它只需要TX和RX两根线,没有时钟线,所以通信双方必须约定好波特率。
3.3.1 时序特点
- 空闲时,数据线为高电平
- 起始位:一个低电平位
- 数据位:5~8位,常用8位
- 停止位:1位或2位高电平
UART的时序容错性比SPI和I2C差。波特率偏差超过2%就可能丢数据。我建议用UART时,先发一个固定数据包做回环测试,确认通信没问题再往下走。
3.4 通信故障排查:三板斧
不管用哪种接口,出问题时的排查思路是相通的。我总结了三板斧:
- 看波形:示波器是亲爹。抓一下SCLK、SDA、MISO这些关键信号,看看时序对不对。
- 查电平:用万用表量一下引脚电压。3.3V的芯片你给5V,不烧才怪。
- 读ID:大多数计量芯片都有芯片ID寄存器。如果能读到ID,说明通信基本通了。
我曾经遇到一个最诡异的故障:SPI通信偶尔失败,频率大概一小时一次。查了三天,最后发现是电源纹波太大,导致芯片内部逻辑偶尔复位。加了个100μF的电解电容就解决了。所以啊,通信问题不一定是通信本身的问题,电源也要查。
3.5 三种接口对比
| 特性 | SPI | I2C | UART |
|---|---|---|---|
| 引脚数 | 4 | 2 | 2 |
| 速度 | 最高(可达几十MHz) | 中等(标准100kHz,快速400kHz) | 较低(常用115200bps) |
| 多设备支持 | 需额外片选 | 地址寻址,最多127个 | 点对点 |
| 全双工 | 是 | 否 | 是 |
| 抗干扰能力 | 较好 | 一般 | 较差 |
我个人建议:新设计优先选SPI,省心。如果引脚紧张,I2C也行。UART嘛,除非是老芯片兼容,否则尽量别碰。
好了,通信协议这块就聊到这儿。下一章咱们开始写真正的驱动代码,到时候我会把寄存器配置、校准流程这些实战内容都掏出来。你先把SPI和I2C的时序吃透,后面就顺了。