3、寄存器访问协议:I2C协议基础、I2C读写时序、SMBus协议、PMBus协议简介
好,咱们进入第三个章节。寄存器配置,说白了就是通过某种“语言”跟PMIC芯片对话。你告诉它“输出电压调到1.8V”,它回你一句“收到,已调好”。这个对话的规则,就是寄存器访问协议。
我个人习惯把这类协议分成三个层次:最底层是物理连线怎么接,中间层是数据怎么打包发送,最上层是命令和数据的含义。今天咱们聊的I2C、SMBus、PMBus,正好对应了这三个层次。
3.1 I2C协议基础
I2C,全称Inter-Integrated Circuit,是飞利浦公司搞出来的。它只用了两根线:SCL(时钟线)和SDA(数据线)。你想想看,一个PMIC芯片,加上主控MCU,两根线就能把所有寄存器都管起来,是不是很省引脚?
I2C总线上可以挂多个设备。每个设备都有一个唯一的7位或10位地址。主设备(通常是MCU)发起通信,从设备(PMIC)响应。嗯,这里要注意:地址不能冲突,否则两个设备同时应答,总线就乱套了。
关键概念:
- 主设备(Master): 控制时钟,发起通信。通常是MCU或SoC。
- 从设备(Slave): 响应主设备。PMIC就是典型的从设备。
- SCL: 时钟线,由主设备驱动。
- SDA: 数据线,双向传输。
我在项目中遇到过一个问题:两个PMIC芯片用了相同的I2C地址,结果MCU一读寄存器,两个芯片同时往SDA上拉电平,数据全乱了。后来我加了一个I2C多路复用器才搞定。所以,选型时一定要确认地址是否可配置。
3.2 I2C读写时序
时序这东西,看着波形图容易晕。我教你一个口诀:“起始、地址、读写、应答、数据、应答、停止”。记住这七个词,I2C时序就掌握了八成。
具体来说:
- 起始条件: SCL为高时,SDA从高变低。表示“我要开始通信了”。
- 发送从设备地址: 主设备在SCL的每个时钟周期发送1位数据,共7位地址 + 1位读写标志(0=写,1=读)。
- 等待应答: 从设备在第9个时钟周期将SDA拉低,表示“我收到了,继续”。
- 发送/接收数据: 每8位数据后跟一个应答位。写操作时主设备发数据,读操作时从设备发数据。
- 停止条件: SCL为高时,SDA从低变高。表示“通信结束”。
避坑指南: 我曾经在调试时发现,读寄存器总是返回0xFF。查了半天,原来是停止条件没发对。主设备在读完最后一个字节后,必须发送“非应答”(NACK)再发停止条件,否则从设备会一直占用总线。
下面是一个典型的I2C写寄存器时序代码示例(伪代码):
// 写寄存器:向地址0x30的设备,寄存器0x10写入0x55
I2C_Start(); // 起始条件
I2C_SendByte(0x30 << 1 | 0); // 设备地址 + 写标志
I2C_WaitAck(); // 等待应答
I2C_SendByte(0x10); // 寄存器地址
I2C_WaitAck(); // 等待应答
I2C_SendByte(0x55); // 要写入的数据
I2C_WaitAck(); // 等待应答
I2C_Stop(); // 停止条件
读操作稍微复杂一点,需要先写寄存器地址,再重新发起起始条件进行读:
// 读寄存器:从地址0x30的设备,读取寄存器0x10的值
I2C_Start(); // 起始条件
I2C_SendByte(0x30 << 1 | 0); // 设备地址 + 写标志
I2C_WaitAck(); // 等待应答
I2C_SendByte(0x10); // 寄存器地址
I2C_WaitAck(); // 等待应答
I2C_Start(); // 重复起始条件
I2C_SendByte(0x30 << 1 | 1); // 设备地址 + 读标志
I2C_WaitAck(); // 等待应答
data = I2C_ReadByte(); // 读取数据
I2C_SendNack(); // 发送非应答
I2C_Stop(); // 停止条件
你可能会问:为什么要搞个“重复起始条件”?直接发停止再发起始不行吗?其实也行,但重复起始可以保证总线不被其他设备抢占,原子性更好。我个人习惯在连续读写时都用重复起始。
3.3 SMBus协议
SMBus,全称System Management Bus,是I2C的一个“加强版”。它由Intel提出,主要用于电源管理和系统监控。说白了,SMBus在I2C的基础上加了一些规矩:
- 时钟频率固定: 最低10kHz,最高100kHz。不像I2C可以跑到400kHz甚至更高。
- 超时机制: 如果时钟低电平超过35ms,设备必须复位。这个设计是为了防止总线死锁。
- 数据包格式: SMBus定义了多种数据包类型,比如写字节、读字节、写字、读字、块读写等。
- 地址解析协议(ARP): 可以动态分配地址,避免地址冲突。
我记得有一次,一个客户说他们的PMIC在高温下偶尔通信失败。我一看波形,发现时钟低电平时间超过了35ms,触发了SMBus的超时复位。后来在软件里加了超时重试机制,问题就解决了。
注意: 虽然SMBus兼容I2C,但反过来不一定成立。如果你的主控只支持I2C,去访问一个SMBus从设备,可能会因为超时机制而出现问题。建议在选型时确认主控和从设备是否都支持SMBus。
SMBus的读写操作比I2C更规范。比如“写字节”操作:
// SMBus写字节:向设备地址0x30,命令码0x10,写入数据0x55
SMBus_Start();
SMBus_SendByte(0x30 << 1 | 0); // 设备地址 + 写
SMBus_WaitAck();
SMBus_SendByte(0x10); // 命令码(相当于寄存器地址)
SMBus_WaitAck();
SMBus_SendByte(0x55); // 数据
SMBus_WaitAck();
SMBus_Stop();
你看,跟I2C几乎一样,对吧?区别在于SMBus对时序要求更严格,而且有PEC(包错误校验)可选。PEC是在数据包末尾加一个CRC校验字节,用来检测传输错误。我建议在可靠性要求高的场合(比如服务器电源管理)开启PEC。
3.4 PMBus协议简介
PMBus,全称Power Management Bus,是在SMBus基础上专门为电源管理制定的协议。它定义了标准的命令集,比如设置输出电压、读取电流、配置保护阈值等。
你想想看,如果没有PMBus,每个PMIC厂商都用自己的寄存器定义,那工程师得多痛苦?换一个芯片就得重写全部驱动代码。PMBus的出现,就是为了解决这个问题。
PMBus的核心特点:
- 标准命令集: 比如VOUT_COMMAND(设置输出电压,命令码0x21)、READ_VOUT(读取输出电压,命令码0x8B)等。
- 线性数据格式: 电压、电流等物理量用线性格式表示,比如V = Y × 2^N,其中Y和N是16位整数。
- 分组操作: 支持同时配置多个PMIC,实现同步上电时序。
- 故障管理: 定义了标准的故障状态寄存器,方便诊断。
举个例子: 用PMBus设置输出电压为1.8V,命令码是0x21,数据格式是线性格式。假设N=-13,Y=1.8 × 2^13 = 14746(0x399A)。那么发送的数据就是:
PMBus_Start();
PMBus_SendByte(0x30 << 1 | 0); // 设备地址
PMBus_WaitAck();
PMBus_SendByte(0x21); // VOUT_COMMAND
PMBus_WaitAck();
PMBus_SendByte(0x9A); // 数据低字节
PMBus_WaitAck();
PMBus_SendByte(0x39); // 数据高字节
PMBus_WaitAck();
PMBus_Stop();
我曾经在一个48V电源模块项目里用过PMBus。当时需要配置过压保护阈值,直接用PMBus的VOUT_OV_FAULT_LIMIT命令(0x40)就搞定了,根本不用翻几百页的数据手册去找寄存器地址。这就是标准化的好处。
3.5 三种协议的对比与选择
说了这么多,到底该用哪个?我整理了一个表格,方便你快速决策:
| 特性 | I2C | SMBus | PMBus |
|---|---|---|---|
| 时钟频率 | 100kHz~3.4MHz | 10kHz~100kHz | 10kHz~400kHz |
| 超时机制 | 无 | 有(35ms) | 有(35ms) |
| 数据包格式 | 自由定义 | 标准格式 | 标准格式 + 命令集 |
| 错误校验 | 无 | 可选PEC | 可选PEC |
| 适用场景 | 通用外设 | 系统管理、电池 | 电源管理 |
| 兼容性 | 基础协议 | 兼容I2C | 基于SMBus |
我的建议是:
- 如果只是简单的PMIC配置,用I2C就够了,代码简单,调试方便。
- 如果系统对可靠性要求高(比如服务器、基站),选SMBus,超时机制能防止总线死锁。
- 如果要做复杂的电源管理(比如多路输出、动态调压),直接上PMBus,标准命令集能省很多事。
个人经验: 我刚开始做PMIC驱动时,总觉得I2C简单,就全用I2C。后来发现,当系统中有多个PMIC时,I2C的地址冲突、总线竞争问题特别头疼。换成SMBus后,这些问题少了很多。所以,别嫌麻烦,前期多花点时间选对协议,后期能省很多调试时间。
好了,这一章就到这里。下一章咱们聊聊具体的寄存器配置实战,包括怎么读数据手册、怎么计算寄存器值、怎么验证配置是否正确。到时候我会拿一个真实的PMIC芯片做例子,手把手带你走一遍流程。