简介:本资源是一套完整的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协议的精髓:线与逻辑与主从模式
I2C(Inter-Integrated Circuit)是一种简单、低速、两线制的串行通信总线。这两根线分别是:
- 串行数据线(SDA):双向,用于传输数据。
- 串行时钟线(SCL):由主设备产生,用于同步数据。
它的第一个核心特点是“线与”逻辑。SDA和SCL线都通过上拉电阻接到正电源,当所有设备都输出高电平时,总线为高;只要有一个设备输出低电平,总线就被拉低。这意味着我们的FPGA在Verilog里驱动这两根线时,不能简单地用assign sda = data_out;。正确的做法是设计一个“输出使能”信号:当需要主动拉低总线时,使能输出,驱动低电平;当需要释放总线(输出高电平)时,需要将端口设置为高阻态(inout端口在Verilog中通常通过tri类型或inout声明,并结合条件赋值来实现),由上拉电阻自然拉高。在代码里,你会看到类似sda_oe(SDA输出使能)这样的信号来控制。
第二个核心是主从模式和多主仲裁。我们这个实验里,FPGA是唯一的主设备,AT24C02是从设备,所以暂时不用考虑多主竞争和仲裁。通信总是由主设备(FPGA)发起,它产生时钟SCL,并控制通信的启动(START)、停止(STOP)条件。
2.2 AT24C02的“地址”与“页”
AT24C02是一颗2Kbit(256字节)的EEPROM。与FPGA通信时,你需要知道它的“电话号码”(设备地址)和“房间号”(字节地址)。
1. 设备地址(7位 + 1位读写方向)AT24C02的7位设备地址是1010+ A2 A1 A0 + R/W#。其中高4位1010是固定标识。A2, A1, A0 这三个引脚的电平状态决定了地址的低3位。在我们的实验工程中,通常将这三个引脚接地,所以设备地址就是1010000(0x50)。加上最后的读写位(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 = 4'd0; // 空闲 localparam [3:0] START = 4'd1; // 产生START条件 localparam [3:0] SEND_ADDR = 4'd2; // 发送7位设备地址+1位读写位 localparam [3:0] WAIT_ACK1 = 4'd3; // 等待从机对地址的应答 localparam [3:0] SEND_BYTE_ADDR = 4'd4; // 发送字节地址 localparam [3:0] WAIT_ACK2 = 4'd5; // 等待对字节地址的应答 localparam [3:0] SEND_DATA = 4'd6; // 发送一个数据字节(写操作) localparam [3:0] WAIT_ACK3 = 4'd7; // 等待对数据的应答 localparam [3:0] READ_DATA = 4'd8; // 读取一个数据字节(读操作) localparam [3:0] SEND_NAK = 4'd9; // 发送非应答位(读操作结束) localparam [3:0] STOP = 4'd10; // 产生STOP条件 localparam [3:0] DELAY = 4'd11; // 写入后的延时等待关键状态转移与位级控制:状态机在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 : 1'bz; // 使能时输出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 <= 1'b0; // 准备拉低SDA sda_oe <= 1'b1; // 使能输出,真正拉低总线 end end // 发送数据位时 SEND_ADDR: begin sda_out <= device_addr[bit_cnt]; sda_oe <= 1'b1; // 驱动总线 end // 释放总线等待ACK时 WAIT_ACK1: begin sda_oe <= 1'b0; // 释放SDA,由上拉电阻拉高,等待从机拉低 end endcase end endmodule注意:在
WAIT_ACK状态,必须在SCL拉高之前提前释放SDA(sda_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 <= 8'h00; i2c_data_wr <= 8'hA5; // 测试数据 led <= 1'b0; end else begin case(test_state) IDLE: begin test_state <= WRITE_DATA; end WRITE_DATA: begin if(!i2c_busy) begin i2c_start <= 1'b1; i2c_rw <= 1'b0; // 写 test_state <= WAIT_WR_DONE; end end WAIT_WR_DONE: begin i2c_start <= 1'b0; 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 <= 1'b1; i2c_rw <= 1'b1; // 读 test_state <= WAIT_RD_DONE; end end WAIT_RD_DONE: begin i2c_start <= 1'b0; if(i2c_done) begin if(i2c_data_rd == 8'hA5) // 比较数据 led <= 1'b1; // 读回正确,点亮LED else led <= 1'b0; // 错误,LED熄灭或闪烁 test_state <= IDLE; // 或进入下一个测试循环 end end endcase end end5. 实战调试:从信号抓取到问题定位
代码编译下载了,但LED没亮,或者读写数据不对。这时候就需要系统性的调试。
5.1 必备工具:逻辑分析仪与示波器
调试数字通信,眼睛看不到信号,必须借助工具。
- 示波器:用于观察SCL和SDA信号的模拟波形质量,检查上升/下降时间、过冲、振铃等。I2C的ACK信号是一个很短的低电平脉冲,用示波器的单次触发抓取START条件后的第9个时钟,看SDA是否被拉低,非常直观。
- 逻辑分析仪:这是调试总线协议的利器。它能长时间、多通道地捕获数字信号,并以时序图或协议解码的形式显示。将SCL和SDA连接到逻辑分析仪,设置触发条件为START(SDA在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地址引脚是否按代码中的设定接地或接VCC?
FPGA引脚配置:
- 在Pin Planner里,确认
sda引脚的类型是bidirectional或input/output,并且没有被错误地设置为输出-only。 - 检查引脚分配是否与板卡原理图一致。
- 在Pin Planner里,确认
时序问题(用逻辑分析仪抓波形):
- START条件:SCL高电平期间,SDA是否有下降沿?
- 数据有效性:在SCL高电平期间,SDA是否稳定(没有毛刺或变化)?数据变化是否只发生在SCL低电平期间?
- ACK应答:在第9个SCL高电平期间,SDA是否被从机拉低?如果一直是高,说明从机没有应答。可能原因:设备地址错误、芯片损坏、总线被锁死(需要断电复位)、或者写操作后没有等待足够的
tWR时间。 - STOP条件:SCL高电平期间,SDA是否有上升沿?
- 时钟频率:SCL的周期是否接近10us(100kHz)?如果FPGA系统时钟分频计算错误,可能导致速率过快,EEPROM跟不上。
代码逻辑问题:
- 双向端口控制:确认在需要释放总线(如等待ACK、读数据位期间)时,
sda_oe信号确实被置为0。 - ACK判断逻辑:是否在SCL高电平的中间位置采样
sda_in来判断ACK?采样太早或太晚可能误判。 - 延时等待:写操作后,是否有一个稳定的5ms延时状态?计数器位宽和计数值是否足够?
- 页边界处理:如果是连续写,代码是否检查并处理了页边界回绕?如果没处理,跨页写入的数据会覆盖本页开头的数据。
- 双向端口控制:确认在需要释放总线(如等待ACK、读数据位期间)时,
5.3 一个典型的调试案例:ACK一直为高
现象:逻辑分析仪显示,FPGA发送设备地址后,第9个时钟的SDA线仍然是高电平(无ACK)。 排查:
- 检查上拉电阻:存在,电压正常。
- 检查设备地址:逻辑分析仪解码显示发送的是
0xA0,正确。 - 检查AT24C02电源和地址引脚:A0,A1,A2接地,地址应为
0xA0,匹配。 - 用示波器单独测量AT24C02的SDA引脚对地电压,发现在FPGA释放总线后,该引脚电压能被上拉至高电平,说明芯片引脚功能正常。
- 怀疑是写保护引脚:AT24C02有一个
WP(Write 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 : 1'bz; // 从机也需三态控制 // 任务:模拟从机响应 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低电平脉冲时,那种对数字电路和通信协议豁然开朗的感觉,是读十遍手册也换不来的。
本文还有配套的精品资源,点击获取