搜"I2C SMBus"的人极多,但大部分文章只讲协议本身。本文从开漏输出电气特性讲到EDK2 I2C协议栈,把SMBus和I2C的关系彻底说清楚。
一、两根线怎么管一整个主板
想象一栋写字楼只有一部电梯,但每层都有几十个人想用。怎么避免一群人同时挤进去?电梯门上有个灯,亮着说明有人在里面,其他人就得等着。
I2C就是靠两根线干了同样的事。一根叫SCL(串行时钟),一根叫SDA(串行数据)。整个主板上所有I2C设备都挂在这两根线上——温度传感器、EEPROM、触摸板、电池管理芯片、Type-C PD控制器,全挤在上面。最夸张的一块板子上,EC挂了7条独立I2C总线,每条总线上挂着3到8个设备不等。
两根线管一整个主板,秘诀就是地址加仲裁。
二、物理层:为什么非要用开漏输出
两根线,SCL是时钟,SDA是数据。关键不是"两根线",关键是这两根线用的是开漏输出。
正常GPIO推挽输出:高电平强驱上去,低电平强拉下来。开漏输出不同——它只能拉低,不能拉高。高电平全靠上拉电阻拽上去。
为什么故意这么做?三个原因,缺一不可:
原因一:多主仲裁的基础
如果两个主机同时往总线上发数据,推挽输出一个拉高、一个拉低,等于VCC直接对地短路——IO口当场烧掉。开漏就不一样了:一个拉低,一个不管,最终总线就是低电平。仲裁机制靠的就是这个——主机发"高"却发现总线是"低",说明有别的兄弟在抢线,自己老实退出。
原因二:电平兼容
笔记本主板上电压域多如牛毛:PCH可能是1.8V IO域,EC是3.3V,DDR5 SPD又是1.1V,某些Type-C PD控制器甚至5V耐压。推挽输出得上电平转换芯片——一个8通道电平转换器几毛钱美金,BOM成本就上去了。开漏输出只需要上拉电阻接到对应电源域就行,一根总线上可以混搭不同电压的设备。
原因三:Clock Stretching
从设备处理请求来不及(比如温度传感器正在做ADC转换),它可以主动把SCL拉低,告诉主机"你等等我"。主机看到SCL没被释放,自动停下来等待。在推挽输出里这事做不到——你推挽输出高电平,从设备怎么拉低?这是SMBus规范里明确要求主机支持的特性。
踩坑:上拉电阻值不是随便抄的
照抄Intel参考设计2.2KΩ,400KHz Fast Mode正常,一切到1MHz Fast Mode Plus,示波器上的波形直接变成三角波——上升沿慢得不像话。
算一下:3.3V电压域,2.2K上拉,总线电容约100pF(PCB走线加3个设备输入电容),RC时间常数τ = 2.2K × 100pF = 220ns。400KHz时钟周期2.5μs,220ns上升沿约占10%,还能接受;1MHz周期1μs,220ns占22%,采样窗口被压缩到780ns,时序裕量几乎为零。
换成1.0KΩ,上升沿降到100ns,波形漂亮了。经验公式:Fast Mode Plus上拉不能超过1.5KΩ,总线电容超过150pF就得考虑I2C buffer。
三、寻址:7位地址的边界和陷阱
I2C地址是7位的,但总线上传的是8位——多出来那一位叫R/W位。最低位为0表示写,为1表示读。实际传输的地址字节 =(7位地址 << 1)| R/W。
地址空间从0x00到0x7F,共128个,但大部分不能随便用:
| 地址范围 | 用途 | 实际可用性 |
|---|---|---|
| 0x00 | 通用调用(General Call) | 广播,不用于寻址 |
| 0x01 ~ 0x07 | 保留/特殊用途 | 基本用不到 |
| 0x08 ~ 0x77 | 常规设备地址 | 112个 |
| 0x78 ~ 0x7B | 10位地址扩展指示 | 笔记本几乎没人用 |
| 0x7C ~ 0x7F | 保留 | 绝不使用 |
笔记本上最拥挤的地址段:0x50~0x57被DDR SPD EEPROM占满(每条DIMM一个SPD地址+一个TSOD温度地址),0x480x4F被各种温度传感器和ADC瓜分,0x200x27通常是GPIO扩展器。BOM选型时同一个I2C地址的两个芯片绝对不能挂同一根总线。
踩坑:7位还是8位,配错一次排查一整天
设备规格书写地址0xA0——有的厂商指7位地址0x50(实际传输左移一位变成0xA0),有的直接给8位写地址。BIOS代码里填了0xA0当7位地址传给EDK2 I2C协议栈,实际发出去的是0x140(超过7位范围),设备完全不响应ACK。
经验法则:如果地址以0xAX结尾,基本是8位表示法。最靠谱的方法是查Linux driver的reg属性——那里面一定是7位地址。EDK2的I2cHost->QueueRequest()接收的是7位地址。
四、SMBus:不是I2C的子集,是"军规版"
很多人一说SMBus就说是"I2C的子集"。不准确。SMBus是在I2C物理层基础上加了大量约束和功能,应该叫军规版I2C。
核心区别
| 特性 | I2C | SMBus |
|---|---|---|
| 最低速率 | 无下限 | >=10KHz |
| 超时机制 | 没有 | 25ms硬超时 |
| 逻辑电平 | 可变(VDD相关) | 固定:VIL<=0.8V, VIH>=2.1V |
| 错误校验 | 无 | PEC(CRC-8) |
| 块传输上限 | 无 | 32字节 |
| 总线挂起 | 允许 | 不许,超时就复位 |
一句话:I2C是自由市场,SMBus是有管理的菜市场——有保安、有时限、有收据。
25ms超时
I2C没有超时——从设备可以靠Clock Stretching无限期拉住SCL,设备挂了总线就死在那里。SMBus规定了25ms超时:从设备拉低SCL累计不能超过25ms,否则主机放弃事务并复位总线。BIOS阶段启动时间预算就那么点,超时处理是必选项。
PEC校验
SMBus每帧末尾可以跟一个CRC-8校验字节。多项式:x^8 + x^2 + x + 1(即0x07)。
// MdePkg/Include/IndustryStandard/SmBus.h
UINT8 CalculateSmbusPec (UINT8 *Buffer, UINTN Length) {
UINT8 Crc = 0;
while (Length--) {
Crc ^= *Buffer++;
for (UINTN i = 0; i < 8; i++) {
if (Crc & 0x80) {
Crc = (UINT8)((Crc << 1) ^ 0x07);
} else {
Crc <<= 1;
}
}
}
return Crc;
}
踩坑:PEC没配对,电池电量显示127%
BIOS里配了PEC但设备不支持——设备把CRC字节当成数据吞掉;没配PEC但设备要求PEC——设备给你的数据经过CRC异或,读到的是随机数。有一次电池电量在BIOS Setup界面显示127%,就是PEC没配对。
踩坑:SMBus和I2C混挂在同一条总线上
早期设计把SMBus设备(DDR SPD)和普通I2C设备(触摸板控制器)挂在PCH同一条SMBus控制器上。触摸板初始化阶段做了30ms Clock Stretching——对I2C合法,但对SMBus超过25ms超时。PCH SMBus控制器超时复位,把SPD读取事务也干掉了,内存初始化直接挂掉。
解决:DDR SPD独占PCH的SMBus通道0,其他低速I2C外设走EC的I2C控制器。物理隔离是解决协议不兼容的最省心方案。
五、DIMM SPD读取——SMBus最经典的用例
每根内存条上有颗SPD EEPROM,I2C地址从0x50开始逐槽偏移。BIOS在MRC之前必须读完SPD才能知道tCL、tRCD、tRP、tRAS这些时序参数。
// PEI阶段通过SMBus读SPD
#define SPD_ADDR_7BIT 0x50
#define DIMM_SPD_READ(slot) (((SPD_ADDR_7BIT + (slot)) << 1) | BIT0)
UINT8 SpdData[512]; // DDR4 SPD最多512字节
for (UINTN Offset = 0; Offset < 512; Offset++) {
Status = SmbusReadByte (
SmbusBar,
DIMM_SPD_READ(0), // 设备地址(含R/W位)
(UINT8)Offset, // SPD内偏移
&SpdData[Offset]
);
if (EFI_ERROR(Status)) {
DEBUG ((DEBUG_ERROR, "SPD[0x%02x] read failed: %r\n", Offset, Status));
break;
}
}
空槽NACK不是错误。 空的DIMM槽没有SPD EEPROM,SMBus发地址过去没设备应答——正常现象。BIOS要根据SA0-SA2地址线电平判断槽位有没有插内存。不要一看到NACK就Assert。
SMBus状态寄存器:调试第一眼
SMBus出问题,90%能在Host Status Register(偏移0x00)找到线索:
| Bit | 名称 | 症状 | 查什么 |
|---|---|---|---|
| 0 | BUSY | 传输进行中 | 正常 |
| 1 | INTR | 传输完成 | 正常 |
| 2 | DEV_ERR | NACK | 地址错了/芯片没电/槽位空 |
| 3 | BUS_ERR | 总线错 | START/STOP时序不对/SCL被拉死 |
| 4 | FAIL | 超时 | 从设备拉低SCL超过25ms/上拉电阻不对 |
调试口诀:DEV_ERR→查地址,BUS_ERR→拿示波器,FAIL→查上拉电阻。
六、UEFI里的I2C协议栈
UEFI规范给I2C定义了两层协议,EDK2实现在MdeModulePkg/Bus/I2c/下:
I2cHost Protocol — 控制器硬件抽象层
一个I2C硬件控制器对应一个I2cHost实例。核心接口QueueRequest()传入请求包,阻塞或异步返回结果。Host驱动负责配置时钟分频器、处理中断、管理FIFO。
// MdePkg/Include/Protocol/I2cHost.h
typedef struct _EFI_I2C_HOST_PROTOCOL {
EFI_I2C_HOST_PROTOCOL_QUEUE_REQUEST QueueRequest;
EFI_I2C_HOST_PROTOCOL_CAPABILITIES *Capabilities;
} EFI_I2C_HOST_PROTOCOL;
I2cIo Protocol — 设备抽象层
对上层驱动隐藏总线号和地址细节。要读温度传感器,不需要知道它在I2C3的0x4C,只通过DeviceGuid查找对应的I2cIo实例。总线号、地址、速率全部从ACPI _CRS或平台HOB查。
// MdePkg/Include/Protocol/I2cIo.h
typedef struct _EFI_I2C_IO_PROTOCOL {
EFI_I2C_IO_PROTOCOL_QUEUE_REQUEST QueueRequest;
EFI_GUID DeviceGuid;
UINT32 DeviceIndex;
UINT32 HardwareRevision;
} EFI_I2C_IO_PROTOCOL;
完整调用链
DXE Driver调 I2cIo->QueueRequest()
→ I2cBusDxe 根据 DeviceGuid 查ACPI表找到Host实例和7位地址
→ I2cHost->QueueRequest()
→ 平台I2C控制器驱动操作硬件寄存器
→ 数据返回
// MdeModulePkg/Bus/I2c/I2cDxe/I2cBus.c
// BIOS阶段I2C读设备的简化流程
EFI_STATUS ReadI2cDevice (
IN EFI_I2C_IO_PROTOCOL *I2cIo,
IN UINT8 RegisterAddr,
OUT UINT8 *Data,
IN UINTN DataLen
)
{
EFI_I2C_REQUEST_PACKET RequestPacket;
UINT8 WriteBuf[1];
// 第一阶段:写寄存器地址(不产生STOP,接RESTART)
WriteBuf[0] = RegisterAddr;
RequestPacket.Operation[0].Flags = 0;
RequestPacket.Operation[0].LengthInBytes = 1;
RequestPacket.Operation[0].Buffer = WriteBuf;
if (DataLen > 0) {
// 第二阶段:连续读
RequestPacket.Operation[1].Flags = I2C_FLAG_READ;
RequestPacket.Operation[1].LengthInBytes = (UINT32)DataLen;
RequestPacket.Operation[1].Buffer = Data;
RequestPacket.OperationCount = 2;
}
return I2cIo->QueueRequest (I2cIo, 0, NULL, &RequestPacket, NULL);
}
BIOS阶段I2C最常见的三个场景:读取SPD配置内存时序(MRC之前,不能错一个字节)、读取温度传感器做Thermal Profile(决定风扇初始转速)、与EC通信查询电池状态。三个场景对延迟要求不高,但对正确性要求极高——读错了风扇不转或内存训练失败,直接不开机。
七、EC怎么管理多条I2C总线
笔记本上I2C总线主力不是PCH,是EC。现代EC(ITE IT5570、Nuvoton NPCX、Microchip MEC系列)通常内置4到8个硬件I2C控制器,各自独立时钟、独立中断、独立DMA通道:
- 一条挂温度和风扇控制
- 一条挂电池和充电芯片
- 一条挂Type-C PD控制器
- 一条挂键盘和触摸板
- 一条挂环境光传感器和加速度计
EC固件里管这些总线是精细活。不能在中断上下文里发完整的I2C读事务——读电池32字节SMBus block耗时几百微秒,中断长时间不返回,EC看门狗直接复位。通常用状态机加DMA:EC发起I2C传输后立即返回,DMA自动从移位寄存器搬数据到内存,DMA完成时产生中断回调。
踩坑:EC休眠后I2C总线被锁死
笔记本合盖进S3休眠,EC部分掉电。但PCH的SMBus还活着——内存VR需要读SPD维持自刷新参数。EC端I2C IO掉电后,IO pad内部ESD保护二极管对地导通,形成低阻抗通路,把总线锁死在低电平。PCH怎么驱都驱不动,整个SMBus废掉。
解决:EC IO上串联隔离电阻(100Ω~200Ω),EC休眠时把IO配置为高阻输入态。
八、PECI:CPU和EC的"暗号"
除了I2C/SMBus,笔记本上还有一根特殊线:PECI(Platform Environment Control Interface)。Intel私有单线协议,一根线,开漏输出,速率2Kbps~2Mbps自适应。没有独立时钟线——接收端从数据里提取时钟(CDR)。
GetTemp:EC每秒发一次
EC每秒发一次GetTemp命令读CPU温度。CPU返回的不是温度数字,是**“离TjMax还差多少度”**:
#define PECI_ADDR_CPU0 0x30
#define PECI_CMD_GETTEMP 0x01
#define TJMAX_DEFAULT 100
UINT8 TxBuf[3] = { 0x00, PECI_CMD_GETTEMP, 0x00 };
UINT8 RxBuf[4];
Status = PeciTransaction(PECI_ADDR_CPU0, TxBuf, 3, RxBuf, &RxLen);
if (Status == PECI_OK) {
UINT8 DtsValue = RxBuf[2]; // DTS值 = 到TjMax的距离
UINT8 ActualTemp = TJMAX_DEFAULT - DtsValue;
UpdateThermalPolicy(ActualTemp);
}
GetTemp返回的是"距离TjMax的下行空间",不是摄氏度。 不同SKU的TjMax不同——i7可能100度,i3可能85度。DTS值都是42的话,i7上是58度,i3上是43度。EC必须从GetDIB或TEMP_TARGET MSR(0x1A2)获取真实TjMax。
温度管理完整链路
CPU晶圆温度 → DTS传感器 → PECI GetTemp → EC固件
→ EC算PWM占空比 → PWM输出 → 风扇转动
→ 空气带走热量 → CPU降温 → DTS变化 → 循环
75%的"风扇控制有问题"最后都是物理问题——风扇坏了、卡住了、线松了。但每个人都先怀疑固件。先拿示波器量风扇反馈脚,再翻代码。
九、调试三板斧
I2C的电气问题,代码是看不出来的。
示波器看波形质量。 至少双通道同时看SCL和SDA。重点检查:上升沿有没有变"爬坡"(上拉电阻不够强),SCL和SDA交叉关系(数据必须在SCL低电平期间变化),ACK位在第9个时钟周期有没有被从设备拉低。
逻辑分析仪看协议内容。 Saleae Logic 8或DSLogic Plus,接SCL/SDA/GND三根线。触发条件设为SDA下降沿且SCL为高(Start Condition)。抓一帧数据用协议解析器自动解码——“I2C Write to 0x50, Register 0x00, Data: 0x1A 0x2B…”,比肉眼数波形快一百倍。
EC Console快速排查。 Chrome OS EC固件自带i2cscan命令遍历总线地址看哪些设备在线,i2cxfer命令手动发读/写事务。比示波器快,前提是EC有调试接口。
踩坑:冷启动必挂,热启动正常
EC初始化时发I2C读命令无ACK。四通道示波器同时抓电源/复位/时钟/数据,发现I2C命令比EC_RESET_N提前6ms发出——EC还在复位状态就被命令轰炸。
解决:把EC_RESET_N的GPIO拉高操作移到I2C Host Init之前,加5ms delay等EC复位恢复。
经验:如果一个bug只出现在冷启动,优先怀疑上电时序和复位信号。 热启动时很多外设不掉电、不复位、状态保留。对比冷热启动的波形差异,往往能直接定位问题。
十、实战踩坑总结
| 坑 | 现象 | 根因 | 经验 |
|---|---|---|---|
| 上拉电阻太大 | 1MHz波形变三角波 | 2.2KΩ × 100pF = 220ns | Fast Mode Plus不超过1.5KΩ |
| 7位/8位地址搞混 | 设备不响应ACK | 规格书给8位,代码填7位 | 查Linux driver的reg属性 |
| SPD地址冲突 | 两个DIMM回同一个地址 | SA0/SA1引脚没接线 | 原理图review时列地址表 |
| SMBus/I2C混挂 | SPD读取被触摸板拖死 | 30ms Clock Stretch超SMBus 25ms | 物理隔离,分总线 |
| PEC没配对 | 电池电量显示127% | 设备期望PEC但主机没发 | 检查设备Datasheet的PEC支持 |
| EC休眠锁死总线 | S3唤醒后SMBus废掉 | ESD二极管对地导通 | 串隔离电阻+IO配高阻 |
| 冷启动I2C无ACK | 热启动正常 | I2C命令比EC_RESET_N提前6ms | 上电时序:复位→等待→通信 |
源码路径索引
| 内容 | 关键文件 |
|---|---|
| I2C Host协议定义 | MdePkg/Include/Protocol/I2cHost.h |
| I2C IO协议定义 | MdePkg/Include/Protocol/I2cIo.h |
| I2C总线驱动 | MdeModulePkg/Bus/I2c/I2cDxe/I2cBus.c |
| SMBus PEC计算 | MdePkg/Include/IndustryStandard/SmBus.h |
| SMBus控制器寄存器 | Silicon/Intel/*/Pch/Include/Register/PchRegsSmbus.h |
| SPD读取库 | edk2-platforms/Silicon/Intel/*/Library/PeiSmbusLib/ |
| 规范 | NXP UM10204 I2C-bus Specification / SMBus Specification 3.0 |

被折叠的 条评论
为什么被折叠?



