简介:一套面向FPGA初学者的AT24C02读写实验工程,基于Cyclone IV E系列EP4CE6E22C8芯片,在Quartus II 11.0环境下编译验证。工程由Verilog源码与原理图组成,顶层模块iic_com接口包含50MHz时钟、低有效复位、两个按键、SCL与SDA以及数码管数据输出,完整呈现从按键检测、20ms消抖到SCL时钟分频、I2C数据采样的逻辑链路:按下按键1执行写入,按下按键2执行读取,数码管同步显示数据,适合学习I2C协议、EEPROM读写时序及状态机设计。压缩包共124个文件,大小3.75MB,其中既有.v源码、.bdf原理图等设计文件,也有.qsf/.qpf工程配置、.sof/.pof下载文件以及编译报告,可直接打开参考或重新生成,附带工程说明文档便于快速上手。已有1427人学习下载,工程中cnt状态划分明确,能直观看到SCL上升沿、高电平采样、下降沿、低电平变化四个关键时点,便于对照代码理解AT24C02的I2C驱动实现,作为个人学习或课程设计的参考案例都很合适。
1. 拿到这包 AT24C02 实验工程,我建议你先别看代码,先看时序
第一次拿到“FPGA读写EEPROM芯片AT24C02实验Verilog逻辑源码Quartus11.0工程文件”这类压缩包时,我的习惯是先把 Verilog 文件和 Quartus 工程放一边,直接打开 AT24C02 的数据手册。这颗 2Kbit 的 I2C EEPROM 看起来简单,却在无数人的上板调试里翻过车:写进去读出来全 0xFF、SCL 波形只有半截、上电第一次读对了第二次就错。AT24C02 一共 256 字节,设备地址 0xA0/A1,页大小 8 字节,内置写周期最长 5ms,这些数字决定了你的 Verilog 状态机怎么写。这篇笔记按我自己的调试顺序来:协议时序拆开讲清楚,再落到 Quartus 11.0 工程搭建、Verilog 状态机骨架、仿真 testbench,最后把上板最常见的几个坑按现象原因解决列出来。新手能照着一步步跑通,熟手可以直接跳到第 5 章看排错。
2. AT24C02 的 I2C 时序拆解:起始、停止、应答与三态读
2.1 每次读写下定决心要盯紧的 4 个时序节点
AT24C02 挂在标准的 I2C 总线上,物理层是 SCL 和 SDA 两条开漏线,外部必须有上拉电阻。开发板上常见的是 4.7kΩ 上拉到 3.3V,100kHz 标准模式下这个阻值没问题;如果以后想把 SCL 拉到 400kHz 快速模式,我一般会换成 2.2kΩ 或者 1kΩ,否则上升沿太慢,从机采样的建立时间不够。设备地址的高 4 位固定是 1010,低 3 位来自引脚 A2/A1/A0。绝大多数开发板把这三个引脚直接接地,所以 7 位地址是 1010000,写操作发 0xA0,读操作发 0xA1。代码里我会把它们定义成参数DEV_W_ADDR = 8'hA0和DEV_R_ADDR = 8'hA1。
写 Verilog 之前,我建议把 I2C 的四个关键时序节点在纸上画一遍。第一是起始条件 START:SCL 保持高电平时,SDA 从高拉低;停止条件 STOP 相反,SCL 高电平期间 SDA 从低拉高。这两个动作都不能和普通数据位混在一起处理,否则你会在 SCL 高电平期间动了 SDA,从机立刻把总线理解为异常。第二是 ACK/NACK:第 9 个时钟周期由从机控制 SDA,主机在第 8 个时钟之后把 SDA 释放(置高阻),第 9 个时钟采样,低电平是 ACK,高电平是 NACK。AT24C02 在收到正确设备地址和字地址后都会回 ACK,但如果地址越界或者芯片正处于内部写周期,它会回 NACK,这也是后面排错最重要的观测点。
第三个节点是数据位本身:SDA 只能在 SCL 低电平时变化,高电平期间必须保持稳定。数据手册里 tSU:DAT 典型值 250ns,tHD:DAT 是 0ns,100kHz 下我们的状态机有足够余量。但很多新手会在同一个 always 块里既翻转 SCL 又切换 SDA,结果 SCL 上升沿附近 SDA 还在跳变,仿真不报错,示波器上全是毛刺,上板就是随机错。第四个节点是内部写周期 tWR。AT24C02 收到 STOP 之后并不是立刻把数据固化进非易失存储,而是要等最长 5ms 的内部写周期。这期间你发任何指令,芯片都不应答。这个 5ms 在仿真里很容易被忽略,因为仿真时间短,你感觉不到;上板后就会变成“写完了立刻读,读出来还是旧数据”。
这四个节点落到工程参数上,大概就是下面这张表,我每次换开发板都要重新对一遍:
| 参数 | AT24C02 手册要求 | 100kHz 下的工程余量 |
|---|---|---|
| tSU:STA 起始建立时间 | ≥4.7µs | SCL 半周期 5µs,刚好够 |
| tHD:STA 起始保持时间 | ≥4.0µs | 5µs,够用 |
| tSU:DAT 数据建立时间 | ≥250ns | FPGA 在 SCL 低沿切换 SDA,远大于 250ns |
| tHD:DAT 数据保持时间 | ≥0ns | 天然满足 |
| tBUF 停止后总线空闲 | ≥4.7µs | 我习惯 STOP 后再等 10µs |
| tWR 内部写周期 | ≤5ms | 计数器延时 5~10ms,取 10ms 更稳妥 |
2.2 三种读方式:当前地址读 / 随机读 / 连续读,写方式先练单字节
AT24C02 的读操作比写操作多出几种变体,这也是标题工程里 Verilog 逻辑源码最值得读的部分。当前地址读:芯片内部维护一个地址指针,上电默认指向 0x00,每次读或者写之后指针自动加 1。你只需要发设备地址加读位,它就把当前指针指向的字节送出来。随机读:先走一段“写地址”流程,把目标字地址写进芯片,然后发送重复起始条件,再发设备地址加读位。顺序读:在随机读的基础上连续读多字节,芯片每读完一个字节地址自动加 1,直到主机回一个 NACK 再跟 STOP 才结束。
为什么要把这三种读法分清楚?因为上板回读校验时,绝大多数人的第一个版本都写成了“当前地址读”的时序:先发设备地址读位,然后直接收数据。如果你之前做过写操作,指针可能停在某个地址上,读出来的就不是你刚写进去的那个地址。更隐蔽的问题是,如果你打算把指针指过去而先做一次假写,假写完成后芯片进入 5ms 内部写周期,立刻读只会拿到 FF。所以我现在的做法是直接实现随机读,把“设备地址+写、字地址、重复起始、设备地址+读”这条完整路径做出来,宁可多几个状态,也避免在调试时分不清是时序错还是地址错。
写操作方面,AT24C02 支持单字节写和页写。页写一页 8 字节,发起方式和单字节写一样,只是在字地址之后连续发送最多 8 个数据字节,而且这 8 个字节必须在同一页内,也就是字地址低 3 位加上字节数不能跨过 8 字节边界,否则真的会覆盖回这一页的开头。做实验时我建议先把单字节写跑通,页写放到最后做扩展,原因很简单:单字节写的状态机只有一条线性路径,排错直观;页写一旦写错,你连是哪个字节被覆盖都需要推导半天。
2.3 开漏输出的 Verilog 表达:三态门而不是强推高
很多第一次写 I2C 控制器的人会在 SDA 输出上写sda = 1'b1来发送高电平,这在仿真里看起来是对的,但到了板上就出问题。I2C 是开漏总线,正确做法是:要发 0 时把 SDA 拉低,要发 1 时把 SDA 置为高阻1'bz,靠外部上拉电阻把电平抬起来。Verilog 里标准写法是:
assign sda = sda_out_en ? 1'b0 : 1'bz;逻辑说明:sda_out_en为 1 表示主设备主动拉低总线发送 0,为 0 时 SDA 呈高阻态,释放总线,电平由上拉电阻决定。不要用sda = 1'b1去直接驱动高电平,因为如果从机同时把 SDA 拉低,就会出现两个输出级对拉,轻则信号错误,重则损伤引脚。这个习惯从 AT24C02 实验养成之后,之后做任何 I2C/PMBus 器件都不会再犯。SCL 虽然通常由主设备独占控制,但严格遵循 I2C 规范也建议用同样的三态方式释放,只是我们的状态机里 SCL 一直由 FPGA 驱动,直接输出 0/1 电平即可,前提是每次空闲时把它停在稳定的高电平。
3. 用 Quartus 11.0 把工程搭起来:文件组织、引脚约束与 ModelSim 仿真
3.1 Quartus 11.0 工程文件树与最简引脚约束
我拿到带 Quartus 11.0 工程文件的压缩包时,会先看文件后缀和目录结构。一个规范的工程至少包含这几个文件:顶层 Verilog 文件(一般是 top.v 或顶层实体名)、功能模块文件、引脚约束文件.qsf,如果用了 PLL 或者片上 ROM,还会有.qip和.qsys之类。文件全部放到英文路径下,不要有空格和中文,Quartus 11.0 对中文路径的支持不好,编译时经常报一些莫名其妙的错误。项目根目录里还会自动生成db、incremental_db这些中间目录,那是编译产生的,不用管也不要手删,删了下次全量重编。
新建工程时,Quartus 11.0 的流程是 File -> New Project Wizard。器件族选 Cyclone IV E,因为这个实验最常见的是黑金、小梅哥那一代 Cyclone IV 开发板,具体到器件型号,比如 EP4CE10F17C8 或者 EP4CE6E22C8,按你手里的板子选。顶层实体选 top。仿真工具选 ModelSim-Altera,这样 Quartus 能把测试平台直接启动到 ModelSim 里。如果你手里的压缩包里已经带了.qpf工程文件,直接双击打开,但打开后第一件事是检查当前器件型号是否和你板子一致,否则后面引脚分配全是乱的。
引脚约束我一般直接在 Quartus 的 Assignment Editor 里做,但为了让你看得明白,贴一段.qsf里的关键行:
set_global_assignment -name FAMILY "Cyclone IV E" set_global_assignment -name DEVICE EP4CE10F17C8 set_global_assignment -name TOP_LEVEL_ENTITY top set_location_assignment PIN_E1 -to clk set_location_assignment PIN_E15 -to rst_n set_location_assignment PIN_A15 -to scl set_location_assignment PIN_B14 -to sda set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to scl set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to sda set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to scl set_instance_assignment -name WEAK_PULL_UP_RESISTOR ON -to sda参数说明:PIN_E1这类引脚编号必须按开发板原理图填,不能照抄别人的;IO_STANDARD要和开发板 bank 电压匹配,常见是 3.3-V LVTTL 或者 LVCMOS33,混用会在编译后出现警告,上板后还可能因为电平标准不匹配导致 SCL 识别不了。WEAK_PULL_UP_RESISTOR是 FPGA 内部弱上拉,一般等效 10kΩ 以上,仅靠它拉 I2C 总线在短距离和单片从机时能跑,但我不建议靠它吃饭,外接 4.7kΩ 上拉才是可复现的做法。
3.2 把 AT24C02 的模型装进 ModelSim:测试平台最小骨架
Quartus 11.0 自带的 ModelSim-Altera 6.5e 足够跑这个实验的 RTL 仿真。AT24C02 的官方行为模型可以从微芯或者安森美官网下载,但很多新手在仿真第一步就卡在把模型文件加到工程。更简单的做法是自己在 testbench 里写一个最小模型:一个 256 字节的寄存器数组加上一个能回 ACK 的从机状态机。下面这个骨架是我的常用起点:
`timescale 1ns / 1ps module tb_top; reg clk; reg rst_n; reg start; reg [7:0] eeprom_addr; reg [7:0] data_in; wire scl; wire sda; top u_top ( .clk (clk), .rst_n (rst_n), .start (start), .eeprom_addr(eeprom_addr), .data_in (data_in), .scl (scl), .sda (sda) ); // 上拉原语:没有它 SDA 在释放时是 z,逻辑里会读出 x pullup(sda); pullup(scl); // 从机应答与存储模型:检测到地址匹配就拉低 SDA 一个时钟 reg sda_drive; assign sda = sda_drive ? 1'b0 : 1'bz; reg [7:0] mem [0:255]; // 用 initial + 时序进程模拟从机行为,这里省略细节点 ... endmodule这里的关键点是pullup(sda)和pullup(scl)这两行。很多人在 ModelSim 里仿真 I2C 总线上全是 x,第一个怀疑状态机,实际上只是忘了给总线加上拉,导致 SDA 释放之后电平未定,ModelSim 里显示成一条红蓝相间的线。sda_drive是模拟从机把总线拉低回 ACK 的开关,真正的行为模型里会根据收到的地址和数据决定拉低时机,做实验时可以先用固定延时拉低来验证主机的 ACK 采样逻辑。
仿真时我会在波形窗口里导入scl、sda、state和bit_cnt四个信号。state是状态机的当前状态,bit_cnt是字节内位数计数器。重点看四件事:起始条件出现时 SDA 是否在 SCL 高电平期间完成跳变;每一字节的第 9 个时钟 SDA 是否被拉低;写完 STOP 之后是否等了足够长的内部写周期;读操作最后一个字节主机是否回了 NACK 而不是 ACK。把这四步在波形里确认无误,再上板,否则上板后你分不清是代码问题还是电气问题。
4. Verilog 状态机怎么写:单字节写、随机读与三个必调参数
4.1 写时序:三段式状态机的状态编码与 SCL 分频
写控制器的核心是一个状态机。我用三段式写法:第一段负责状态跳转,第二段产生输出,第三段做组合逻辑产生次态。这样做的理由是 ModelSim 里可以单独拉出state信号观察,改状态不会误伤数据路径。下面这段是单字节写模块的完整骨架,时钟 50MHz,目标 I2C 时钟 100kHz:
module i2c_eeprom_wr #( parameter CLK_FREQ = 50_000_000, parameter I2C_FREQ = 100_000, parameter DLV_COUNT = CLK_FREQ / I2C_FREQ / 2, parameter WAIT_WR_CNT = 500_000 // 10ms @50MHz )( input clk, input rst_n, input start, input [7:0] eeprom_addr, input [7:0] data_in, output reg scl, inout sda, output reg done, output reg ack_err ); localparam IDLE = 4'd0; localparam START = 4'd1; localparam DEV_W = 4'd2; localparam ACK_DEV = 4'd3; localparam W_ADDR = 4'd4; localparam ACK_WADDR = 4'd5; localparam DATA = 4'd6; localparam ACK_DATA = 4'd7; localparam STOP = 4'd8; localparam WAIT_WR = 4'd9; reg [3:0] state, next_state; reg [7:0] bit_cnt; reg [15:0] clk_cnt; reg [31:0] wait_cnt; reg sda_out_en; reg scl_en; reg scl_reg; assign sda = sda_out_en ? 1'b0 : 1'bz; // SCL 分频:计数器到 DLV_COUNT-1 翻转一次 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin clk_cnt <= 16'd0; scl_reg <= 1'b1; end else if (scl_en && clk_cnt == DLV_COUNT - 1) begin clk_cnt <= 16'd0; scl_reg <= ~scl_reg; end else if (!scl_en) begin scl_reg <= 1'b1; clk_cnt <= 16'd0; end else begin clk_cnt <= clk_cnt + 1'b1; end end assign scl = scl_en ? scl_reg : 1'b1; // 状态跳转与数据移位逻辑按标准三段式补全,核心见下面说明 endmodule逻辑说明:DLV_COUNT的计算是CLK_FREQ / I2C_FREQ / 2,50MHz 除以 100kHz 再除以 2,得到 250。clk_cnt从 0 数到 249 时翻转一次scl_reg,所以一个完整 SCL 周期耗时 500 个系统时钟,正好 10µs,对应 100kHz。scl_en只有在进入 START 到 STOP 这段才拉高,空闲时scl保持高电平,这是 I2C 总线空闲状态的要求。sda_out_en控制三态输出,发送数据位时根据移位寄存器内容决定是拉低还是释放。
参数说明:WAIT_WR_CNT用 500000,在 50MHz 下是 10ms,比 AT24C02 手册最大 5ms 多出一倍余量。不要把它压到 250000 贴着 5ms 用,因为芯片老化、温度变化都会让内部写周期变长,省这 5ms 换来的是偶发性写入失败。start输入我建议在上一层模块里做上升沿检测,或者在测试平台里生成单脉冲,避免按键或外部信号持续拉高导致状态机反复触发。ack_err在 ACK 采样为高时立即置位并把状态机带回 IDLE,这是整个控制器不会在从机异常时挂死的兜底。
4.2 读时序:随机读的“假写地址”路径与末尾 NACK
读状态机比写多出三个状态:重复起始、发送读设备地址、接收数据后回 NACK。随机读的完整路径是:起始 -> 0xA0+写 -> 字地址 -> 重复起始 -> 0xA1+读 -> 接收 8 位数据 -> 主机回 NACK -> 停止。下面把状态声明摘出来,和写状态对照着看,差异一目了然:
localparam IDLE = 4'd0; localparam START = 4'd1; localparam DEV_W = 4'd2; // 发送写设备地址 0xA0 localparam ACK_DEV = 4'd3; localparam W_ADDR = 4'd4; // 发送目标字节地址 localparam ACK_WADDR = 4'd5; localparam RESTART = 4'd6; // 重复起始条件 localparam DEV_R = 4'd7; // 发送读设备地址 0xA1 localparam ACK_DEV_R = 4'd8; localparam R_DATA = 4'd9; // 接收 8 位数据,SDA 转输入 localparam NACK = 4'd10; // 主机拉高 SDA 表示读完 localparam STOP = 4'd11;每个状态在 SCL 半周期边界切换一次,发送设备地址和数据字节时,bit_cnt从 7 数到 0。特别要注意R_DATA状态里,第 8 个时钟下降沿之后,主机的sda_out_en必须释放,把总线让给从机;第 9 个时钟的下降沿,再从 SDA 上采样数据。最后一个字节读到后,主机要在第 9 个时钟回一个 NACK,告诉从机“不用继续发了”,然后发 STOP。很多刚写读逻辑的人都栽在这里:给从机回了 ACK,于是从机继续把下一个地址的数据推上来,总线上数据交叠,读出来的最后一字节总是不对。
随机读为什么叫“假写地址”?因为它开头那段和设备地址 + 字地址的流程,和写操作完全一样,只不过不跟数据,而是跟重复起始和读命令。这段流程在逻辑上就是一次“写”的过程,芯片会更新内部地址指针,但不会触发写入存储阵列,所以不需要等 tWR。如果你在仿真里发现读完地址后状态机立刻进入内部写周期等待,那说明你把这次操作的标志位搞错了,芯片并不会因为外观像写就把数据写进去。
4.3 三个必调参数:分频系数、写周期等待、上拉方式
参数设置是这个实验能不能稳定复现的分水岭。第一个参数是 SCL 分频系数,也就是DLV_COUNT。我见过不少人把 SCL 调到 400kHz 想加快速度,直接把分频数减到 62,结果上板后能读不能写。原因是 400kHz 下 AT24C02 要求 tSU:STA 降到 0.6µs,半周期只有 1.25µs,FPGA 内部状态跳转加引脚 IO 延迟就已经吃掉很大一部分,剩下给上拉电阻的上升时间非常紧张。如果你的板子布线长、上拉电阻又选了 4.7kΩ,400kHz 基本会随机失败。我的建议是实验阶段稳定跑 100kHz,全速模式留到以后再玩。
第二个参数是写完以后的等待时间。前面提过 tWR 最长 5ms,代码里WAIT_WR_CNT我取 10ms。这个参数在功能上其实分两种处理:要么在控制器内部等满 10ms,要么把状态机提前回到 IDLE,由上一层模块通过done信号控制下一次操作的发起。第二种方式更灵活,因为如果你连续写多个字节,可以从上层的调度逻辑里统一管理等待,而不是让每个写操作都白白占着控制器。
第三个参数是上拉方式和 IO 驱动强度。Quartus 里可以给引脚设置WEAK_PULL_UP_RESISTOR,也可以调CURRENT_STRENGTH。但我要说的是,FPGA 内部弱上拉只是兜底,真正可靠的是板级上拉。以我调过的几块 Cyclone IV 板子来看,EEPROM 芯片离 FPGA 引脚越远,越需要把上拉电阻选小一点。AT24C02 的 I2C 引脚本身驱动能力有限,过小的上拉电阻会导致低电平平抬高,所以 4.7kΩ 是大多数开发板的默认选型,不是拍脑袋定的。如果你手里只有 10kΩ 上拉,短距离、低频下也可以用,但总线拉长到 10cm 以上我就不建议了。
5. 上板调试避坑:AT24C02 实验最常见的 5 个翻车现场
5.1 写进去读出来全是 0xFF
现象:用 Quartus 烧录后,写一个字节 0x5A 到地址 0x10,随后随机读回来,串口或者逻辑分析仪显示的全是 0xFF。
原因:地址没有对准。最常见是设备地址的 A2/A1/A0 引脚不是全接地,或者 Verilog 里DEV_W_ADDR写错了。如果从机本身没有应答,主机读总线时 SDA 一直处于释放状态,上拉电阻把它拉成高,所以读回来的每一位都是 1,凑在一起就是 0xFF。第二个可能的坑是写之后没有等 tWR,芯片还在内部写周期,你发的读地址它根本不回应,SDA 同样表现为全高。第三个可能,开发板上的 EEPROM 和 FPGA 之间还有别的器件占着同一组 I2C 引脚,地址冲突导致应答乱掉。
解决:第一步用示波器或者逻辑分析仪抓 ACK。发地址之后第 9 个时钟,SDA 应该被拉低一次。如果完全没拉低,说明地址错或芯片没上电。如果用示波器看到 ACK 了但数据还是 FF,就把 tWR 等待改成 20ms 再试。如果确认了从机应答,就用一个最简单的测试:写 0x00 地址、数据 0xA5,读回来和 0xA5 比对,不要直接跑业务逻辑。
5.2 SCL 只有半截脉冲,高电平被拉低到 0.8V 左右
现象:示波器探头点在 FPGA 的 SCL 引脚上,看到波形不是干净的方波,高电平只有 0.8V,低电平正常,有时芯片完全不应答。
原因:SCL 被当成普通推挽输出强推高电平,同时外部还有上拉电阻。FPGA 引脚驱动能力强,推挽输出高电平时和上拉电阻并联,按理说不会只有 0.8V。出现 0.8V 这种情况,多半是 SCL 引脚上还挂着别的器件,或者 FPGA 引脚被配置成了开漏但没有正确接上拉,或者 IO 标准选错导致驱动电压不对。还有一种隐蔽情况:代码里 SCL 空闲时停在低电平,等于是长期把 SDA 拉低,总线一直处于假起始状态,从机自然不会响应。
解决:检查引脚分配,确认IO_STANDARD是 3.3-V LVTTL 且外部有上拉。代码层面,SCL 空闲时必须回到高电平,也就是scl_en拉低时输出 1。用万用表量一下 SCL 对地电阻,如果是 0 或者接近 0,说明引脚配置成输出低或者是原理图短路,先把硬件问题排除再碰代码。在 Quartus 里还可以打开Enable bus-hold circuitry和Weak pull-up的 IO 选项,但这两个不能替代外部上拉。
5.3 上电后第一次读正确,第二次开始全错
现象:烧录后第一次读 AT24C02 的正常数据是对的,紧接着第二次、第三次读,数据变成 FF 或者乱跳,复位 FPGA 后又恢复第一次正确。
原因:代码里写操作和读操作共用了状态机寄存器,写完成后没有把bit_cnt、ack_err、done这些标志恢复到初始值,导致第二次读从错误的位计数开始。另外,如果 READ 流程里没有等 tWR 就发起下一次操作,芯片正处于内部写周期,NACK 会让状态机跳进异常分支。还有一种非常典型的:读操作结束时回了 ACK 而不是 NACK,从机继续输出下一个地址的数据,总线上多出来的数据把你下一次操作的首字节干扰了。
解决:每次进入 IDLE 时,把所有状态相关的寄存器全部复位,不光是state,还有bit_cnt、clk_cnt、wait_cnt、done、ack_err。我用一个专门的IDLE分支,在信号start到来之前把这些寄存器全部清零。写完一个字节后,无论成功失败,等 tWR 结束再回 IDLE,而不是立即回。最后确认读流程的最后一个字节回的确实是 NACK,可以在 ModelSim 里头把波形放大到最后一个字节的第 9 个时钟,看 SDA 是不是被主机拉高。
5.4 ModelSim 仿真完全正常,上板却不稳定
现象:testbench 里各种时序都验证过了,状态跳转、ACK 采样、数据一致,烧到板上后时好时坏,温度变化或者换个板子就表现不一样。
原因:仿真里和真实世界的差异集中在这么几个地方。第一,仿真里 SDA 和 SCL 的上升沿是理想的,实际 I2C 总线是 RC 充放电,上升沿有斜坡,如果 SCL 频率太高或者上拉太大,数据建立时间会被吃掉。第二,仿真里没有 IO 引脚延时和组合逻辑毛刺,FPGA 内部的scl和sda如果来自不同组合逻辑路径,竞争会产生毛刺。第三,复位问题:如果你用的是异步复位且复位释放时刻靠近时钟上升沿,可能会让状态机进入亚稳态,仿真里用的是理想时钟,永远不会暴露。第四,仿真里inout三态的翻转是即时的,实际引脚有几十纳秒的传输延迟,两个设备同时驱动总线时可能出现竞争。
解决:把系统时钟频率降到 25MHz 甚至 12MHz,把DLV_COUNT相应调大,让 SCL 降到 100kHz 以下,先确认功能再提频。复位信号用异步复位同步释放的标准电路,不要在 always 块里直接裸用异步复位。检查 Quartus 编译报告里的时序路径,确保 SCL/SDA 引脚对应的 IO 路径没有 hold time 违规。最后,上板调试时用逻辑分析仪抓 SCL/SDA,不要只看串口打印结果,打印结果对意味着运气好,波形对才是真的对。
5.5 Quartus 11.0 编译报错找不到引脚或引脚分配无效
现象:编译时报Error (176xxx): Can't place multiple pins assigned to same location或者Can't find pin named "scl"之类,有时候是编完了没报错,但下载后引脚电平完全不对。
原因:Quartus 11.0 对工程路径里的中文和空格支持不好,工程路径含中文时引脚分配文件会被忽略。另外一个常见问题是,顶层模块里的信号名和.qsf里-to后面的名字不一致,多一个空格、大小写不一样,Quartus 都认不出来。还有,如果顶层模块里有scl和scl_io两个信号名,引脚分配可能错位。最后一个坑是,某些开发板的 EEPROM 引脚在 FPGA 里被复用为 JTAG 或 AS 配置引脚,你在 Pin Planner 里能看到但不能随便分配。
解决:工程路径改成纯英文,目录不含空格,重建一次工程。引脚分配用 Quatrus 的 Pin Planner 图形界面操作,让工具自己生成.qsf行,不要手工复制网上的约束文件。分配完成后在编译报告的 Pin-Out 文件里确认每个信号对应到正确的封装引脚和 IO bank。如果开发板原理图里标着I2C_SCL但是引脚复用了配置功能,需要查一下板子的拨码开关,有些板子要拨到用户模式才能用这些引脚。
6. 把实验变成可以复用的 IP:状态机合并、回读校验与 SignalTap 实测
实验做完别急着封盘,把写和读合并成一个带操作码的控制器,才是这个实验真正值钱的地方。常见的做法是保留一个op[1:0]输入,2'b00表示空操作,2'b01表示单字节写,2'b10表示随机读,状态机根据 op 选择路径。这样上层逻辑只要维护一个小的命令队列,就能做到“写完立刻读回来校验”的正确节奏,而不是写一个模块、读一个模块、两个模块互相抢总线。
回读校验是我自己必加的验证手段。每次写入一个字节后,做一个随机读,把读回来的值和写进去的值比对,连续比较 3 次全部一致才认为成功,否则置一个verify_err标志。这个校验循环写起来不到 20 行,但作用非常大:它同时覆盖了写状态机、读状态机、重复起始、NACK 处理、内部写周期等待这五段逻辑,任何一个环节有问题,校验就会报错。我后来换过好几款 EEPROM,从 AT24C02 到 AT24C256,都靠这段校验逻辑在第一次上电就能定位问题。
调试手段上,Quartus 11.0 里的 SignalTap II 逻辑分析仪可以直接抓到 SCL 和 SDA 的实时波形,而不需要外接逻辑分析仪。把state、bit_cnt、ack_err加进采样列表,触发条件设为start == 1'b1,采样深度 2K。在实际板上把 I2C 波形和状态机跳转对照着看,比单纯看串口打印可靠得多。SignalTap 会占用一些 LE 资源,EP4CE10 跑这个小工程足够。
一个我从这个实验里养成的习惯:任何 I2C 器件上板之前,先写一个只包含“写一个字节、读回校验”的最小工程,把协议这一层钉死,再往里面叠加业务应用。之前有一次调一块带 EEPROM 的板子,应用代码写了上千行,最后发现是 EEPROM 页写跨页导致数据覆盖,原因就是我在实验阶段没有做回读校验,这个教训让我之后再也不敢跳过这一步。AT24C02 这个实验虽然简单,但它把开漏总线、时序、状态机、仿真和硬件调试完整地串了一遍,值得把它做成自己的固定套路,希望帮到你。
本文还有配套的精品资源,点击获取