简介:面向FPGA初学者的AT24C02 EEPROM读写实验完整工程,基于Quartus 11.0开发,目标器件为Cyclone IV E系列EP4CE6E22C8,演示FPGA通过I2C接口对EEPROM芯片执行写入与读取的过程。核心源码iic_com.v实现主控逻辑,含20ms按键消抖、100kHz SCL时钟分频、高电平中间采样、低电平中间变化等关键时序处理,通过按键1和按键2分别触发写、读操作,结果由数码管显示。压缩包共124个文件,约3.75MB,除Verilog源码外,还包括qpf/qsf工程文件、bsf/bdf模块图、sof/pof烧录文件,以及cdb/hdb/rpt等Quartus生成的分析综合报告与辅助数据,便于完整还原设计流程。资源已有1427人学习,代码注释详细、模块边界清晰,很适合FPGA学习者作为I2C协议与EEPROM控制器设计的参照范例,也可直接用于课程设计或开发板功能验证,能够帮助入门者从RTL代码到下载配置形成完整认识。
1. FPGA读写EEPROM芯片AT24C02:这份能直接落地的Quartus工程到底该怎么看
如果你最近刚摸到FPGA开发板,多半会卡在"板子能点灯、能跑计数器,但一到通信协议就不知道怎么下手"这个坎上。AT24C02正是最适合拿来练手的从机芯片——挂在一根I2C总线上,只有256字节存储空间,逻辑简单到只有写、读两种操作,但恰恰因为简单,反而把I2C最关键的时序约束暴露得一清二楚。这份工程文件是基于EP4CE6E22C8这颗Cyclone IV芯片做的,核心逻辑全部用Verilog写完并附带了Quartus 11.0的完整工程。它能解决的是"上电自动写入一段数据再读回比对"这个闭环流程:你把工程烧进板子,连接好AT24C02和I2C引脚,LED或者串口就能告诉你读写是否成功。适合正在做FPGA课程设计、或者准备自己封装I2C控制器IP核的人。别指望它有多炫的架构,它的价值在于把"时序拆开"这件事讲得很直白。
2. 工程结构拆解:I2C的时序约束与EP4CE6E22C8的资源边界
2.1 I2C总线为什么难写,难点都在物理层约束
I2C只有两根线:SCL(时钟)和SDA(数据)。表面上看,SCL输出高低电平、SDA输出数据位,写数据时在SCL高电平期间锁存,读数据时在SCL低电平期间切换SDA方向,就完成了最基础的时序。但AT24C02的数据手册里有一条硬约束:SDA只能在SCL为低电平时发生变化,SCL高电平期间SDA必须保持稳定,否则从机会把这种跳变当作起始或停止条件。这也是多数人第一次用Verilog写I2C最容易翻车的地方——SDA的翻转时刻没有严格对齐SCL的低电平区间,结果波形看起来对、逻辑分析仪上也有应答,但数据就是错的。
此外还有两个信号完整性约束需要留意:一是SCL高电平的输入高门限一般是0.7×VDD,如果板子用的是3.3V供电,那高电平必须拉到2.3V以上,很多FSM里的信号在内部翻转没问题,一旦输出到引脚变成高阻或弱驱动,就会被外部上拉电阻拉出奇怪的沿;二是I2C标准模式100kHz要求SCL高电平和低电平各自不小于4.7us,因此时钟分频不能只算频率,还要算占空比。常见的做法是让计数器按半周期交替翻转,SCL低电平区间留够时间给SDA切换。
2.2 工程里的文件划分逻辑
打开这份工程,你会看到设计的模块划分比较清晰:顶层负责例化、时钟分频单独拎出来、I2C底层时序驱动和读写状态机分开。文件级的功能划分如下表所示:
| 文件(常见命名) | 职责 | 关键接口 |
|---|---|---|
| top_eeprom.v 或类似顶层 | 例化时钟分频、读写控制、I2C驱动 | clk_50m、rst_n、scl、sda、led |
| clk_div.v | 把50MHz时钟分频得到SCL半周期计数 | clk、rst_n、scl_tick 或分频后的时钟使能 |
| i2c_drive.v | SCL/SDA引脚时序,输出起始、停止、数据位、应答采样 | cmd_start、cmd_write、cmd_stop、sda方向控制 |
| eeprom_rw.v | 组织页写、读回、比对,生成控制命令序列 | i2c_busy、i2c_ack、write_done、read_data |
这种划分最大的好处是调试时不用动时序驱动,只在读写控制里加状态。如果你后续想移植到别的器件,比如从Cyclone IV换到Artix-7,只需要改顶层引脚约束,I2C驱动部分几乎是通用的。EP4CE6E22C8的逻辑单元不到6000个,但I2C驱动加读写状态机总共只消耗大约200到300个LE,剩下的资源放其他业务逻辑完全够用,这也是I2C实验适合做入门项目的原因。
2.3 时钟分频的计算与实现
板上主时钟常见的有50MHz和27MHz两种,EP4CE6E22C8核心板最常见的配置是50MHz有源晶振。目标SCL频率是100kHz,即一个周期10us。标准模式下每周期需要高低电平各5us,也就是250个50MHz时钟周期,因此分频计数器需要计数到125翻转一次。考虑到边沿抖动和上拉电阻带来的上升沿变缓,实际工程里我会把分频目标稍微调快一点,比如按SCL 90kHz设计,计数到139翻转一次,这样即使引脚上有寄生电容也不会导致SCL高电平过短。
如果你用SignalTap去抓SCL的实际波形,会发现边沿不是理想的直角。除了示波器探头负载,更常见的原因是分频计数器的位宽太窄或者翻转逻辑没有用寄存器输出。以下是一段典型的分频代码,注意SCL翻转的计数判断:
reg [7:0] cnt; // 8位计数器,最大计数255,够90kHz使用 reg scl_reg; // SCL寄存器输出,避免组合毛刺 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 8'd0; scl_reg <= 1'b1; // 空闲时SCL保持高 end else begin if (cnt >= (DIV_CNT - 1)) begin cnt <= 8'd0; scl_reg <= ~scl_reg; end else begin cnt <= cnt + 1'b1; end end end assign scl = scl_reg; // 只有寄存器输出SCL,禁止组合逻辑直连引脚这里DIV_CNT就是半周期计数值,50MHz主频下想得到100kHz,DIV_CNT取125;想得到90kHz,取139。注意一个细节:cnt的比较用了大于等于DIV_CNT-1,这样实际翻转周期是DIV_CNT个时钟,而不是DIV_CNT+1。如果你用等号判断,那翻转周期会多一个时钟,SCL频率会偏低约0.8%,对EEPROM来说绰绰有余,但对后续扩展I2C从机通信来说,累计误差可能在多个字节后让时序错位。
SDA的方向控制同样要放在这个模块内。写数据时SDA引脚作为输出,读应答时切换成高阻输入。FPGA引脚没有真正的开漏,但配置成三态后行为上等价于开漏,前提是外部有上拉电阻。
3. 核心Verilog代码逐段拆解:写状态机与读状态机的转移条件
3.1 I2C启动与停止条件的状态实现
I2C的起始条件是SCL高电平期间SDA产生一个下降沿,停止条件是SCL高电平期间SDA产生一个上升沿。这两个条件必须由主控主动产生,而且时序顺序不能反。多数情况下,错误的启动时序表现为板上强上拉电阻(比如1kΩ)把SDA拉得太快,导致从机根本检测不到下降沿——这不是逻辑错误,是电气问题。
在状态机里,启动条件要拆成两步走:先拉高SCL,再拉低SDA。为了保证时序,一般用独立状态:
localparam IDLE = 3'd0; localparam START1 = 3'd1; // SCL先保持高 localparam START2 = 3'd2; // SCL为高时拉低SDA localparam STOP1 = 3'd5; // SCL为低时抬SDA localparam STOP2 = 3'd6; // SCL拉高后保持SDA高 case (state) IDLE: begin scl_out = 1'b1; sda_out = 1'b1; state = START1; end START1: begin scl_out = 1'b1; // SCL保持高 sda_out = 1'b0; // 拉低SDA,此时SCL高,产生下降沿 state = START2; end START2: begin scl_out = 1'b0; // SCL拉低,准备传第一个bit state = WRITE_BIT; end这里SDA的方向control保存在驱动模块里。START1状态里scl_out和sda_out虽然都是寄存器输出,但这两个信号可能在不同的时钟周期发生变化,所以状态切换之间至少留一个周期。如果两个状态连续切换不做时钟等待,SDA下降沿和SCL低电平之间只有组合逻辑延时,几乎必定导致从机误判起始条件。
3.2 写数据的字节状态机:逐位移位与ACK采样点
写字节本质上就是8次循环:在SCL低电平期间把SDA切到对应位,然后SCL拉高让从机采样,再拉低准备下一位。这个循环在Verilog里通常不是用for循环,而是用计数器加位移位寄存器。AT24C02的写时序和大部分I2C从机一样,第9个时钟是ACK应答位,从机拉低SDA表示收到。这里有一个常见误区:ACK采样点应该在SCL高电平的后半段,不能跳变沿就立即采样。
reg [7:0] tx_shift; // 待发送数据 reg [3:0] bit_cnt; // 当前bit位置,0~7为数据位,8为ACK位 reg sda_din; // 采样到的SDA电平 always @(posedge clk) begin if (state == WRITE_BIT) begin if (bit_cnt <= 7) begin sda_out = tx_shift[7]; // 先发最高位 tx_shift = {tx_shift[6:0], 1'b0}; // 左移一位 if (scl_reg == 1'b1) begin // SCL高电平期间保持sda不变 end else begin // SCL低电平期间可以切换sda end end else begin sda_out = 1'bz; // 释放SDA,让从机拉低应答 if (scl_reg == 1'b1) sda_din <= sda_in; // SCK高电平后段采样ACK end bit_cnt = bit_cnt + 1'b1; end end这段代码里sda_out在高阻和输出之间切换,是FPGA模拟I2C开漏行为的关键。如果你直接把sda_out设成固定高低电平,从机拉低SDA时会产生总线冲突,轻则ACK检测失败,重则损坏引脚。采样点放在scl_reg为高电平时,而不是在上升沿采样,因为从机在SCL高电平期间才把SDA电平驱动到位,上升沿瞬间数据可能还在跳变。
3.3 写EEPROM的完整操作序列:从起始条件到写周期等待
AT24C02的一次字节写操作包括:起始条件、发送从机地址(0xA0写方向)、发送字节地址、发送数据字节、停止条件。这些步骤之间必须有ACK等待。状态机里如果收到NACK,常见处理是直接跳回IDLE并拉高错误标志。写完之后还有一个内部写周期tWR,典型值是5ms,这期间EEPROM不响应任何命令,如果主控急着读回数据,读到的都是0xFF。
localparam SEND_ADDR = 4'd1; // 发送0xA0 localparam WAIT_ACK1 = 4'd2; // 等待从机ACK localparam SEND_MADDR = 4'd3; // 发送AT24C02内部的字节地址 localparam WAIT_ACK2 = 4'd4; // 等待ACK localparam SEND_DATA = 4'd5; // 发送一字节数据 localparam WAIT_ACK3 = 4'd6; // 等待ACK localparam WRITE_WAIT = 4'd7; // 延时5ms等待内部写周期写周期的延时不能用简单的空转计数,因为50MHz下5ms需要计数250000次,计数器位宽至少要18位。有些工程会直接用一个5ms的阻塞延时,但这样整个FPGA的时序逻辑都堵住了。更好的方式是把WRITE_WAIT状态设计成"子状态机加使能信号",在等待期间其他模块还能正常工作。不过对单任务的实验工程来说,阻塞等待也够用,只是你在做更复杂系统时要记得改掉这个习惯。
3.4 读回数据时的字节读与顺序读差异
读EEPROM数据有两种路径:一种是当前地址读,一种是随机地址读。随机地址读需要先写入目标字节地址,然后重新发送起始条件,再发送0xA1读方向。很多初学者只发了地址就卡在读数据上,就是因为少了一个Repeated START。AT24C02支持连续读:主控在第9个时钟如果发送ACK,从机就继续输出下一地址的数据;主控在第9个时钟发送NACK,从机就停止输出。这个区别决定了你能不能一次读回一整片数据。
读字节时,SDA方向必须在最后一次数据位移位完成后切换成输入。注意,是从机在SCL高电平期间把数据送上SDA,主控在SCL高电平期间采样。为了保证主控采到的是稳定的电平,读状态机的采样点同样放在SCL高电平的后半段,而不是上升沿。
reg [7:0] rx_shift; reg r_ack; always @(posedge clk) begin if (state == READ_BIT) begin if (bit_cnt <= 7) begin // SCL高电平时采样SDA if (scl_reg == 1'b1) begin rx_shift <= {rx_shift[6:0], sda_in}; end end else begin // 最后一个bit后决定发ACK还是NACK if (read_num == READ_NUM_MAX) r_ack <= 1'b1; // 已经是最后一个字节,发NACK else r_ack <= 1'b0; // 继续读,发ACK end end end这里r_ack的电平方向要跟SDA方向切换配合,ACK时SDA被拉低,NACK时SDA为高。你可以把ACK和NACK理解成主控对从机说的"继续"和"够了"。
4. Quartus 11.0的完整操作流程:从导入工程到SignalTap抓波形
4.1 编译工程与管脚约束检查
拿到工程文件后,第一步不是直接编译,而是打开Quartus 11.0,File → Open Project,选择.qpf工程文件。工程文件里通常会带上引脚约束(.qsf),但不同板卡的引脚差异很大,你需要打开Assignments → Pin Planner对照自己板子的原理图确认每个信号的位置。
管脚约束里有三个信号必须手工核对:时钟引脚、SDA、SCL。时钟错误会导致PLL锁定不上甚至烧录器都连不上芯片;SDA和SCL引脚如果正好接到FPGA的普通IO而不是专用IO块,I2C时序不会有太大问题,但如果你的板子上有上拉电阻接的是3.3V,那这两个引脚的电平标准要设置成LVCMOS33,不能默认成LVTTL,否则高电平阈值会偏低导致误采样。
| 信号名 | 方向 | 电平标准 | 建议位置 |
|---|---|---|---|
| clk_50m | input | LVCMOS33 | 板载晶振对应引脚 |
| rst_n | input | LVCMOS33 | 按键或拨码开关 |
| scl | output | LVCMOS33 | 直连AT24C02的SCL |
| sda | inout | LVCMOS33 | 直连AT24C02的SDA |
| led_result | output | LVCMOS33 | LED指示灯 |
有些核心板(尤其是自带板载EEPROM的型号)已经把AT24C02固定在板子上,那就不需要额外接线,直接在引脚约束里把I2C信号指到对应的板载引脚即可。如果你用的是裸板加外挂模块,注意SDA和SCL都要接上拉电阻到VCC,我一般选4.7kΩ,兼容性最好。你还要确认reset按键是不是上电默认低电平,如果板上复位是高电平有效,那工程里的nRST逻辑就要反相。
4.2 编译后要重点看哪些报告
编译完成后,看Compilation Report的Fitter Resources Summary,重点关注Total logic elements和Total pins。I2C驱动加读写控制大约占用200到300个logic elements,如果看到编译报告显示占用几千个LE,说明某个地方产生了不必要的组合逻辑膨胀,最典型的原因是把case的default分支写成了锁存器。
时序收敛方面,I2C工作在100kHz,逻辑深度根本构成不了时序瓶颈,只要编译报告里没有critical warning关于clock setup violation,就可以直接下载。还有个容易忽略的检查点:Pin Planner里SDA是不是被设置成了tristate属性。有些工程把SDA直接定义成output,这样在看波形时SDA方向永远输出,从机的应答位永远拉不进来。
下载烧录用Quartus自带的Programmer,选择JTAG模式。EP4CE6E22C8使用JTAG方式烧写的速度很慢,大约几秒到几十秒,耐心等就好。如果发现Programmer找不到设备,多半是电源不稳或者下载器线太长,把JTAG线缩短到20cm以内通常能解决。
4.3 SignalTap II抓I2C波形的正确设置
SignalTap II是验证I2C时序最省事的工具,不需要示波器就能看到内部信号。打开Tools → SignalTap II Logic Analyzer,首先要配置采样时钟。I2C的SCL是100kHz,但采样时钟必须用50MHz主时钟,否则采不到SCL高电平里的毛刺。添加信号时不要只加scl和sda,还要把状态机的state信号加进去,这样才能对照着看当前处于哪个状态。
信号添加后要重新编译一次,因为SignalTap会插入到工程里,程序会额外占用约200个LE和1个M9K块。如果你的板子EP4CE6E22C8本身资源紧张,把触发条件设成读取数据的起始地址值,然后设置触发位置为"center"这样能看到触发前后的波形。下载后运行SignalTap,按板子上的复位键重新触发,等波形窗口捕捉到SDA上的起始下降沿后停止。
从抓到的波形里,你看三个地方就够了:第一是SDA下降沿是否发生在SCL高电平期间;第二是SDA上的数据位是否只在SCL低电平期间变化;第三是第9个时钟后SDA是否被拉低应答。这三个点全对,AC时序就算验证通过了。有个细节经验:SignalTap采样时钟用50MHz时,SCL的高电平宽度在采样图上应该是125个采样点左右,如果看到明显短于这个值,说明SCL翻转逻辑有问题,往分频计数器上找原因。
5. 避坑手册:AT24C02读写最常见的五个隐形门槛
5.1 上电后EEPROM不响应,波形上压根看不到ACK
现象:SignalTap抓到的波形在发送从机地址0xA0之后,SDA一直为高,没有任何低电平应答出现。原因:SDA引脚的输出方向没有在ACK采样前切换成高阻,FPGA内部始终把SDA拉到高电平,从机拉低SDA时产生总线冲突。解决:确认i2c_drive模块里SDA方向控制信号与状态机同步,特别是在发送完最后一个数据位后,下一个时钟周期就必须让SDA变高阻,而不是等ACK采样时再切换。
还有一个隐蔽点:AT24C02的地址是7位设备地址加一位方向位,0xA0的含义是10100000,第八位是0表示写。如果你的代码里拼成了0xA1发给EEPROM写方向,从机自然不理会。这个8位数据的位序错乱问题,在调试时看波形很难发现,我一般会先在发送地址时打一个内部标志位,确认sda_out上出现的第一个bit是1。
5.2 写完数据立刻读回,读出来的全是0xFF
现象:状态机跑完写序列,紧接着进入读序列,读回的数据每个字节都是0xFF。原因:AT24C02的内部写周期tWR没有等待,写入完成后芯片还在把数据从页缓冲搬到EEPROM单元,这个期间芯片不响应外部命令,SDA处于高阻状态,主控读到的就是全1。解决:在写停止条件后插入至少5ms延时再发起读请求。50MHz主频下,5ms延时计数值是250000,注意计数器位宽至少18位。如果你做了页写(一次写16字节),tWR同样需要等待,但单个字节写和页写的等待时间是一样的,都是5ms附近。
经验是可以用软件延时代替硬件延时,但如果你在写操作之后马上去读,读到的0xFF不一定是EEPROM没有完成写周期,也可能是读流程少了Repeated START。区分方法:用SignalTap抓读序列的起始条件,如果波形里只有一个起始条件,那问题大概率不是wiring而是缺少了重新生成起始条件的步骤。随机读必须发送两次起始条件,第一次写入字节地址,第二次才进入读模式。
5.3 SCL波形上升沿极缓,高电平时间明显比低电平短
现象:示波器上看到的SCL波形不是对称方波,高电平部分有个很长的爬升斜坡,低电平部分却陡峭。原因:板上SCL和SDA的上拉电阻阻值过大或没有上拉,加上SCL引脚电容导致充电时间常数太大。EP4CE6E22C8的IO引脚输出低电平很快,但高电平要靠外部上拉电阻去拉。解决:检查SCL和SDA的外部上拉电阻,I2C标准允许在3.3V下用4.7kΩ到10kΩ之间,如果你的板上是100kΩ这种弱上拉,波形就一定会变形。改用2.2kΩ能改善上升沿,但要注意驱动能力不要超过I2C标准(最大3mA灌电流)。
如果板上用的是阻值合适的上拉电阻,但波形依然很缓,那就要查一下引脚配置的slew rate。Quartus引脚设置里有个选项叫Slow/Fast Slew Rate,默认是Slow,对I2C这种低速总线来说Slew Rate影响不大,但如果你是直接用SignalTap抓波形本来就有低通效果,再加上引脚延迟,看起来就会很缓。可以尝试把I2C引脚设置成Fast slew rate然后重新编译,通常高电平实际宽度能改善0.2us左右。
5.4 状态机在SEND_WAIT卡死,计数器溢出不跳转
现象:程序烧进去后LED没有任何反应,SignalTap显示状态机停在某个状态不再前进。原因:延时计数器到达翻转条件后,状态机没有跳转到下一个状态,通常是因为计数器的比较条件写成了等于而不是大于等于。运行过程中如果计数器因为某种原因多走了一个时钟,等于条件的判断就永远不成立。解决:所有计数器比较条件都改成大于等于,并且把状态跳转放在计数器达到上限的同一个时钟周期内执行,避免额外延迟状态。
另外检查一下所有的localparam定义是否有位宽不匹配。count >= 18'd249999这行代码,如果count是14位的话,249999会被截断成4095,延时瞬间结束,写周期还没完成就跳去读了。位宽溢出的问题很难一眼看出来,建议把所有计数器的位宽统一使用$clog2来计算。
5.5 用锁存器临时存数据,结果综合出一堆Latch
现象:在状态机里用了always块加if但没有else,或者用assign语句给同一个变量赋值,Quartus编译报告里出现"Inferred latch for signal xxx"。原因:条件分支没有覆盖所有可能,综合器只能生成锁存器保持值。锁存器在FPGA上非常吃资源,而且时序分析容易出问题。解决:把if补全成if-else,让每个always块都对变量在所有分支下赋值,尤其是case状态机的default状态必须存在。如果你发现write_data的中间变量被综合成锁存器,多半是case分支里漏了赋值,在default里把所有非状态变量都赋一次默认值即可。
还有一个跟锁存器类似但更隐蔽的坑:在状态机里对一个输出信号同时用阻塞赋值和非阻塞赋值。比如data_out在某个状态里用等号赋值,在另一个状态里用<=赋值,Quartus 11.0综合后会报mismatch警告,实际生成的电路可能同时存在既有寄存器又有组合逻辑路径。控制I2C的scl和sda建议一律使用非阻塞赋值,让所有输出在时钟沿统一更新,避免不同状态间产生亚稳态。
6. 进阶扩展:从单字节读写走向页写和连续读
完成了基本读写闭环之后,我建议你立刻做三件事:把单字节写改成页写,把当前地址读改成随机地址读,再加上一个自动校验流程。页写一次能写入最多16字节,但有个约束是这些字节必须落在同一页内,页大小为8字节(AT24C02的标准页写限制)。如果你写入的首地址是0x08,那你可以连续写0x08到0x0F这8个字节;如果地址跨过了页边界,比如从0x0A开始写10个字节,超过页末尾的地址会被自动回卷到该页的开头,覆盖前面写的数据。这个特性在时序上完全兼容标准I2C写序列,你只需要在发送内存地址后直接连续发送多个字节数据,EEPROM会在第9个时钟自动切换到页缓冲模式。
连续读则利用前面提过的ACK/NACK机制,主控在读数据的过程中持续发送ACK,EEPROM就会不断把地址递增输出下一个字节,直到主控发送NACK和停止条件。这个特性意味着你能一次性把256字节全部读回,不用反复启动随机读。页写和连续读配合使用,就是"写一整页,再连续读一页比对"的经典测试方案。
在工程实现上,你可以加一个测试模块:
reg [7:0] test_data [0:15]; // 16字节测试数据 reg [3:0] byte_index; reg [7:0] read_buf [0:15]; reg write_start; reg read_start; reg compare_ok;先执行页写:发送0xA0写地址,再发送目标首地址0x00,然后连续发16个字节,停止。等待5ms后执行连续读:发送0xA0地址,发送0x00字节地址,Repeated START,再发0xA1读地址,连续读16字节,最后NACK停止。对比read_buf和test_data数组,全部一致则点亮成功指示灯。这个流程覆盖了I2C的绝大多数操作组合,也是很多应用场景的实际需求,比如把配置参数存在EEPROM里,上电时读出来生效。
如果你做的系统里EEPROM空间紧张,可以考虑用AT24C02的WP引脚。这个引脚拉高时写保护生效,任何写操作都会被忽略。很多板卡把WP直接接地,如果你是用外挂模块,务必确认WP脚电平状态,否则你会遇到"写操作状态机跑完了,但EEPROM内容始终不变"的诡异问题——从机没有返回NACK,因为写保护是静默忽略,而不像地址错误那样产生NACK。
从工程可维护性的角度,你也可以把i2c_drive模块改成可参数化的形式,用parameter定义主时钟频率和目标SCL频率,自动计算分频值。这样当你从EP4CE6E22C8迁移到其他FPGA器件时,不需要改一行代码。类似的还有把总线地址0xA0做成parameter,方便切换到AT24C04、AT24C08这类不同容量的EEPROM,它们的设备地址会根据A2引脚的电平变化一个bit。
最后说一个我的习惯:读完EEPROM后的数据不要急着比对,先让LED显示启动状态和完成状态,然后让数据在串口上打印出来,用串口助手看一遍原始数据再谈逻辑对不对。在那之后我每次做I2C调试都会强制走一遍这个过程——用SignalTap确认波形时序、用串口看数据、再对代码做走查,几乎能在十分钟之内定位到问题所在。这套工程文件我建议你不要只把它当参考代码,一定要上手改一改分频值、换一换读写地址、把单字节改成页写,改过一轮之后,I2C才算真正拿捏住了。希望帮到你。
本文还有配套的精品资源,点击获取