尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

FPGA通过I2C接口读写EEPROM:状态机设计、时序分析与Verilog实现

FPGA通过I2C接口读写EEPROM:状态机设计、时序分析与Verilog实现 简介本资源为基于FPGA实现EEPROMAT24C64读写功能的完整工程实践方案面向数字电路与嵌入式系统方向的初学者及FPGA开发入门者解决非易失性存储器在可编程逻辑平台上的协议时序控制、地址管理与数据交互等核心问题。压缩包共234个文件总计6.23MB涵盖Verilog源码.v、Quartus综合/布局布线数据库.cdb/.hdb/.rpt、编译日志.qmsg/.logdb、引脚约束.qsf、顶层模块信息.ammdb/.bpm及配置说明.txt/.readme等关键类型结构完整、工程可直接编译下载。已有104人学习下载资源包含可运行的e2prom_top顶层模块及配套测试逻辑提供I²C时序仿真验证路径、寄存器映射说明与擦写寿命保障设计参考便于读者深入理解EEPROM底层通信机制并快速迁移至物联网终端、穿戴设备等需小容量掉电保存场景的FPGA应用开发。1. 项目思路与整体架构1.1 为什么大家都在用EEPROM做读写验证EEPROOM在FPGA开发板的出场率非常高基本属于必备外设。我见过很多初学者一开始不理解明明FPGA内部有Block RAM又大又快为什么还要挂一颗I2C接口的EEPROM在外面读写速度还慢得不行这个问题的答案其实就藏在EEPROM的“E”上——Electrically Erasable电可擦除掉电不丢数据。FPGA内部的BRAM一断电数据全没而EEPROM可以长期保存配置参数、校准系数、设备序列号这类“上电之后必须马上能用”的信息。另一个很实际的原因是接口简单。EEPROM用的是I2C总线总共就两根线SCL时钟、SDA数据硬件连接极其省引脚。我之前做过一个多通道采集板FPGA引脚紧张到要抠着用但挂两颗EEPROM总共只占了两个IO口比SPI Flash还省一根线。对FPGA来说I2C虽然是慢速接口但恰恰因为慢非常适合作为入门级通信协议来练手——状态机思路、时序约束、跨时钟域处理这些基本功都能在这个小项目里过一遍。所以这个项目的核心价值不在于“把EEPROM读写跑通”这个结果而在于通过实现I2C主控制器把FPGA里最常用的“状态机设计方法”和“时序分析方法”真正落到代码上。芯片手册里的时序图是死的只有自己用状态机实现一遍才会对建立时间、保持时间、ACK时序窗口这些概念有切身体会。1.2 I2C总线与EEPROM的基础关系I2C总线是Philips现在的NXP搞出来的两线制串行总线标准模式100kbit/s快速模式400kbit/s高速模式能到3.4Mbit/s。EEPROM这类存储器用I2C接口再合适不过因为存储器的读写天然就是“地址数据”的模式跟I2C总线“先发地址、再传数据”的帧结构非常契合。以最常见的AT24C02为例容量2Kbit256字节挂在I2C总线上有一个7位器件地址高4位固定为1010低3位由A2、A1、A0引脚电平决定。所以理论上一条I2C总线上最多可以挂8颗AT24C02通过A2/A1/A0区分。实际项目中我习惯把A2A1A0全部接地器件地址就是0xA0写方向/0xA1读方向省事且不容易出错。顺便说一句I2C总线上可以同时挂多个不同器件EEPROM、温度传感器、RTC时钟芯片都能共存。只要每个器件的7位地址不冲突通信时通过地址就能精准选中目标器件。FPGA做主控制器时总线上所有从机的地址规划是硬件设计阶段就要确定下来的否则后面调试全是坑。1.3 我的架构设计思路我在这颗AT24C02读写控制器的实现上最终确定的是“一个主状态机两个分频器”的结构。整个I2C控制器以状态机为绝对核心划分了IDLE、START、SEND_ADDR、SEND_DATA、STOP等十几个状态每个状态下根据当前操作类型读还是写推进不同的分支。SCL时钟由系统时钟分频得到比如50MHz系统时钟跑100kHz的I2C就需要500分频。分频器同时要生成SCL高电平中点的采样窗口和低电平中点的切换窗口这是保证数据稳定的关键——I2C协议规定数据只能在SCL低电平期间变化高电平期间必须保持稳定。我遇到过很多人写I2C控制器用延时来模拟时序结果SCL生成和SDA切换全挤在同一条时间线上仿真能过上板就莫名其妙出问题。正确的做法是SCL高/低电平的时长要独立可控SDA数据的切换只能在SCL为低时发生采样则只能在SCL为高时进行。这个三者的时间关系捋清楚了I2C基本就通了一半。数据通路方面我不建议把写缓冲和读缓冲做成FIFO这个场景数据量不大直接做寄存器堆即可。发送时用一个字节的移位寄存器每个SCL时钟沿移出一位接收时同样移位第9个时钟周期判断ACK。寄存器级的设计逻辑清晰、时序收敛容易对刚入门FPGA的人来说也更友好。2. I2C时序核心与EEPROM读写协议2.1 SCL/SDA时序基础与起始停止条件先花点篇幅把I2C最底层的时序说透。I2C是半双工同步串行总线SCL是主控制器产生的时钟SDA是双向数据线。SDA的数据变化严格受限于SCL的电平有两条铁律数据只能在SCL低电平期间切换。SCL高电平期间SDA必须保持稳定。这两条铁律是I2C区别于SPI等接口的本质特征。SPI的MOSI、MISO是独立的线主从方向分离所以可以做到全双工。I2C的SDA是同一根线如果数据跳变发生在SCL高电平期间就会被从机误判为起始或停止条件直接导致通信错乱。起始条件START的定义SCL保持高电平期间SDA从高跳变为低。停止条件STOP的定义SCL保持高电平期间SDA从低跳变为高。特别注意“SCL高电平期间”这个前提所以起始和停止条件的产生必然要用到SCL高电平的周期。我在设计中专门留了一个SCL_HIGH状态在SCL为高时先插入半个周期延时然后翻转SDA确保起始/停止条件的建立时间满足芯片手册要求。AT24C02的数据手册上起始条件建立时间tSUSTA典型值是600ns停止条件建立时间tSUSTO典型值也是600nsSCL低电平时间tLOW典型值1.3us高电平时间tHIGH典型值0.6us。以100kHz总线频率计算半个周期是5us完全满足这些时序要求。事实上只要分频后SCL能稳定在100kHz左右所有建立/保持时间都远大于手册最小要求这也是I2C接口很“皮实”的原因之一。2.2 单字节写与页写操作详解AT24C02的写操作分两种字节写Byte Write和页写Page Write。字节写的帧格式是起始条件 - 器件地址写方向- 字地址8位- 待写数据 - 停止条件。每次传输过程中EEPROM会在收到每个字节后拉低SDA应答ACK。发送完数据并收到ACK之后EEPROM还需要一段时间把数据写入内部的非易失存储单元这段时间称为写周期Write Cycle Time典型值5ms。在写周期内EEPROM对任何外部命令都不响应更准确说是不产生ACK应答。所以代码里在写完一个字节后必须等待至少5ms才能发起下一次操作。页写则是把同一页内的多个字节连续写入。AT24C02的页大小是8字节意味着一次页写最多可以写8个字节。例如字地址是0x00可以连续写0x00到0x07但如果从0x06开始写4个字节地址走到0x07之后再继续写地址会回卷到0x00把之前写的0x00和0x01覆盖掉。这个回卷特性是所有EEPROM都有的很多新手在这里踩坑——我见过有人在页写时跨页连续写了16个字节结果前8个和后8个互相覆盖数据全乱了。我给控制器做的写操作是“任意长度写”在状态机里判断起始地址加上剩余字节数是否超过当前页边界。如果超过就拆成两次页写或者干脆逐字节写。对于256字节空间的AT24C02来说逐字节写虽然慢一点每字节5ms但逻辑最简单、最不容易出错。实际项目如果对写入时间不敏感我强烈建议直接逐字节写。2.3 随机读与连续读操作详解读操作比写操作要绕一些因为I2C总线在读取过程中有个“方向切换”的动作。随机读Random Read的帧格式是起始条件 - 器件地址写方向- 字地址 - 重复起始条件 - 器件地址读方向- 读出数据 - 非应答NACK- 停止条件。第一次发器件地址时方向位是0写目的是告诉EEPROM“我要操作的地址是哪个”重新发送起始条件后方向位变成1读EEPROM才会把该地址的数据放到SDA上。这里容易出错的地方在于地址发送完进入读方向之后要等一个SCL周期让EEPROM切换SDA方向如果紧接着就去采样经常采到的是高阻态下的不确定电平。连续读Sequential Read则是在随机读的基础上读出一个字节后主机回ACKEEPROM自动将地址加1继续输出下一个字节。除了最后一个字节回NACK前面每一字节都要回ACK。我在设计中让主状态机在读状态内通过一个计数器控制当前是第几个字节读到最后一个字节时在第9个SCL周期保持SDA为高即不回ACK然后生成停止条件。3. 硬件环境与信号定义3.1 上拉电阻怎么选I2C总线是开漏结构SCL和SDA都有内部的开漏输出晶体管只能主动拉低不能主动拉高。高电平靠外部上拉电阻提供。经典上拉电阻取值是4.7kΩ这个值在100kHz标准模式下足够。如果总线电容较大、挂载器件较多可以改用2.2kΩ或1kΩ代价是功耗稍微增加。在FPGA开发板上用EEPROM时重点检查板卡原理图上是否已经带了上拉电阻。像黑金、正点原子、Digilent这些开发板EEPROM附近的SCL/SDA都会预留4.7kΩ上拉。自己画板的话上拉电阻一定不要省这是I2C通信不稳定的头号原因。我调试时有块转接板就是因为SDA缺上拉读出来的数据每隔一段就出现一个错误的0查了半天才发现是上拉电阻虚焊。3.2 FPGA引脚约束与PCB布线注意点FPGA侧SCL和SDA都设置为普通的双向IO或者OD输出。Xilinx 7系列里SDA需要实例化IOBUF原语IOBUF的控制端T端为高时输出高阻为低时输出数据。SCL则可以直接用OBUF或普通输出引脚跑。SDA必须设置为双向端口inout并且这个双向引脚的方向切换时机必须和状态机严格配合。在发送数据期间方向控制信号拉低引脚输出在释放总线等待ACK期间方向控制信号拉高引脚变为高阻由外部上拉电阻拉到高电平。这里有个细节从“输出数据”切换到“输入采样”的时刻最好安排在SCL低电平时完成避免切换瞬间在SDA上产生毛刺。引脚约束XDC文件里我习惯将SCL约束到支持LVCMOS33电平的普通IOSDA同样。由于I2C频率低SI分析基本没有压力但最好加上PULLUP属性双保险。3.3 开发板与EEPROM型号选择开发板上最常见的EEPROM是AT24C02、AT24C04、AT24C64、M24C02这些引脚兼容主要区别是容量和多出来的地址线。AT24C02的A2/A1/A0引脚都是地址脚AT24C04则只用A2/A1A0变成最高位地址线AT24C08以上地址脚进一步减少。所以代码里I2C控制器的器件地址寄存器应该做成可配置的方便切换不同型号。如果你手头是STM32、树莓派这些平台EEPROM的读写代码就完全不是这个思路了因为MCU自带I2C外设直接调库函数就行。FPGA上没有现成的I2C控制器必须从底层时序用Verilog手搓这也就是这个项目最有学习价值的地方。我建议从AT24C02开始如果做项目需要更大容量再上AT24C64。容量越大内部页大小也越大AT24C64的页大小是32字节写满整片反而更快。4. 关键代码实现与仿真4.1 状态机核心代码解析我采用三段式状态机实现I2C主控制器下面给出最关键的核心代码解释每个状态的作用和跳转条件。module i2c_eeprom_ctrl #( parameter CLK_FREQ 50_000_000, parameter SCL_FREQ 100_000 )( input wire clk, input wire rst_n, // 用户接口 input wire start, input wire wr_en, // 1写 0读 input wire [7:0] addr, input wire [7:0] wdata, output wire [7:0] rdata, output wire done, output wire ack_err, // I2C接口 inout wire sda, output wire scl ); localparam IDLE 5d0; localparam START1 5d1; localparam START2 5d2; localparam SEND_ADDR 5d3; localparam SEND_SUBA 5d4; localparam RESTART 5d5; localparam SEND_WD 5d6; localparam STOP1 5d7; localparam STOP2 5d8; localparam READ_DAT 5d9; localparam REG_WR 5d10; localparam ACK_CHK 5d11;状态机从IDLE收到start信号后先进入START1产生起始条件随后在SCL低电平期间逐位发送器件地址和字地址。每次发送完8位数据后进入ACK_CHK状态释放SDA总线并采样ACK。写数据的路径走SEND_SUBA之后的SEND_WD读数据的路径则在RESTART状态重新发送起始条件后切换方向。实际代码中我最看重的是“字节内位计数器”和“状态机状态计数器”的协同。数据位计数器bit_cnt在0到7之间循环每来一个SCL低电平沿移出一位第8位之后的下一个周期就是ACK窗口。之前写代码时试图把ACK检测逻辑直接放在SEND_ADDR状态下用if/else判断结果分支条件越来越多状态机变得不可维护。后来把ACK检测独立成状态每个发送类状态结束都跳转到ACK_CHK结构立刻清爽了。4.2 关键代码ACK检测与SDA方向控制// SDA方向控制 reg sda_dir; wire sda_in; assign sda sda_dir ? 1bz : sda_out; // ACK检测 always (posedge clk or negedge rst_n) begin if (!rst_n) begin ack_flag 1b1; end else if (state ACK_CHK) begin ack_flag sda_in; // 低电平为ACK高电平为NACK end end在ACK_CHK状态必须保证SDA方向已经切为输入高阻让从机能够拉低SDA应答。方向切换的时机需要在进入ACK_CHK状态之前的一个SCL低电平周期完成否则从机的ACK信号会被主机的输出驱动掩盖掉。这是I2C调试中非常经典的一个问题方向切换慢了半拍导致主机永远检测不到ACK错误地认为从机不响应。sda_out寄存器的设计也有讲究我采用一个8位移位寄存器作为发送缓冲每次SCL低电平期间移位一次高位在前。读方向的数据则从sda_in采样后写入移位寄存器读完一个字节再锁存到rdata输出寄存器。这样发送和接收共用一套移位逻辑代码量可以省不少。4.3 Testbench仿真过程仿真时除了例化EEPROM控制器的DUT还需要一个I2C从机模型来模拟EEPROM行为。最省事的办法是直接使用AT24C02的Verilog仿真模型网上有现成的或者在testbench里写一个简化的I2C从机行为模型。下面给出testbench的核心骨架module tb_i2c_eeprom; reg clk; reg rst_n; reg start; reg wr_en; reg [7:0] addr; reg [7:0] wdata; wire [7:0] rdata; wire done; wire ack_err; wire sda; wire scl; // 待测控制器例化 i2c_eeprom_ctrl #( .CLK_FREQ(50_000_000), .SCL_FREQ(100_000) ) dut ( .clk(clk), .rst_n(rst_n), .start(start), .wr_en(wr_en), .addr(addr), .wdata(wdata), .rdata(rdata), .done(done), .ack_err(ack_err), .sda(sda), .scl(scl) ); // EEPROM仿真模型例化 AT24C02 u_eeprom ( .A0(1b0), .A1(1b0), .A2(1b0), .WP(1b0), .SDA(sda), .SCL(scl) );仿真的时序检查要点有三个起始条件是否满足SDA先于SCL跳变的要求、写入数据在SCL高电平期间是否稳定、停止条件后是否有足够的总线空闲时间。Vivado的仿真器在这些方面做得不错Modelsim则需要自己加一些断言来检查。我一般会在testbench里加两个简单断言一是SCL高电平期间SDA不能跳变二是start信号拉高后必须在若干个时钟周期内观察到SDA上的起始条件。这两个断言能省下大量手工看波形的精力。仿真波形上万用鼠标拖动波形看信号变化太费眼了。我通常直接把sda、scl、state、bit_cnt这几个关键信号拖到同一个窗口按字节为单位放大波形检查。如果发现某个字节的ACK漏了第一时间找SCL的上升沿是否和SDA的拉低重叠。4.4 上板实测要点仿真通过后上板实测的流程是先通过串口或者按键触发一次写操作把一组固定数据写入EEPROM然后断电重启再触发读操作把读回来的数据和预期值比对。我没有在板子上跑操作系统的条件就直接用ILA集成逻辑分析仪抓内部信号抓的状态向量包括state、bit_cnt、sda_out、sda_in、done等全部声明为mark_debug。上板后最容易发现的两个问题第一个是电平不匹配。EEPROM供电3.3VFPGA的IO bank如果配成1.8VSDA/SCL电平就对不上通信完全失败。查一下原理图确认EEPROM的VCC和FPGA的IO bank电压一致。第二个是复位时序。FPGA上电瞬间EEPROM可能还没准备好此时如果控制器立刻发起操作会收到NACK。我在控制器里加了一个100ms的上电延时确保EEPROM稳定后再允许start信号触发操作。5. 常见问题排查与调试技巧5.1 问题速查表问题现象可能原因排查方向写操作后回读全为0xFF地址发送错误或器件应答异常用ILA抓器件地址发送帧确认方向位是否正确SDA始终为高器件无ACK上拉电阻虚焊/缺失示波器测SCL/SDA静态电平应为高电平ACK时有时无写周期未等待或时钟过快写操作后至少延时5ms再发下一次命令连续读数据错位最后一个字节没有回NACK检查读状态机的最后一个字节ACK控制逻辑页写数据被覆盖跨越页边界写数据将写地址按页边界拆分上板运行偶尔卡死状态机在无响应时死等在状态机中加入超时跳转NACK后回到IDLEI2C信号电平不对FPGA IO bank电压与EEPROM不匹配核对原理图电压域5.2 关于ACK检测不稳定遇到最多的还是ACK检测不稳定的问题。I2C从机拉低SDA响应有一个最大延迟时间标准模式是0.9us也就是在SCL低电平结束前必须完成拉低。如果主机的ACK采样窗口太靠近SCL上升沿就有可能在SDA尚未完全拉低时采到高电平。解决方案是让ACK采样点落在SCL高电平的中段而不是边沿处。我在设计中将ACK_CHK状态设置为在SCL高电平的中点采样以100kHz的SCL频率计算高电平持续5us中点采样点距离上升沿2.5us远大于从机的应答延迟时间鲁棒性足够。5.3 写周期等待的细节与优化前面提到EEPROM写入需要5ms左右的内部写周期。这个等待不能省但可以用“启动下一次操作前查询”的方式做优化而不是硬等。AT24C02支持一种“ACK查询”机制写周期内器件不响应一旦写周期结束再次发送器件地址时就会收到ACK。利用这个特性代码可以在发出写命令后反复发送器件地址写方向直到收到ACK表示写周期结束。不过我实测下来AT24C02的写周期大概在3-5ms之间查询机制虽然省时但增加了一部分状态机复杂度。如果系统对写入时间不敏感比如配置参数只在开机时写一次直接延时5ms更稳妥。如果频繁写入比如每秒记录一次日志用ACK查询机制可以显著提升效率。5.4 在FPGA里调试I2C的独门技巧最后分享一个个人经验调试I2C时序时单纯靠FPGA内部的ILA抓信号往往因为触发条件设置不对而抓不到故障现场。我的做法是先在testbench里把时序跑熟对每个状态的耗时心中有数然后把ILA的触发条件设置为“检测到NACK”这样一旦上板出现总线异常ILA能立刻抓下现场数据。另一个技巧是使用逻辑分析仪。FPGA的IO引脚被I2C占用的同时板卡上通常会引出测试点把逻辑分析仪的探头夹在SCL和SDA上直接在总线上抓包观察时序波形比FPGA内部ILA更直观。这也是为什么我在硬件设计阶段就会预留I2C测试点——调试效率和便利性会大幅提升。对EEPROM这种成熟器件FPGA端的控制器实现完全可以做成通用IP换不同型号的EEPROM时只需修改器件地址和页大小参数。这个项目做完之后整套I2C主控制器代码可以被复用到温度传感器、RTC、ADC配置寄存器读写等场景性价比非常高。你在自己实现的过程中遇到问题欢迎回来交流具体波形和状态机状态很多时候就是一两根信号的时序关系没对齐。本文还有配套的精品资源点击获取
返回列表