本文基于Vivado2020平台,利用正点原子领航者7020开发板控制EEPROM读写。
整篇文章会首先对AT24C64技术文档进行分析,其次分析AT24C64在FPGA上的引脚分配情况,最后逐步分析正点原子给出的Verilog代码,对E2PROM的读写过程进行仿真分析。
EEPROM(Electrically Erasable Promgrammable Read Only Memory,E2PROM)即电可擦除可编程只读存储器。领航者开发板用到的E2PROM为AT24C64,其存储容量为64Kb,内部分为256页,每页32字节,共有8192个字节,读写操作都是以字节为基本单位,共有65536个bit。
AT24C64的引脚图如图1所示,其引脚的功能如表1所示。
引脚名称 | 引脚功能 |
---|---|
A0-A2 | 地址输入 |
SDA | 串行数据 |
SCL | 串行时钟输入 |
WP | 写保护 |
引脚的具体分析如下。
AT24C64家用温度范围为0℃ - 70℃,工作 V c c V_{cc} Vcc 为1.8V - 5.5V。低时钟脉冲最低时间在5V下为1.2 μ s \mu s μs,高时钟脉冲最低时间在5V下为0.6 μ s \mu s μs。写入周期,在5V下最大不超过10 m s ms ms。
想要正常使用AT24C64,我们需要知道其是如何开始和结束操作的,两者之间的周期又是如何写入和读出数据的。
EEPROM需要在起始操作后输入一个字节的器件地址,来使能后续的读写操作。该器件地址的高四位是固定的,低四位中的三位由A2-A0决定,最后一位为读/写操作位(0/1)。由于低4位中的三位由引脚决定,因此在一个总线上可以同时存在8个从机设备。器件地址的电平情况如图4所示。
由于AT24C64存储器的内存为64Kb,因此当器件地址被从机接收,应答被主机接收完成后。需要输入两个字节的字地址,用来确定写入的位置。每一个字节的字地址被写入后,都要等待从机的应答位,当两个应答位都是低电平代表有效写入。之后,在写入一个字节的数据,从机在收到数据后会输出低电平应答。当所以信息都写入完毕后,从机的微处理器会阻止后续内容的写入。在等待一个从机内部的写入周期后,才能正常写入。这一写入周期最大不超过10 m s ms ms。
对于页写入来说,同字节写入相同,只不过在写入一个字节的数据后不发送停止信号,而是继续发送三个字节的数据信息。如果发送的字节数据超过四个字节,则会在当前页覆盖写入,即第五个字节的写入地址重新定位到第一个字节写入的位置。
读操作分为三种不同的方式,分别是:
在Vivado中构建仿真程序,其中各模块端口及信号连接如图8所示。
基于上述原理图,我们将逐一分析各个模块的主要内容,以及模块参数的意义。
首先,我们先看顶层模块。
module e2prom_top(
input sys_clk,
input sys_rst_n,
output iic_scl, // EEPROM 时钟线SCL
inout iic_sda, // EEPROM 数据线SDA
output led
);
// parameter define
parameter SLAVE_ADDR = 7'b1010_000; // EEPROM器件地址
parameter BIT_CTRL = 1'b1; // 字地址位控制参数
parameter CLK_FREQ = 26'd50_000_000; // I2C_DRI模块的驱动时钟频率
parameter I2C_FREQ = 18'd250_000; // I2C的SCL时钟频率
parameter L_TIME = 17'd125_000; // LED闪烁时间参数
parameter MAX_BYTE = 16'd256; // 读写测试的字节个数
// wire define
wire dri_clk; // I2C操作时钟 T=1us 在i2c_dri中分频
wire i2c_exec; // I2C触发控制
wire [15:0] i2c_addr; // I2C操作地址; AT24C64: 64kB=8KB=8192B=2^13B
wire [ 7:0] i2c_data_w; // I2C写入数据
wire i2c_done; // I2C操作结束标志
wire i2c_ack; // I2C应答标志
wire i2c_rh_wl; // I2C读写控制
wire [ 7:0] i2c_data_r; // I2C读出数据
wire rw_done; // E2PROM读写测试完成
wire rw_result; // E2PROM读写测试结果 0:失败 1:成功
// main code
// EEPROM读写测试模块
e2prom_rw #(
.MAX_BYTE (MAX_BYTE)
) u_e2prom_rw (
.dri_clk (dri_clk), // 时钟信号
.rst_n (sys_rst_n), // 复位信号
// I2C interface
.i2c_exec (i2c_exec), // IC2触发执行信号
.i2c_rh_wl (i2c_rh_wl), // IC2读写控制信号
.i2c_addr (i2c_addr), // IC2器件内地址
.i2c_data_w (i2c_data_w), // IC2写数据
.i2c_data_r (i2c_data_r), // IC2读数据
.i2c_done (i2c_done), // IC2一次操作完成
.i2c_ack (i2c_ack), // IC2应答标志
// user interface
.rw_done (rw_done), // E2PROM读写测试完成
.rw_result (rw_result) // E2PROM读写测试结果 0:失败 1:成功
);
// I2C驱动模块
i2c_dri #(
.SLAVE_ADDR (SLAVE_ADDR), // EEPROM芯片从机地址
.CLK_FREQ (CLK_FREQ), // 模块输入时钟频率
.I2C_FREQ (I2C_FREQ) // IIC_SCL的时钟频率
) u_i2c_dri(
.clk (sys_clk),
.rst_n (sys_rst_n),
// I2C interface
.i2c_exec (i2c_exec), // I2C执行触发信号
.bit_ctrl (BIT_CTRL), // 器件地址位(16b\8b)选择
.i2c_rh_wl (i2c_rh_wl), // I2C读写控制
.i2c_addr (i2c_addr), // I2C内地址
.i2c_data_w (i2c_data_w), // I2C写数据
.i2c_data_r (i2c_data_r), // I2C读数据
.i2c_done (i2c_done), // I2C一次操作完成标志
.i2c_ack (i2c_ack), // I2C应答标志
.scl (iic_scl), // I2C的SCL时钟信号
.sda (iic_sda), // I2C的SDA信号
// user interface
.dri_clk (dri_clk) // I2C操作时钟
);
// led指示模块
rw_result_led #(
.L_TIME(L_TIME ) // 控制led闪烁时间
) u_rw_result_led(
.clk (dri_clk ),
.rst_n (sys_rst_n ),
.rw_done (rw_done ),
.rw_result (rw_result ),
.led (led )
);
endmodule
在顶层模块中,主要是对其他模块的引用,以及模块与模块之间参数的连接。在这一模块中所有的参数基本都是内部连接线(wire),该定义指的是数值的传递是实时的,在一个模块中内部连接线上数值的变换会立即传递到另一个模块中,不会存在延迟,而寄存器(reg)变量根据条件可能会产生延迟。
在顶层模块中,我们需要着重关注的参数是 i 2 c _ e x e c i2c\_exec i2c_exec 、 i 2 c _ d o n e i2c\_done i2c_done 和 d r i _ c l k dri\_clk dri_clk,第一个参数是读写操作开始的标志,第二个参数是读写操作中传输一字节信息完成的标志,第三个操作是读写操作的执行时钟,这同FPGA自身的时钟是不同的。这三个参数,我们在后续会重新强调。
在读写操作开始前,我们首先看一下驱动模块 i 2 c _ d r i i2c\_dri i2c_dri。
module i2c_dri #(
parameter SLAVE_ADDR = 7'b1010_000, // 从机地址
parameter CLK_FREQ = 26'd50_000_000, // 输入时钟频率
parameter I2C_FREQ = 18'd250_000 // 时钟频率
)
(
input clk,
input rst_n,
// I2C interface
input i2c_exec, // I2C触发执行信号
input bit_ctrl, // 字地址位控制(16b/8b)
input i2c_rh_wl, // I2C读写控制信号
input [15:0] i2c_addr, // I2C器件内地址
input [ 7:0] i2c_data_w, // I2C写数据
output reg [ 7:0] i2c_data_r, // I2C读数据
output reg i2c_done, // I2C一次操作完成
output reg i2c_ack, // I2C应答标志 0:应答;1:未应答
output reg scl, // I2C的SCL时钟信号,单方面输出I2C控制时钟
inout sda, // I2C的SDA信号
// user interface
output reg dri_clk // 驱动I2C的驱动时钟
);
//localparam define
localparam st_idle = 8'b0000_0001; // 空闲状态
localparam st_sladdr = 8'b0000_0010; // 发送器件地址(slave address)
localparam st_addr16 = 8'b0000_0100; // 发送16位字地址
localparam st_addr8 = 8'b0000_1000; // 发送8位字地址
localparam st_data_wr = 8'b0001_0000; // 写数据(8 bit)
localparam st_addr_rd = 8'b0010_0000; // 发送器件地址读
localparam st_data_rd = 8'b0100_0000; // 读数据(8 bit)
localparam st_stop = 8'b1000_0000; // 结束I2C操作
//reg define
reg sda_dir ; // I2C数据(SDA)方向控制
reg sda_out ; // SDA输出信号
reg st_done ; // 状态结束
reg wr_flag ; // 写标志
reg [ 6:0] cnt ; // 计数
reg [ 7:0] cur_state ; // 状态机当前状态
reg [ 7:0] next_state; // 状态机下一状态
reg [15:0] addr_t ; // 地址
reg [ 7:0] data_r ; // 读取的数据
reg [ 7:0] data_wr_t ; // I2C需写的数据的临时寄存
reg [ 9:0] clk_cnt ; // 分频时钟计数
//wire define
wire sda_in ; // SDA输入信号
wire [8:0] clk_divide ; // 模块驱动时钟的分频系数
//*****************************************************
//** main code
//*****************************************************
// SDA控制
assign sda = sda_dir ? sda_out : 1'bz; // SDA输出高阻
assign sda_in = sda; // SDA数据输入
assign clk_divide = (CLK_FREQ/I2C_FREQ) >> 2'd2 ; // 模块驱动时钟的分频系数 200=1100_1000; >>2=0_0011_0010=50
// clk 50Mhz每计数200可以得到250khz(4us)的信号
// 想要得到1Mhz(1us)的信号频率需要计数50
// 生成I2C的SCL的四倍频率的驱动时钟用于驱动I2C的操作
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
dri_clk <= 1'b0;
clk_cnt <= 10'd0;
end
else if(clk_cnt == (clk_divide[8:1] - 9'd1)) begin
clk_cnt <= 10'd0;
dri_clk <= ~dri_clk;
end
else
clk_cnt <= clk_cnt + 10'b1;
end
// (三段式状态机)同步时序描述状态转移
// 1us进入一次
always @(posedge dri_clk or negedge rst_n) begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end
// 组合逻辑判断状态转移条件
// 敏感变量有cur_state、i2c_exec、st_done
// 包括等号右边的量;if()case()内的量
always @(*) begin
next_state = st_idle; // 避免干扰
case(cur_state)
st_idle: begin // 空闲状态
if(i2c_exec) begin // 启动工作标志位
next_state = st_sladdr; // 等待器件地址写入
end
else
next_state = st_idle; // 持续空闲
end
st_sladdr: begin
if(st_done) begin // 等待器件地址写入完成
if(bit_ctrl) // 判断是16位还是8位字地址
next_state = st_addr16;
else
next_state = st_addr8 ;
end
else
next_state = st_sladdr;
end
st_addr16: begin // 写16位字地址
if(st_done) begin
next_state = st_addr8;
end
else begin
next_state = st_addr16;
end
end
st_addr8: begin // 8位字地址
if(st_done) begin
if(wr_flag==1'b0) // 读写判断
next_state = st_data_wr;
else
next_state = st_addr_rd;
end
else begin
next_state = st_addr8;
end
end
st_data_wr: begin // 写数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_wr;
end
st_addr_rd: begin // 写地址以进行读数据
if(st_done) begin
next_state = st_data_rd;
end
else begin
next_state = st_addr_rd;
end
end
st_data_rd: begin // 读取数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_rd;
end
st_stop: begin // 结束I2C操作
if(st_done)
next_state = st_idle;
else
next_state = st_stop ;
end
default: next_state = st_idle; // 零状态改为空闲状态
endcase
end
// 时序电路描述状态输出
// 1us进入一次
always @(posedge dri_clk or negedge rst_n) begin
//复位初始化
if(!rst_n) begin
scl <= 1'b1; // SCL初始为高电平
sda_out <= 1'b1; // SDA初始为高电平
sda_dir <= 1'b1; // 表示是否是主机控制,0是从机
i2c_done <= 1'b0;
i2c_ack <= 1'b0;
cnt <= 7'b0;
st_done <= 1'b0;
data_r <= 8'b0;
i2c_data_r<= 8'b0;
wr_flag <= 1'b0;
addr_t <= 16'b0;
data_wr_t <= 8'b0;
end
else begin
st_done <= 1'b0 ;
cnt <= cnt +7'b1 ;
case(cur_state)
st_idle: begin // 空闲状态
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_done<= 1'b0;
cnt <= 7'b0;
// i2c_exec 置1后启动读或写操作
// 该操作在器件判断地址的周期后
if(i2c_exec) begin
wr_flag <= i2c_rh_wl ; // 读写状态
addr_t <= i2c_addr ; // 器件内地址
data_wr_t <= i2c_data_w; // 写数据
i2c_ack <= 1'b0; // 应答标志
end
end
st_sladdr: begin // 写地址(器件地址和字地址)
case(cnt)
7'd1 : sda_out <= 1'b0; // 开始I2C,发送起始信号
7'd3 : scl <= 1'b0; // SCL拉低,SDA改变电平
7'd4 : sda_out <= SLAVE_ADDR[6]; // 传送器件地址
7'd5 : scl <= 1'b1; // SCL拉高,SDA稳定不变
7'd7 : scl <= 1'b0; // SCL维持两个周期后拉低
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0]; // 发送完器件地址
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b0; // 0:写
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0; // 高阻态,SDA给从机控制
sda_out <= 1'b1; // 未应答情况下为高电平
end
7'd37: scl <= 1'b1;
7'd38: begin // 从机应答在SCL为高电平
st_done <= 1'b1; // 操作完成
if(sda_in == 1'b1) // 高电平表示未应答
i2c_ack <= 1'b1; // 拉高应答标志位
end
7'd39: begin
scl <= 1'b0; // 应答后拉低等待后续信号
cnt <= 7'b0; // 计数值归零
end
default : ;
endcase
end
st_addr16: begin // 写入高八位地址
case(cnt)
7'd0 : begin
sda_dir <= 1'b1 ; // 拉高后重回主机控制
sda_out <= addr_t[15]; // 传送字地址
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[14];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[13];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[12];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[11];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[10];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[9];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[8];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0; // 从机控制
sda_out <= 1'b1; // 未应答情况下为高电平
end
7'd33: scl <= 1'b1;
7'd34: begin // 从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) // 高电平表示未应答
i2c_ack <= 1'b1; // 拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_addr8: begin
case(cnt)
7'd0: begin
sda_dir <= 1'b1 ; // 主机控制
sda_out <= addr_t[7]; // 字地址
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin // 从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) // 高电平表示未应答
i2c_ack <= 1'b1; // 拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_data_wr: begin // 写数据(8 bit)
case(cnt)
7'd0: begin
sda_dir <= 1'b1;
sda_out <= data_wr_t[7]; // I2C写8位数据
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= data_wr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= data_wr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= data_wr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= data_wr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= data_wr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= data_wr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= data_wr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin // 从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) // 高电平表示未应答
i2c_ack <= 1'b1; // 拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_addr_rd: begin // 写地址以进行读数据
case(cnt)
7'd0 : begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd1 : scl <= 1'b1;
7'd2 : sda_out <= 1'b0; // 重新开始
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= SLAVE_ADDR[6]; // 传送器件地址
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b1; // 1:读
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0; // 从机控制
sda_out <= 1'b1; // 默认未应答
end
7'd37: scl <= 1'b1;
7'd38: begin // 从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) // 高电平表示未应答
i2c_ack <= 1'b1; // 拉高应答标志位
end
7'd39: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_data_rd: begin // 读取数据(8 bit)
case(cnt)
7'd0: sda_dir <= 1'b0;
7'd1: begin
data_r[7] <= sda_in;
scl <= 1'b1;
end
7'd3: scl <= 1'b0;
7'd5: begin
data_r[6] <= sda_in ;
scl <= 1'b1 ;
end
7'd7: scl <= 1'b0;
7'd9: begin
data_r[5] <= sda_in;
scl <= 1'b1 ;
end
7'd11: scl <= 1'b0;
7'd13: begin
data_r[4] <= sda_in;
scl <= 1'b1 ;
end
7'd15: scl <= 1'b0;
7'd17: begin
data_r[3] <= sda_in;
scl <= 1'b1 ;
end
7'd19: scl <= 1'b0;
7'd21: begin
data_r[2] <= sda_in;
scl <= 1'b1 ;
end
7'd23: scl <= 1'b0;
7'd25: begin
data_r[1] <= sda_in;
scl <= 1'b1 ;
end
7'd27: scl <= 1'b0;
7'd29: begin
data_r[0] <= sda_in;
scl <= 1'b1 ;
end
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: st_done <= 1'b1; // 非应答
7'd35: begin
scl <= 1'b0;
cnt <= 7'b0;
i2c_data_r <= data_r;
end
default : ;
endcase
end
st_stop: begin // 结束I2C操作
case(cnt)
7'd0: begin
sda_dir <= 1'b1; // 结束I2C,主机控制
sda_out <= 1'b0; // 结束前拉低
end
7'd1 : scl <= 1'b1;
7'd3 : sda_out <= 1'b1; // 结束拉高
7'd15: st_done <= 1'b1;
7'd16: begin
cnt <= 7'b0;
i2c_done <= 1'b1; // 向上层模块传递I2C结束信号
end
default : ;
endcase
end
endcase
end
end
endmodule
驱动模块的作用有:
在读写测试模块中,主要进行的操作是控制写入地址及写入数据,并延迟5 m s ms ms,等待写入操作的完成。
module e2prom_rw(
input dri_clk, // I2C操作时钟信号
input rst_n, // 复位信号
// I2C interface
output reg i2c_rh_wl, // I2C读写控制信号
output reg i2c_exec, // I2C触发执行信号
output reg [15:0] i2c_addr, // I2C内地址
output reg [ 7:0] i2c_data_w, // I2C写数据
input [ 7:0] i2c_data_r, // I2C读数据
input i2c_done, // I2C一次操作完成
input i2c_ack, // I2C应答标志
// user interface
output reg rw_done, // E2PROM读写测试完成
output reg rw_result // E2PROM读写测试结果 0:失败 1:成功
);
// parameter define
// EEPROM写数据需要间隔时间;读数据不需要
parameter WR_WAIT_TIME = 14'd5000; // 写入间隔时间 5ms <= 10ms
parameter MAX_BYTE = 16'd256; // 读写测试的字节个数
// reg define
reg [1:0] flow_cnt; // 状态流控制
reg [13:0] wait_cnt; // 延时计数器
//*****************************************************
//** main code
//*****************************************************
// EEPROM读写测试,先写后读,并比较结果是否一致
// 1us进入一次
always @ (posedge dri_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;
// $display("%d", flow_cnt);
case(flow_cnt)
2'd0: begin
wait_cnt <= wait_cnt + 14'b1; // 延时计数
// 每5ms进入一次,改变写入地址和写入数据
// 只对写数据做时间要求,对读数据不做要求
if(wait_cnt == (WR_WAIT_TIME - 14'b1)) begin
wait_cnt <= 14'b0;
// EEPROM写入达到最大位
if(i2c_addr == MAX_BYTE) begin
i2c_addr <= 16'b0; // 读地址索引归零
i2c_rh_wl <= 1'b1; // 开始读
flow_cnt <= 2'd2; // 读模式切换
end
// 写操作
else begin
flow_cnt <= flow_cnt + 2'b1; // 写状态切换
i2c_exec <= 1'b1; // 拉高点平
end
end
end
2'd1: begin
// 单次写入完成
if(i2c_done == 1'b1) begin
flow_cnt <= 2'd0;
i2c_addr <= i2c_addr + 16'b1; // 地址增加
i2c_data_w <= i2c_data_w + 8'b1; // 写入数据增加
end
end
2'd2: begin
flow_cnt <= flow_cnt + 2'b1;
i2c_exec <= 1'b1;
end
2'd3: begin
// 单次读出完成
if(i2c_done == 1'b1) begin
// 读出值错误或者I2C未应答
// 地址和读出数据需要相同是因为写入的时候是相同的
if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
rw_done <= 1'b1;
rw_result <= 1'b0;
end
// 读出为最大地址
else if(i2c_addr == (MAX_BYTE - 16'b1)) begin
rw_done <= 1'b1;
rw_result <= 1'b1;
end
// 读出为正常地址,继续读出后续地址
else begin
flow_cnt <= 2'd2;
i2c_addr <= i2c_addr + 16'b1;
end
end
end
default:;
endcase
end
end
endmodule
LED模块主要用于判断读写操作能否正确进行。
module rw_result_led (
input clk , // 时钟信号
input rst_n , // 复位信号
input rw_done , // 错误标志
input rw_result , // E2PROM读写测试完成
output reg led // E2PROM读写测试结果 0:失败 1:成功
);
// parameter define
parameter L_TIME = 17'd125_000;
//reg define
reg rw_done_flag; // 读写测试完成标志
reg [16:0] led_cnt ; // led计数
//*****************************************************
//** main code
//*****************************************************
// 读写测试完成标志
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rw_done_flag <= 1'b0;
else if(rw_done)
rw_done_flag <= 1'b1;
end
// 错误标志为1时PL_LED0闪烁,否则PL_LED0常亮
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
led_cnt <= 17'd0;
led <= 1'b0;
end
else begin
if(rw_done_flag) begin
if(rw_result) // 读写测试正确
led <= 1'b1; // led灯常亮
else begin // 读写测试错误
led_cnt <= led_cnt + 17'd1;
if(led_cnt == (L_TIME - 17'b1)) begin
led_cnt <= 17'd0;
led <= ~led; // led灯闪烁
end
else
led <= led;
end
end
else
led <= 1'b0; // 读写测试完成之前,led灯熄灭
end
end
endmodule
我们以写操作为例进行分析,读操作请读者自行分析。
50ms时i2c_exec
置1,开始写操作。后续操作中e2prom_rw.v都在2’d1状态中循环等待。1us后i2c_exec
在e2prom_rw.v中置0,同时读写状态、内地址、写数据、应答标志被寄存。
1us后i2c_dri.v中的cur_state
状态被切换为st_sladdr,在过1us后sda_out
信号拉低,向从机发送起始信号,此时SCL的电平为高电平,从机接收到下降沿后判断SCL状态,为高电平则认为是有效的起始信号,并将State的值+1。
之后SCL拉低,使得主机能够改变SDA的状态,并将地址输入,最高位地址为高电平产生上升沿。从机在检验到上升沿后判断SCL的状态,发现是低电平证明主机在向SDA写数据,在判断State的状态,进入read_in任务中,用于后续将主机写在SDA上的数据读入。
主机将数据写入完毕后,SCL的电平拉高,从机在检验到SCL的上升沿后将SDA上的数据读入。依次读入8个数据后,SCL保持在高电平,SDA保持在需要写入的最后一位电平状态。
之后主机将SCL电平拉低,并在1us后释放SDA的占用。从机在检验到SCL的下降沿时,经过1.25us的延时抢占SDA的控制权,并输出有效的从机应答0。
注意:在主机释放SCL时,由于SCL在正常情况下是上拉电阻所以出现0.25us的高电平。
在从机发送完有效应答后,在主机释放SDA的控制权的1us后,主机将SCL置1,要求SDA保持稳定状态,此时SDA的控制权还在从机上,从机发送在SDA上的应答低电平稳定。在经过1us后,主机将st_done
即操作完成标志位置1,并判断SDA上的信号是否为应答信号。在经过1us,主机将SCL的电平拉低,此时从机检测到SCL的下降沿,经过1us后释放SDA,此时SDA不受两者控制,即图中对应位置,同时由于上一个1us中st_done
的置位所以cur_state
的状态转化为写入高8位字地址。也就是说在下一个周期到来时,已经进入了其他的case模块中,之间没有间隔时间。
之后是高八位和低八位字地址的写入,在此不在赘述。
需要注意的是在发送低八位字地址时,由于最后一位是1,因此在第八个上升沿到来之前,SDA就被主机置位为1,在上升沿到来时被传入从机。2us后也就是在该上升沿之后的第一个下降沿,主机在该下降沿的1us后释放SDA的控制,从机在检验到下降沿后经过1.25us的延时控制SDA,从而使得SDA的电平变为有效低电平。
主机释放SDA的1us后将SCL的电平调高,在过1us读取SDA的有效应答状态。在过1us将SCL电平拉低,使其出现下降沿并将状态变量cur_state
进行调整,从机检测到后,经过1us的延时退出SDA的控制。
从机释放SDA的控制后,经过器件地址的判断,在将State的值变更为2’b10,并进入write_to_eeprom任务中。主机此时正在控制SDA,并将第一个数据发送到SDA上。在write_to_eeprom中,从机也是先读入1个字节,其中最低位的数值为1。
在最后一个上升沿,经过2us后将SCL拉低。此时从机检测到第一个下降沿后,经过1.25us开始占用SDA发送应答信号,主机则在下降沿后经过1us释放SDA。
在经过1us,主机在将SCL拉高,用于读取SDA上的应答信号。在经过2us后,在将SCL拉低,经过1us后从机释放SDA,退出task。并将数据写入对应地址位后,将State的状态置0。
至此,读操作结束。具体代码请查看正点原子官网关于FPGA的资源包。
本文介绍了AT24C64的引脚功能,并分析了如何对其进行读写操作,需要额外注意的是起始操作和终止操作的条件。最后,我们在Vivado中编写了读写操作的代码,对其进行仿真的同时,分析了仿真波形的结果。