I2C协议实战详解:从硬件时序到SMBus,两根线怎么管一整个主板

搜"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 ~ 0x7B10位地址扩展指示笔记本几乎没人用
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

核心区别

特性I2CSMBus
最低速率无下限>=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名称症状查什么
0BUSY传输进行中正常
1INTR传输完成正常
2DEV_ERRNACK地址错了/芯片没电/槽位空
3BUS_ERR总线错START/STOP时序不对/SCL被拉死
4FAIL超时从设备拉低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 = 220nsFast 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

媳妇爱吃酸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值