Verilog implements I2C controller (Xiaomei Ge's idea)----detailed analysis

i2c_bit_shift

The module block diagram is as follows.
Insert image description here
Input and output signals:
Insert image description here
The overall idea is as follows:
through the combination of input commands, an 8-byte data transmission is completed. 6 commands are defined,

WR Write Data Request (6'b000_001)
STA Start bit request (6'b000_010)
RD Read data request (6'b000_100)
STO Stop bit request (6'b001_000)
ACK Acknowledgment bit request (6'b010_000)
NACK No response to request (6'b100_000)

The idea of ​​applying a sequence machine in a state machine is adopted. Each state is divided into four steps to complete the transmission of one bit of data, and is controlled by a counter. The defined state is as follows (one-hot encoding):

state description
IDLE idle state
GEN_STA generate start signal
WR_DATA Write data status
RD_DATA Read data status
CHECK_ACK Detect response signal
GEN_ACK generate response signal
GNE_STO generate stop signal

The state machine transitions look like this:
Insert image description here

The code is as follows, with very clear comments:

module i2c_bit_shift (
    input   wire            clk         ,
    input   wire            rst_n       ,
    input   wire    [5:0]   cmd         ,       //控制总线实现各种传输的各种命令组合
    input   wire            go          ,       //整个模块的启动信号
    input   wire    [7:0]   Tx_Data     ,       //总线要发送的8位数据

    output  reg             i2c_sclk    ,       //i2c时钟总线
    inout   wire            i2c_sdat    ,       //i2c数据总线
    output  reg             Trans_Done  ,       //传输完成信号
    output  reg     [7:0]   Rx_Data     ,       //总线要接收的8位数据
    output  reg             ack_o               //从机时候应答标志
);                                          

    localparam      IDLE        =       7'b0000_001,     //空闲状态
                    GEN_STA     =       7'b0000_010,     //产生起始信号
                    WR_DATA     =       7'b0000_100,     //写数据状态
                    RD_DATA     =       7'b0001_000,     //读数据状态
                    CHECK_ACK   =       7'b0010_000,     //检测应答信号
                    GEN_ACK     =       7'b0100_000,     //产生应答信号
                    GNE_STO     =       7'b1000_000;     //产生停止信号

    localparam      WR          =       6'b000_001,      //写数据请求
                    STA         =       6'b000_010,      //起始位请求
                    RD          =       6'b000_100,      //读数据请求
                    STO         =       6'b001_000,      //停止位请求 
                    ACK         =       6'b010_000,      //应答位请求
                    NACK        =       6'b100_000;      //无应答请求
    

    parameter       SYS_CLOCK   =       50_000_000;      //系统采用50MHz时钟
    parameter       SCL_CLOCK   =       400_000   ;      //SCL总线时钟采用400khz
    localparam      SCL_CNT_M   =       SYS_CLOCK / SCL_CLOCK / 4 - 1;  //产生时钟SCL计数器最大值

    
    reg                 en_div_cnt      ;               //计数器使能信号
    reg                 i2c_sda_oe      ;               //三态门使能信号,1:输出,0:输入
    reg     [6:0]       state           ;               //状态机的状态变量
    reg     [5:0]       cnt             ;               //计数器
    reg                 i2c_sdat_o      ;               //输出的i2c_sdat_o
    reg     [19:0]      div_cnt         ;               //分频信号

    wire    sclk_plus = (div_cnt == SCL_CNT_M);         //dic_cnt计数到最大值产生一个高脉冲

    //reset为低时,若干信号的初始化
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            en_div_cnt <= 1'b0;
            i2c_sda_oe <= 1'b1;
            state <= IDLE;
            cnt <= 0;
            i2c_sdat_o <= 1'b0;
            i2c_sclk <= 1'b1;
            Rx_Data <= 8'd0;
        end
    end
    //一段式状态机
    always @(posedge clk or negedge rst_n) begin
        case(state)
            IDLE:   begin
                        Trans_Done <= 1'b0;
                        i2c_sda_oe <= 1'b1;
                        if(go) begin                            //整个模块的启动信号go
                            en_div_cnt <= 1'b1;                 //使能div_cnt计数器,跳转的状态都需要这个计数器
                            if(cmd & STA )                      //起始命令
                                state <= GEN_STA;               //跳转到起始状态
                            else if(cmd & WR)                   //写命令
                                state <= WR_DATA;               //跳转到写数据状态
                            else if(cmd & RD)                   //读命令
                                state <= RD_DATA;               //跳转到读数据状态
                            else 
                                state <= IDLE;                  //都不满足则跳转到IDLE状态
                        end
                        else begin
                            en_div_cnt <= 1'b0;                 //模块未启动
                            state <= IDLE;                      //跳转到IDLE状态,且不使能div_cnt计数器
                        end
                    end
            GEN_STA:begin                                       //i2c_sclk为高电平时,i2c_sda从高电平变成低电平产生起始位
                        if(sclk_plus) begin                     //只有在sclk_plus产生高脉冲的时候,div_cnt计数到最大值
                            if(cnt == 3)                        //cnt计数到3,归0
                                cnt <= 0;
                            else 
                                cnt <= cnt + 1'b1;              //否则计数器自增1
                            case(cnt)                                                   //分四步
                                0:begin i2c_sdat_o <= 1'b1; i2c_sda_oe <= 1'b1; end     //cnt为0时,i2c_sdat_o拉高,三态门输出
                                1:begin i2c_sclk <= 1'b1; end                           //cnt为1时,拉高i2c_sclk
                                2:begin i2c_sdat_o <= 0; i2c_sclk <= 1; end             //cnt为2时,继续拉高i2c_sclk,拉低i2c_sdat_o
                                3:begin i2c_sclk <= 0; end                              //cnt为3时,拉低i2c_sclk
                                default: begin i2c_sdat_o <= 1'b1; i2c_sclk <= 1'b1; end
                            endcase

                            if(cnt == 3) begin
                                if(cmd & WR)
                                    state <= WR_DATA;           //跳转到写数据状态
                                else if(cmd & RD)
                                    state <= RD_DATA;           //跳转到读数据状态
                            end
                        end
                    end
            WR_DATA:begin                                       //写数据时,只有在sclk为低电平的时候,sda才可以变化
                        if(sclk_plus) begin                     //8位数据,每位数据分4步,故cnt计数器计数到31
                            if(cnt == 31)
                                cnt <= 0;
                            else 
                                cnt <= cnt + 1'b1;
                            case(cnt)
                                0,4,8,12,16,20,24,28:                           
                                    begin           
                                        i2c_sdat_o <= Tx_Data[7-cnt[4:2]];      //在GNE_STA状态时,i2c_sclk拉低了,所以此时把Tx_data一位传给i2s_sdat_o
                                        i2c_sda_oe <= 1'b1;                     //三态门输出使能
                                    end
                                1,5,9,13,17,21,25,29:
                                    begin
                                        i2c_sclk <= 1'b1;                       //sclk posedge 拉高i2c_sclk
                                    end
                                2,6,10,14,18,22,26,30:
                                    begin
                                        i2c_sclk <= 1'b1;                       //sclk keep high 继续拉高i2c_sclk
                                    end
                                3,7,11,15,19,23,27,31:
                                    begin
                                        i2c_sclk <= 1'b0;                       //sclk negedge 拉低i2c_sclk,以便在下次传输Tx_Data的一位
                                    end
                                default: begin i2c_sdat_o <= 1; i2c_sclk <= 1; end
                            endcase

                            if(cnt == 31) begin
                                state <= CHECK_ACK;                             //Tx_Data的8位写完之后,跳转到检查应答位状态
                            end
                            
                        end
                    end
            CHECK_ACK:begin
                        if(sclk_plus) begin
                            if(cnt == 3)
                                cnt <= 0;
                            else
                                cnt <= cnt + 1;
                            
                            case(cnt)                                                           //应答位的检查依然分四步完成,使用计数器cnt来控制
                                0:  begin i2c_sda_oe <= 1'b0; i2c_sclk <= 1'b0; end             //拉低i2c_sclk信号,并且设置三态门为输入
                                1:  begin i2c_sclk <= 1; end                                    //拉高i2c_sclk
                                2:  begin ack_o <= i2c_sdat; i2c_sclk <= 1; end                 //拉高i2c_sclk,并且把i2c_sdat赋值给ack_o,
                                3:  begin i2c_sclk <= 0; end                                    //拉低i2c_sclk
                                default:    begin   i2c_sdat_o <= 1'b1; i2c_sclk <= 1'b1; end   //上面连续两次拉高i2c_sclk是为了占空比为50%的i2c_sclk时钟
                            endcase

                            if(cnt == 3) begin
                                if(cmd & STO)
                                    state <= GNE_STO;                                           //跳转到产生停止位状态
                                else begin
                                    state <= IDLE;
                                    Trans_Done <= 1'b1;                                         //一次传输完成
                                end     
                            end
                        end

                      end
            GNE_STO :   begin                                                                   //在i2c_sclk为高电平,i2c_sda从低到高则为停止信号
                            if(sclk_plus) begin                                                 //与产生起始信号类似
                                if(cnt == 3)
                                    cnt <= 0;
                                else
                                    cnt <= cnt + 1'b1;
                                
                                case(cnt)
                                    0:  begin i2c_sdat_o <= 1'b0; i2c_sda_oe <= 1'b1; end       //三态门输出, i2c_sda拉低
                                    1:  begin i2c_sclk <= 1'b1; end                             //拉高i2c_sclk
                                    2:  begin i2c_sdat_o <= 1'b1; i2c_sclk <= 1'b1; end         //拉高i2c_sda,i2c_sclk
                                    3:  begin i2c_sclk <= 1'b1; end
                                    default: begin i2c_sdat_o <= 1'b1; i2c_sclk <= 1'b1; end
                                endcase

                                if(cnt == 3) begin
                                    Trans_Done <= 1'b1;                                         //传输完成
                                    state <= IDLE;  
                                end
                            end
                        end
            
            RD_DATA:    begin                                                                   //取数据
                            if(sclk_plus) begin
                                if(cnt == 31)
                                    cnt <= 0;
                                else
                                    cnt <= cnt + 1'b1;                                          //8位,每位分4步完成
                                
                                case(cnt)
                                0,4,8,12,16,20,24,28:
                                    begin
                                        i2c_sda_oe <= 1'b0;
                                        i2c_sclk <= 0;                          //set data,拉低i2c_sclk,三态门设置为输入
                                    end
                                1,5,9,13,17,21,25,29:
                                    begin
                                        i2c_sclk <= 1'b1;                       //sclk posedge, 拉高i2c_scclk
                                    end
                                2,6,10,14,18,22,26,30:
                                    begin
                                        i2c_sclk <= 1'b1;                       //sclk keep high 利用移位的方法来存储,技巧需掌握
                                        Rx_Data <= {
    
    Rx_Data[6:0], i2c_sdat};
                                    end
                                3,7,11,15,19,23,27,31:                          //sclk negedge
                                    begin
                                        i2c_sclk <= 1'b0;
                                    end
                                default: begin i2c_sdat_o <= 1; i2c_sclk <= 1; end
                                endcase

                                if(cnt == 31) begin
                                    state <= GEN_ACK;
                                end
                            end
                        end
            GEN_ACK:    begin                                                   //产生应答信号
                            if(sclk_plus) begin                                 //分四步完成
                                if(cnt == 3)
                                    cnt <= 0;
                                else
                                    cnt <= cnt + 1'b1;
                                
                                case(cnt)
                                    0:  begin   
                                            i2c_sda_oe <= 1'b1;                 //三态门输出
                                            i2c_sclk <= 1'b0;                   //拉低i2c_sclk
                                            if(cmd & ACK)                       //产生ACK,输出0
                                                i2c_sdat_o <= 1'b0;
                                            else if(cmd & NACK)                 //产生NOACK,输出1
                                                i2c_sdat_o <= 1'b1;
                                        end
                                    1:  begin i2c_sclk <= 1'b1; end
                                    2:  begin i2c_sclk <= 1'b1; end
                                    3:  begin i2c_sclk <= 1'b0; end
                                    default:    begin i2c_sdat_o <= 1; i2c_sclk <= 1; end
                                    
                                endcase

                                if(cnt == 3) begin
                                    if(cmd & STO)
                                        state <= GNE_STO;                       //跳转到产生停止位状态
                                    else begin
                                        state <= IDLE;
                                        Trans_Done <= 1'b1;                     //传输完成
                                    end
                                end
                            end
                        end
            default:    begin 
                            i2c_sclk <= 1'b1;
                            Trans_Done <= 1'b0;
                            i2c_sdat_o <= 1'b0;
                            i2c_sda_oe <= 1'b1;
                            cnt <= 0;
                        end
        endcase
    end 
    
    //在en_div_cnt使能,div_cnt开始奇数,计数大最大值循环计数
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)
            div_cnt <= 20'd0;
        else if(en_div_cnt) begin
            if(div_cnt < SCL_CNT_M)
                div_cnt <= div_cnt + 1'b1;
            else
                div_cnt <= 0;
        end
        else
            div_cnt <= 20'd0;
    end

    assign i2c_sdat = !i2c_sdat_o && i2c_sda_oe ? 1'b0 : 1'bz;              //三态门设置

endmodule

The tested tb file is as follows, writing 1, 2, 3, 4, and 5 to 0x0A, 0x0B, 0x0C, 0x0D, and 0X0F respectively. Then read them out again.

`timescale 1ns/1ns
module i2c_bit_shift_tb;

	reg Clk;
	reg Rst_n;
	
	reg [5:0]Cmd;
	reg Go;
	wire [7:0]Rx_DATA;
	reg [7:0]Tx_DATA;
	wire  Trans_Done;
	wire  ack_o;
	wire i2c_sclk;
	wire i2c_sdat;
	
	pullup PUP (i2c_sdat);
	
	localparam 
		WR =  6'b000001,	// 写请求
		STA = 6'b000010,	//起始位请求
		RD =  6'b000100,	//读请求
		STO = 6'b001000,	//停止位请求
		ACK = 6'b010000,	//应答位请求
		NACK = 6'b100000;	//无应答请求

/*
	i2c_bit_shift i2c_bit_shift(
		.Clk(Clk),
		.Rst_n(Rst_n),
		.Cmd(Cmd),
		.Go(Go),
		.Rx_DATA(Rx_DATA),
		.Tx_DATA(Tx_DATA),
		.Trans_Done(Trans_Done),
		.ack_o(ack_o),
		.i2c_sclk(i2c_sclk),
		.i2c_sdat(i2c_sdat)
	);
*/
	i2c_bit_shift i2c_bit_shift(
		.clk(Clk),
		.rst_n(Rst_n),
		.cmd(Cmd),
		.go(Go),
		.Rx_Data(Rx_DATA),
		.Tx_Data(Tx_DATA),
		.Trans_Done(Trans_Done),
		.ack_o(ack_o),
		.i2c_sclk(i2c_sclk),
		.i2c_sdat(i2c_sdat)
	);

	M24LC04B M24LC04B(
		.A0(0), 
		.A1(0), 
		.A2(0), 
		.WP(0), 
		.SDA(i2c_sdat), 
		.SCL(i2c_sclk), 
		.RESET(~Rst_n)
	);
	
	initial Clk = 1;
	always #10 Clk = ~Clk;
	
	initial begin
		Rst_n = 0;
		Cmd = 0;
		Go = 0;
		Tx_DATA = 0;
		#2001;
		Rst_n = 1;
		#2000;
		
		write_one_byte(8'hA0,8'h0A,8'hd1);
		//#20000;
		write_one_byte(8'hA0,8'h0B,8'hd2);
		//#20000;
		write_one_byte(8'hA0,8'h0C,8'hd3);
		//#20000;
		write_one_byte(8'hA0,8'h0D,8'hd4);
		//#20000;
		write_one_byte(8'hA0,8'h0F,8'hd5);
		//#20000;
		
		read_one_byte(8'hA0,8'h0A);
		//#20000;
		read_one_byte(8'hA0,8'h0B);
		//#20000;
		read_one_byte(8'hA0,8'h0C);
		//#20000;
		read_one_byte(8'hA0,8'h0D);
		//#20000;
		read_one_byte(8'hA0,8'h0F);
		//#20000;
		$stop;	
	end
	
	task write_one_byte;
		input [7:0]device_id;						//器件地址
		input [7:0]mem_address; 					//寄存器地址
		input [7:0]data;							//需要写的数据
		begin
			Cmd = WR | STA;							//写入产生起始位和写数据命令
			Go = 1;									//启动整个模块
			Tx_DATA = device_id;					//把器件地址赋值给Tx_Data
			#20;		
			Go = 0;									//延迟一个时钟周期拉低go信号
			#200;
			@(posedge Trans_Done);					//等待此次传输数据完成
			#20;
			//#20000;
			
			Cmd = WR;								//传入写数据命令
			Go = 1;									//启动整个模块
			Tx_DATA = mem_address;					//把寄存器地址赋值给Tx_Data
			#20;
			Go = 0;									//延迟一个时钟周期拉低go信号
			#200;
			@(posedge Trans_Done);					//等待此次数据传输完成
			//#20000;
			#20
		
			Cmd = WR | STO;							//传入写数据和产生停止位的命令
			Go = 1;									//启动整个模块
			Tx_DATA = data;							//把需要写入的数据赋值给Tx_Data
			#20;
			Go = 0;									//延迟一个时钟周期拉低go信号
			#200;
			@(posedge Trans_Done);					//等待此次传输数据完成
			#20000;	
		end
	endtask
	
	task read_one_byte;
		input [7:0]device_id;						//传入器件地址
		input [7:0]mem_address;						//传入寄存器地址
		begin
			Cmd = WR | STA;							//传入写数据和产生开始位的命令
			Go = 1;									//启动整个模块
			Tx_DATA = device_id;					//把器件地址赋值给Tx_Data
			#20;
			Go = 0;									//延迟一个时钟周期拉低go信号
			#200;
			@(posedge Trans_Done);					//等待此次传输数据完成
			#20000;
		
			Cmd = WR | STO;
			Go = 1;
			Tx_DATA = mem_address;
			#20;
			Go = 0;
			#200;
			@(posedge Trans_Done);
			//#20000;
			#20;
			
			Cmd = WR | STA;
			Go = 1;
			Tx_DATA = device_id | 8'd1;
			#20;
			Go = 0;
			#200;
			@(posedge Trans_Done);
			//#20000;
			#20;
			
			Cmd = RD | NACK | STO;
			Go = 1;
			#20;
			Go = 0;
			#200;
			@(posedge Trans_Done);
			//#20000;
			#20;			
		end
	endtask
	
	

endmodule

The simulation waveform is as follows, which shows that the data was successfully read:
Insert image description here

I2C control module

Since the i2c_bit_shift module can only send one byte of data, writing input or reading output at a time requires writing several bytes. Therefore, several tasks of writing bytes can be assigned to write input and input respectively, both of which start from the IDLE state. However, these tasks are sequential and must wait for the completion of the previous task. At this time, our Trans_done signal comes in handy. Therefore, it can be designed using a state machine.
For a write-once:
Insert image description here
for a read-once:
Insert image description here
there is the following status (encoded with one-hot encoding):

IDLE idle state
WR_REG write status
WAIT_WR_DONE Waiting for write completion status
WR_REG_DONE Write completion status
RD_REG read status
WAIT_RD_DONE Waiting for read completion status
RD_REG_DONE read completion status

State transition diagram:
Insert image description here

The code of the control module is as follows (there are very detailed comments in the code), so I won’t explain it further.


module i2c_ctrl(
    input   wire            clk         ,               //时钟信号
    input   wire            rst_n       ,               //复位信号
    input   wire            wrreg_req   ,               //写请求信号
    input   wire            rdreg_req   ,               //读请求信号
    input   wire    [15:0]  addr        ,               //16位地址输入       
    input   wire            addr_mode   ,               //输入地址的模式,0:8位的地址 1:16位的地址
    input   wire    [7:0]   wrdata      ,               //总线发送的8位数据
    output  reg     [7:0]   rddata      ,               //总线接收的8位数据
    input   wire    [7:0]   device_id   ,               //i2c器件的ID
    output  reg             RW_Done     ,               //读/写完成的标志
    output  reg             ack         ,               //从机是否应答标志
    output  wire            i2c_sclk    ,               //i2c时钟总线
    inout   wire            i2c_sdat  
);


localparam  IDLE        =       7'b0000_001,            //空闲状态
            WR_REG      =       7'b0000_010,            //写状态
            WAIT_WR_DONE=       7'b0000_100,            //等待写完成状态
            WR_REG_DONE =       7'b0001_000,            //写完成状态
            RD_REG      =       7'b0010_000,            //读状态
            WAIT_RD_DONE=       7'b0100_000,            //等待读完成状态
            RD_REG_DONE =       7'b1000_000;            //读完成状态

localparam  WR          =       6'b000_001,             //写数据请求
            STA         =       6'b000_010,             //起始位请求
            RD          =       6'b000_100,             //读数据请求
            STO         =       6'b001_000,             //停止位请求 
            ACK         =       6'b010_000,             //应答位请求
            NACK        =       6'b100_000;             //无应答请求

                                       

reg     [5:0]   cmd         ;
reg     [7:0]   Tx_Data     ;
reg             go          ;
reg     [6:0]   state       ;
reg     [7:0]   cnt         ;
//reg     [15:0]  reg_addr    ;

wire            Trans_Done  ;
wire            ack_o       ;
wire    [7:0]   Rx_Data     ;
wire    [15:0]  reg_addr    ;

i2c_bit_shift i2c_bit_shift (
    .clk        (clk) ,
    .rst_n      (rst_n) ,
    .cmd        (cmd) ,  
    .go         (go) ,  
    .Tx_Data    (Tx_Data) ,  

    .i2c_sclk   (i2c_sclk) ,  
    .i2c_sdat   (i2c_sdat) ,  
    .Trans_Done (Trans_Done) ,  
    .Rx_Data    (Rx_Data) ,  
    .ack_o      (ack_o)    
);  
assign reg_addr = (addr_mode) ? addr : {
    
    addr[7:0], addr[15:8]};
    
    always @(posedge clk or negedge rst_n)
        if(!rst_n) begin
            cmd <= 6'd0;
            Tx_Data <= 8'd0;
            go <= 1'b0;
            rddata <= 8'd0;
            state <= IDLE;
            ack <= 0;
        end
        else begin
            case(state)
                RD_REG: begin                                                       //读寄存器状态包括读数据的四个task
                            state <= WAIT_RD_DONE;                                  //跳转等待读完成状态
                            case(cnt)
                                0:  write_byte(WR | STA, device_id) ;               //产生起始位并写入器件地址
                                1:  write_byte(WR, reg_addr[15:8])  ;               //写入寄存器地址的高8位
                                2:  write_byte(WR, reg_addr[7:0]);                  //写入寄存器地址的低8位
                                3:  write_byte(WR | STA, device_id | 8'd1);         //产生起始位并写入器件地址(最后一位是1,表示读)
                                4:  read_byte(RD | NACK | STO);                     //读数据并且产生NACK,STOP信号
                                default:;
                            endcase
                        end
                WAIT_RD_DONE:begin
                            go <= 1'b0;                                             //拉低go信号
                            if(Trans_Done) begin                                    //等待此次数据传输完成
                                if(cnt <= 3)
                                    ack <= ack | ack_o;
                                case(cnt)
                                    0:  begin cnt <= 1; state <= RD_REG; end        //cnt为0时,cnt加1,跳转读寄存器状态
                                    1:  begin
                                            state <= RD_REG;
                                            if(addr_mode)                           //根据地址模式
                                                cnt <= 2;                           //addr_more为1:16位地址
                                            else
                                                cnt <= 3;                           //addr_mode位0,8位地址
                                        end
                                    2:  begin
                                            cnt <= 3;
                                            state <= RD_REG;
                                        end
                                    3:  begin
                                            cnt <= 4;
                                            state <= RD_REG;
                                        end
                                    4:  state <= RD_REG_DONE;                       //cnt为4时,跳转到读完成状态                   
                                    default:    state <= IDLE;
                                endcase
                            end
                        end
                RD_REG_DONE:begin
                            RW_Done <= 1'b1;                                        //拉高读写完成信号
                            rddata <= Rx_Data;                                      //把读出的数据Rx_Data赋值给rddata
                            state <= IDLE;                                          //跳转到IDLE状态
                        end

                WR_REG: begin
                            state <= WAIT_WR_DONE;                                  //跳转到等待写完成状态
                            case(cnt)
                                0:  write_byte(WR | STA, device_id);                //产生起始位并写入器件地址
                                1:  write_byte(WR, reg_addr[15:8]);                 //写入寄存器地址的高8位
                                2:  write_byte(WR, reg_addr[7:0]);                  //写入寄存器地址的低8位
                                3:  write_byte(WR | STO, wrdata);                   //写入数据并且产生停止位
                                default:;
                            endcase
                        end

                WAIT_WR_DONE:begin
                                go <= 1'b0;                                         //拉低go信号
                                if(Trans_Done) begin                                //等待此次传输数据完成
                                    ack <= ack | ack_o;
                                    case(cnt)
                                        0:  begin cnt <= 1; state <= WR_REG; end    //cnt为0时,跳转写寄存器状态
                                        1:  begin
                                                state <= WR_REG;                    
                                                if(addr_mode)                       //不同的地址模式
                                                    cnt <= 2;                       //决定时写入16位还是8位
                                                else
                                                    cnt <= 3;
                                            end
                                        2:  begin
                                                cnt <= 3;
                                                state <= WR_REG;
                                            end
                                        3:  state <= WR_REG_DONE;                   //cnt为3时,跳转写完成状态
                                        default: state <= IDLE;
                                    endcase
                                end
                            end

                WR_REG_DONE:begin
                                RW_Done <= 1'b1;                                    //拉高读写完成信号
                                state <= IDLE;                                      //跳转到IDLE状态
                            end
                IDLE:       begin
                                cnt <= 0;
                                ack <= 0;
                                RW_Done <= 1'b0;
                                if(wrreg_req)                           //写请求则进入写寄存器状态
                                    state <= WR_REG;
                                else if(rdreg_req)                      //读请求则进入读寄存器状态
                                    state <= RD_REG;
                                else
                                    state <= IDLE;
                            end
                default:    state <= IDLE;
            endcase
end     


//读一个字节的任务
task read_byte;
    input   [5:0]   ctrl_cmd        ;           //传入命令
    begin                       
        cmd <= ctrl_cmd             ;           //将传入的命令赋值给cmd
        go <= 1'b1                  ;           //启动整个模块
    end
endtask

//写一个字节的任务
task write_byte;
    input   [5:0]   ctrl_cmd        ;           //传入命令
    input   [7:0]   wr_byte_data    ;           //传入要写入的8位字节数据
    begin
        cmd <= ctrl_cmd             ;           //将传入的命令赋值给cmd
        Tx_Data <= wr_byte_data     ;           //将传入的8位字节数据赋值给Tx_Data
        go <= 1'b1                  ;           //启动整个模块
    end    
endtask

endmodule



Simulated tb file:

`timescale 1ns/1ns
module i2c_control_tb;

	reg Clk;
	reg Rst_n;
	
	reg wrreg_req;
	reg rdreg_req;
	reg [15:0]addr;
	reg addr_mode;
	reg [7:0]wrdata;
	wire [7:0]rddata;
	reg [7:0]device_id;
	wire RW_Done;
	
	wire  ack;
	wire i2c_sclk;
	wire i2c_sdat;
	
	pullup PUP (i2c_sdat);
	
	i2c_ctrl i2c_control(
		.clk(Clk),
		.rst_n(Rst_n),
		.wrreg_req(wrreg_req),
		.rdreg_req(rdreg_req),
		.addr(addr),
		.addr_mode(addr_mode),
		.wrdata(wrdata),
		.rddata(rddata),
		.device_id(device_id),
		.RW_Done(RW_Done),
		.ack(ack),		
		.i2c_sclk(i2c_sclk),
		.i2c_sdat(i2c_sdat)
	);
	/*
	i2c_control i2c_control(
		.Clk(Clk),
		.Rst_n(Rst_n),
		.wrreg_req(wrreg_req),
		.rdreg_req(rdreg_req),
		.addr(addr),
		.addr_mode(addr_mode),
		.wrdata(wrdata),
		.rddata(rddata),
		.device_id(device_id),
		.RW_Done(RW_Done),
		.ack(ack),		
		.i2c_sclk(i2c_sclk),
		.i2c_sdat(i2c_sdat)
	);
	*/


	M24LC64 M24LC64(
		.A0(0), 
		.A1(0), 
		.A2(0), 
		.WP(0), 
		.SDA(i2c_sdat), 
		.SCL(i2c_sclk), 
		.RESET(~Rst_n)
	);
	
	initial Clk = 1;
	always #10 Clk = ~Clk;
	
	initial begin
		Rst_n = 0;
		rdreg_req = 0;
		wrreg_req = 0;
		#2001;
		Rst_n = 1;
		#2000;
		
		write_one_byte(8'hA0,8'h0A,8'hd1);
		//#20000;
		write_one_byte(8'hA0,8'h0B,8'hd2);
		//#20000;
		write_one_byte(8'hA0,8'h0C,8'hd3);
		//#20000;
		write_one_byte(8'hA0,8'h0D,8'hd4);
		//#20000;
		write_one_byte(8'hA0,8'h0F,8'hd5);
		//#20000;
		
		read_one_byte(8'hA0,8'h0A);
		//#20000;
		read_one_byte(8'hA0,8'h0B);
		//#20000;
		read_one_byte(8'hA0,8'h0C);
		//#20000;
		read_one_byte(8'hA0,8'h0D);
		//#20000;
		read_one_byte(8'hA0,8'h0F);
		//#20000;
		$stop;	
	end
	
	task write_one_byte;
		input [7:0]id;
		input [7:0]mem_address; 
		input [7:0]data;
		begin
			addr = {
    
    8'd0,mem_address};
			device_id = id;
			addr_mode = 1;
			wrdata = data;
			wrreg_req = 1;
			#20;
			wrreg_req = 0;
			@(posedge RW_Done);
			#20000;	
		end
	endtask
	
	task read_one_byte;
		input [7:0]id;
		input [7:0]mem_address;
		begin
			addr = {
    
    8'd0,mem_address};
			device_id = id;
			addr_mode = 1;
			rdreg_req = 1;
			#20;
			rdreg_req = 0;
			@(posedge RW_Done);
			#20000;			
		end
	endtask
	
	

endmodule

It can be seen from the simulation waveform chart that the data was successfully read and written.

Insert image description here

Guess you like

Origin blog.csdn.net/weixin_45614076/article/details/126545998