Menu Close

I2C接口EEPROM 芯片控制器(Controller)设计以及芯片Verilog模型仿真

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]

AT24C02D.zip

[/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]

test_i2c.zip

[/content_control]

Posted in FPGA, FPGA 教材教案, FPGA开发板, Verilog, Verilog, Vivado, Vivado, 元器件, 存储器, 开发工具, 开发工具, 开发板, 开发语言, 教材与教案, 文章, 编程语言, 资料区

1 Comment

发表回复

相关链接