FPGA之EEPROM实验
一、简介
EEPROM,即电可擦除可编程只读存储器,本次实验所用的是AT24C64型号,其存储容量为64Kbit,内部分为256页,每一页32字节(Byte),共有8192个字节,且其读写操作都以字节为基本单位。而关于如何对AT24C64进行读写操作呢?AT24C64采用两线串行接口的双向数据传输协议–I2C协议来实现读写操作,因此本次实验的重点则是了解I2C协议。(本次实验基于正点原子所设置的实验)
实验任务:
本次实验的任务是先向EEPROM的存储器地址0-255分别写入数据0-255,写完后再读取存储器地址0-255中的数据,若读取的值全部正确则LED灯常亮,反之LED灯闪烁。
关于I2C:
I2C总线由数据线SDA和SCL构成通信线路,即可以发送数据,也可以接收数据。
在进行总体介绍之前,先要解释几个重要的概念:
(1)起始信号和结束信号:顾名思义,这个I2C传输开始和结束的标志,当起始信号出现时,意味着可以开始进行数据的传输,当结束信号出现时,意味着单次数据传输的结束。
起始信号的产生:在SCL为高时,拉低SDA(SCL = 1 && SDA = 0)
结束信号的产生:在SCL为高时,SDA从低变为高(SCL = 1 && SDA = 1)
I2C整体时序图(图源正点原子手册):
(2)器件地址: 每个I2C器件都有一个器件地址,本次实验中器件地址的构成为固定部分和可编程部分。器件地址的存在非常必须,因为有的实验中可能用到多个I2C器件,而如果没有器件地址,就不知道要传输数据到哪个器件中。
(3)字地址: I2C器件内部会有可供读写的寄存器或者存储器,因此我们进行读写时,需要指定存储单元的地址,即字地址。器件地址和字地址是不一样的,器件地址是用于分别数据要传输到哪个器件,字地址是器件中存储单元的地址,即要传输数据到器件的哪个存储单元。
(注意:字地址的判断也必不可少,即判断是1Byte还是2Byte的数据)
那么基本概念介绍完毕,接着介绍I2C具体时序:
在SCL为低时,允许SDA改变数据位,SCL为高时,SDA不能够改变;相当于一个时钟周期传输1bit,经过8个时钟周期传输8bit,而在第8个时钟周期末,主机释放SDA以使从机应答(即主机释放控制权),在第9个时钟周期,从机将SDA拉低以应答;如果在第9个时钟周期,SCL为高时SDA未检测到低电平,则视为非应答,表明此次数据传输失败。在第9个时钟周期末,从机释放SDA使主机继续传输数据,若主句发送停止信号,则此次传输结束。(注意:数据以8bit即一个字节为单位串行发出,其最先发送的是字节的最高位)
发送顺序:(写数据–单次写)
起始信号--------器件地址+写命令------从机应答-------字地址--------从机应答--------8bit数据--------从机应答--------从机发送停止信号
发送顺序:(读数据–随机读)
起始信号--------器件地址+写命令--------从机应答--------字地址--------从机应答--------主机发送起始信号--------器件地址+读命令--------从机应答--------从机发送8bit数据--------主机非应答--------主机发送停止信号
介绍完以上,接下来介绍总体模块分布:
(1)顶层模块
(2)EEPROM读写模块:主要进行总体读写的控制切换,并输出比较结果
(3)I2C驱动模块:I2C的具体时序
(4)LED模块:通过结果控制灯亮或是闪烁
二、代码及分析
顶层模块:eeprom_top
module eeprom_top(
input sys_clk,
input sys_rst_n,
output iic_scl,
inout iic_sda,
output led
);
parameter SLAVE_ADDR = 7'b1010000;
parameter BIT_CTRL = 1'b1;
parameter CLK_FREQ = 26'd50_000_000;
parameter I2C_FREQ = 18'd250_000;
parameter L_TIME = 17'd125_000;
wire dri_clk;
wire i2c_exec;
wire [15:0] i2c_addr;
wire [7:0] i2c_data_w;
wire i2c_done;
wire i2c_ack;
wire i2c_rh_wl;
wire [7:0] i2c_data_r;
wire rw_done;
wire rw_result;
eeprom_rw u_eeprom_rw(
.clk (sys_clk) ,
.rst_n (sys_rst_n) ,
.i2c_rh_wl (i2c_rh_wl) ,
.i2c_exec (i2c_exec) ,
.i2c_addr (i2c_addr) ,
.i2c_data_w (i2c_data_w) ,
.i2c_data_r (i2c_data_r) ,
.i2c_done (i2c_done) ,
.i2c_ack (i2c_ack) ,
.rw_done (rw_done) ,
.rw_result (rw_result)
);
i2c_dri #(
.SLAVE_ADDR (SLAVE_ADDR),
.CLK_FREQ (CLK_FREQ) ,
.I2C_FREQ (I2C_FREQ)
)
u_i2c_dri(
.clk (sys_clk),
.rst_n (sys_rst_n),
.i2c_exec (i2c_exec),
.bit_ctrl (BIT_CTRL),
.i2c_rh_wl (i2c_rh_wl),
.i2c_addr (i2c_addr),
.i2c_data_w (i2c_data_w),
.i2c_data_r (i2c_data_r),
.i2c_done (i2c_done),
.i2c_ack (i2c_ack),
.scl (iic_scl),
.sda (iic_sda),
.dri_clk (dri_clk)
);
led_alarm #(.L_TIME(L_TIME))
u_led_alarm(
.clk (dri_clk), // !!
.rst_n (sys_rst_n),
.rw_done (rw_done),
.rw_result (rw_result),
.led (led)
);
endmodule
EEPROM读写模块:eeprom_rw
module eeprom_rw(
input clk,
input rst_n,
output reg i2c_rh_wl,//读写控制位
output reg i2c_exec,//执行信号,每出现一次就执行一位数据的读写
output reg [15:0] i2c_addr,//地址
output reg [7:0] i2c_data_w,//写数据(我们造的)
input [7:0] i2c_data_r,//(读数据)
input i2c_done,//整个I2C进行了一整次读/写完毕的信号
input i2c_ack,//应答信号
output reg rw_done,//判断读写测试是否完毕
output reg rw_result//判断读写测试是否成功
);
parameter WR_WAIT_TIME = 14'd5000;//每写一位数据都要等待的时长
parameter MAX_BYTE = 16'd256;
reg [1:0] flow_cnt;//计数器,用于状态转换
reg [13:0] wait_cnt;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
flow_cnt <= 2'b0;
i2c_rh_wl <= 1'b0;
i2c_exec <= 1'b0;
i2c_addr <= 16'b0;
i2c_data_w<= 8'b0;
wait_cnt <= 14'b0;
rw_done <= 1'b0;
rw_result <= 1'b0;
end
else begin
i2c_exec <= 1'b0;
rw_done <= 1'b0;
case(flow_cnt)
//2'd0:写数据等待时间+状态切换(读/写)
2'd0 : begin
wait_cnt <= wait_cnt + 1'b1;
if(wait_cnt == WR_WAIT_TIME - 1'b1) begin
wait_cnt <= 14'b0;
if(i2c_addr == MAX_BYTE)begin
i2c_rh_wl <= 1'b1;
i2c_addr <= 16'b0;
flow_cnt <= 2'd2;
end
else begin
flow_cnt <= flow_cnt + 1'b1;
i2c_exec <= 1'b1;
end
end
end
//2'd1:写数据
2'd1 : begin
if(i2c_done == 1'b1)begin //??????????????????????????
flow_cnt <= 2'd0;
i2c_addr <= i2c_addr +