1、概括
前文设计基于FPGA的IIC接口模块,本文将使用eeprom来验证该模块的设计。为了便于查看读写波形,采用两个按键来控制对eeprom数据的读写,当按键0按下后,FPGA向eeprom的前64个存储地址写入地址对应的数据,当按键1按下后,FPGA从eeprom的前64个存储地址读取数据。
该eeprom的原理图如下所示,其中A2、A1、A0表示该eeprom器件地址的低3位,器件地址的高4位固定为4’b1010。wp是写保护引脚,该引脚为低电平才能向eeprom中写入数据。24c02只能存储256字节数据,所以存储地址只有一个字节的数据。
该eeprom的单字节读写时序如下图所示:
该eeprom的页读写时序如下所示:
就是标准的IIC时序,只需要调用前文实现的IIC接口模块即可。
顶层模块的对应的RTL图如下所示,包含两个按键消抖模块对两路按键输入信号消抖,at24c02模块对IIC接口模块的读写进行控制,iic_drive模块驱动IIC时序。
顶层模块对应的参考代码如下所示:
module top #(
parameter TIME_20MS = 20_000_000 ,//按键消抖时间,默认20MS;
parameter TIME_CLK = 10 ,//系统时钟周期,默认10ns;
parameter FCLK = 100_000_000 ,//系统时钟频率,默认100MHz。
parameter FSCL = 250_000 ,//IIC时钟频率,默认400KHz。
parameter REG_ADDR_BYTE_NUM = 1 ,//寄存器地址字节数,最小值为1;
parameter DATA_BYTE_NUM = 1 ,//读写数据字节数,最小值为1.
parameter DELAY_TIME = 10_000_000 //延迟时间,10ms.
)(
input clk ,//系统时钟信号;
input rst_n ,//系统复位信号,低电平有效;
input [1 : 0] key ,//按键输入信号;
output led ,
output scl ,
inout sda
);
wire [1 : 0] key_out ;
wire start ;
wire rw_flag ;
wire [REG_ADDR_BYTE_NUM*8-1 : 0] reg_addr ;
wire [DATA_BYTE_NUM*8-1 : 0] wdata ;
wire [DATA_BYTE_NUM*8-1 : 0] rdata ;
wire rdata_vld ;
wire rdy ;
wire ack_flag ;
genvar i ;
//例化两个按键消抖模块,对输入的两路按键信号进行消抖;
generate
for(i=0 ; i<2 ; i=i+1)begin : KEY_C
key #(
.TIME_20MS ( TIME_20MS ),
.TIME_CLK ( TIME_CLK )
)
u_key (
.clk ( clk ),//系统时钟,100MHz。
.rst_n ( rst_n ),//系统复位,低电平有效。
.key_in ( key[i] ),//待输入的按键输入信号,默认低电平有效;
.key_out ( key_out[i]) //按键消抖后输出信号,当按键按下一次时,输出一个时钟宽度的高电平;
);
end
endgenerate
//例化eeprom的读写控制模块;
at2402 #(
.TCLK ( TIME_CLK ),//系统时钟周期,10ns.
.DELAY_TIME ( DELAY_TIME),//延迟时间,10ms.
.REG_ADDR_BYTE_NUM ( REG_ADDR_BYTE_NUM ),//寄存器地址字节数;
.DATA_BYTE_NUM ( DATA_BYTE_NUM ) //读写数据字节数。
)
u_at2402 (
.clk ( clk ),//系统时钟信号;
.rst_n ( rst_n ),//系统复位信号,低电平有效;
.key ( key_out ),//按键输入,分别控制写入和读取eeprom。
.rdy ( rdy ),//iic控制器忙闲指示信号,高电平表示空闲。
.rdata ( rdata ),//从IIC读出的数据;
.rdata_vld ( rdata_vld ),//IIC读出数据指示信号;
.start ( start ),//开始进行读写操作信号;
.rw_flag ( rw_flag ),//读写指示信号,高电平表示进行读操作;
.reg_addr ( reg_addr ),//寄存器地址;
.wdata ( wdata ),//需要写入寄存器的数据;
.led ( led ) //错误指示灯。
);
//例化IIC接口驱动模块;
iic_drive #(
.FCLK ( FCLK ),//系统时钟频率,默认100MHz。
.FSCL ( FSCL ),//IIC时钟频率,默认400KHz。
.REG_ADDR_BYTE_NUM ( REG_ADDR_BYTE_NUM ),//寄存器地址字节数;
.DATA_BYTE_NUM ( DATA_BYTE_NUM ) //读写数据字节数。
)
u_iic_drive (
.clk ( clk ),//系统时钟信号;
.rst_n ( rst_n ),//系统复位信号,低电平有效;
.start ( start ),//开始进行读写操作;
.rw_flag ( rw_flag ),//读写标志信号,高电平表示读操作,低电平表示写操作;
.reg_addr ( reg_addr ),//寄存器地址,读写操作时共用的地址信号;
.wdata ( wdata ),//写数据;
.rdata ( rdata ),//读数据信号;
.rdata_vld ( rdata_vld ),//读数据输出使能信号,高电平有效;
.rdy ( rdy ),//模块忙闲指示信号,位高电平时可以接收上游模块的读写使能信号;
.scl ( scl ),//IIC的时钟信号;
.ack_flag ( ack_flag ),//高电平表示应答失败;
.sda ( sda ) //IIC的双向数据信号;
);
//例化ILA调试模块
//ila_0 u_ila_0 (
// .clk ( clk ),//input wire clk
// .probe0 ( scl ),//input wire [0:0] probe0
// .probe1 ( u_iic_drive.sda_in ),//input wire [0:0] probe1
// .probe2 ( u_iic_drive.state_c ),//input wire [6:0] probe2
// .probe3 ( u_iic_drive.ack_flag ),//input wire [0:0] probe3
// .probe4 ( u_iic_drive.l2h_flag ),//input wire [0:0] probe4
// .probe5 ( u_iic_drive.h2l_flag ),//input wire [0:0] probe5
// .probe6 ( u_iic_drive.wr_flag ),//input wire [0:0] probe6
// .probe7 ( u_iic_drive.rd_flag ),//input wire [0:0] probe7
// .probe8 ( u_iic_drive.end_div_cnt ),//input wire [0:0] probe8
// .probe9 ( u_iic_drive.rw_flag ),//input wire [0:0] probe9
// .probe10 ( u_iic_drive.sda_out ),//input wire [0:0] probe10
// .probe11 ( u_iic_drive.state_n ),//input wire [6:0] probe11
// .probe12 ( u_iic_drive.reg_addr ),//input wire [7:0] probe12
// .probe13 ( u_iic_drive.bit_cnt ),//input wire [3:0] probe13
// .probe14 ( u_iic_drive.bit_cnt_num ),//input wire [3:0] probe14
// .probe15 ( u_iic_drive.div_cnt ),//input wire [8:0] probe15
// .probe16 ( u_iic_drive.start ),//input wire [0:0] probe16
// .probe17 ( u_iic_drive.rdata ),//input wire [7:0] probe17
// .probe18 ( u_iic_drive.rdata_vld ),//input wire [0:0] probe18
// .probe19 ( u_iic_drive.sda_out_en ),//input wire [0:0] probe19
// .probe20 ( u_iic_drive.rdy ) //input wire [0:0] probe20
//);
endmodule
当写入寄存器地址长度和读写数据长度为1字节,对应的TestBench文件如下所示:
`timescale 1 ns/1 ns
module test();
localparam CYCLE = 10 ;//系统时钟周期,单位ns,默认10ns;
localparam RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;
localparam FCLK = 100_000_000 ;//系统时钟频率,默认100MHz。
localparam FSCL = 400_000 ;//IIC时钟频率,默认400KHz。
localparam TIME_20MS = 2000*CYCLE ;
localparam DELAY_TIME = 80000 ;
reg clk ;//系统时钟,默认100MHz;
reg rst_n ;//系统复位,默认低电平有效;
reg [1 : 0] key ;
wire led ;
wire scl ;
wire sda ;
top #(
.TIME_20MS ( TIME_20MS ),
.TIME_CLK ( CYCLE ),
.DELAY_TIME ( DELAY_TIME),
.FCLK ( FCLK ),
.FSCL ( FSCL )
)
u_top (
.clk ( clk ),
.rst_n ( rst_n ),
.key ( key ),
.led ( led ),
.scl ( scl ),
.sda ( sda )
);
eeprom u_eeprom0 (
.scl ( scl ),
.sda ( sda )
);
//生成周期为CYCLE数值的系统时钟;
initial begin
clk = 0;
forever #(CYCLE/2) clk = ~clk;
end
//生成复位信号;
initial begin
#1; rst_n <= 1'b1; key <= 2'b11;
#2; rst_n <= 0;//开始时复位10个时钟;
repeat(RST_TIME)@(posedge clk);
rst_n <= 1'b1;
repeat(3)@(posedge clk);
key_task(0);
repeat(1300000)@(posedge clk);
key_task(1);
repeat(1300000)@(posedge clk);
repeat(100)@(posedge clk);
$stop;//停止仿真;
end
task key_task(
input [1 : 0] value
);
begin
key[value] <= 1'b1;
@(posedge clk);
key[value] <= 1'b0;
repeat(7)begin
repeat({$random} % (TIME_20MS/2))@(posedge clk);
key[value] <= ~key[value];
end
key[value] <= 1'b0;
repeat(TIME_20MS*2)@(posedge clk);
repeat(7)begin
key[value] <= ~key[value];
repeat({$random} % (TIME_20MS/2))@(posedge clk);
end
key[value] <= 1'b1;
end
endtask
endmodule
其中使用的eeprom仿真模型是在网上找到的,这个文件只支持读写数据长度为1字节,需要的同学可以研究下。
`timescale 1ns / 1ps
`define timeslice 100
module eeprom (
input scl ,
inout sda
);
reg out_flag ;
reg [7 : 0] memory [2047 : 0] ;
reg [10 : 0] address ;
reg [7 : 0] memory_buf ;
reg [7 : 0] sda_buf ;
reg [7 : 0] shift ;
reg [7 : 0] addr_byte ;
reg [7 : 0] ctrl_byte ;
reg [1 : 0] State ;
integer i ;
// -------------------------------------
parameter r7 = 8'b10101111 ;
parameter r6 = 8'b10101101 ;
parameter r5 = 8'b10101011 ;
parameter r4 = 8'b10101001 ;
parameter r3 = 8'b10100111 ;
parameter r2 = 8'b10100101 ;
parameter r1 = 8'b10100011 ;
parameter r0 = 8'b10100001 ;
parameter w7 = 8'b10101110 ;
parameter w6 = 8'b10101100 ;
parameter w5 = 8'b10101010 ;
parameter w4 = 8'b10101000 ;
parameter w3 = 8'b10100110 ;
parameter w2 = 8'b10100100 ;
parameter w1 = 8'b10100010 ;
parameter w0 = 8'b10100000 ;
//--------------------------------------
assign sda= (out_flag == 1) ? sda_buf[7] : 1'bz;
//--------------寄存器和存储器初始化--------------
initial begin
addr_byte = 0;
ctrl_byte = 0;
out_flag = 0;
sda_buf = 0;
State = 2'b00;
memory_buf = 0;
address = 0;
shift = 0;
for(i = 0 ; i <= 2047 ; i = i + 1)
memory[i] = 0;
end
//--------------启动信号检测--------------
always @(negedge sda)
if(scl == 1)begin
State = State + 1;
if(State == 2'b11)
disable write_to_eeprm;
end
//--------------主状态机--------------
always @(posedge sda)
if(scl == 1)
stop_W_R;
else begin
casex(State)
2'b01 : begin
read_in;
if(ctrl_byte == w7||ctrl_byte == w6|| ctrl_byte == w5 || ctrl_byte == w4 || ctrl_byte == w3 || ctrl_byte == w2 ||ctrl_byte == w1 ||ctrl_byte == w0)begin
State = 2'b10;
write_to_eeprm;
end
else
State = 2'b00;
end
2'b11 :
read_from_eeprm;
default : State = 2'b00;
endcase
end
//--------------操作停止--------------
task stop_W_R;
begin
State = 2'b00;
addr_byte = 0;
ctrl_byte = 0;
out_flag = 0;
sda_buf = 0;
end
endtask
//--------------读进控制字和存储单元地址--------------
task read_in;
begin
shift_in(ctrl_byte);
shift_in(addr_byte);
end
endtask
//--------------EEPROM--------------
task write_to_eeprm;
begin
shift_in(memory_buf);
address = {ctrl_byte[3:1],addr_byte};
memory[address] = memory_buf;
$display("eeprm---memory[%0h]=%0h",address,memory[address]);
State = 2'b00;
end
endtask
//--------------EEPROM读操作--------------
task read_from_eeprm;
begin
shift_in(ctrl_byte);
if(ctrl_byte == r7 || ctrl_byte == r6 || ctrl_byte == r5 || ctrl_byte == r4 || ctrl_byte == r3 || ctrl_byte == r2 || ctrl_byte == r1 || ctrl_byte == r0)begin
address = {ctrl_byte[3:1],addr_byte};
sda_buf = memory [address];
shift_out;
State = 2'b00;
end
end
endtask
//--------------SDA 数据线上的数据存入寄存器 ,数据在SCL的高电平有效--------------
task shift_in;
output[7:0] shift;
begin
@(posedge scl) shift[7] = sda;
@(posedge scl) shift[6] = sda;
@(posedge scl) shift[5] = sda;
@(posedge scl) shift[4] = sda;
@(posedge scl) shift[3] = sda;
@(posedge scl) shift[2] = sda;
@(posedge scl) shift[1] = sda;
@(posedge scl) shift[0] = sda;
@(negedge scl)begin
#`timeslice;
out_flag = 1;
sda_buf = 0;
end
@(negedge scl)
#`timeslice out_flag = 0;
end
endtask
//--------------EEPROM存储器中的数据通过SDA数据线输出,数据在SCL低电平时变化--------------
task shift_out;
begin
out_flag = 1;
for(i = 6 ; i >= 0 ; i = i - 1)begin
@(negedge scl);
# `timeslice;
sda_buf = sda_buf << 1;
end
@(negedge scl) # `timeslice sda_buf[7] = 1;
@(negedge scl) # `timeslice out_flag = 0;
end
endtask
endmodule
2、eeprom控制模块
该模块就是检测按键按下,然后产生eeprom的读写开始信号,驱动iic模块生成对应读写时序。当按键0按下时,将写eeprom标志信号wr_flag拉高,由于两次写操作需要间隔一段时间,使用一个计数器来计数这段时间,当时间到达后把iic驱动模块的读写开始信号拉高,并且把寄存器地址和需要写入的数据输出给该模块。还需要一个计数器来记录写入多少个数据了,当数据全部写入eeprom之后,把wr_flag拉低。
按键1按下后,将读eeprom的标志信号rd_flag拉高,这里为了后续比较号使用ila抓取信号,两次读操作也加了写操作一样的延时。就是把写操作的寄存器全部都读一遍。
将读出的数据与写入数据进行对比,如果不一致则led输出低电平,否则输出高电平。这个其实没有必要,因为这里采用ila抓取信号可以更加直观。
该模块的代码比较简单,就不再详细讲解,主要的参考代码如下所示:
//产生读指示信号,初始值为低电平,按键1按下后拉高,读出指定地址数据后拉低;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
rd_flag <= 1'b0;
end
else if(end_cnt)begin//数据全部读出后拉低;
rd_flag <= 1'b0;
end
else if(key[1])begin//按键1按下时拉高;
rd_flag <= 1'b1;
end
end
//产生写指示信号,初始值为低电平,按下按键0后拉高,写入指定数据后拉高;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
wr_flag <= 1'b0;
end
else if(end_cnt)begin//写入规定数据后拉低;
wr_flag <= 1'b0;
end
else if(key[0])begin//按键0按下后拉高;
wr_flag <= 1'b1;
end
end
//延时计数器,每次写操作结束后,都需要延时一段时间,确保写入的数据被稳定存入eeprom内部;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//
cnt_r <= 0;
end
else if(add_cnt_r)begin
if(end_cnt_r)
cnt_r <= 0;
else
cnt_r <= cnt_r + 1;
end
end
assign add_cnt_r = (wr_flag || rd_flag) && rdy;//当一次写操作结束时,开始对系统时钟计数;
assign end_cnt_r = add_cnt_r && cnt_r == DELAY_CNT - 1;
//计数器cnt用来记录每次按下按键后需要读写的数据个数;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)
cnt <= 0;
else
cnt <= cnt + DATA_BYTE_NUM;
end
end
assign add_cnt = end_cnt_r;
assign end_cnt = add_cnt && cnt >= 64-1;
//如果读标志有效,则该信号拉高,表示进行读操作,否则默认进行写操作;
always@(posedge clk)begin
rw_flag <= rd_flag;
end
//生成读写的开始信号,还有读写操作的寄存器地址和需要写入的数据信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
reg_addr <= 0;
start <= 1'b0;
end
else if(add_cnt)begin
reg_addr <= cnt;
start <= 1'b1;
end
else begin
start <= 1'b0;
end
end
//一次写入EEPROM内部的数据;
generate
if(DATA_BYTE_NUM == 1)begin//每次发送1字节数据;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
led <= 1'b0;
wdata <= 0;
end
else if(add_cnt)
wdata <= cnt;
else
led <= (cnt == rdata) && rdata_vld;
end
end
else if(DATA_BYTE_NUM == 2)begin//每次发送2字节数据;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
led <= 1'b1;
wdata <= 0;
end
else if(add_cnt)
wdata <= {cnt[7:0],(cnt[7:0]+8'd1)};
else
led <= ({cnt[7:0],(cnt[7:0]+8'd1)} == rdata) && rdata_vld;
end
end
else if(DATA_BYTE_NUM == 3)begin//每次发送3字节数据;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
led <= 1'b1;
wdata <= 0;
end
else if(add_cnt)
wdata <= {cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2)};
else
led <= ({cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2)} == rdata) && rdata_vld;
end
end
else if(DATA_BYTE_NUM == 4)begin//每次发送4字节数据;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
led <= 1'b1;
wdata <= 0;
end
else if(add_cnt)
wdata <= {cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3)};
else
led <= ({cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3)} == rdata) && rdata_vld;
end
end
else if(DATA_BYTE_NUM == 5)begin//每次发送5字节数据;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
led <= 1'b1;
wdata <= 0;
end
else if(add_cnt)
wdata <= {cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3),(cnt[7:0]+8'd4)};
else
led <= ({cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3),(cnt[7:0]+8'd4)} == rdata) && rdata_vld;
end
end
else if(DATA_BYTE_NUM == 6)begin//每次发送6字节数据;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
led <= 1'b1;
wdata <= 0;
end
else if(add_cnt)
wdata <= {cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3),(cnt[7:0]+8'd4),(cnt[7:0]+8'd5)};
else
led <= ({cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3),(cnt[7:0]+8'd4),(cnt[7:0]+8'd5)} == rdata) && rdata_vld;
end
end
else if(DATA_BYTE_NUM == 7)begin//每次发送7字节数据;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
led <= 1'b1;
wdata <= 0;
end
else if(add_cnt)
wdata <= {cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3),(cnt[7:0]+8'd4),(cnt[7:0]+8'd5),(cnt[7:0]+8'd6)};
else
led <= ({cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3),(cnt[7:0]+8'd4),(cnt[7:0]+8'd5),(cnt[7:0]+8'd6)} == rdata) && rdata_vld;
end
end
else begin//每次发送8字节数据,此处最多写了一次写入或者读出8字节数据的程序,如果想要一次从EEPROM中写入更多数据,则需要将这个数据拼接更多位进行测试。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin
led <= 1'b1;
wdata <= 0;
end
else if(add_cnt)
wdata <= {cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3),(cnt[7:0]+8'd4),(cnt[7:0]+8'd5),(cnt[7:0]+8'd6),(cnt[7:0]+8'd7)};
else
led <= ({cnt[7:0],(cnt[7:0]+8'd1),(cnt[7:0]+8'd2),(cnt[7:0]+8'd3),(cnt[7:0]+8'd4),(cnt[7:0]+8'd5),(cnt[7:0]+8'd6),(cnt[7:0]+8'd7)} == rdata) && rdata_vld;
end
end
endgenerate
3、模块仿真
前文的按键消抖模块、IIC驱动模块都是使用前文详细讲解且开源的模块,此处不再赘述。直接对本工程进行仿真。
首先将顶层模块的写寄存器地址字节数、读写数据字节数均设置为1。使用vivado的仿真如下图所示:
把上图中写数据的一段时序放大后如下图所示,首先发送起始位,然后写入器件地址和写指示位,之后应答位。然后发送写寄存器地址26,之后写入数据26,最后发送停止位。完成向地址26中写入26的时序。
下图是读出地址为20的时序,首先发送起始位、器件地址、写寄存器地址,之后发送重复起始位,然后器件地址,之后就读取数据,最后发送停止位。图中蓝色信号就是三态门使能信号,该信号为低电平时关闭三态门,主机释放数据总线,从机驱动数据总线。
从地址20中读出的数据是20,由此验证写入和读出数据均没有问题。
接下来对页写和页读时序进行仿真,由于这个at2402的仿真模型只支持单字节的读写时序,所以多字节超出的部分从机不会应答,此处只是借助仿真查看下读写时序是否与预期一致即可,具体验证可以在后文的ILA抓取信号处验证。
将顶层模块的读写数据字节数的参数DATA_BYTE_NUM设置为3,就是每次读写3个地址的数据,然后运行仿真,结果如下所示。相比图7的读写时间将大幅度减小。
将上图中写入寄存器数据的一段仿真时序放大,如下图所示,向寄存器地址为30开始的三个寄存器中分别写入数据30、31、32。仿真时序没有问题,从机没有应答是因为仿真模型不支持这种模式,没有关系。
将上述读数据时序放大,如下图所示,从连续读出45、46、47地址处的数据。前面的时序都没有问题,主要关注在读数据阶段主机应答信号产生是否正确,主机在接收前两字节数据后都将数据线拉低应答,接收最后一字节数据后将数据线拉高,不应答从机然后发送停止位,由此仿真时序没有问题。
然后就是验证一下寄存器地址多字节的IIC读、写时序,将顶层模块的寄存器地址长度REG_ADDR_BYTE_NUM参数设置为3,读、写数据的长度DATA_BYTE_NUM设置为1,然后运行仿真,总体仿真结果如下图所示。
将上图中写数据的部分时序放大,如下图所示,向寄存器地址24’d12中写入12。连续输入三字节地址数据后,输入需要写入的数据,注意先写高字节数据的高位。
将图13中读时序放大,结果如下所示,寄存器地址包含3字节数据,读取24’d35中的数据,3字节寄存器地址发送完毕后,发送重复起始位,之后依次发送器件地址,之后读数据,最后发送停止位。
多字节数据的读写时序与前文的eeprom页读写时序是一样的,因此就不再单独仿真了。仿真到此结束,由于仿真模型的问题,后续几个仿真只能查看读写的时序是否符合要求,不能通过读写的数据得到仿真结果,如果有兴趣可以试着修改仿真模型,达到要求。
4、上板测试
上面已经对仿真做了仿真,本文采用的eeprom的寄存器地址只有一个字节,所以不能对多字节的地址读写进行验证,但是通过前文仿真结果也应该不会有问题了。
首先抓取单个存储地址数据的读写时序,把顶层模块的写寄存器地址长度参数(REG_ADDR_BYTE_NUM)设置为1字节,读写数据长度的参数(DATA_BYTE_NUM)也设置为1字节。然后对工程综合、实现,查看该工程的资源消耗如下图所示,消耗89个LUT资源和65个触发器资源。
将bit文件下载到开发板中,ILA将start高电平作为触发条件。按下按键0抓取的信号如下图所示:
将上图中的一帧数据放大,如下图所示,向地址5中写入5,对应的时序与前文仿真的时序基本一致,这里的sda_in就是iic数据总线的状态。
按下按键1,FPGA读取eeprom前64个地址的数据,ILA抓取的时序如下所示。
将上图中的一帧数据放大,如下图所示,从地址5中读出数据,最后读出的数据为5,证明前面写入和本次读出的数据均正常。
然后对页读写时序进行测试,把顶层模块的读写数据长度的参数(DATA_BYTE_NUM)设置为5字节,然后对工程进行综合、实现,查看该工程消耗的资源。当读写数据为5字节时IIC驱动模块消耗135个LUT、160个触发器资源。
然后按下按键0,抓取的信号如下图所示:
将上述的写时序放大,结果如下图所示,向寄存器地址5、6、7、8、9中分别写入5、6、7、8、9。ILA抓取的时序与前文仿真结果基本一致,首先写入第一字节数据5之后,继续发送下一字节数据6,直到写入5字节数据后,才发送停止位结束写入时序。
按下按键1,ILA抓取读时序如下图所示:
将上述的读时序放大,结果如下图所示,向寄存器地址10开始连续读取5字节的数据。由于ILA抓取信号的位宽就设置成了8位,所以下图ILA输出的信号就只显示了最后输出的那一字节数据,实际上是包含5字节数据的,只是ILA显示不出来,需要修改IP位宽。
上图中紫色信号是IIC数据总线,天蓝色信号是IIC时钟信号,黄色信号是主机的数据输出使能,低电平表示主机释放数据总线。读取的第一字节就是10,然后依次是11、12、13、14。由此连续读出了地址10、11、12、13、14的数据。
证明多字节的IIC读写时序正常,没有问题,也就验证了IIC接口模块设计无误。
至此,IIC接口模块的设计和验证就将清楚了,如果不理解的可以留言,接口时序电路的设计首先都要查看官方技术手册,然后再去查找具体芯片对该接口时序的要求。要特别注意该接口的时钟频率、读写间隔时间、建立时间、保持时间,像这种数据再时钟中部赋值的低速串行接口时序,建立时间和保持时间一般是不会存在问题的。
最后如果要获取本设计的工程文件,在后台回复“基于FPGA的eeprom读写工程”(不包括引号)即可,工程中ILA默认是被注释掉的。IIC接口模块已经全部做了参数化处理,使用时直接例化修改parameter参数即可,不需要额外修改其余任何代码。