I2C接口在小颗非易失性存储芯片EEPROM上经常使用。复习I2C接口特点,速度规定及读写操作过程参考文章I2C接口基础知识一,I2C接口基础知识二。本文在I2C接口设计文章I2C 主设备分频器设计,I2C 主设备启动及读写控制Verilog程序设计的基础上,实现I2C EEPROM 芯片Controller的设计及联合I2C EEPROM 芯片Verilog模型仿真。之前的SPI Verilog模型的仿真验证( SPI EEPROM 芯片Verilog模型仿真,SPI Flash Verilog 程序 仿真 )比较简单,只是在test bench文件中模拟了接口协议。本次工程的仿真验证将会基于一个完整设计的I2C EEPROM 芯片Controller,并且如果想要在EEPROM芯片之外的工程中使用I2C Controller只需要稍加改造就可以实现。
1. I2C EEPROM芯片Controller设计
对于I2C EEPROM芯片Controller设计,本文采用了模块化的方法,将一个相对复杂的Controller分为了3个主要的子模块:I2C 时钟分频器模块,I2C读写操作实现模块和I2C命令调用模块。I2C 时钟分频器模块产生I2C操作的分频信号,用来控制发送时钟,或发送接收数据的频率。I2C读写操作实现模块使用I2C 时钟分频器模块的baudtick,并在I2C命令调用模块命令的作用下进行I2C读写操作。三个模块之间信号的交流如图1所示:

图1
- I2C命令调用模块(I2C_engine.v)
目前在I2C命令调用模块中没有进行调用,只是将相关的控制信号封装了一层,再传递出去,代码如下所示:
`timescale 1ns / 1ps
module I2C_engine
#(
parameter CLK_F = 100_000_000,
parameter SPEED = 100_000,
parameter SIM = 0
)
(
input inclk,
output clk_en,
input [7:0] wr_data,
output [7:0] rd_data,
input start,
input stop,
input [1:0] i2c_op, // i2c_op[1] == 1 op enable, == 0 op disable; i2c_op[0] == 1 read op, i2c_op[0] == 0 ,write op
input rd_last,
output i2c_op_rdy,
input [ 31: 0 ] div_addr,
input [ 31: 0 ] div_in,
input div_en,
inout SDA,
inout SCL,
output i2c_ack,
input rst
);
wire scl_dev_out;
wire sda_dev_out;
wire clk_en_half;
assign SDA = sda_dev_out ? 1'bZ : 1'b0;
assign SCL = scl_dev_out ? 1'bZ : 1'b0;
SCl_en
#(
.CLK_F ( CLK_F ) ,
.SPEED ( SPEED )
)
SCl_en_inst
(
.sys_clk ( inclk ),
.div_addr ( div_addr ),
.div_in ( div_in ),
.div_en ( div_en ),
.clk_en ( clk_en ),
.clk_en_half(clk_en_half),
.rst ( rst )
);
i2c_phy i2c_phy_inst
(
.sys_clk ( inclk ),
.clk_en ( clk_en ),
.clk_en_half(clk_en_half),
.wr_data ( wr_data ),
.rd_data ( rd_data ),
.rd_last ( rd_last ),
.i2c_op ( i2c_op ),
.i2c_op_rdy ( i2c_op_rdy ),
.start ( start ),
.stop ( stop ),
.scl_out ( scl_dev_out ),
.sda_out ( sda_dev_out ),
.sda_in ( SDA ),
.i2c_ack ( i2c_ack ),
.rst ( rst )
);
endmodule
可以看出在I2C命令调用模块I2C_engine.v中除了例化I2C时钟分频器模块SCL_en.v和I2C读写操作实现模块i2c_phy.v之外,最重要的是将inout信号SDA和SCL进行合并:
assign SDA = sda_dev_out ? 1’bZ : 1’b0;
assign SCL = scl_dev_out ? 1’bZ : 1’b0;
- I2C时钟分频器模块(SCL_en.v)
I2C的数据/时钟的发送和接收都是由I2C时钟分频模块输出的clk_en和clk_en_half控制的,这里完全使用参数化的设置,可以按照设计改变I2C的速度。代码如下:
`timescale 1ns / 1ps
module SCl_en
#(
parameter CLK_F = 100_000_000 , // default input frequency
parameter SPEED = 100_000 // IIC speed
)
(
input sys_clk,
input [ 31: 0 ] div_addr,
input [ 31: 0 ] div_in,
input div_en,
output reg clk_en,
output reg clk_en_half,
input rst
);
localparam DIV_COE = CLK_F / ( SPEED * 2 ) - 1; // calculate frequency division coefficient
reg [ 31: 0 ] div_f_r;
reg [ 31: 0 ] count_r;
reg rst_r;
//div_in div_f_r
always@( posedge sys_clk or posedge rst )
if ( rst )
begin
div_f_r <= DIV_COE;
rst_r <= 1'b1;
end
else
begin
rst_r <= 1'b0;
if ( div_en && ( div_addr == 32'hFFFF_0001 ) ) //device address is for future potential register development
begin
div_f_r <= div_in;
rst_r <= 1'b1;
end
end
always@( posedge sys_clk or posedge rst_r )
if ( rst_r )
begin
count_r <= 0;
clk_en <= 1'b0;
clk_en_half <= 1'b0;
end
else
begin
clk_en <= 1'b0;
clk_en_half <= 1'b0;
if ( count_r == div_f_r[31:1] ) // half the frequency
clk_en_half <= 1'b1;
if ( count_r == div_f_r )
begin
count_r <= 0;
clk_en <= 1'b1;
end
else
count_r <= count_r + 1;
end
endmodule
其中参数CLK_F指的是当前整个模块的系统时钟频率,SPEED指的是I2C频率。这里也是以I2C 4倍频率的信号控制(四个相位分别用两个二倍频的clk_en和clk_en_half错沿对齐),示意图如图2所示:
图2
DIV_COE是计算出来的分频系数,通过赋值给div_f_r(分频计数的阈值)信号,在代码主体中用于分频产生clk_en和clk_en_half两个信号。除了可以通过DIV_COE设置I2C频率,还可以通过设置正确的div_addr = 32’hFFFF_0001和置高div_en信号将div_f_r赋值为div_in。对应代码如下所示:
if ( div_en && ( div_addr == 32'hFFFF_0001 ) )
begin
div_f_r <= div_in;
rst_r <= 1'b1;
end
主要产生clk_en和clk_en_half两个信号的分频部分如下:
always@( posedge sys_clk or posedge rst_r )
if ( rst_r )
begin
count_r <= 0;
clk_en <= 1'b0;
clk_en_half <= 1'b0;
end
else
begin
clk_en <= 1'b0;
clk_en_half <= 1'b0;
if ( count_r == div_f_r[31:1] ) // half the frequency
clk_en_half <= 1'b1;
if ( count_r == div_f_r )
begin
count_r <= 0;
clk_en <= 1'b1;
end
else
count_r <= count_r + 1;
end
- I2C读写操作实现模块(i2c_phy.v)
I2C读写操作实现模块接收I2C命令调用模块传递的控制信号来进行I2C的读写操作,如图3所示。start,stop用于控制I2C读写操作实现模块发送start和stop位。i2c_op表示当前读或写操作。rd_last表示连续读的最后一个读操作。同样,I2C读写操作实现模块也传递给I2C命令调用模块一些反馈信号。i2c_op_rdy表示I2C命令调用模块传递过来的命令已经操作完成,比如start位已经发出,已经成功读出一个字节等。i2c_ack输出在写操作的时候slave设备回过来的ACK信号,注意在本工程中没有对i2c_ack信号进行判断,因为本模块也支持除I2C协议外的两线通信接口(two-wire interface,其通信协议中没有强制规定ACK必须是低电平,也可能是高电平),如果有需要可以自行添加判断ACK信号。
除此之外,可以从图3看到,I2C读写操作实现模块接收准备写的数据,并提供读操作获取的数据。时钟线主要由master发出,所以FPGA模块只输出SCL时钟,但是对于FPGA来说,可能接收数据,也可能发送数据。

图3
模块代码如下:
`timescale 1ns / 1ps
module i2c_phy
(
input sys_clk,
input clk_en,
input clk_en_half,
output reg [7:0] rd_data = 0,
input [ 7: 0 ] wr_data,
input start,
input stop,
input [1:0] i2c_op, // i2c_op[1] == 1 op enable, == 0 op disable; i2c_op[0] == 1 read op, i2c_op[0] == 0 ,write op
input rd_last,
output reg i2c_op_rdy = 0,
input sda_in,
output reg scl_out = 1,
output reg sda_out = 1,
output reg i2c_ack = 0,
input rst
);
reg rd_last_r = 0;
reg [ 7: 0 ] i2c_wr_val;
reg [ 2: 0 ] sft_count;
reg [ 4: 0 ] i2c_st = 0;
always@( posedge sys_clk or posedge rst )
if ( rst )
begin
scl_out <= 1'b1;
sda_out <= 1'b1;
i2c_op_rdy <= 0;
i2c_ack <= 0;
sft_count <= 7;
i2c_st <= 0;
end
else
case ( i2c_st )
0:
begin //idle
i2c_ack <= 0;
rd_last_r <= rd_last;
i2c_op_rdy <= 0;
sft_count <= 7;
i2c_wr_val <= wr_data;
casex({start,stop,i2c_op})
4'b1xxx: i2c_st <= 1; // start bit
4'b01xx: i2c_st <= 4; // stop bit
4'b0010: i2c_st <= 7; // write
4'b0011: i2c_st <= 12; // read
default i2c_st <= 0;
endcase
end
1: // start bit send ... waiting for 1 clk_en
begin
if ( clk_en )
begin
scl_out <= 1'b1;
sda_out <= 1'b1;
i2c_st <= 2;
end
end
2:
begin
scl_out <= 1'b1;
if ( clk_en )
begin
sda_out <= 1'b0;
i2c_st <= 3;
end
end
3:
begin
if ( clk_en )
begin
i2c_op_rdy <= 1;
scl_out <= 1'b0;
i2c_st <= 0;
end
end
4: // stop
begin
sda_out <= 1'b0;
if ( clk_en )
begin
scl_out <= 1'b1;
i2c_st <= 5;
end
end
5:
begin
if ( clk_en )
begin
sda_out <= 1'b1;
i2c_st <= 6;
end
end
6:
begin
if ( clk_en )
begin
sda_out <= 1'b1;
scl_out <= 1'b1;
i2c_op_rdy <= 1;
i2c_st <= 0;
end
end
7: // write
begin
if ( clk_en )
begin
scl_out <= 1'b0;
i2c_st <= 8;
end
end
8:
begin
if(clk_en_half)
sda_out <= i2c_wr_val[ sft_count ];
if ( clk_en )
begin
scl_out <= 1'b1;
if ( sft_count == 0 )
i2c_st <= 9;
else
begin
sft_count <= sft_count - 1;
i2c_st <= 7;
end
end
end
9:
begin
if ( clk_en )
begin
scl_out <= 1'b0;
i2c_st <= 10;
end
end
10:
begin
if(clk_en_half)
sda_out <= 1'b1;
if ( clk_en )
begin
scl_out <= 1'b1;
i2c_ack <= sda_in;
i2c_st <= 11;
end
end
11:
begin
if ( clk_en )
begin
scl_out <= 0;
i2c_op_rdy <= 1;
i2c_st <= 0;
end
end
12: // read
begin
sda_out <= 1;
if ( clk_en )
begin
scl_out <= 0;
i2c_st <= 13;
end
end
13:
begin
if ( clk_en )
begin
rd_data[ sft_count ] <= sda_in;
scl_out <= 1'b1;
if ( sft_count == 0 )
i2c_st <= 14;
else
begin
sft_count <= sft_count - 1;
i2c_st <= 12;
end
end
end
14:
begin
if(clk_en)
begin
scl_out <= 0;
i2c_st <= 15;
end
end
15:
begin
if(clk_en_half)
begin
if(rd_last_r) sda_out <= 1;
else sda_out <= 0;
end
if(clk_en)
begin
scl_out <= 1;
i2c_st <= 16;
end
end
16:
begin
if(clk_en)
begin
scl_out <= 0;
i2c_st <= 17;
end
end
17:
begin
if(clk_en_half)
begin
scl_out <= 0;
sda_out <= 1;
i2c_op_rdy <= 1;
i2c_st <= 0;
end
end
default:
i2c_st <= 0;
endcase
endmodule
模块中主要实现了一个状态机i2c_st,关于其状态转移示意图如图4所示,注意图中大部分状态的转移条件都是由I2C时钟分频器模块提供的clk_en:

图4
在i2c_st状态机的状态0中判断当前模块需要执行什么操作,可能是start,stop,读还有写,操作分为如下:
-
- 状态0可以跳到状态1拉出start波形
实现start波形的代码如下(部分截取):
1: //
begin
if ( clk_en )
begin
scl_out <= 1'b1;
sda_out <= 1'b1;
i2c_st <= 2;
end
end
2:
begin
scl_out <= 1'b1;
if ( clk_en )
begin
sda_out <= 1'b0;
i2c_st <= 3;
end
end
3:
begin
if ( clk_en )
begin
i2c_op_rdy <= 1;
scl_out <= 1'b0;
i2c_st <= 0;
end
end
start对应的波形如图5所示:

图5
从图5可以看出start波形是保持SCL为高电平,SDA由高电平变为低电平,也就是说在SCL为高电平期间,将SDA拉出下降沿。注意SDA和SCL的波形高低电平都是由clk_en触发的。在start位结束后,返回状态0,并将i2c_op_rdy置位。
-
- 状态0跳到状态4拉出stop波形
生成stop波形的代码如下(部分截取):
4: // stop
begin
sda_out <= 1'b0;
if ( clk_en )
begin
scl_out <= 1'b1;
i2c_st <= 5;
end
end
5:
begin
if ( clk_en )
begin
sda_out <= 1'b1;
i2c_st <= 6;
end
end
6:
begin
if ( clk_en )
begin
sda_out <= 1'b1;
scl_out <= 1'b1;
i2c_op_rdy <= 1;
i2c_st <= 0;
end
end
对应的stop波形如图6所示。

图6
通过先将SCL置高,SDA保持为低电平,之后再只将SDA置位,就可以产生stop波形。也就是说,在SCL为高电平期间,将SDA拉出上升沿。同样的注意SDA和SCL的波形高低电平都是由clk_en触发的。在生成stop位后,返回状态0,并将i2c_op_rdy置位。
-
- 状态0跳到状态7进行写操作
在I2C_phy.v模块中一次完整的写操作写一个字节,代码如下(部分截取):
0:
begin //idle
i2c_ack <= 0;
rd_last_r <= rd_last;
i2c_op_rdy <= 0;
sft_count <= 7;
i2c_wr_val <= wr_data;
casex({start,stop,i2c_op})
4'b1xxx: i2c_st <= 1; // start bit
4'b01xx: i2c_st <= 4; // stop bit
4'b0010: i2c_st <= 7; // write
4'b0011: i2c_st <= 12; // read
default i2c_st <= 0;
endcase
end
7: // write
begin
if ( clk_en )
begin
scl_out <= 1'b0;
i2c_st <= 8;
end
end
8:
begin
if(clk_en_half)
sda_out <= i2c_wr_val[ sft_count ];
if ( clk_en )
begin
scl_out <= 1'b1;
if ( sft_count == 0 )
i2c_st <= 9;
else
begin
sft_count <= sft_count - 1;
i2c_st <= 7;
end
end
end
9:
begin
if ( clk_en )
begin
scl_out <= 1'b0;
i2c_st <= 10;
end
end
10:
begin
if(clk_en_half)
sda_out <= 1'b1;
if ( clk_en )
begin
scl_out <= 1'b1;
i2c_ack <= sda_in;
i2c_st <= 11;
end
end
11:
begin
if ( clk_en )
begin
scl_out <= 0;
i2c_op_rdy <= 1;
i2c_st <= 0;
end
end
上示代码中还附带了状态0,因为在状态0中信号sft_count被置为7。状态7和8配合将i2c_wr_val中的8个bit按clk_en和clk_en_half的节奏发送,信号sft_count作为i2c_wr_val 的bit索引使用。
对应的波形图如图7所示:

图7
从图7可以看出,状态7和状态8配合发送一个bit,不断重复,将一个完整字节的bit 7 – bit 0依次发送,与I2C协议的先发MSB是一致的。在状态9,10和11中再多发出一个时钟周期,等待I2C协议接收方回复ACK。在写操作完成后,返回状态0,并将i2c_op_rdy置位。
-
- 状态0跳到状态12进行读操作
读操作对应代码如下(部分截取)。在I2C_phy.v模块中,一次完整的读操作也只读出1个字节。如果需要读出多个字节,需要由上层控制重复读操作。和写操作类似,读操作需要使用信号sft_count来索引读出的bit,并组合成一个完整的字节。sft_count在状态0被置为7。
0:
begin //idle
i2c_ack <= 0;
rd_last_r <= rd_last;
i2c_op_rdy <= 0;
sft_count <= 7;
i2c_wr_val <= wr_data;
casex({start,stop,i2c_op})
4'b1xxx: i2c_st <= 1; // start bit
4'b01xx: i2c_st <= 4; // stop bit
4'b0010: i2c_st <= 7; // write
4'b0011: i2c_st <= 12; // read
default i2c_st <= 0;
endcase
end
12: // read
begin
sda_out <= 1;
if ( clk_en )
begin
scl_out <= 0;
i2c_st <= 13;
end
end
13:
begin
if ( clk_en )
begin
rd_data[ sft_count ] <= sda_in;
scl_out <= 1'b1;
if ( sft_count == 0 )
i2c_st <= 14;
else
begin
sft_count <= sft_count - 1;
i2c_st <= 12;
end
end
end
14:
begin
if(clk_en)
begin
scl_out <= 0;
i2c_st <= 15;
end
end
15:
begin
if(clk_en_half)
begin
if(rd_last_r) sda_out <= 1;
else sda_out <= 0;
end
if(clk_en)
begin
scl_out <= 1;
i2c_st <= 16;
end
end
16:
begin
if(clk_en)
begin
scl_out <= 0;
i2c_st <= 17;
end
end
17:
begin
if(clk_en_half)
begin
scl_out <= 0;
sda_out <= 1;
i2c_op_rdy <= 1;
i2c_st <= 0;
end
end
图8所示为读操作的时序图,可以看到状态11和状态12配合读出一个bit,在读完一个字节后,状态14,15发出ACK,状态16和17返回状态0,并将i2c_op_rdy置位。注意在状态15中还判断了是否读最后一个字节(rd_last_r),如果读的是最后一个字节,返回NACK。如果是连续读中间的一个字节,返回ACK,符合I2C协议规定。

图8
图9所示为连续读中间的一个字节,可以看到SDA发出的是ACK。

图9
图10所示为读最后一个字节,SDA发出的是NACK。

图10
2. I2C EEPROM芯片Verilog模型仿真
与之前使用SPI EEPROM芯片Verilog模型仿真类似,在仿真中验证I2C controller,如果仿真正确可以再进行编译,进一步的调试。本文选择的是Microchip芯片AT24C02D。
- 在Microchip公司选择芯片的步骤如下所示:
- 在Microchip官网主菜单Tools and Resources下选择Documentation,如图11所示:
图11
-
- 在界面左侧点击打开Other Resources > Verilog Models,如图12所示,选择本文将要使用的I2C接口EEPROM芯片AT24C02D:

图12
AT24C02D.v文件下载(已打包成zip):
[content_control]
[/content_control]
- 添加仿真test bench文件tb_i2c.v调度I2C controller来与AT24C02D芯片沟通
图13所示为I2C test bench tb_i2c.v模块给I2C命令调用模块提供命令调用信号。在仿真时,start,stop,i2c_op,rd_last等信号都由tb_i2c.v模块提供。
图13
tb_i2c.v代码如下:
`timescale 1ns / 1ps
module tb_i2c(
);
parameter PERIOD = 10 ;
parameter FREQUANCY = 100_000_000; //100M
parameter SPEED = 100_000; // 100K
parameter SPEED1 = 400000; //400K
parameter DEV_ADDR = 7'b101_0000;
parameter SIM = 1;
reg sys_clk;
always #( PERIOD / 2 ) sys_clk = ~sys_clk; //100M
reg rst;
wire clk_en;
reg [ 31: 0 ] div_in; //for baudrate register
wire [ 31: 0 ] div_addr = 32'hFFFF_0001; //register address for changing the baudrate
reg div_en; //write enable signal, synchronized with sys_clk;
reg start = 0;
reg stop = 0;
reg [1:0] i2c_op = 0; // i2c_op[1] == 1 op enable, == 0 op disable; i2c_op[0] == 1 read op, i2c_op[0] == 0, write op
initial
begin
rst = 1'b1;
sys_clk = 0;
#50 rst = 1'b0;
#13_000_000;
$stop;
end
reg [7:0] wr_data = 0;
wire [7:0] rd_data;
reg rd_last = 0;
wire i2c_op_rdy;
reg [31:0] cnt = 0;
reg [3:0] st = 0;
always@( posedge sys_clk or posedge rst )
if ( rst )
begin
cnt <= 0;
st <= 0;
div_in <= 0;
div_en <= 1'b0;
start <= 0;
end
else
begin
case ( st )
0:
begin
cnt <= cnt + 1;
if(&cnt[8:0])
begin
cnt <= 0;
start <= 1;
st <= 1;
end
end
1:
begin
start <= 0;
if(i2c_op_rdy)
begin
i2c_op <= 2'b10; // write operation
wr_data <= {DEV_ADDR, 1'b0}; // write address
st <= 2;
end
end
2:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
i2c_op <= 2'b10; // write operation
wr_data <= 8'h00; // write memory array adress 00
st <= 3;
end
end
3:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
i2c_op <= 2'b10; // write operation
wr_data <= cnt; // write data
cnt <= cnt + 1;
if(cnt == 7 )
st <= 4;
end
end
4:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
stop <= 1;
st <= 5;
end
end
5:
begin
stop <= 0;
cnt <= 0;
if ( i2c_op_rdy )
begin
stop <= 0;
st <= 6;
end
end
6: // read
begin
cnt <= cnt + 1;
if(cnt == 5_000_00) // program need 5ms delay
begin
cnt <= 0;
start <= 1;
st <= 7;
end
end
7:
begin
start <= 0;
if(i2c_op_rdy)
begin
i2c_op <= 2'b10; // write operation
wr_data <= {DEV_ADDR, 1'b0}; // write address
st <= 8;
end
end
8:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
i2c_op <= 2'b10; // write operation
wr_data <= 8'h00; // write memory array adress 00
st <= 9;
end
end
9: // restart
begin
i2c_op <= 0;
if(i2c_op_rdy)
begin
cnt <= 0;
start <= 1;
st <= 10;
end
end
10:
begin
start <= 0;
if(i2c_op_rdy)
begin
i2c_op <= 2'b10; // write operation
wr_data <= {DEV_ADDR, 1'b1}; // write address
st <= 11;
end
end
11:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
i2c_op <= 2'b11; // read operation
cnt <= cnt + 1;
if(cnt == 7 )
begin
rd_last <= 1;
st <= 12;
end
end
end
12:
begin
rd_last <= 0;
i2c_op <= 0;
if ( i2c_op_rdy )
begin
stop <= 1;
st <= 13;
end
end
13:
begin
stop <= 0;
cnt <= 0;
if ( i2c_op_rdy )
begin
stop <= 0;
st <= 14;
end
end
14:
begin
div_in <= FREQUANCY / (SPEED1 * 2 ) - 1; //切换到400K进行测试
div_en <= 1'b1;
st <= 15;
end
15:
begin
div_en <= 1'b0;
cnt <= 0;
st <= 0;
end
default:
st <= 0;
endcase
end
wire SDA;
wire SCL;
PULLUP SDA_inst ( .O(SDA) );
PULLUP SCL_inst ( .O(SCL) );
I2C_engine
#(
.CLK_F ( FREQUANCY ) , //default with 100Mhz
.SPEED ( SPEED ) , //default speed 100k
.SIM ( SIM )
)
I2C_engine_inst
(
.inclk ( sys_clk ),
.clk_en ( clk_en ),
.wr_data ( wr_data ),
.rd_data ( rd_data ),
.rd_last ( rd_last ),
.start ( start ),
.stop ( stop ),
.i2c_op ( i2c_op ),
.i2c_op_rdy ( i2c_op_rdy ),
.div_addr ( div_addr ),
.div_in ( div_in ),
.div_en ( div_en ),
.SDA ( SDA ),
.SCL ( SCL ),
.i2c_ack ( i2c_ack ),
.rst ( rst )
);
AT24C02D #
(.SLAVE_ADDRESS(DEV_ADDR))
AT24C02D_inst
(
.SDA (SDA),
.SCL (SCL),
.WP (0)
);
endmodule
因为tb_i2c.v用来控制读写EEPROM芯片AT24C02D,参考AT24C02的芯片技术手册,AT24C02D EEPROM芯片容量为2 Kbit,也就是256个字节(256 X 8 bit)。页写最多可以写8个字节,超过8个字节会写错,回卷到之前的值(rollover)。每次写操作过后,需要等待EEPROM 5 ms的时间。模块中主要实现了一个状态机st,关于其状态转移示意图如图14所示

图14
tb_i2c.v模块操作可以分解如下:
-
- 写操作(状态0-5)
0:
begin
cnt <= cnt + 1;
if(&cnt[8:0])
begin
cnt <= 0;
start <= 1;
st <= 1;
end
end
1:
begin
start <= 0;
if(i2c_op_rdy)
begin
i2c_op <= 2'b10; // write operation
wr_data <= {DEV_ADDR, 1'b0}; // write address
st <= 2;
end
end
2:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
i2c_op <= 2'b10; // write operation
wr_data <= 8'h00; // write memory array adress 00
st <= 3;
end
end
3:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
i2c_op <= 2'b10; // write operation
wr_data <= cnt; // write data
cnt <= cnt + 1;
if(cnt == 7 )
st <= 4;
end
end
4:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
stop <= 1;
st <= 5;
end
end
5:
begin
stop <= 0;
cnt <= 0;
if ( i2c_op_rdy )
begin
stop <= 0;
st <= 6;
end
end
6: // read
begin
cnt <= cnt + 1;
if(cnt == 5_000_00) // program need 5ms delay
begin
cnt <= 0;
start <= 1;
st <= 7;
end
end
EEPROM页写操作示意图如图15所示:

图15
对应上述状态后,如图16所示:

图16
状态5将stop信号置低,注意状态6在等待了5 ms后才重新开始下一次的操作。
仿真图如图17示,可以看出I2C波形符合设计,从地址0开始,写了8个byte的数,分别为8’h00-8’h07。

图17
-
- 读操作(状态6-13)
6: // read
begin
cnt <= cnt + 1;
if(cnt == 5_000_00) // program need 5ms delay
begin
cnt <= 0;
start <= 1;
st <= 7;
end
end
7:
begin
start <= 0;
if(i2c_op_rdy)
begin
i2c_op <= 2'b10; // write operation
wr_data <= {DEV_ADDR, 1'b0}; // write address
st <= 8;
end
end
8:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
i2c_op <= 2'b10; // write operation
wr_data <= 8'h00; // write memory array adress 00
st <= 9;
end
end
9: // restart
begin
i2c_op <= 0;
if(i2c_op_rdy)
begin
cnt <= 0;
start <= 1;
st <= 10;
end
end
10:
begin
start <= 0;
if(i2c_op_rdy)
begin
i2c_op <= 2'b10; // write operation
wr_data <= {DEV_ADDR, 1'b1}; // write address
st <= 11;
end
end
11:
begin
i2c_op <= 0;
if ( i2c_op_rdy )
begin
i2c_op <= 2'b11; // read operation
cnt <= cnt + 1;
if(cnt == 7 )
begin
rd_last <= 1;
st <= 12;
end
end
end
12:
begin
rd_last <= 0;
i2c_op <= 0;
if ( i2c_op_rdy )
begin
stop <= 1;
st <= 13;
end
end
13:
begin
stop <= 0;
cnt <= 0;
if ( i2c_op_rdy )
begin
stop <= 0;
st <= 14;
end
end
EEPROM随机读 + 连续读 操作示意图如图18所示:

图18
对应上述状态后,如图19所示:

图19
状态13将stop信号置低。仿真图如图20所示,可以看出I2C波形符合随机读 + 连续读的设计,从地址0开始,读出了8个byte的数,分别为8’h00-8’h07。

图20
-
- 改变I2C速度(14-15)
14:
begin
div_in <= FREQUANCY / (SPEED1 * 2 ) - 1; //切换到400K进行测试
div_en <= 1'b1;
st <= 15;
end
15:
begin
div_en <= 1'b0;
cnt <= 0;
st <= 0;
end
在状态14改变div_in,置位使能div_en,以及在tb_i2c.v模块中将div_addr设置为32’hFFFF_0001,使得将相关信号可以传递近SCL_en.v模块,改变分频的计数,从而改变clk_en和clk_en_half的频率, 如下SCL_en代码(部分截取)所示:
always@( posedge sys_clk or posedge rst )
if ( rst )
begin
div_f_r <= DIV_COE;
rst_r <= 1'b1;
end
else
begin
rst_r <= 1'b0;
if ( div_en && ( div_addr == 32'hFFFF_0001 ) )
begin
div_f_r <= div_in;
rst_r <= 1'b1;
end
end
仿真图如图21所示,可以明显看出原本I2C的速度是通过设置传入SCL_en.v模块的参数SPEED,进而设置DIV_COE得到的100k,之后在状态14修改成400k的I2C速度。

图21
3. 总结
本文设计了一个通用的两线制协议(包括I2C) controller,并使用芯片产家提供的芯片Verilog模型配合controller测试。仿真图结果与对应的芯片操作时序图一致,也证明了I2C controller设计正确,包括芯片编程等待的时间5 ms也很好的模拟了出来。总的来说,使用芯片Verilog模型仿真是一种很好的验证手段,可以在没有真实EEPROM器件的时候,验证controller 的正确性。
参考代码下载:
[content_control]
[/content_control]
老师,您好,想请教下为什么tb文件 0 状态机 最开始需要等待9’b1_1111_1111 这么多个时钟周期在开始呢?