最近刷FPGA校招笔试题,发现I2C主机设计出现频率明显变高。我用三段式状态机写,仿真时起始条件拉低SDA的时机总对不上,读数据阶段ACK也收不到。想问下这类题在笔试里到底考什么?状态机该怎么划分状态才不容易漏掉重复起始和应答采样?有没有推荐的调试顺序,比如先跑单字节写再跑随机读写?
2026年FPGA校招笔试Verilog代码题,用状态机实现「I2C主机读写EEPROM」时总在起始条件和ACK应答位卡住,该怎么拆解?
12 个回答

先别急着堆状态。你卡的两处其实是同一件事:SDA 什么时候允许变、什么时候必须被采样。起始条件就是 SCL 高时把 SDA 拉低,ACK 是第 9 个 SCL 高电平中间读 SDA。建议先只写单字节写,用 $display 在每个状态打印 scl/sda,跑通再扩。你现在的分频参数和 EEPROM 型号是什么?

起始条件其实就是 SCL 还是高的时候把 SDA 从高拉到低,很多人写反成先拉低 SCL 再动 SDA,时序自然对不上。ACK 则是第 9 个 SCL 高电平中间读 SDA,建议先只跑单字节写,用 $display 打每个状态的 scl/sda 再看。

先别加状态,先把两个动作的物理含义记牢:SDA 只能在 SCL 低电平期间变,SCL 高电平期间必须稳定,起始就是 SCL 高时把 SDA 拉低。ACK 是第 9 个 SCL 高电平中间读 SDA。你先把单字节写跑通,每个状态 $display 打印 scl/sda,波形一看就明白了。

先说一个我当年踩过的坑:起始条件不是「拉低 SDA」这个动作本身,而是「SCL 还是高的时候把 SDA 从高拉到低」这个先后关系。很多人写 FSM 时把它拆成两个状态,结果 SCL 已经被拉低了才动 SDA,波形上就变成了一个普通的数据位下降沿,从机根本不认。我的建议是先别管页写、随机读,把状态机砍到只剩 IDLE→START→ADDR→ACK→DATA→STOP 六个状态,每个状态只做一件事,并且用 $display 把当前状态名和 scl/sda 一起打出来。ACK 采样要记住是在第 9 个 SCL 高电平的中间读 SDA,不是在下降沿读。笔试里考的就是这两点:时序边界和状态不遗漏。你现在分频参数是按多少算的,SCL 打算跑 100k 还是 400k?

先说结论:笔试代码题不是考你能不能真把 EEPROM 读写跑起来,而是考你对时序边界的理解有没有落到代码里。所以别一上来就写完整读写加页写加随机地址,先砍到只做单字节写。状态就六个:IDLE、START、ADDR、ACK、DATA、STOP。START 里只做一件事——SCL 保持高,SDA 从高拉低;ACK 采样固定在第 9 个 SCL 高电平的中间去读 SDA,不要在下降沿读。很多人卡住是因为把「拉低 SDA」和「拉低 SCL」的先后写反了,波形上就不是起始条件了。建议每进一个状态就 $display 打印状态名和 scl/sda,对着波形看一遍比改十遍代码有用。你现在分频是打算按 100k 还是 400k 算的?

先说一个最容易被忽略的点:笔试里考 I2C 主机,其实不太看你最后能不能真把 EEPROM 读写跑通,而是看你有没有把「谁在什么时候可以动 SDA」这件事想清楚。你卡的两个地方本质是同一个问题——SCL 高电平期间 SDA 必须稳定,所以起始条件是 SCL 高时把 SDA 从高拉低,停止是 SCL 高时把 SDA 从低拉高,其余数据变化都得等 SCL 低。ACK 收不到,八成是你在第 9 个 SCL 周期没把 SDA 设成输入,或者采样点放在了下降沿而不是高电平中间。所以拆解顺序建议就一条线:先只写单字节写,状态砍到 IDLE、START、ADDR、ACK、DATA、STOP,每个状态只干一件事,用 $display 把状态名和 scl/sda 一起打出来,对着波形看一眼比改十遍代码有用。等你单字节写稳了,再补页写和随机读,顺便把重复起始也加进去。你现在的分频是奔着 100k 还是 400k 去的?

先说个容易被忽略的风险:笔试代码题通常不会给你真 EEPROM 的器件模型,仿真里从机是你自己写的一个「理想从机」,所以你在起始条件和 ACK 上卡住,八成不是电路理解错,而是你的从机模型和主机是同一套错误假设,两个错的碰在一起反而更难查。建议你换一种做法,主机先不动,单独写一个只回 ACK 的傻从机,它只做三件事:检测起始、在第 9 个 SCL 高电平中间把 SDA 拉低、其余时间放掉 SDA。主机先用这个傻从机把单字节写跑通,波形对了再换成带地址译码的从机。这样你能把「主机时序」和「从机响应」两个变量分开,卡点会立刻缩小。另外一个常见误区是分频参数拍脑袋写,SCL 频率其实取决于你的系统时钟和期望速率,100k 和 400k 下每个相位要数多少个周期得先算清楚,建立保持时间不够,仿真里 ACK 也可能采到亚稳态的边沿。状态划分上,重复起始不用单独加一个 START2 状态,START 状态里判断「当前是否已发过地址」就能复用,但笔试里如果你不确定,宁可多写一个 RESTART 状态把逻辑写直白,评卷看的是状态不漏、边界清楚,不是状态数最少。你现在系统时钟是多少兆,打算让 SCL 跑多快,这个先定下来再动状态机。

先给你一个可能没那么直观的排查顺序:不要从状态机改起,先从时钟分频的数值反推。很多人卡在起始条件和 ACK,表面上像状态划分问题,实际上根子在自己算的 SCL 半周期太短,SDA 刚拉下去 SCL 的高电平窗口就过去了,从机采样时看到的是抖动或者根本没稳定。尤其是笔试仿真里系统时钟常给 50MHz 或者 100MHz,你要跑 100k 的 SCL,一个半周期是 250 或 500 个系统时钟,数错了差一个数量级,波形上表现出来就是『时序好像对但 ACK 就是不回』。所以建议你先拿纸算一遍分频计数器该数到几,把 SCL 的占空比、SDA 相对 SCL 的建立时间写清楚,再去看状态机。
另一个维度是方向控制。I2C 的 SDA 是双向的,主机发地址和数据时要驱动它,收 ACK 和读数据时得放成高阻再采样。很多人的写法是全程 assign sda = sda_out,忘了在 ACK 位切方向,从机拉低了你还在往外驱高,仿真里就是两个驱动打架,结果采到 X 或者一直高。稳妥的做法是用一个 sda_oe 使能信号,状态机进到采样阶段就把 oe 拉低,配合三态写法,波形上一眼能看出谁在驱动。
至于你问的调试顺序,单字节写确实应该排第一,因为它同时覆盖了起始、地址、ACK、数据、停止这几件事,跑通它等于主干通了。页写和随机读是加分支,不是加地基。你现在的分频参数是按什么系统时钟算的?这个先确认下,可能比改状态机更省时间。

先说一个很多人笔试时没意识到的点:仿真里你写的从机模型,往往就是你自己脑子里那套时序的投影。主机卡在起始条件和 ACK,有时候不是主机错了,而是主机和从机用了同一个错误假设,两个错互相配合,波形看起来「像那么回事」,但一到真实器件或者别人写的参考模型上就崩。所以别急着改状态机,先干一件事:把从机换成最傻的版本,它只检测起始、只在第 9 个 SCL 高电平中间把 SDA 拉低,别的什么都不做。主机先用这个傻从机把单字节写跑通,波形对了,再把从机换成带地址译码和 ACK 判断的版本。这样你把「主机时序」和「从机响应」两个变量分开了,卡点会立刻缩小到一半。
另一个维度是分频参数。100k 的 SCL 在 50MHz 系统时钟下,半周期是 250 个周期,很多人拍脑袋写个 50 或者 100,SCL 频率直接翻几倍,从机采样窗口根本对不上,ACK 自然回不来。建议先把分频计数器该数到几算清楚,再把 SCL 高低电平的占空比和 SDA 相对 SCL 的建立时间写在纸上,然后才去看状态机。顺序反了,你会在状态划分上白改很多遍。
至于状态怎么切,我倾向于按「SDA 什么时候允许变」来切,而不是按「协议阶段」切。IDLE、START、ADDR、ACK、DATA、STOP 这六个够用了,重复起始先不写,等单字节写稳了再补。每个状态只干一件事,进状态就 $display 打印状态名和 scl/sda,对着波形看一眼比改十遍代码有用。你现在分频是按 100k 还是 400k 算的?这个数直接决定你后面所有时序的余量。

起始条件就一句话:SCL 还是高的时候把 SDA 从高拉低,别先拉 SCL。ACK 是第 9 个 SCL 高电平中间读 SDA,不是下降沿读。先只跑单字节写,状态砍到六个,每进一个状态 $display 打 scl/sda,波形一看就清楚了。
发表回答
登录后可在本页底部提交回答