
简介面向FPGA学习者的E2PROM读写测试Verilog HDL工程基于Quartus 18.0设计目标器件为Cyclone IV E系列EP4CE10F17C8实现对AT24C16 EEPROM的I2C读写操作并通过LED指示测试结果。工程采用模块化设计包含顶层模块、读写控制模块、I2C驱动模块和LED告警模块I2C驱动支持字节写、页写、当前地址读、随机读及顺序读等操作读写控制模块则完成写入数据再读回比对以检验存储与通信是否正确顶层使用状态机调度逻辑清晰且易于移植。压缩包内共263个文件整体大小约6.57MB除Verilog及VHDL源码外还包含Quartus工程配置、编译报告、仿真数据库与可下载比特流文件可直接打开工程编译并上板验证。目前已有388人学习下载。这一工程既适合初学者理解I2C协议与FPGA时序设计也可作为课程设计或毕业设计中EEPROM读写模块的完整参考模板节省搭建工程与调试驱动的时间。1. 从 E2PROM 读写测试工程文件入手先认清这套东西在验证什么拿到“E2PROM读写测试Verilog HDL设计Quartus工程文件.zip”这类压缩包多半是课设、毕设或者临时接手旧项目的场景。解压后最常见的动作是打开 .qpf、全编译、USB-Blaster 下载然后板上 EEPROM 写进去的数据回读全是 0xFF。问题很少出在“测试逻辑”而在那条很少被解释的链路I2C 时序、器件地址、页边界和 tWR 写周期。这套 Verilog HDL 工程真正值钱的地方是把你从“点亮一颗 LED”带到“跟一个真实器件完成协议握手”。它和 DDR4 读写测试那种靠时序训练和数据比对的方式完全是两套打法读清协议才是关键。适合正在啃 Quartus 的新手也适合想快速复用工程、不想在时序上栽跟头的工程师。2. E2PROM 器件模型与 I2C 时序参数读懂状态图再写 Verilog2.1 先分清是哪种 E2PROM并行 EEPROM 与 I2C EEPROM标题里的 E2PROM 是 EEPROM 的常见写法网上这类“读写测试”工程里九成以上都是 AT24C 系列 I2C 接口器件。它的物理接线很简洁SCL 时钟线、SDA 双向数据线、WP 写保护外加电源。但同样叫 E2PROM 的还有一批并行器件比如 28C64、AT28C256它们带 8 位数据总线CE/OE/WE 控制线写入时要求 WE 脉冲宽度不低于 100ns读取时还要等 OE 到数据有效之间的延迟。两类的验证思路完全不同并行器件是“给时序信号看数据总线”I2C 器件是“靠状态机在两根线上搬运字节”。判断一个工程面向哪类不用看文档直接看顶层端口。如果顶层只有 scl、sda_in、sda_out 这类三线接法就是 I2C如果看到 data_i[7:0]、data_o[7:0] 和 we_n、ce_n、oe_n说明工程带的是并行 EEPROM。后面所有讨论都按前者展开这也是标题“读写测试”默认的常见做法。拿到工程先做这一步能省掉后面一整天的迷茫。2.2 AT24C 系列的器件地址与页结构A2A1A0 怎么影响测试数据I2C 总线上的第一个字节是器件地址。AT24C02 的地址字节固定格式为 1010 A2 A1 A0 R/WA2/A1/A0 是芯片引脚电平单芯片板上通常接地所以写地址是 0xA0读地址是 0xA1。到了 AT24C04 容量翻倍A0 位被“内部块选择”占用地址字节变成 0xA0 与 0xA2 两个AT24C08 再吃掉 A1AT24C16 则把 A2 也占掉。写测试前先查板上的丝印和原理图确认器件地址没错否则第一步就会卡在无应答上。页结构是第二个容易坑人的地方。AT24C02 容量 256 字节分成 8 页每页 16 字节。页写PAGE WRITE一次最多写 16 字节且不能跨页写地址落在页边界附近时内部地址计数器会回绕到本页开头把数据覆盖到错误位置。读操作同样会回绕。测试序列如果从 0x0D 开始连续写 5 个字节数据会被写到 0x0D、0x0E、0x0F、0x00、0x01而不是用户直觉里的 0x12。这是读写测试“数据错乱”最常见的原因之一。型号容量页大小写器件地址地址注意点AT24C02256 B16 B0xA0页边界 0x0F/0x1F/...AT24C04512 B16 B0xA0 / 0xA2P0 位替代 A0 参与寻址AT24C081 KB16 B0xA0 / 0xA2 / 0xA4P1 位替代 A1AT24C162 KB16 B0xA0 0xAEP2 位替代 A2建议测试数据不要从 0 页的 0x00 开始写因为许多板卡出厂会在这个区域烧录配置信息。我一般把测试地址选在 0x10既跨过第一页边界附近又不会撞上未初始化区域。2.3 I2C 总线时序参数START/STOP、ACK 与 tWR 写周期I2C 协议只有两条线却定义了完整的状态转换。主机把 SDA 在 SCL 高电平期间从 1 拉到 0产生 START 条件从 0 拉回 1 产生 STOP 条件。每个字节由 8 个 SCL 脉冲传输第 9 个脉冲由从机拉低 SDA 表示应答 ACK。E2PROM 收到最后一个字节后会进入内部写周期期间器件不响应任何命令这段时间用 tWR 表示AT24C 系列典型值是 5ms。开始写 Verilog 之前最好把数据手册里的时序参数表摆在手边。下面这组是 AT24C02 在 100kHz 标准模式下的关键值400kHz 快速模式的参数明显收紧。参数100kHz 标准模式400kHz 快速模式单位SCL 时钟频率100400kHztSUSTA起始条件建立时间4.70.6ustHDSTA起始条件保持时间4.00.6ustSUDAT数据建立时间250100nstHDDAT数据保持时间00us上升/下降时间30020ns对 FPGA 来说最容易违反的是 tSU:DATSDA 必须在 SCL 上升沿之前至少 100ns快速模式就稳定。不少初学者把 SDA 和 SCL 在同一拍翻转结果器件读到的数据错位。设计控制器时明确“SDA 在 SCL 低电平期间变化在 SCL 高电平期间保持”就能避开这个坑。2.4 SCL 频率与分频系数从系统时钟推导出 100kHz常见开发板系统时钟是 50MHz周期 20ns。要产生 100kHz 的 SCL周期为 10us半周期 5us即 250 个系统时钟周期。分频系数可以参数化写成parameter SCL_FREQ_KHz 100; // 目标 SCL 频率单位 kHz parameter CLK_FREQ_MHz 50; // 系统时钟单位 MHz localparam WORD_CLK_DIV (1_000_000 / (2 * SCL_FREQ_KHz)) / (1_000 / CLK_FREQ_MHz) - 1;1_000_000 / (2 * SCL_FREQ_KHz)算出 SCL 半周期对应的纳秒数对 100kHz 就是 5000ns1_000 / CLK_FREQ_MHz算出单个系统时钟周期的纳秒数对 50MHz 是 20ns两者相除得到计数上限 250。时序状态机里每计满 250 拍翻转一次 SCL 输出SDA 也在该时刻切换。把频率参数化而不是写死数值后续要切到 400kHz 快速模式时只需改一行参数。用计数器的中间时刻切换 SDA、结束时刻翻转 SCL就能天然满足前面说的数据建立时间。3. Quartus 工程结构与 Verilog HDL 控制器实现从模块划分到可综合代码3.1 工程文件清单里到底有什么.qpf / .qsf / .sdc / .v解压 Quartus 工程后常见一堆文件真正编译烧录需要的只有几类。.qpf 是 Quartus 项目文件双击打开工程.qsf 是设置文件记录器件型号、顶层实体、引脚分配和综合选项它决定工程能不能编译正确.qsf 中如果带 .sdc 约束说明工程在时序约束下工作通常至少包含一条 create_clock 定义系统时钟。Verilog 源码通常有顶层、分频、I2C 控制器、显示模块几个角色。它们的分工可以用一张表概括。文件作用编译时谁需要.qpf项目入口Quartus 打开工程.qsf器件型号、顶层、引脚分配全编译必须.sdc时钟与输入输出延时约束TimeQuest 检查.v设计源代码综合与布线如果拿到旧工程Quartus 版本跨度大比如 9.1 的工程用 16.0 打开会提示升级升级后 .qsf 会被重写引脚分配一般能保留但个别 IP 核需要重新生成。用文本编辑器打开 .qsf 看一段常见内容set_global_assignment -name FAMILY Cyclone IV E set_global_assignment -name DEVICE EP4CE6E22C8 set_global_assignment -name TOP_LEVEL_ENTITY e2prom_test_top set_location_assignment PIN_E10 -to clk_50m set_location_assignment PIN_E11 -to rst_n set_location_assignment PIN_F5 -to sda set_location_assignment PIN_F6 -to scl set_global_assignment -name SDC_FILE e2prom_test.sdcFAMILY 和 DEVICE 两行指定芯片型号和封装下载时选错器件会直接报 mismatch。TOP_LEVEL_ENTITY 必须和顶层模块名一致。set_location_assignment 的引脚号要按照自己板卡原理图改不同开发板共用一套引脚的情况很少。新版 Quartus Prime 仍然兼容这套 qsf 语法所以从 Quartus II 13.1 换到 Prime 18.0工程文件可以沿用只是第一次编译会慢一些。3.2 分频与三态 IO把 50MHz 转成 SCL 并处理双向 SDAI2C 的 SDA 是双向端口Verilog 里要用 inout并靠三态门控制输出。分频模块只负责产生 SCL 半周期的 tick 信号I2C 控制器在每个 tick 边界推进状态。常见做法是分频器输出一个单周期脉冲而不是直接生成占空比 50% 的时钟因为状态机需要在 SCL 下降沿、上升沿附近分别处理不同的操作。module scl_tick_gen #( parameter CLK_FREQ_MHz 50, parameter SCL_FREQ_KHz 100 )( input wire clk, input wire rst_n, output reg tick ); localparam CNT_MAX (1_000_000 / (2 * SCL_FREQ_KHz)) / (1_000 / CLK_FREQ_MHz) - 1; reg [15:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 16d0; tick 1b0; end else if (cnt CNT_MAX) begin cnt 16d0; tick 1b1; // 每 250 拍输出一个脉冲 end else begin cnt cnt 1b1; tick 1b0; end end endmoduletick 信号每 5us 出现一次I2C 控制器把它当作“半拍”事件第一个 tick 拉低 SCL 并改变 SDA 上的数据第二个 tick 拉高 SCL 让从机采样。这样 SDA 天然是在 SCL 低电平期间改变的tSU:DAT 得到保证。CNT_MAX 的推导和 2.4 节一致只是这里把计数动作独立成模块仿真时直接把 SCL_FREQ_KHz 改成 10000 就能加速波形观察。3.3 I2C 控制器状态机IDLE、START、发字节、ACK、STOP控制器的核心是状态机。常见写法是六个状态空闲、起始、发送字节、等待应答、等待写周期、停止。发送字节时用一个 bit_cnt 从 7 数到 0第 8 拍结束后释放 SDA第 9 拍读取从机 ACK。下面是状态定义和主转移的骨架完整模块还要按 2.3 节的时序参数在状态切换处插入计数等待。localparam S_IDLE 3d0; // 总线空闲等待命令 localparam S_START 3d1; // 产生 START 条件 localparam S_BYTE 3d2; // 移位发送一个字节 localparam S_ACK 3d3; // 采样从机应答 localparam S_TWR 3d4; // 写周期等待 localparam S_STOP 3d5; // 产生 STOP 条件 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; scl 1b1; sda_out 1b1; bit_cnt 3d7; end else if (scl_tick) begin case (state) S_IDLE: if (start_cmd) state S_START; S_START: begin sda_out 1b0; // SCL 高电平期间拉低 SDA state S_BYTE; bit_cnt 3d7; tx_byte cmd_addr; // 载入器件地址 0xA0 end S_BYTE: begin scl 1b0; sda_out tx_byte[bit_cnt]; // 高位在前 if (bit_cnt 0) state S_ACK; else bit_cnt bit_cnt - 1b1; end S_ACK: begin sda_out 1bz; // 释放 SDA让从机拉低 scl 1b1; if (!sda_in_sync) state S_TWR; else state S_STOP; // 无应答按停止处理 end // S_TWR 中等待 twr_done随即进入写地址或数据阶段 endcase end end这段代码的关键点有两个。第一个是 S_START 和 S_BYTE 之间的 SCL 电平衔接START 条件要求 SCL 为高时 SDA 产生负跳变之后 SCL 必须拉低才能开始传数据所以进 S_BYTE 的第一个动作是把 SCL 置 0。第二个是 ACK 采样SDA 释放为高阻后要先同步打两拍消除亚稳态再判断从机是否应答。AT24C 无应答时 SDA 会被上拉电阻保持为高状态机最好进 STOP 而不是死锁。3.4 读写测试主控写入固定序列再回读比较用 LED 展示结果有了 I2C 控制器剩下的是测试主控逻辑。我的习惯是设计成按键触发单次测试按下按键后主控按“合并写 3 字节 → 等待 5ms → 随机读回 3 字节 → 逐字节比较”的流程工作结果用 LED 指示。合并写可以一次 PAGE WRITE 完成也可以写成三次单字节写测试工程用三次单字节写更直观方便用示波器观察完整地址序列。reg [1:0] st; reg ok_flag; always (posedge clk or negedge rst_n) begin if (!rst_n) begin st 2d0; ok_flag 1b0; end else case (st) 2d0: if (key_neg) begin // 按键下降沿触发 i2c_dev_addr 8ha0; // 器件写地址 i2c_word_addr 8h10; // 写入起始地址 i2c_wdata 8ha5; // 测试数据 A5 st 2d1; end 2d1: begin start_write; // 发写命令脉冲给控制器 if (write_done) st 2d2; end 2d2: begin start_read; // 回读同一地址 if (read_done) begin ok_flag (i2c_rdata 8ha5); st 2d0; end end endcase endstart_write和start_read在完整工程里是发给 I2C 控制器的命令脉冲控制器完成后回 write_done/read_done。比较时只比较读回的最后一个字节因为前面命令字节会先出现在总线上有效数据对应读序列的最后一拍。若 ok_flag 为 1LED 常亮几次为 0 则闪烁不用串口也能一眼看出测试结果。测试地址选 0x10 正是 2.2 节说的避开出厂占用的首页。4. ModelSim 仿真与 Quartus 联合调试上板之前先让波形说话4.1 testbench 怎么搭模拟从机响应和按键信号仿真环境有两个任务产生时钟、复位、按键信号以及模拟 E2PROM 从机回波。AT24C 行为模型可以简化成收到器件地址字节后若匹配 0xA0/0xA1 就回 ACK写操作把地址和数据存进一个数组读操作把数组内容移出。下面是 testbench 的关键结构重点看从机握手回写。module tb_e2prom; reg clk 1b0; always #10 clk ~clk; // 50MHz周期 20ns reg rst_n, key_n; wire scl, sda; e2prom_test_top dut ( .clk_50m (clk), .rst_n (rst_n), .key_n (key_n), .scl (scl), .sda (sda) ); i2c_slave_model #(.DEV_A2A1A0(3b000)) u_slave ( .scl(scl), .sda(sda) ); initial begin rst_n 0; key_n 1; #200 rst_n 1; // 复位释放 #5000 key_n 0; #100 key_n 1; // 模拟按键按下 #20_000_000 $finish; // 覆盖 tWR 与读操作 end endmodule#10翻转一次产生 20ns 周期正好对应 50MHz。按键用“拉低 100ns 再释放”模拟真实按键在工程里要加消抖。从机模型内部用一个数组做存储每个字节的第九拍采样 SDA 判断 ACK。仿真时间要覆盖完整流程一次单字节写、tWR 5ms、一次读至少 20ms#20_000_000是 20ms这个时间必须给够否则看不到回读结果。4.2 波形上盯哪几个点START、ACK 与第九个 SCL 上升沿仿真跑完先看整体结构再放大局部。第一眼看 scl 和 sda 上是否有 START/STOP 边界第二眼看每个字节的第 9 个 SCL 上升沿SDA 是否被从机拉低第三眼看写入完成后主控是否等待了 tWR 再发起读。用 ModelSim 的 cursor 直接对比 scl 和 sda 的电平关系能快速定位“SDA 在 SCL 高电平时翻转”这类非法时序。常见问题在波形上特征很明显。ACK 永远为高说明器件地址没对上检查 2.2 节的 A2A1A0。数据错位说明 SDA 在 SCL 上升沿附近变化回到 3.3 状态机里检查“tick 后半拍才更新 SDA”这个节奏。波形里写命令重复发送说明主控误判 write_done往往是状态机出口条件多打了一个 tick。把状态寄存器和 bit_cnt 一起加进波形列表比单看总线更容易定位是哪个状态停住。4.3 Quartus II 无法启动 ModelSim 与仿真设置一组对照排查Quartus 调用外部 ModelSim 最常报的就是 “cannot launch ModelSim”原因集中在路径与仿真工具绑定。先到 Tools - Options - EDA Tool Options 里检查 ModelSim 安装路径再到 Assignments - Settings - EDA Tool Settings - Simulation 把仿真工具设为 ModelSim-Altera并指定 Test Bench 文件。报错现象常见原因处理步骤cannot launch ModelSim路径没配置或版本位数不匹配指定 modelsim.exe 完整路径64 位对 64 位仿真波形空白仿真工程缺少顶层模块Settings 里重新 Add Test Bench绑定测试模块报找不到 IP 核.qsf 版本升级后 IP 路径失效重新 Generate IP或把工程放到原路径仿真数据为 Xtestbench 未给 rst_n 初始值initial 块先拉低复位再释放老版本 Quartus II 13.1 自带 ModelSim-Altera 10.1d新版 Quartus Prime 的仿真设置入口稍有变化但排查顺序不变。仿真通过了再上板板级调试才有意义。5. 上板调试的排错清单写入不生效、回读 0xFF 与烧录链路问题5.1 写入后读回总是 0xFF九成是没等 tWR 或地址不对上板之后第一个经典现象是写完立刻读会读到 0xFF。原因基本是内部写周期冲突AT24C 在 tWR 期间对总线命令无响应读请求落在写周期里器件自然给不出数据。先看主控里有没有“写完成后再延时 5ms”的步骤没有就加在地址、数据都正确的前提下插一个 6ms 延时再发读命令问题通常当场消失。第二个原因是 WP 引脚被拉高整个芯片进入写保护写操作全部丢弃。查原理图里 WP 接法开发板上通常接地或由跳线控制被跳线拉高时会犯这个毛病。排错顺序建议固定成先读固定地址比如 0x10读出来若是 0xFF 且地址正确基本排除器件供电用示波器看写波形确认 STOP 条件之后的确等了足够时间再看 WP 是低还是高。这套顺序能覆盖绝大多数写入不生效的场景。注意GPIO 直连的 SCL/SDA 走线通常没有端接调试时用杜邦线越短越好手捏着线头看波形没有参考价值。5.2 回读数据与写入不一致页边界、SCL 速率和总线竞争数据写进去了但读回来对不上按三件事排查。第一件是页边界回绕参考 2.2 节把测试数据控制在页内。第二件是 SCL 频率过高400kHz 快速模式对线缆和上拉电阻要求高杜邦线悬空连接时边沿过缓容易造成数据采样错误先降到 100kHz 验证。第三件是总线竞争少数开发板的 SCL/SDA 与板上其他 I2C 器件共享从机地址冲突或总线被另一主机占用都会造成回读错乱断电后用万用表量两个引脚的电平再查原理图确认是否与 EEPROM 之外的外设复用。// 回读比较时的延时控制6ms 延时计数器 reg [19:0] twr_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) twr_cnt 20d0; else if (state S_TWR) twr_cnt twr_cnt 1b1; // 0.02us 一拍计满 6ms end assign twr_done (twr_cnt 20d300000);20 位计数器最大值约 100 万对 50MHz 时钟能计到 20ms。这里把 tWR 等待做成显式状态而不是敷衍的延时是为了让波形能直接看出等待时间调试时把 twr_cnt 加进波形列表一眼就能判断控制器是否给了器件足够的时间。5.3 下载与烧录的杂项USB-Blaster 驱动、Programmer 卡顿和引脚分配下载环节三个高频问题。第一是设备管理器里找不到 USB-BlasterQuartus II 安装目录下通常有 64 位驱动手动指向 inf 文件安装即可Quartus Prime 安装包驱动路径有变化找不到时用 WinUSB 工具更换驱动也能解决。第二是 Quartus Programmer 打开文件时卡顿常见原因是工程目录在机械盘或杀毒软件实时扫描把 .sof 复制到本地临时目录再加载通常就顺畅Programmer 界面卡住时不要反复点等几秒或直接重启软件。第三是引脚分配遗漏FPGA 的 JTAG 可以正常下载但功能不跑先看 Assignment Editor 里 scl/sda 是否都分配了引脚且电平标准与板卡一致。现象处置找不到 USB-Blaster 设备手动安装 inf 驱动或更换 WinUSB 驱动Programmer 打开文件卡顿把 .sof 复制到本地临时目录再加载下载正常但功能不跑检查 Assignment Editor 引脚与电平标准6. 把读写测试工程改造成可靠模块连续写读与自校验6.1 参数化总线频率与命令队列让控制器可复用前面手写状态机已经能完成单字节读写要变成可复用 IP第一步把所有频率常数和地址参数化。器件地址、起始地址、每次读写的字节数都用 parameter 暴露出来顶层例化时只需改参数。第二步把主控的状态机改成“接受命令帧”的模式主控侧定义取指令接口I2C 控制器侧定义 read_req / write_req 请求和 done、error 响应两个模块用寄存器握手。这样同一套 I2C 控制器既能服务 E2PROM也能服务温度传感器或 RTC 芯片。6.2 给命令加超时与重试从演示工程走向可靠模块AT24C 在写周期里收到的任何命令都不会应答如果主控没有等待机制重试逻辑会和器件内部写周期打架。可靠做法是发起命令前先确认总线空闲发送后等 ACK 超时超时则重新发送 START 并重试一次重试两次仍失败就进入 ERROR 状态。超时计数可以直接借用 5.2 的 twr_cnt 寄存器把 error_flag 和状态编码映射到板载 LED。调试时看到 LED 闪烁次数不同就能判断是器件无应答还是写周期等待不够这一招比一根一根量波形快得多。6.3 最后留一个验证清单测试工程才算闭环改造完成后按下面的顺序自测第一次上电全读写一遍验证掉电保持单字节写读验证寻址跨页连续写 20 字节验证页边界处理把 SCL 参数改成 400kHz 验证高速模式是否稳定人为把 WP 拉高验证错误上报路径。每一步都记录写入地址和数据回读不一致时回到第 5 章的排错顺序。走到这里标题里的那套工程文件已经被消化成自己手里的可复用模块下一次再做带电可擦除存储的板卡直接例化即可不用回头翻数据手册。本文还有配套的精品资源点击获取