用verilog 实现的 i2c控制模块

verilog 实现i2c控制

本文涉及代码可以从下方链接下载:
https://gitee.com/huangzhc3/i2c_sim

i2c协议

首先要先学习一下i2c协议的基础,这里有一份官方文档(https://www.nxp.com/docs/en/user-guide/UM10204.pdf),也可以参考一些博客,总的来说i2c协议的基础原理不难,因为主要就是两根线的控制(SCL、SDA)。

i2c仿真模型(EEPROM)

当掌握了i2c协议的基础之后,建议以EEPROM为例子入手,有一篇博客介绍的很好:https://www.cnblogs.com/ninghechuan/p/9534893.html(需要有一定verilog基础),下面贴出原文中给出的eeprom仿真模型:

`timescale 1ns/1ns
`define timeslice 1250
//`define timeslice 300

module EEPROM_AT24C64(
    scl,
    sda
);
    input scl;               //串行时钟线
    inout sda;               //串行数据线

    reg out_flag;            //SDA数据输出的控制信号

    reg[7:0] memory[8191:0]; //数组模拟存储器
    reg[12:0]address;        //地址总线
    reg[7:0]memory_buf;      //数据输入输出寄存器
    reg[7:0]sda_buf;         //SDA数据输出寄存器
    reg[7:0]shift;           //SDA数据输入寄存器
    reg[7:0]addr_byte_h;     //EEPROM存储单元地址高字节寄存器
    reg[7:0]addr_byte_l;     //EEPROM存储单元地址低字节寄存器
    reg[7:0]ctrl_byte;       //控制字寄存器
    reg[1:0]State;           //状态寄存器

    integer i;

    //---------------------------
    parameter
        r7 = 8'b1010_1111,  w7 = 8'b1010_1110,   //main7
        r6 = 8'b1010_1101,  w6 = 8'b1010_1100,   //main6
        r5 = 8'b1010_1011,  w5 = 8'b1010_1010,   //main5
        r4 = 8'b1010_1001,  w4 = 8'b1010_1000,   //main4
        r3 = 8'b1010_0111,  w3 = 8'b1010_0110,   //main3
        r2 = 8'b1010_0101,  w2 = 8'b1010_0100,   //main2
        r1 = 8'b1010_0011,  w1 = 8'b1010_0010,   //main1
        r0 = 8'b1010_0001,  w0 = 8'b1010_0000;   //main0
    //---------------------------

    assign sda = (out_flag == 1) ? sda_buf[7] : 1'bz;

    //------------寄存器和存储器初始化---------------
    initial
    begin
        addr_byte_h    = 0;
        addr_byte_l    = 0;
        ctrl_byte    = 0;
        out_flag     = 0;
        sda_buf      = 0;
        State        = 2'b00;
        memory_buf   = 0;
        address      = 0;
        shift        = 0;

        for(i=0;i<=8191;i=i+1)
            memory[i] = 0;
    end

    //启动信号
    always@(negedge sda)
    begin
        if(scl == 1)
        begin
            State = State + 1;
            if(State == 2'b11)
                disable write_to_eeprom;
        end
    end

    //主状态机
    always@(posedge sda)
    begin
        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_eeprom;    //写操作
                    end
                    else
                        State = 2'b00;
                        //State = State;
                end

                2'b11:
                    read_from_eeprom;

                default:
                    State = 2'b00;
            endcase
        end
    end     //主状态机结束

    //操作停止
    task stop_W_R;
    begin
        State        = 2'b00;
        addr_byte_h  = 0;
        addr_byte_l  = 0;
        ctrl_byte    = 0;
        out_flag     = 0;
        sda_buf      = 0;
    end
    endtask

    //读进控制字和存储单元地址
    task read_in;
    begin
        shift_in(ctrl_byte);
        shift_in(addr_byte_h);
        shift_in(addr_byte_l);
    end
    endtask

    //EEPROM的写操作
    task write_to_eeprom;
    begin
        shift_in(memory_buf);
        address = {addr_byte_h[4:0], addr_byte_l};
        memory[address] = memory_buf;
        State = 2'b00;
    end
    endtask

    //EEPROM的读操作
    task read_from_eeprom;
    begin
        shift_in(ctrl_byte);
        if(ctrl_byte == r7 || ctrl_byte == w6
            || ctrl_byte == r5  || ctrl_byte == r4
            || ctrl_byte == r3  || ctrl_byte == r2
            || ctrl_byte == r1  || ctrl_byte == r0)
        begin
            address = {addr_byte_h[4:0], addr_byte_l};
            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)
            begin
                #`timeslice;
                out_flag = 0;
            end
        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
//eeprom.v文件结束

这段代码主要仿真了eeprom的i2c读写模型,你可以把它当做是一个虚拟的i2c设备,通过向这个虚拟设备传输信号,观察i2c是如何实现对eeprom的控制,因此,我认为这个模型很适合i2c的入门和仿真。该模块的示意图如下图所示:
用verilog 实现的 i2c控制模块_第1张图片

i2c 控制逻辑

做好上面的准备工作之后,开始写i2c的控制逻辑,网上很多代码是将所有i2c的读写都放在一段代码中,用一个状态机全部做完,我觉得这种代码结构不清晰,于是打算把代码拆分成如下两个模块:

  1. i2c_driver:i2c的驱动模块,将一些基本指令转化为i2c控制信号
  2. i2c_tran:i2c_driver的上级,把一些基本指令封装为更简便的高级指令

下面是实现的代码,感兴趣的同学欢迎讨论。
要注意的是,这段代码仅在eeprom仿真模型中通过,目前还没上板,主要是应答信号的处理和时序方面可能会跟具体器件相关!

module i2c_driver(
    input             rst_n,
    input             clk_50M,
    input             i2c_valid,
    input  [1:0]      i2c_ctrl, 
    input             i2c_sclk,
    input             transfer_en,
    input             capture_en,
    // 00: start
    // 01: stop
    // 10: write
    // 11: read
    input  [7:0]      i2c_d_in,

    output [7:0]      i2c_d_out,
    output            i2c_done,
    output reg        ack_r,
    // i2c port
    output            scl,
    inout             sda,

    // debug
    output  [2:0]     i2c_state
);


reg transfer_en_d1, transfer_en_d2;
always @(posedge clk_50M) begin
  transfer_en_d1 <= transfer_en;
  transfer_en_d2 <= transfer_en_d1;
end

// ---------------------state machine------------------
reg [2:0]   state,next_state;
reg [4:0]   wr_bit_cnt, rd_bit_cnt;

parameter   idle   = 3'd0; 
parameter   start  = 3'd1;
parameter   stop   = 3'd2;
parameter   write  = 3'd3;
parameter   wack   = 3'd4;
parameter   read   = 3'd5;
parameter   rack   = 3'd6;

always @ (posedge clk_50M) begin
  if (!rst_n) 
    state <= idle;
  else 
    state <= next_state;
end
always @ (*) begin
  if (transfer_en_d1)
  case (state)
    idle:     if (i2c_valid) begin
                case(i2c_ctrl)
                  2'b00:    next_state = start;
                  2'b01:    next_state = stop;
                  2'b10:    next_state = write;
                  2'b11:    next_state = read;
                  default:  next_state = idle;
                endcase
              end else 
                next_state = idle;
    start:    next_state = idle;
    stop:     next_state = idle;
    write:    if (wr_bit_cnt > 5'd7) 
                next_state = wack;
              else
                next_state = write;
    wack:     if (ack_r == 1'b0)
                next_state = idle;
              else 
                next_state = wack;
    read:     if (rd_bit_cnt > 5'd7)
                next_state = rack;
              else 
                next_state = read;
    rack:     if (ack_r == 1'b1)
                next_state = idle;
              else 
                next_state = rack;
    default:  next_state = idle;
  endcase
  else next_state = next_state;
end
// -----------------------------------------------------------

// ---------------------scl enable------------------------------
reg scl_en;
always @(posedge clk_50M) begin
  if ((!rst_n) || (state == idle))
    scl_en <= 1'b0;
  else scl_en <= 1'b1;
end
// -------------------------------------------------------------

// -------------------------sda-------------------------------
reg         sda_r;
reg         sda_link; // inout direction ctrl: 1-out, 0-in
// sda_link should be set to 0 before read
assign sda = (sda_link) ? sda_r : 1'bz;
assign scl = scl_en && i2c_sclk;

wire        cur_data_bit;
always @(posedge clk_50M) begin
  if (!rst_n) begin
    sda_link <= 1'b1;
    sda_r <= 1'b0;
  end else 
  case (state)
    start:    if (transfer_en_d2) begin sda_link <= 1'b1; sda_r <= 1'b1; end
              else if (capture_en)  begin sda_link <= 1'b1; sda_r <= 1'b0; end
                         else  begin sda_link <= sda_link; sda_r <= sda_r; end
    stop:     if (transfer_en_d2) begin sda_link <= 1'b1; sda_r <= 1'b0; end
              else if (capture_en)  begin sda_link <= 1'b1; sda_r <= 1'b1; end 
                         else  begin sda_link <= sda_link; sda_r <= sda_r; end
    write:    if (transfer_en_d2) begin sda_link <= 1'b1; sda_r <= cur_data_bit; end
                         else  begin sda_link <= sda_link; sda_r <= sda_r; end

    wack:     if (transfer_en_d2) begin sda_link <= 1'b0; sda_r <= sda_r; end
                         else  begin sda_link <= sda_link; sda_r <= sda_r; end
    read:     if (transfer_en_d2) begin sda_link <= 1'b0; sda_r <= sda_r; end
                         else  begin sda_link <= sda_link; sda_r <= sda_r; end
    rack:     if (transfer_en_d2) begin sda_link <= 1'b0; sda_r <= sda_r; end
                         else  begin sda_link <= sda_link; sda_r <= sda_r; end
    default:  begin sda_link <= sda_link; sda_r <= sda_r; end
  endcase
end
// ------------------------------------------------------------

// -----------------------write data reg-----------------------
reg [7:0]   wr_data_r;
always @(posedge clk_50M) begin 
  if (!rst_n)
    wr_data_r <= 8'd0;
  else if ((i2c_valid) && (i2c_ctrl == 2'b10))
    wr_data_r <= i2c_d_in;
  else if (capture_en) wr_data_r <= {wr_data_r[6:0], 1'b0};
  else wr_data_r <= wr_data_r;
end
assign cur_data_bit = wr_data_r[7];
// ------------------------------------------------------------

// -----------------------write_bit_count----------------------
always @(posedge clk_50M) begin
  if ((!rst_n) || (state == idle)) begin
    wr_bit_cnt <= 5'd0;
  end else if ((capture_en) && (state == write))
    wr_bit_cnt <= wr_bit_cnt + 5'd1;
  else wr_bit_cnt <= wr_bit_cnt;
end
// -------------------------------------------------------------

// ---------------------read bit reg----------------------------
reg [7:0]   rd_data_r;
always @(posedge clk_50M) begin
  if ((!rst_n) || ((state == idle) && (i2c_valid)))
    rd_data_r <= 8'd0;
  else if ((capture_en) && (state == read))
    rd_data_r <= {rd_data_r[6:0], sda};
  else 
    rd_data_r <= rd_data_r;
end
// -------------------------------------------------------------

// ---------------------i2c data out----------------------------
assign i2c_d_out = (state == idle) ? rd_data_r : 8'dx;
// -------------------------------------------------------------

// ---------------------read_bit_count--------------------------
always @(posedge clk_50M) begin
  if ((!rst_n) || (state == idle))
    rd_bit_cnt <= 5'd0;
  else if ((capture_en) && (state == read))
    rd_bit_cnt <= rd_bit_cnt + 5'd1;
  else rd_bit_cnt <= rd_bit_cnt;
end
// -------------------------------------------------------------

// ----------------------ack reg--------------------------------
always @(posedge clk_50M) begin
  if ((!rst_n) || (state == idle))
    ack_r <= 1'b1;
  else if (capture_en) begin
  if (state == write)
    ack_r <= 1'b1;
  else if (state == read)
    ack_r <= 1'b0;
  else if ((state == wack) || (state == rack))
    ack_r <= sda;
  else 
    ack_r <= ack_r;
  end else 
    ack_r <= ack_r;
end
// -------------------------------------------------------------

// ---------------------i2c done---------------------------------
assign i2c_done = (next_state == idle) && (!i2c_valid);
// -------------------------------------------------------------

// -----------------------debug---------------------------------
assign i2c_state = state;
// -------------------------------------------------------------
endmodule

上面是实现的i2c_driver的代码,i2c_tran的代码暂不给出,主要的设计思想是:首先根据系统时钟求出参考的时钟sclk_r,之后在sclk_r的高低电平的中点做标记,标记信号就是代码中的transfer_en和capture_en信号,这两个信号维持时间是一个50MHz的clock cycle,最后,再根据i2c协议的读写要求,用状态机依次实现。
为了便于使用和调试,我还加入了简单的握手信号:valid和done。这部分的逻辑如下:当发送机把指令和数据准备好后,会看当前的接收机是否空闲(done是否为1),若空闲,则发出一个持续几个周期的valid脉冲;接收机接收到valid脉冲后立刻把done拉低(忙碌),同时锁存此刻的输入指令和数据,并根据指令开始跑一个状态机周期,当接收机再次回到初始态时,再次把done信号置1,表示空闲。
下面是我的testbench:

`timescale 1ns/1ns
module test_top();
reg clk_50M, rst_n, data_valid, mode;
reg [7:0] devaddr, subaddr1, subaddr2, data_in;
wire tran_done;
wire [7:0] data_out;
wire scl, sda;

i2c_tran u1(
  .clk_50M(clk_50M), .rst_n(rst_n),
  .data_valid(data_valid), .mode(mode),
  .devaddr(devaddr), .subaddr1(subaddr1), .subaddr2(subaddr2), .data_in(data_in),
  .tran_done(tran_done), .data_out(data_out), 
  .scl(scl), .sda(sda),
  .ack_r(ack_r));
  
EEPROM_AT24C64 eeprom(.scl(scl), .sda(sda));

initial clk_50M = 0;
always begin
  #5 clk_50M = ~clk_50M;
end

initial begin
  rst_n = 1;
  data_valid = 0;
  #100;
  rst_n = 0;
  #100;
  rst_n = 1;
  // prepare data
  mode = 0;
  devaddr = 8'hAE;
  subaddr1 = 8'h00;
  subaddr2 = 8'h00;
  data_in = 8'h31;
  #200 data_valid = 1; // set valid to 1
  #5000 data_valid = 0;// set valid to 0

  #160000;
  mode = 0;
  devaddr = 8'hAE;
  subaddr1 = 8'h00;
  subaddr2 = 8'h01;
  data_in = 8'h13;
  #200 data_valid = 1;
  #5000 data_valid = 0;

  #160000;
  mode = 1;
  devaddr = 8'hAE;
  subaddr1 = 8'h00;
  subaddr2 = 8'h00;
  data_in = 8'h00;
  #200 data_valid = 1;
  #5000 data_valid = 0;

  #160000;
  mode = 1;
  devaddr = 8'hAE;
  subaddr1 = 8'h00;
  subaddr2 = 8'h01;
  data_in = 8'h00;
  #200 data_valid = 1;
  #5000 data_valid = 0;

  #200;
  mode = 0;
  devaddr = 8'h00;
  subaddr1 = 8'h00;
  subaddr2 = 8'h00;
  data_in = 8'h00;
  $stop;
end
endmodule

其中,这样一段代码就是一次读/写指令:

  mode = 0;
  devaddr = 8'hAE;
  subaddr1 = 8'h00;
  subaddr2 = 8'h00;
  data_in = 8'h31;
  #200 data_valid = 1; // set valid to 1
  #5000 data_valid = 0;// set valid to 0
  1. mode 为0 代表写数据指令,为1则代表是读数据指令
  2. devaddr 代表i2c设备的8位地址
  3. subaddr1 和 subaddr2 代表子地址段
  4. data_in 代表要往地址写入的数据
  5. data_valid 信号要在数据载入完成后再置为1,并及时清零(再一次读写完成前)
    下面是我的仿真波形,为了便于调试,我将ack信号也引出。
    用verilog 实现的 i2c控制模块_第2张图片
    从波形上可以看到,通过i2c_driver和i2c_tran程序搭建的模块,可以成功将数据写入和读出模拟eeprom模块,说明程序的基本时序符合i2c协议的标准。

上板!!

经过一天的调试,终于上板成功,先附上工程目录结构:
用verilog 实现的 i2c控制模块_第3张图片
工程包括一个i2c_top.v,主要是用于控制button、led、pll、ila等模块,并且包含了一个小小的状态机用于发出指令(idle-load-waits),button每按一次会发出一个持续几百个周期的data_valid信号,用于启动一次操作。
并且每按一次指令内容更换一次,如下面的代码所示

    3'd1:     begin mode <= 1'b0; devaddr <= 8'hA0; subaddr<= 8'h00; data_in<= 8'h13; end
    3'd2:     begin mode <= 1'b0; devaddr <= 8'hA0; subaddr<= 8'h01; data_in<= 8'h31; end
    3'd3:     begin mode <= 1'b1; devaddr <= 8'hA0; subaddr<= 8'h00; data_in<= 8'h00; end
    3'd4:     begin mode <= 1'b1; devaddr <= 8'hA0; subaddr<= 8'h01; data_in<= 8'h00; end

4条指令分别是:

  1. 往0xA0的设备的0x00地址写入数据0x13
  2. 往0xA0的设备的0x01地址写入数据0x31
  3. 往0xA0的设备的0x00地址读取数据
  4. 往0xA0的设备的0x01地址读取数据
    下图是ILA抓到的波形,可以看到成功读回0x13和0x31两个值
    用verilog 实现的 i2c控制模块_第4张图片
    用verilog 实现的 i2c控制模块_第5张图片

你可能感兴趣的:(i2c,verilog,fpga,verilog)