
简介本资源是一套完整的FPGA与EEPROM芯片AT24C02通信实验工程面向数字电路初学者、嵌入式硬件开发入门者及高校电子类课程实践者聚焦I²C总线协议在FPGA上的底层实现与调试。工程基于Quartus 11.0开发环境适配Cyclone IV E系列EP4CE6E22C8器件包含可直接编译下载的Verilog源码如iic_com.v、完整工程配置文件qpf/qsf、综合与布局布线报告rpt/summary、引脚约束pin、时序分析结果sdc相关db文件及烧录文件sof/pof共124个文件压缩包仅3.75MB轻量易上手。已有1426人学习下载代码结构清晰含按键消抖模块20ms采样、I²C时钟分频逻辑生成100kHz SCL、起始/停止/读写状态机及数码管数据显示接口配套readme与bak备份文件便于理解修改。读者可快速掌握FPGA驱动I²C外设的核心流程复现读写验证并迁移至其他AT24Cxx系列芯片。1. 项目背景与核心价值最近在整理一些老项目的资料翻出来一个十多年前用Quartus II 11.0做的FPGA读写AT24C02 EEPROM的实验工程。这个项目虽然不大但麻雀虽小五脏俱全从I2C总线协议的Verilog实现到与外部芯片的实际通信再到Quartus工程的完整构建几乎涵盖了FPGA初学者入门数字系统设计、学习总线协议和进行板级验证的所有关键环节。我猜很多朋友在刚开始接触FPGA时都会把I2C、SPI这类常用串行总线的驱动实现作为第一个“有点挑战”的实战项目。网上的代码片段很多但能直接编译、下载并成功在板子上跑起来的完整工程却不多见更别提那些藏在配置细节和时序约束里的“坑”了。这个“FPGA读写EEPROM芯片AT24C02实验”项目就是一个能直接“抄作业”的完整参考。它不仅仅是一段Verilog代码而是一个包含了顶层模块、I2C主控制器、测试激励、引脚分配、编译设置乃至仿真脚本的Quartus II 11.0工程包.qpf和.qsf文件。你拿到手用对应版本的Quartus打开配置好板卡和下载器就能直接看到FPGA是如何通过几根线SDA, SCL与一颗小小的EEPROM芯片“对话”完成数据的写入和读出。对于正在学习Verilog、想理解I2C协议、或者需要快速实现一个非易失性存储接口的工程师来说这个工程的价值在于它的“可复现性”和“教学完整性”。它避开了只讲理论或只给片段代码的弊端提供了一个从代码到硬件的全链路视角。2. I2C总线协议与AT24C02芯片要点解析在动手看代码之前我们必须先吃透“对手”——I2C协议和AT24C02这颗芯片。很多初学者写驱动失败不是逻辑不对而是根本没搞清楚协议和芯片手册的规定。2.1 I2C协议的精髓线与逻辑与主从模式I2CInter-Integrated Circuit是一种简单、低速、两线制的串行通信总线。这两根线分别是串行数据线SDA双向用于传输数据。串行时钟线SCL由主设备产生用于同步数据。它的第一个核心特点是“线与”逻辑。SDA和SCL线都通过上拉电阻接到正电源当所有设备都输出高电平时总线为高只要有一个设备输出低电平总线就被拉低。这意味着我们的FPGA在Verilog里驱动这两根线时不能简单地用assign sda data_out;。正确的做法是设计一个“输出使能”信号当需要主动拉低总线时使能输出驱动低电平当需要释放总线输出高电平时需要将端口设置为高阻态inout端口在Verilog中通常通过tri类型或inout声明并结合条件赋值来实现由上拉电阻自然拉高。在代码里你会看到类似sda_oeSDA输出使能这样的信号来控制。第二个核心是主从模式和多主仲裁。我们这个实验里FPGA是唯一的主设备AT24C02是从设备所以暂时不用考虑多主竞争和仲裁。通信总是由主设备FPGA发起它产生时钟SCL并控制通信的启动START、停止STOP条件。2.2 AT24C02的“地址”与“页”AT24C02是一颗2Kbit256字节的EEPROM。与FPGA通信时你需要知道它的“电话号码”设备地址和“房间号”字节地址。1. 设备地址7位 1位读写方向AT24C02的7位设备地址是1010 A2 A1 A0 R/W#。其中高4位1010是固定标识。A2, A1, A0 这三个引脚的电平状态决定了地址的低3位。在我们的实验工程中通常将这三个引脚接地所以设备地址就是10100000x50。加上最后的读写位0为写1为读构成一个完整的8位地址字节写操作10100000(0xA0)读操作10100001(0xA1)2. 字节地址8位AT24C02只有256字节所以用一个8位的地址0x00~0xFF就能访问所有位置。在发送设备地址并得到应答后紧接着就要发送这个字节地址。3. 页写操作关键优化点AT24C02支持“页写”模式这是提高写入效率的关键。它的页大小为8字节。这意味着在一次写操作序列中一个START后到下一个STOP前主设备可以在发送完起始字节地址后连续发送最多8个字节的数据。芯片内部有一个“页指针”每收到一个数据字节地址会自动加1。当指针到达页边界例如从地址0x07收到数据后指向0x08时如果再继续发送指针会回绕到该页的起始地址0x00导致数据被覆盖。这是页写操作最容易出错的地方我们的Verilog代码必须对连续写入的字节数进行计数和管理确保不超过页边界或者主动处理页边界跨越。4. 写入周期时间关键时序参数EEPROM在接收到写入命令STOP条件后需要内部时间将数据从缓存写入非易失存储单元这个时间称为“写入周期时间”tWR。AT24C02的典型值是5ms。在这段时间内芯片不会响应新的通信即发送ACK。因此我们的FPGA驱动必须在发起一次写操作发出STOP后等待至少5ms才能开始下一次操作。一个健壮的驱动必须包含这个延时逻辑通常用一个计数器来实现。3. Verilog I2C主控制器设计与代码拆解理解了协议和芯片我们来看FPGA这边如何用Verilog实现一个I2C主控制器。这个控制器本质上是一个状态机它按照I2C的比特流和字节流时序精确地控制SDA和SCL的变化。3.1 顶层模块架构一个典型的工程顶层模块比如i2c_eeprom_top.v会包含以下部分时钟与复位输入系统时钟如50MHz和复位信号。用户控制接口例如start启动一次读写、rw读/写命令、addr字节地址、data_wr要写的数据、data_rd读回的数据、busy控制器忙标志、done操作完成标志。I2C物理接口inout sda和output scl。注意SDA必须是双向口。实例化I2C主控制器核心将用户接口信号连接到内部的I2C控制器模块。3.2 I2C控制器状态机设计这是整个驱动的核心。状态机需要处理多个层级的时序位时序、字节时序、操作序列时序。状态定义示例简化localparam [3:0] IDLE 4d0; // 空闲 localparam [3:0] START 4d1; // 产生START条件 localparam [3:0] SEND_ADDR 4d2; // 发送7位设备地址1位读写位 localparam [3:0] WAIT_ACK1 4d3; // 等待从机对地址的应答 localparam [3:0] SEND_BYTE_ADDR 4d4; // 发送字节地址 localparam [3:0] WAIT_ACK2 4d5; // 等待对字节地址的应答 localparam [3:0] SEND_DATA 4d6; // 发送一个数据字节写操作 localparam [3:0] WAIT_ACK3 4d7; // 等待对数据的应答 localparam [3:0] READ_DATA 4d8; // 读取一个数据字节读操作 localparam [3:0] SEND_NAK 4d9; // 发送非应答位读操作结束 localparam [3:0] STOP 4d10; // 产生STOP条件 localparam [3:0] DELAY 4d11; // 写入后的延时等待关键状态转移与位级控制状态机在SEND_ADDR,SEND_BYTE_ADDR,SEND_DATA,READ_DATA这些状态时还需要一个子计数器bit_cnt来控制8个比特的逐位发送或接收。例如在SEND_ADDR状态根据bit_cnt从最高位MSB开始将地址/数据的相应位放到SDA上注意输出使能控制。然后产生一个SCL脉冲先确保SDA稳定然后拉高SCL保持一段时间再拉低SCL。bit_cnt减1直到8位全部发送完毕进入等待应答WAIT_ACKx状态。SCL时钟生成I2C标准模式速率为100kbps。假设FPGA系统时钟是50MHz那么一个SCL时钟周期需要500个系统时钟。我们需要一个分频计数器来产生SCL时钟。在发送或接收数据的位周期内SCL的低电平期间允许SDA变化SCL的高电平期间SDA必须保持稳定。状态机必须严格遵循这个时序。SDA双向端口处理Verilog实现技巧这是很多新手卡住的地方。在顶层SDA声明为inout。在I2C控制器模块内我们需要三个信号来控制它module i2c_master ( input wire clk, // ... 其他端口 inout wire sda, // 顶层连接 output wire scl ); reg sda_out; // 要输出的数据 reg sda_oe; // 输出使能1驱动输出0高阻释放 wire sda_in; // 从总线上读取的输入数据 // 关键的三态门驱动逻辑 assign sda (sda_oe) ? sda_out : 1bz; // 使能时输出sda_out否则高阻 assign sda_in sda; // 始终读取总线上的实际电平 // 状态机中控制 sda_out 和 sda_oe // 例如发送START条件SCL高时SDA一个下降沿 always (posedge clk) begin case(state) START: begin if(scl_high) begin // 确保SCL为高 sda_out 1b0; // 准备拉低SDA sda_oe 1b1; // 使能输出真正拉低总线 end end // 发送数据位时 SEND_ADDR: begin sda_out device_addr[bit_cnt]; sda_oe 1b1; // 驱动总线 end // 释放总线等待ACK时 WAIT_ACK1: begin sda_oe 1b0; // 释放SDA由上拉电阻拉高等待从机拉低 end endcase end endmodule注意在WAIT_ACK状态必须在SCL拉高之前提前释放SDAsda_oe 0以便从机有机会在SCL高电平期间将SDA拉低表示应答。判断ACK时是在SCL高电平期间采样sda_in如果为低表示应答正常。3.3 读写操作序列的拼接单个字节的读写是基础完整的EEPROM操作需要将多个状态按序列组合。随机写一个字节START发送设备地址写方向0xA0等待ACK。发送字节地址addr[7:0]等待ACK。发送数据字节data_wr[7:0]等待ACK。STOP进入DELAY状态延时约5ms。随机读一个字节START发送设备地址写方向0xA0等待ACK。 // 注意读操作前先发一个“哑写”来设置内部地址指针发送字节地址addr[7:0]等待ACK。START(重复起始条件)发送设备地址读方向0xA1等待ACK。读取数据字节SCL脉冲下逐位读入sda_in。主设备发送NAK非应答表示这是最后一个字节。STOP连续页写最多8字节序列与随机写类似但在第4步之后不发送STOP而是重复“发送数据-等待ACK”的步骤直到发送完所需字节数不超过页边界最后发送STOP。控制器内部需要维护一个发送字节计数器。4. Quartus II 11.0工程配置与板级调试要点有了代码下一步是让它在真实的FPGA板卡上跑起来。这个老版本的Quartus工程里藏着一些现在看起来“古早”但至关重要的配置。4.1 工程设置与引脚分配用Quartus II 11.0打开提供的.qpf工程文件。首先检查设备型号Device是否与你手头的板卡一致。老工程可能指定了一个特定型号比如Cyclone II EP2C5T144。如果你用的板子FPGA型号不同需要先更改设备设置Assignments - Device。引脚分配.qsf文件或Pin Planner这是连接逻辑Verilog中的端口和物理芯片引脚的关键。工程里应该已经有一个.qsf文件包含了类似下面的约束set_location_assignment PIN_23 -to clk set_location_assignment PIN_45 -to rst_n set_location_assignment PIN_67 -to sda set_location_assignment PIN_68 -to scl set_location_assignment PIN_10 -to led[0] // 可能用LED指示状态clk连接到板载晶振输出的引脚。rst_n连接到复位按钮或拨码开关注意是低电平有效。sda和scl这是最重要的。必须分配到FPGA的普通IO引脚上并且在板卡上通过一个上拉电阻通常4.7kΩ或10kΩ连接到VCC如3.3V。如果板卡没有预留上拉你需要自己外接。led用于调试比如用LED闪烁表示读写完成或出错。双向引脚设置对于sda这个inout类型的引脚Quartus需要知道它的电气标准。在Pin Planner里找到sda对应的引脚检查其I/O Standard是否设置正确如3.3-V LVTTL。对于双向总线通常不需要特殊设置综合器能根据inout类型推断。4.2 编译、下载与在线调试配置好引脚后进行全编译Start Compilation。留意编译报告中的警告一些关于未使用的引脚或逻辑的警告可以忽略但如果有关于时序Timing的严重警告需要关注。下载到板卡前确保下载器如USB-Blaster驱动已安装在Quartus的Programmer中能识别到设备。选择正确的.sof文件SRAM Object File掉电丢失或生成并下载.jic文件通过File - Convert Programming Files配置到EPCS串行配置芯片掉电不丢失。上电后的第一步验证不要急于读写EEPROM。先写一个最简单的测试让scl端口以100kHz的频率输出方波用示波器测量。如果能看到稳定的、频率正确的时钟信号说明FPGA的基本功能、引脚分配、下载流程都是通的。这是硬件调试的“生命信号”。4.3 系统集成与测试激励在提供的工程中除了I2C控制器应该还有一个顶层测试模块。这个模块的作用是产生一个较慢的时钟例如从50MHz分频出几Hz用于控制读写操作的节奏。实例化I2C控制器并按照预设的流程如先写后读发起操作。将读回的数据与写入的数据比较并通过LED或数码管显示结果成功/失败。例如一个简单的测试流程可以是// 状态机控制测试流程 always (posedge clk_slow or negedge rst_n) begin if(!rst_n) begin test_state IDLE; i2c_start 0; i2c_addr 8h00; i2c_data_wr 8hA5; // 测试数据 led 1b0; end else begin case(test_state) IDLE: begin test_state WRITE_DATA; end WRITE_DATA: begin if(!i2c_busy) begin i2c_start 1b1; i2c_rw 1b0; // 写 test_state WAIT_WR_DONE; end end WAIT_WR_DONE: begin i2c_start 1b0; if(i2c_done) begin test_state DELAY_5MS; // 等待EEPROM内部写入完成 delay_cnt 0; end end DELAY_5MS: begin if(delay_cnt 5MS_DELAY) begin test_state READ_DATA; end else begin delay_cnt delay_cnt 1; end end READ_DATA: begin if(!i2c_busy) begin i2c_start 1b1; i2c_rw 1b1; // 读 test_state WAIT_RD_DONE; end end WAIT_RD_DONE: begin i2c_start 1b0; if(i2c_done) begin if(i2c_data_rd 8hA5) // 比较数据 led 1b1; // 读回正确点亮LED else led 1b0; // 错误LED熄灭或闪烁 test_state IDLE; // 或进入下一个测试循环 end end endcase end end5. 实战调试从信号抓取到问题定位代码编译下载了但LED没亮或者读写数据不对。这时候就需要系统性的调试。5.1 必备工具逻辑分析仪与示波器调试数字通信眼睛看不到信号必须借助工具。示波器用于观察SCL和SDA信号的模拟波形质量检查上升/下降时间、过冲、振铃等。I2C的ACK信号是一个很短的低电平脉冲用示波器的单次触发抓取START条件后的第9个时钟看SDA是否被拉低非常直观。逻辑分析仪这是调试总线协议的利器。它能长时间、多通道地捕获数字信号并以时序图或协议解码的形式显示。将SCL和SDA连接到逻辑分析仪设置触发条件为STARTSDA在SCL高时下降沿然后以高于1MHz的采样率捕获。好的逻辑分析仪软件能直接解析出I2C协议把地址、数据、ACK/NAK以十六进制形式显示出来一目了然。5.2 常见问题排查清单按照这个清单一步步查能解决90%的问题电源和上拉FPGA和AT24C02的供电是否正常稳定SDA和SCL线上是否有上拉电阻这是最最常见的问题没有上拉总线永远是低电平或不确定。用万用表量一下SDA/SCL对地电压在FPGA释放总线时应该是接近VCC如3.3V如果不是检查上拉电阻是否焊接或连接。物理连接FPGA的SDA、SCL引脚是否和AT24C02的对应引脚正确连接有没有接反AT24C02的A0, A1, A2地址引脚是否按代码中的设定接地或接VCCFPGA引脚配置在Pin Planner里确认sda引脚的类型是bidirectional或input/output并且没有被错误地设置为输出-only。检查引脚分配是否与板卡原理图一致。时序问题用逻辑分析仪抓波形START条件SCL高电平期间SDA是否有下降沿数据有效性在SCL高电平期间SDA是否稳定没有毛刺或变化数据变化是否只发生在SCL低电平期间ACK应答在第9个SCL高电平期间SDA是否被从机拉低如果一直是高说明从机没有应答。可能原因设备地址错误、芯片损坏、总线被锁死需要断电复位、或者写操作后没有等待足够的tWR时间。STOP条件SCL高电平期间SDA是否有上升沿时钟频率SCL的周期是否接近10us100kHz如果FPGA系统时钟分频计算错误可能导致速率过快EEPROM跟不上。代码逻辑问题双向端口控制确认在需要释放总线如等待ACK、读数据位期间时sda_oe信号确实被置为0。ACK判断逻辑是否在SCL高电平的中间位置采样sda_in来判断ACK采样太早或太晚可能误判。延时等待写操作后是否有一个稳定的5ms延时状态计数器位宽和计数值是否足够页边界处理如果是连续写代码是否检查并处理了页边界回绕如果没处理跨页写入的数据会覆盖本页开头的数据。5.3 一个典型的调试案例ACK一直为高现象逻辑分析仪显示FPGA发送设备地址后第9个时钟的SDA线仍然是高电平无ACK。 排查检查上拉电阻存在电压正常。检查设备地址逻辑分析仪解码显示发送的是0xA0正确。检查AT24C02电源和地址引脚A0,A1,A2接地地址应为0xA0匹配。用示波器单独测量AT24C02的SDA引脚对地电压发现在FPGA释放总线后该引脚电压能被上拉至高电平说明芯片引脚功能正常。怀疑是写保护引脚AT24C02有一个WPWrite Protect引脚接高电平时禁止写入。检查原理图和实际连接发现WP引脚悬空。悬空对于CMOS输入是不确定状态可能内部上拉为高导致写保护生效将WP引脚明确接地后ACK出现读写正常。提示对于数字芯片不要悬空任何输入引脚包括地址引脚、写保护引脚。必须通过电阻上拉或下拉到确定的电平。6. 工程代码的优化与扩展思路这个基础工程跑通后我们可以从几个方向对它进行优化和扩展让它更健壮、更实用。6.1 添加仿真测试平台Testbench原始工程可能没有或只有一个简单的Testbench。建立一个完善的仿真环境至关重要可以在上板前发现大部分逻辑错误。timescale 1ns/1ps module tb_i2c_eeprom(); reg clk; reg rst_n; wire sda; wire scl; // ... 连接被测模块(DUT) // 生成时钟 always #10 clk ~clk; // 50MHz // 模拟AT24C02的行为 reg [7:0] mem [0:255]; // 模拟EEPROM内存 reg sda_drive; assign sda (sda_oe_slave) ? sda_drive : 1bz; // 从机也需三态控制 // 任务模拟从机响应 task i2c_slave_response; input [7:0] exp_addr; // 期望的设备地址 // 监听START比较地址发送ACK... endtask initial begin // 初始化 clk 0; rst_n 0; #100 rst_n 1; // 发起测试序列 // ... #10000 $stop; end endmodule在ModelSim或QuestaSim中运行这个Testbench可以观察内部状态机的跳转、SDA/SCL的波形、以及模拟EEPROM内存中的数据变化彻底验证控制器的逻辑。6.2 设计更友好的用户接口基础的控制器接口start, addr, data_wr等比较原始。可以封装一个更高级的接口比如FIFO接口用户将要写入的多个字节地址和数据写入一个FIFO由控制器自动按页处理连续写入。读操作也类似将读回的字节存入FIFO供用户读取。存储器映射接口将EEPROM的256字节空间映射到FPGA内部的一个地址空间。用户像访问RAM一样通过地址总线、数据总线和读写使能信号来操作EEPROM背后的控制器自动处理所有的I2C协议、等待延时和错误重试。6.3 增加错误处理与重试机制工业级的驱动必须考虑错误处理。超时机制在任何等待ACK的状态加入超时计数器。如果超过一定时间比如1ms还没收到ACK则跳出当前操作报告错误如拉高一个error信号并回到IDLE状态。总线忙检测在发起START前先检测一段时间内SDA和SCL是否都为高总线空闲避免干扰正在进行的其他通信虽然本实验是单主。自动重试当发生NACK或超时错误时可以自动重试整个操作若干次比如3次如果仍然失败再上报。写验证在写入数据后可以自动发起一次读操作比较读回的数据与写入的是否一致实现自校验。6.4 跨时钟域与时序约束如果用户接口时钟和I2C控制器工作的时钟不是同一个就需要处理跨时钟域信号如用户发起的start脉冲。通常使用同步器两级触发器来处理单比特控制信号。对于多比特的数据信号如addr,data_wr最好使用异步FIFO或握手协议。对于高速系统还需要在Quartus中设置时序约束.sdc文件确保内部逻辑能稳定运行在系统时钟频率下。对于这个低速的I2C应用时序约束通常不是问题但养成添加基本约束如时钟定义的习惯是好的工程实践。这个十多年前的Quartus II 11.0工程在今天看来其工具版本虽旧但其中蕴含的I2C协议理解、状态机设计、双向端口处理、板级调试方法依然是FPGA开发中非常经典和实用的技能。通过解剖这样一个完整的项目我们得到的不仅仅是一个能用的EEPROM读写程序更是一套处理低速串行总线、进行FPGA-外设交互、以及调试硬件系统的完整方法论。当你亲手用示波器抓到那个完美的ACK低电平脉冲时那种对数字电路和通信协议豁然开朗的感觉是读十遍手册也换不来的。本文还有配套的精品资源点击获取