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命令调用模块中没有进行调用,只是将相关的控制信号封装了一层,再传递出去,代码如下所示:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
`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的速度。代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
`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。对应代码如下所示:
|
1 2 3 4 5 |
if ( div_en && ( div_addr == 32'hFFFF_0001 ) ) begin div_f_r <= div_in; rst_r <= 1'b1; end |
主要产生clk_en和clk_en_half两个信号的分频部分如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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
模块代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
`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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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波形的代码如下(部分截取):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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模块中一次完整的写操作写一个字节,代码如下(部分截取):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
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。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
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。
图11

图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代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
`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)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
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)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
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)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
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代码(部分截取)所示:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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]