vlsiNote EN 中文
Author verified by published 2026-10-20 03:29 UTC updated 2026-10-20 03:29 UTC 3 views

I2C slave 仿真复盘:6 个 Bug、2 处修复与一套可复用验证方法

Other languages 中文 (original)English
一次 I2C slave 的补充仿真查出 6 个问题:最严重的写通路失效被原 testbench 的“友好主机”行为完整掩盖,另外 5 个是地址应答、重启事务、SCL 毛刺和当前地址读。本文给诊断方法、采样点裕量账和复验矩阵。

一次 I2C slave 的补充仿真查出 6 个问题:最严重的写通路失效被原 testbench 的“友好主机”行为完整掩盖,另外 5 个是地址应答、重启事务、SCL 毛刺和当前地址读。本文给诊断方法、采样点裕量账和复验矩阵。

1. 结论先行

被测对象是一个用高频时钟(约 70 MHz)过采样 SCL/SDA、靠 4 级同步延时补偿定位采样点的 I2C slave。原有仿真“全过”,补充仿真后得到 6 个问题、2 处修复:

编号 问题 机理一句话 严重度 状态
A 写通路在真实主机下完全失效 R/W 位用裸输入 sda_i 采样,采样点落在 SCL 下降沿之后约 112 ns(SCL 低电平期) 高(合规主机必现) 已修(4 处改 sda_i_sync_d4)
B 地址不匹配仍拉低 SDA 应答 sda_oe 在地址第 8 拍无条件驱动 高 已修(加 dev_chk_pass 门控)
C 重启后的读地址不校验 只判 i2c_ack,不判 dev_chk_pass → 应答非本机地址并驱动数据 高(总线冲突) 未修
D 重启 + 写地址时抢占总线 RESTART 固定跳 SLV_ADDR_RD,不看重启后首字节的 R/W 位 高 未修
E SCL 40 ns 毛刺造成虚假寄存器写 边沿检测无毛刺滤波,一次毛刺 = 多一个 bit 中 未修
F 复位后直接读会静默改写地址指针 读地址被当成写流程的地址字节,随后 8 个读时钟被解析成寄存器地址(全 1 → 0xFF) 中 未修

一句话总结:修复的两条都在“采样时刻”和“输出使能门控”这类一行改动上,代价不在于改代码,而在于先把现象量化到能证明“这行确实错了”。

2. 问题零:原仿真为什么看不见最严重的问题

原 testbench 的主机 BFM 在发完每个字节后一直保持驱动 SDA 直到 ACK 结束(tb_sda_oe 不释放)。这恰好满足“主机在 SCL 下降沿之后仍然保持数据”这种非标准用法,于是 Bug A 永远不触发。

task i2c_wr_byte(...);
   // wait ack
   tb_scl_o = 1'b0;   // 只拉低 SCL;SDA 仍被主机驱动为最后一个 bit 的值
   #T tb_scl_o = 1'b1;

教训:验证环境会把 DUT 的缺陷固化进“正常行为”。判断一个慢速串行从机的仿真是否可信,先看 BFM 三件事:① 每字节第 8 拍后有没有释放 SDA;② SDA 相对 SCL 有没有偏斜;③ 位周期是不是只试了一个值。

3. 诊断环境:把主机行为参数化,再加三个探针

新建的回归环境把主机时序做成 4 个参数:

参数 含义 扫描范围
T 1/4 位周期(一个 bit = 4T) 总线 100 kHz ~ 1 MHz
THD SCL 下降沿 → SDA 改变的延时,即主机 t_HD;DAT 0 ~ 625 ns(对齐规范)
TS_BYTE 1 = 第 8 拍后释放 SDA(真实主机);0 = 像原 TB 一样一直驱动 两种都跑
MSCL_D 主机侧 SCL 到达延时,模拟偏斜 0 ~ 120 ns

三个观测手段是这次能定性定量的关键:

  1. 记分板:把 DUT 实际驱动出的寄存器写(wr_en 边沿采样)与预期写序列逐条比对——地址、数据、顺序、条数。这样“一个寄存器都没写进去”是判据,不是印象。
  2. 非法驱动探针:sda_oe=1 若出现在读数据/ACK 窗口之外就打印 [ILLEGAL-DRIVE],判总线冲突,并按采样点计数(用例里出现的 699、88 都是这个计数)。
  3. 采样点探针:直接打印 R/W 位被判决的时刻、当时的 sda_i / d2 / d4 / 总线 SDA / 主机是否仍驱动。

17 个用例,./run.sh 一次跑完,每个用例一份 logs/{case}.log;EXTRA="+TV=130" 可指定主机 t_HD;DAT。(后续针对具体 bug 又补了地址译码表、重启事务、直接读等用例,全量 21 个。)

4. Bug A:一行采样点,先量化再动代码

4.1 定位

地址 R/W 位的判据(判定“写”、“读”、以及 RDDATA 续传的三处)直接用了未同步、未做延时补偿的裸输入 sda_i;而移位寄存器用的是 4 级延时补偿的 sda_i_sync_d4,地址比对用 d2。三者里只有 sda_i 那一处落在 SCL 低电平期。

4.2 实测采样时刻

探针与波形对齐后:R/W 位的判决发生在 SCL 下降沿(从机管脚)后约 112 ns ≈ 4 个时钟——这正是“2 级同步 + 2 级边沿检测”的固定延时。此时合规主机早已释放 SDA(规范 t_HD;DAT 下限是 0 ns),从机采到的是释放后的 1,于是:

  • 写地址 0x40 被判成“读”(wr_op=0)→ 地址字节 ACK 后 FSM 走到寄存器地址阶段就回 IDLE;
  • 后续数据字节全部 NACK,一个寄存器都不写;
  • 读序列全部返回 0xFF(因为重启识别依赖 wr_op,写地址判错 → 重启识别不了)。

读数时的一个坑:探针打印的 “+140.9 ns” 比真正的判决拍(112 ns)晚一个时钟,因为探针要等 ACK 更新后才触发。探针时刻 ≠ 判决时刻,裕量必须按判决拍算,否则会把这行的错误归因到别处。

4.3 阈值扫描:把“猜”变成“数”

主机真实释放(TS_BYTE=1),扫 t_HD;DAT:

t_HD;DAT 结果
625 / 300 / 200 / 150 / 130 / 120 / 115 ns 全过(写 + 读回正确)
110 / 105 / 100 / 95 / 90 / 85 / 80 / 70 / 0 ns 全失败(数据字节 NACK、0 次寄存器写、读回 0xFF)

临界点在 110~115 ns,与 112 ns 的采样点完全吻合。

关键的对照用例:让主机一路保持 SDA(TS_BYTE=0),t_HD;DAT 从 625 扫到 0 全过。这一条排除了“速率/位周期/相位”的错误归因——失败只取决于“主机何时释放 SDA”。

4.4 裕量账(本次最值得复用的写法)

把仿真里的一切换算到总线侧、相对 SCL 下降沿的窗口,再和规范要求对表:

采样源 等效采样时刻(相对 SCL 下降沿) 对主机的要求 与规范相容性
sda_i(原) 之后 +86 … +114 ns(SCL 低电平期) 主机须把 SDA 保持 ≥115 ns 与 t_HD;DAT ≥ 0 冲突,合规主机 100% 失效
d2(地址比对,一轮后仍在用) 之前 0 … 29 ns 几乎无裕量,靠“下降沿之前”侥幸正确 薄
d4(移位寄存器 / 修复后的判据) 之前 57 … 86 ns(SCL 高电平期内) 只要求 SDA 在下降沿前约 86 ns 已稳定 即规范的 t_SU;DAT(250 ns 量级),合规主机都满足

判据不是“改完跑过了”,而是“改完之后从机不再要求主机做规范之外的动作”。

4.5 修法与复验

// 第一轮:R/W 判据 + RDDATA 续传
194: else if (bit_cnt == 4'd8 && ~sda_i_sync_d4) i2c_ns = RDDATA;
266: else if (cs == SLV_ADDR_WR && bit_cnt == 4'd7 && sda_i_sync_d4 == 1'b0) wr_op <= 1;
282: else if (cs == SLV_ADDR_RD && bit_cnt == 4'd7 && sda_i_sync_d4 == 1'b1) ...
// 第二轮:地址 A0 位比对,统一采样点
223: if (bit_cnt == 4'd6 && {p_dat[5:0], sda_i_sync_d4} == addr)

以最苛刻的 t_HD;DAT = 0 为主判据复验:

用例 内容 结果
mon 写 + 读全流程 + 采样点探针 PASS
wr_ts t_HD;DAT 16 点(625 → 0 ns) PASS 192 项,每点失败数 0
thd0 THD=0,100 k ~ 1 MHz 共 8 个速率 PASS 96 项
thd0_rd16 THD=0,连读 20 字节(含 0xFF 回卷) PASS
thd0_ph 时钟相位扫 8/15 个点 全 PASS
addr_a0 从机地址 A0=1(地址字节 0x42/0x43),15 个相位点 全 PASS
回归 wr_hold / rd_basic 旧“主机保持 SDA”用法 PASS → 没改坏旧用法

5. Bug B:未选中地址也 ACK,一个门控修掉

SLV_ADDR_WR 第 8 拍时 sda_oe/i2c_ack 无条件拉低,不判 dev_chk_pass。多从机总线上,未选中设备必须 NACK,否则主机误判“设备存在”。

修法(一行门控):

if (bit_cnt == 4'd7 && (((cs == SLV_ADDR_WR) && dev_chk_pass) ||
                        cs == REG_ADDR || cs == WRDATA ||
                        cs == RESTART  || cs == SLV_ADDR_RD))
  sda_oe <= 1'b1;

复验除了原来的地址不匹配用例,另加一张地址译码表(DUT 7 位地址 0x20,逐字节发 START+地址+看 ACK):

主机发送 0x40 0x41 0x42 0x43 0x20 0x21 0x60 0x61 0xA0 0xA1 0xBF 0xFE
期望 ACK ACK NACK NACK NACK NACK NACK NACK NACK NACK NACK NACK
实测 ACK ACK NACK NACK NACK NACK NACK NACK NACK NACK NACK NACK

12/12 正确,包括“A0 位只差一位”的 0x42/0x43。全量回归 21 个用例:16 PASS,未通过的 5 个正是尚未修的 C/D/E/F 加一个预期失败的偏斜用例——这就是“没有回归”的证据形态。

一个遗留的不一致:i2c_ack 寄存器本身仍无条件下拉(它只决定 FSM 走 SLV_ADDR_WR → REG_ADDR/IDLE 的分支,不驱动总线)。外部行为已经对了,但内部状态与总线不一致;要不要一起门控,属于“可选统一”,不是 bug。

6. 仍未修的四个:现象、机理、判据

编号 实测现象 机理 建议修法
C restart + 非本机读地址 0xA1:从机 ACK 并驱动出 mem 数据(实测返回 0x3C) 读地址阶段只判 i2c_ack 不判 dev_chk_pass,而 dev_chk_pass 又在 START 时被清零 读地址阶段同样门控 dev_chk_pass
D 写数据 → restart → 写地址(组合事务):从机无条件进 RDDATA,699 个采样点非法驱动,主机的寄存器地址字节被 NACK RESTART → SLV_ADDR_RD 固定跳转,不看重启后首字节 R/W 重启后先按地址字节解析,再按 R/W 决定去向
E SCL 上 40 ns 低脉冲(高电平期间):出现提前 ACK 和一次虚假写 mem[0x40] <= 0xA0,事后 bit_cnt 残留在 3 边沿只要被 2 级同步打到(≥ 约 29 ns)就 bit_cnt + 1,无毛刺滤波 用 3 拍稳定确认边沿,或对 SCL 加与 SDA 同等的抗毛刺处理(规范本来要求 50 ns 级抑制)
F 复位后直接发读地址 0x41:地址 ACK 正确,但读回 0xFF,内部指针被改成 0xFF 地址字节阶段“只要地址匹配就去寄存器地址阶段”,不看 R/W → 随后的 8 个读时钟被当成寄存器地址解析 地址匹配后按 R/W 分流;或在文档里明确只支持“写地址 + 寄存器地址 + [重启 + 读地址]”

对 C/D 要额外注意:单从机系统里它们只是浪费,挂多颗器件时就是真实的总线冲突——会把本机数据驱动到别人的地址上,并破坏对其它器件的访问。

7. 顺手发现的三个非 bug 风险点

  1. sda_o 是纯组合输出(由寄存器堆读数据直接选择),读数据没有输出寄存 → 从寄存器堆读延时到布线的总和必须落在半个位周期内;建议在 SCL 低电平期把读数据打一拍出去。
  2. 一个内部读使能信号只产生了、没引出端口,是死逻辑。
  3. 4 级同步链路约 86 ns 的延时补偿与时钟频率硬绑定:一旦时钟不是 70 MHz,所有采样时刻常数都要重算。建议参数化,或改写成“在 SCL 高电平中点采样”这种不依赖绝对延时的形式——这属于结构性问题,不是这次 bug 的修补范围。

8. 过程中踩的坑(流程,不是 RTL)

坑 现象 处理
环境变量 直接跑脚本报工具找不到 先 module load vcs/2023 verdi/2023,或显式指定工具安装路径
错误归因 一开始以为是总线速率太快 用“主机一直保持 SDA”的对照用例排除速率/相位,锁定“何时释放”
探针误导 [MON] 打印的时刻比判决拍晚一个时钟 按判决拍算裕量,并在笔记/报告里写清两者差 1 个时钟
只改一半 第一轮只改了 3 处判据,漏掉地址比对处 d2(裕量只有 0~29 ns) 第二轮把 4 个采样点全部统一为 d4,再全量复跑
门控不彻底 修 B 后总线行为对了,内部 i2c_ack 仍无条件下拉 明确记录为“外部正确、内部待统一”,不当作已修
报告可读性 终端/emacs 里看 md 表格对不齐 写了个 md→HTML 转换脚本(表格带边框),源 md 一改就重生成

9. 可复用清单(慢速串行从机验证)

  1. 先审 BFM:会不会一直是“友好主机”(持续驱动、单一速率、无偏斜)。
  2. 把所有主机时序参数化,尤其 t_HD;DAT / t_SU;DAT 要扫到规范下限。
  3. 三个探针一个都不能少:记分板(比“写进去没有”)、非法驱动(判总线冲突)、采样时刻(判裕量)。
  4. 结论写成裕量账:把仿真时刻换算到总线侧相对 SCL 下降沿的窗口,与规范参数对表;不要只写“改完通过了”。
  5. 每个“失败”都要配一个对照用例,用来排除最容易被误信的那个错误归因。
  6. 修一处,跑相关用例 + 全量回归一次;用“总数 / PASS / FAIL 且 FAIL 都能解释”来声明没有回归。
  7. 明确哪些失败是合法主机行为之外的(例:SDA 在 SCL 高电平期间变化本身就是 START/STOP,不算 bug),否则会被当成噪音淹掉真问题。

10. 数字速查

项 值
裸输入采样点 SCL 下降沿后约 112 ns ≈ 4 个时钟
失效临界 t_HD;DAT 110~115 ns 之间(115 过、110 失败)
原采样窗口 下降沿后 +86 … +114 ns
改后采样窗口(d4) 下降沿前 57 … 86 ns
回归规模 21 个用例,16 PASS,5 FAIL(4 个未修 + 1 个预期失败)
地址译码表 12/12 正确
非法驱动计数(D/E) 699 个采样点 / 88 个采样点

11. 术语表

  • t_HD;DAT(data hold time):SCL 下降沿之后数据必须保持的时间,规范下限 0 ns。本文最核心的那个参数。
  • t_SU;DAT(data setup time):数据在 SCL 上升沿之前必须稳定的时间(标准/快速模式 250 ns 量级)。
  • current address read:不带寄存器地址字节、直接读“当前指针”位置的事务。
  • repeated START:不发 STOP、直接再发一个 START 切换方向或换目标。
  • combined transaction:写 + repeated START + 写/读 的组合事务,I2C 允许。
  • open-drain / 线与:总线只有“拉低”和“释放”两态,多设备只能拉低,靠上拉拉高。
  • 过采样:用远高于总线速率的高频时钟采样 SCL/SDA,靠同步链定位采样点;优点是省时钟域,代价是采样时刻与时钟频率绑定。
  • 2 级同步 / 4 级延时补偿:跨时钟域同步链,级数决定采样点相对边沿的位置,也就是本文的裕量来源。
  • scoreboard(记分板):把 DUT 输出与预期逐条比对的检查器,“写进去没有”要靠它证明。
  • BFM(bus functional model):testbench 里模拟主机的模型;它的“习惯”会决定 DUT 的哪些缺陷能被看见。
I2CRTL-simulationdebugverificationVCS

Credibility

How far would you trust this note? 0-10.

How this note earned its label

Author verified bill2026 2026-10-20 03:29 UTC

Comments (0)

No comments yet.
Markdown source RSS Share card: light · dark The author checked this note line by line.