[FPGA] Detailed explanation of the design and implementation of the IIC protocol universal host interface

1. Get to know IIC

        IIC (I2C) protocol is a serial communication protocol used to connect microcontrollers and peripheral devices. The IIC protocol only requires two signal lines (clock line SCL and data line SDA) to complete communication between devices; supports multiple hosts and multiple slaves Computer communication, distinguishing different devices throughdevice address; in standard mode, it can reach 100Kbit/s,; has a response mechanism that can detect the correctness of data and the existence of equipment. 3.4Mbit/ sHigh speed mode can reach 400Kbit/s, Fast mode can reach

2. Detailed explanation of the agreement

        Inidle state, both the clock line and the data line remainhigh level. The specific transmission process of the IIC protocol is as follows:

        1. The host sends the start signal, that is, when the clock line SCL remains high, the data line SDA changes from high to high. Flat to low level transition.
        2. The host sends the slave device address and read-write control bit< /span> /span>, that is, when the clock line SCL remains high, the data line SDA Transition from low level to high level. The stop signal indicates the end of an IIC communication. stop signal         6. The host sends signal. The response signal indicates that the host needs to continue receiving data bytes, and the non-response signal indicates that the host has completed receiving or an error has occurred. The non-acknowledge signal is achieved by the host pulling the data line SDA high during the last clock cycle. or non-responsesignal< as neededsends a responsehost, then the host releases the data line SDA and the slave sends the data word Festival. After each byte is received, the read operation         5. If the host sends a signal. If the slave receives the data byte, it pulls the data line SDA low to indicate a response signal. If the slave cannot receive the data byte or an error occurs, then keep the data line SDA high and wait for the master to send a stop signal or a new start signal. host continues to send Data byte, after each byte is sent, the data line SDA is released and waits for the slave's response, then the write operation         4. If the host sends a signal. If there is no match, keep the data line SDA high and wait for the host to send a stop signal or a new start signal. pulls the data line SDA low to indicate the response receives the address and control bits, it performs address recognition. If it matches, it will perform address recognition on the next clock. During the cycle, After the slave machine
        3. , the device address of the slave is generally 7 bits, and the read and write control bit is 1 bit, 0 indicates a write operation, and 1 indicates a read operation. After sending the 8-bit data, the master releases the data line SDA and waits for the slave's response signal.


        The important start bit and end bit are shown in the figure below, and in the IIC protocol, the device that controls the data line will control the SDA data line whenSCL is low level< /span>:SCL is high level, sample the SDA data line when

The start bit and end bit of the IIC protocol

Data changes and collection

3. Specific reading and writing timing

        The specific read and write timing diagram is similar to the picture below. This is taken from the manual of a device that supports the IIC protocol. It is worth mentioning that in the picture below, The gray part indicates that the data bus SDA is controlled by the master, and the white part indicates that the data bus SDA is controlled by the slave.

        When writing data (I2C Register Wr as shown below): First, the host sends a start bit, followed by a seven-bit wide Device address plus one-bit wide write control bit, slave Response once; then the host sends one byte of register address, from The machine responds once; then the host sends more than one byte of data , the slaveresponds; and finally sends a stop bit.

       When reading data (I2C Register Rd as shown below): First, the host sends a start bit, followed by a seven-bit wide Device address plus one-bit wide write control bit; then The master sendsregister address, and the slaveresponds; then resend< /span>non-response. stop bit signal; finally send a . After getting the last byte of data, the host sends a the host responds; the slave machine sends data, responds, used to indicate that the next read operation is to be performed, and the slave machineRead control bit and device address, start bit

IIC protocol

4. Design part

        Everyone on the website has already explained the design of IIC in great detail. I won’t give a simple explanation here. The following is mainly my personal understanding and design of IIC.

        1.Interface design

        As a widely used protocol, we should design it into a universal module as much as possible so that it can be used directly next time. Therefore, the interface definition of the module should be as shown in the figure:

IIC module interface

        clk  Transportation time;

        rst   is the reset signal (high effective);

        txdat   indicates the data that needs to be output through IIC;

        txget   indicates that the signal of this byte has been output;

        rxdat   represents the data obtained from sda;

        rxvld   means the data is valid;

        dev_addr   represents the device address;

        reg_addr   represents the read and write address (register address);

        cmd_op   represents a read and write command;

        op_byte_num   indicates the number of bytes read and written;

        iic_work_en   indicates the module starts working signal;

        busy   indicates the working status signal;

        scl  Display IIC time;

        sda  Display IIC number;

        2. State machine design

        Because it will be more complicated to make an IIC protocol, the form of a state machine is used here to clarify each operation. But generally the IIC state design we see, including when I designed the IIC before, divides each operation into a state. For example, the start bit is a state, the sending device address is a state, and the read and write control bits are One state, receiving a response and sending a non-response are one state respectively, etc. This division is very detailed, and each operation can be clearly understood at a glance, but I think doing so will make the state jump more complicated, making the entire state The machine becomes very bloated, as shown below.

Previous IIC state block diagram design
My previous IIC state block diagram design

        So, I re-conceived the design plan: 9 bits (9bit) are used as one cycle design, that is, every 9 bits or 1 bit is a state. The specific status is as follows:

        ​ ​ ​State 1: IDLE means idle state;

        ​​​​State 2: START represents the starting bit;

        State 3: DEV_CMD represents the device address bit, read and write control bit width and the slave’s response bit;

        State 4: ADDR represents the address bit of the transmitting register and the response bit of the receiving slave;

        ​​​​Status 5: WDATA represents the write data part;

        ​​​​Status 6: RDATA represents the read data part;

        ​​​​State 7: STOP represents the stop bit;

Redesigned IIC state block diagram
Redesigned IIC state block diagram

        3. Timing design

        writing data timing:

Write timing 1

Write timing 2

        Reading data timing:

Read timing 1

Read timing 2

        4. Code design

        ​ ​ ​ First, define the module

        ​​​​Among them, the clock I use is 100Mhz, and the reset signal is active high:

module i2c_master_interface #(
    parameter ADDR_LEN  =   1,
    parameter IIC_CLOCK =   400000
    ) (
    input           i_clk           ,
    input           i_rst           ,

    input   [7:0]   i_txdat         ,
    output          o_txget         ,
    output  [7:0]   o_rxdat         ,
    output          o_rxvld         ,
    input   [6:0]   i_dev_addr      ,
    input [ADDR_LEN*8-1:0] i_reg_addr,
    input           i_cmd_op        ,
    input   [3:0]   i_op_byte_num   ,
    input           i_iic_work_en   ,
    output          o_busy          ,

//output o_output_en,
    inout           io_scl          ,
    inout           io_sda           
);



endmodule

        The second step is to define the parameters

localparam  SCL_PERIOD  =   1000/(IIC_CLOCK/100000) ,   // in_clk = 100MHz
            SCL_HALF    =   SCL_PERIOD >> 1         ,
            LOW_HALF    =   SCL_HALF >> 1           ,
            HIGH_HALF   =   (SCL_PERIOD+SCL_HALF)>>1;

localparam  IDLE    =   7'b000_0001,
            START   =   7'b000_0010,
            DEV_CMD =   7'b000_0100,
            ADDR    =   7'b000_1000,
            WDATA   =   7'b001_0000,
            RDATA   =   7'b010_0000,
            STOP    =   7'b100_0000;

localparam  WRITE_BIT   =   1'b0,
            READ_BIT    =   1'b1,
            OP_WRITE    =   1'b0,
            OP_READ     =   1'b1;

        The third step is to define the signals to be used

reg [6:0]   state_c ;
reg [6:0]   state_n ;

reg [7:0]   cnt_scl ;
reg         reg_scl ;
reg [3:0]   cnt_bit ;
wire        end_cnt_bit;
reg [3:0]   bit_num ;
reg         rw_bit  ;

reg [3:0]   cnt_byte;
reg [3:0]   op_byte_num     ;

reg [ADDR_LEN*8-1:0] reg_addr_1;
reg [7:0]   reg_addr_2      ;

reg [7:0]   reg_txdat       ;
reg         txget           ;

reg         reg_sda         ;
reg         reg_i_sda       ;
reg [7:0]   rx_data         ;
reg         reg_ack         ;
reg         rxvld           ;
reg         busy            ;

wire o_sda;
wire i_sda;
wire o_scl;
wire i_scl;
wire output_en;

        The fourth step is to construct a finite state machine

        ​​​​According to logic, the jump conditions of the state machine are as follows:

Constructing a state machine

// FMS
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        state_c <= IDLE;
    end
    else begin
        state_c <= state_n;
    end
end

always @(*) begin
    case (state_c)
        IDLE    : begin
            if (i_iic_work_en) begin
                state_n = START;
            end
            else begin
                state_n = state_c;
            end
        end
        START   : begin
            if (end_cnt_bit) begin
                state_n = DEV_CMD;
            end
            else begin
                state_n = state_c;
            end
        end
        DEV_CMD : begin
            if (end_cnt_bit && reg_ack) begin   // NO ACK
                state_n = IDLE;
            end
            else if ((rw_bit == WRITE_BIT) && end_cnt_bit && (reg_ack == 0)) begin   //
                state_n = ADDR;
            end
            else if ((rw_bit == READ_BIT) && end_cnt_bit && (reg_ack == 0)) begin    //
                state_n = RDATA;
            end
            else begin
                state_n = state_c;
            end
        end
        ADDR    : begin
            if (end_cnt_bit && reg_ack) begin
                state_n = IDLE;
            end
            else if ((i_cmd_op == OP_WRITE) && (cnt_byte == (ADDR_LEN - 1)) && end_cnt_bit && (reg_ack == 0)) begin  //
                state_n = WDATA;
            end
            else if ((i_cmd_op == OP_READ) && (cnt_byte == (ADDR_LEN - 1)) && end_cnt_bit && (reg_ack == 0)) begin  //
                state_n = START;
            end
            else begin
                state_n = state_c;
            end
        end
        WDATA   : begin
            if (end_cnt_bit && reg_ack) begin
                state_n = IDLE;
            end
            else if ((cnt_byte == (op_byte_num - 1)) && end_cnt_bit && (reg_ack == 0)) begin  //
                state_n = STOP;
            end
            else begin
                state_n = state_c;
            end
        end
        RDATA   : begin
            if ((cnt_byte == (op_byte_num - 1)) && end_cnt_bit) begin   //
                state_n = STOP;
            end
            else begin
                state_n = state_c;
            end
        end
        STOP    : begin
            if (end_cnt_bit) begin
                state_n = IDLE;
            end
            else begin
                state_n = state_c;
            end
        end
        default: state_n = IDLE;
    endcase
end

        The fifth step is to construct intermediate logic

// cnt_bit  bit_num
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        cnt_bit <= 0;
    end
    else if ((state_c != IDLE) && (cnt_scl == (SCL_PERIOD - 1))) begin
        if (end_cnt_bit) begin
            cnt_bit <= 0;
        end
        else begin
            cnt_bit <= cnt_bit + 1;
        end
    end
end
assign end_cnt_bit = (state_c != IDLE) && (cnt_scl == (SCL_PERIOD - 1)) && (cnt_bit == (bit_num - 1));

always @(*) begin
    case (state_c)
        IDLE    : bit_num = 0;
        START   : bit_num = 1;
        DEV_CMD : bit_num = 9;
        ADDR    : bit_num = 9;
        WDATA   : bit_num = 9;
        RDATA   : bit_num = 9;
        STOP    : bit_num = 1;
        default : bit_num = 0;
    endcase
end

// rw_bit
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        rw_bit <= WRITE_BIT;
    end
    else begin
        case (i_cmd_op)
            OP_WRITE : rw_bit <= WRITE_BIT;
            OP_READ  : begin
                if ((state_c == STOP) && end_cnt_bit) begin
                    rw_bit <= WRITE_BIT;
                end
                else if ((state_c == DEV_CMD) && (rw_bit == WRITE_BIT) && end_cnt_bit && (reg_ack == 0)) begin   //
                    rw_bit <= READ_BIT;
                end
            end
            default  : rw_bit <= rw_bit;
        endcase
    end
end

// cnt_byte  op_byte_num
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        cnt_byte <= 0;
    end
    else if (state_c == IDLE) begin
        cnt_byte <= 0;
    end
    else if ((state_c == ADDR) && end_cnt_bit && (reg_ack == 0)) begin
        if (cnt_byte == (ADDR_LEN - 1)) begin
            cnt_byte <= 0;
        end
        else begin
            cnt_byte <= cnt_byte + 1;
        end
    end
    else if ((state_c == RDATA || state_c == WDATA) && end_cnt_bit && (reg_ack == 0)) begin
        if (cnt_byte == (op_byte_num - 1)) begin
            cnt_byte <= 0;
        end
        else begin
            cnt_byte <= cnt_byte + 1;
        end
    end
end

always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        op_byte_num <= 1;
    end
    else if ((state_c == START) && end_cnt_bit) begin
        op_byte_num <= i_op_byte_num;
    end
end


// reg_addr_1  reg_addr_2
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        reg_addr_1 <= 0;
    end
    else if ((state_c == START) && end_cnt_bit) begin
        reg_addr_1 <= i_reg_addr;
    end
end

always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        reg_addr_2 <= 0;
    end
    else if (state_c == ADDR) begin
        reg_addr_2 <= reg_addr_1[((ADDR_LEN - cnt_byte)*8-1) -: 8];  //
    end
end

// reg_txdat  txget
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        reg_txdat <= 8'hff;
    end
    else if (state_c == WDATA) begin
        reg_txdat <= i_txdat;
    end
end

always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        txget <= 1'b0;
    end
    else if ((state_c == WDATA) && end_cnt_bit) begin
        txget <= 1'b1;
    end
    else begin
        txget <= 1'b0;
    end
end

        The sixth step is to build the logic on SCL.

// cnt_scl  reg_scl
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        cnt_scl <= 0;
    end
    else if (state_c != IDLE) begin
        if (cnt_scl == (SCL_PERIOD - 1)) begin
            cnt_scl <= 0;
        end
        else begin
            cnt_scl <= cnt_scl + 1;
        end
    end
end

always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        reg_scl <= 0;
    end
    else if (cnt_scl == SCL_HALF - 1) begin
        reg_scl <= 1;
    end
    else if (cnt_scl == SCL_PERIOD - 1) begin
        reg_scl <= 0;
    end
    else begin
        reg_scl <= reg_scl;
    end
end

        The seventh step is to build the logic on SDA

// reg_sda
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        reg_sda <= 1'b1;
    end
    else begin
        case (state_c)
            START   : begin
                if (cnt_scl == LOW_HALF - 1) begin
                    reg_sda <= 1'b1;
                end
                else if (cnt_scl == HIGH_HALF -1) begin
                    reg_sda <= 1'b0;
                end
            end
            DEV_CMD : begin
                if (cnt_scl == LOW_HALF - 1) begin
                    case (cnt_bit)
                        0 : reg_sda <= i_dev_addr[6];
                        1 : reg_sda <= i_dev_addr[5];
                        2 : reg_sda <= i_dev_addr[4];
                        3 : reg_sda <= i_dev_addr[3];
                        4 : reg_sda <= i_dev_addr[2];
                        5 : reg_sda <= i_dev_addr[1];
                        6 : reg_sda <= i_dev_addr[0];
                        7 : reg_sda <= rw_bit;
                        default: reg_sda <= reg_sda;
                    endcase
                end
            end
            ADDR    : begin
                if (cnt_scl == LOW_HALF - 1) begin
                    case (cnt_bit)
                        0 : reg_sda <= reg_addr_2[7];
                        1 : reg_sda <= reg_addr_2[6];
                        2 : reg_sda <= reg_addr_2[5];
                        3 : reg_sda <= reg_addr_2[4];
                        4 : reg_sda <= reg_addr_2[3];
                        5 : reg_sda <= reg_addr_2[2];
                        6 : reg_sda <= reg_addr_2[1];
                        7 : reg_sda <= reg_addr_2[0];
                        default: reg_sda <= reg_sda;
                    endcase
                end
            end
            WDATA   : begin
                if (cnt_scl == LOW_HALF - 1) begin
                    case (cnt_bit)
                        0 : reg_sda <= reg_txdat[7];
                        1 : reg_sda <= reg_txdat[6];
                        2 : reg_sda <= reg_txdat[5];
                        3 : reg_sda <= reg_txdat[4];
                        4 : reg_sda <= reg_txdat[3];
                        5 : reg_sda <= reg_txdat[2];
                        6 : reg_sda <= reg_txdat[1];
                        7 : reg_sda <= reg_txdat[0];
                        default: reg_sda <= reg_sda;
                    endcase
                end
            end
            RDATA   : begin     // SACK
                if ((cnt_scl == LOW_HALF - 1) && (cnt_byte == op_byte_num - 1) && (cnt_bit == 8)) begin  //
                    reg_sda <= 1'b1;    // NACK
                end
                else if ((cnt_scl == LOW_HALF - 1) && (cnt_bit == 8)) begin
                    reg_sda <= 1'b0;    // ACK
                end
            end
            STOP    : begin
                if (cnt_scl == LOW_HALF - 1) begin
                    reg_sda <= 1'b0;
                end
                else if (cnt_scl == HIGH_HALF -1) begin
                    reg_sda <= 1'b1;
                end
            end
            default : reg_sda <= reg_sda;
        endcase
    end
end

// reg_i_sda  rx_data  reg_ack  rxvld
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        reg_i_sda <= 1'b1;
    end
    else begin
        reg_i_sda <= i_sda;
    end
end

always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        rx_data <= 0;
    end
    else if ((state_c == RDATA) && (cnt_scl == HIGH_HALF - 1)) begin
        case (cnt_bit)
            0 : rx_data[7] <= reg_i_sda;
            1 : rx_data[6] <= reg_i_sda;
            2 : rx_data[5] <= reg_i_sda;
            3 : rx_data[4] <= reg_i_sda;
            4 : rx_data[3] <= reg_i_sda;
            5 : rx_data[2] <= reg_i_sda;
            6 : rx_data[1] <= reg_i_sda;
            7 : rx_data[0] <= reg_i_sda;
            default: rx_data <= rx_data;
        endcase
    end
end

always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        reg_ack <= 1'b1;
    end
    else if ((state_c == DEV_CMD || state_c == ADDR || state_c == WDATA) && (cnt_bit == 8) && (cnt_scl == HIGH_HALF -1)) begin
        reg_ack <= reg_i_sda;
    end
end

always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        rxvld <= 1'b0;
    end
    else if ((state_c == RDATA) && end_cnt_bit) begin
        rxvld <= 1'b1;
    end
    else begin
        rxvld <= 1'b0;
    end
end

        Step 8: Construct the busy signal

// busy
always @(posedge i_clk or posedge i_rst) begin
    if (i_rst) begin
        busy <= 1'b0;
    end
    else if (i_iic_work_en) begin
        busy <= 1'b1;
    end
    // else if ((state_c == STOP) && end_cnt_bit) begin
    //     busy <= 1'b0;
    // end
    else if (state_c == IDLE) begin
        busy <= 1'b0;
    end
    else begin
        busy <= 1'b1;
    end
end

        ​ ​ ​ The ninth step, assignment output

// output
assign o_scl = (state_c == IDLE)? 1 : reg_scl;
assign o_txget = txget;
assign o_rxdat = rx_data;
assign o_rxvld = rxvld;
assign o_busy = busy;
assign o_sda = reg_sda;
assign output_en = (state_c == RDATA)? ((cnt_bit == 8)? 1:0) : ((cnt_bit == 8)? 0:1);

//assign o_output_en = output_en;

        The last step is to build the three-state gate

        ​ ​ Here we use the primitive IO_BUF in vivado to build:

IOBUF #(
   .DRIVE(12), // Specify the output drive strength
   .IBUF_LOW_PWR("TRUE"),  // Low Power - "TRUE", High Performance = "FALSE" 
   .IOSTANDARD("DEFAULT"), // Specify the I/O standard
   .SLEW("SLOW") // Specify the output slew rate
) iobuf_inst_sda (
   .O   (   i_sda   ),     // Buffer output
   .IO  (  io_sda   ),   // Buffer inout port (connect directly to top-level port)
   .I   (   o_sda   ),     // Buffer input
   .T   (~output_en )      // 3-state enable input, high=input, low=output
);

IOBUF #(
   .DRIVE(12), // Specify the output drive strength
   .IBUF_LOW_PWR("TRUE"),  // Low Power - "TRUE", High Performance = "FALSE" 
   .IOSTANDARD("DEFAULT"), // Specify the I/O standard
   .SLEW("SLOW") // Specify the output slew rate
) iobuf_inst_scl (
   .O   (   i_scl   ),     // Buffer output
   .IO  (  io_scl   ),   // Buffer inout port (connect directly to top-level port)
   .I   (   o_scl   ),     // Buffer input
   .T   (     0     )      // 3-state enable input, high=input, low=output
);

        5. Simulation verification

        writing timing

Write timing simulation

        Reading timing:

Read timing simulation

         Among them, we can clearly see the following key information:

        Starting position:

        ​ ​ ​ State 2, device address and write control bits:

         State 3, sending register address:

        ​ ​ ​ Data writing operation, 5 bytes are written:

        The RESTART status during the read operation, the device address, the read control bit and the 1-byte data read out:

        Stop bit: 

5. Description

        As can be seen from the above design, the SCL I designed goes from low to high. At the same time, when in the start bit state, SCL will also go from low to high. This is consistent with what we usually understand as SCL maintaining at the start bit. The high level does not match, as shown in the figure below:

         But after my actual testing on the board, this solution is feasible.

        Second point, it can be seen from the above design that due to the influence of sequential logic, the output of the busy signal will be delayed by one cycle compared to the state signal, which means that it is best to use Detect the falling edge of busy to determine whether the IIC interface is still working, or change the assignment logic of the busy signal.

Guess you like

Origin blog.csdn.net/nazonomaster/article/details/133964227