I2C接口控制器设计与验证(思路+代码+详细注释)

目录

一、I2C协议解析

1.I2C协议基本规定

2.I2C写流程

3.I2C读流程

二、 I2C 控制器实现思路解析

三、I2C传输逻辑模块: i2c_bit_shift 

1.接口设计

2.SCL时钟设计

3.各状态转换(状态机设计)

4.波形分析

四、I2C顶层模块:I2C_control

1.接口设计

2.模块例化

3.6个命令(cmd)和7个状态

4.读和写任务

5.状态机设计

6.仿真

本文是根据B站小梅哥FPGA设计编写。

一、I2C协议解析

I2C由两条总线构成:SDA(串行数据总线)和SCL(串行时钟总线),属于半双工,任意时刻只能有一个主机。(总线上一主多从)

1.I2C协议基本规定

(1)在时钟(SCL)为高电平的时候,数据总线(SDA)必须保持稳定,所以数据总线(SDA)
在时钟(SCL)为低电平的时候才能改变。

(2)在时钟(SCL)为高电平的时候,数据总线(SDA)由高到低的跳变为总线起始信号
在时钟(SCL)为高电平的时候,数据总线(SDA)由低到高的跳变为总线停止信号

(3)应答,当 IIC 主机(不一定是发送端还是接收端)将 8 位数据或命令传出后,会将数据
总线(SDA)释放,即设置为输入,然后等待从机应答(低电平 0 表示应答,1 表示非
应答
),此时的时钟仍然是主机提供的。

2.I2C写流程

提醒:器件地址:有的器件地址在出厂时地址就设置好了,用户不可以更改(例如 OV7670 器件地址为固定的 0x42),有的确定了几位,剩下几位由硬件确定(比如常见的 I2C 接口的EEPROM 存储器,留有 3 个控地址的引脚,由用户自己在硬件设计时确定)。

存储器地址:相当于器件里的存储单元,例如,EEPROM 存储器,内部就是顺序编址的一系列存储单元;型号为 OV7670 的 CMOS 摄像头(OV7670 的该接口叫 SCCB 接口,其实质也是一种特殊的 I2C 协议,可以直接兼容 I2C协议),其内部就是一系列编址的可供读写的寄存器。

(1)主机将SDA设置为输出:发起起始信号;

(2)主机传输器件地址字节,其中最低位为 0,表明为写操作;
(3)主机设置 SDA 为三态门输入,读取从机应答信号;

(4)读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
(5)主机设置 SDA 为三态门输入,读取从机应答信号;
(6)读取应答信号成功,主机设置 SDA 为输出,传输待写入的数据;
(7)设置 SDA 为三态门输入,读取从机应答信号;
(8)读取应答信号成功,主机产生 STOP 位,终止传输。

2字节存储器地址流程差不多不过多赘述

3.I2C读流程

(1)主机设置 SDA 为输出:主机发起起始信号;
(2)主机传输器件地址字节,其中最低位为 0,表明为写操作
(3)主机设置 SDA 为三态门输入,读取从机应答信号;
(4)读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
(5)主机设置 SDA 为三态门输入,读取从机应答信号;
(6)读取应答信号成功,主机发起起始信号
(7)主机传输器件地址字节,其中最低位为 1,表明为读操作
(8)设置 SDA 为三态门输入,读取从机应答信号;
(9)读取应答信号成功,主机设置 SDA 为三态门输入,读取 SDA 总线上的一个字节的
数据;
(10)产生无应答信号(高电平)(无需设置为输出高电平,因为总线会被自动拉高);
(11)主机产生 STOP 位,终止传输。

2字节存储器地址流程差不多不过多赘述

二、 I2C 控制器实现思路解析

无论是读or写,每个步骤可以归结为以上五个步骤:

写2字节操作:①(写器件地址,最后一位为0)->②(写存储器地址)->②(写第一字节数据)->③(写第二字节数据并停止)

读一字节操作:①(写器件地址,最后一位为0)->②(写存储器地址)->①写器件地址,最后一位为1)->⑤(读数据并停止)

其他情况皆适用,这里只例举两个例子。

这一思想到目前你可能无法理解怎么用,当学到本章第四小节I2C顶层控制模块便可理解了。

三、I2C传输逻辑模块: i2c_bit_shift 

1.接口设计

module i2c_bit_shift(
    clk,
    rst_n,
    
    cmd,
    go,
    rx_data,
    tx_data,
    trans_done,
    ack_o,
    i2c_sclk,
    i2c_sdat
    );
    
    input clk;//模块工作时钟,50M 时钟
    input rst_n;//复位,低电平有效
    
    input [5:0] cmd;//控制总线实现各种传输操作的各种命令的组合
    input go;//整个模块的启动使能信号
    output reg [7:0] rx_data;//总线收到的 8 位数据,读操作时读到的数据由此端口输出
    input [7:0] tx_data;//总线要发送的 8 位数据,需要传输的数据经此端口传入该模块
    output reg trans_done;//发送或接收 8 位数据完成标志信号,高标志位
    output reg ack_o;//从机是否应答标志
    output reg i2c_sclk;//i2c 时钟总线
    inout i2c_sdat;//i2c 数据总线

2.SCL时钟设计

我们根据 I2C 的协议标准,这里采用的是快速模式(400kb/s)来作为总线的工作时钟,所以我们将 SYS_CLOCK 设置成 50_000_000,SCL_CLOCK 是用来配置时钟(SCL)总线的频率,通过这两个参数就可以计算出参数 SCL_CNT_M 要计数到多少就能产生期望的总线工作时钟(SCL)频率

每个状态的处理都可以分为四步(如果这里无法理解,接下来状态机代码设计里便可理解了)

//系统时钟采用 50MHz
parameter SYS_CLOCK =50_000_000;
//SCL 总线时钟采用 400kHz
parameter SCL_CLOCK =400_000;
//产生时钟 SCL 计数器最大值
localparam SCL_CNT_M = SYS_CLOCK/SCL_CLOCK/4-1;
reg i2c_sdat_o;//发送数据电平,1:z(因为总线有上拉电阻,高阻态就是拉高);0:0。
reg i2c_sdat_oe;//高电平SDA为输出模式,低电平为输入模式

//得到每个时刻的脉冲信号sclk_plus,作为序列使能信号
reg [19:0]div_cnt;
reg en_div_cnt;//只在数据传输过程中计数,只在状态机中赋值

    always@(posedge clk or negedge rst_n)
    if(!rst_n)
        div_cnt<=20'b0;
    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'b0;
        
    wire sclk_plus =(div_cnt == SCL_CNT_M);//当到达时刻时,置高电平
    
    assign i2c_sdat = !i2c_sdat_o && i2c_sdat_oe ? 1'b0:1'bz;//先计算!i2c_sdat,再&&,最后?://i2c_sdat_oe高电平为输出模式,i2c_sdat_o==1时i2c_sdat=z(由外部上拉电阻把数据线拉高,i2c_sdat_o==0时i2c_sdat=0)

3.各状态转换(状态机设计)

有部分连线在真实过程中不会出现,画出来是为了保证状态机完整性。

从上面的状态转移图可以看出,这里总共分成了 7 个状态,刚开始复位后的默认状态(IDLE)、产生起始信号状态(GEN_STA)、写数据状态(WR_DATA)、读数据状态(RD_DATA)、检测从机是否应答状态(CHECK_ACK)、给从机应答状态(GEN_ACK)、产生停止位状态(GEN_STO)

//状态机7个状态定义:独热码
    localparam
    IDLE = 7'b0000001, //空闲状态
    GEN_STA = 7'b0000010, //产生起始信号
    WR_DATA = 7'b0000100, //写数据状态
    RD_DATA = 7'b0001000, //读数据状态
    CHECK_ACK = 7'b0010000, //检测应答状态
    GEN_ACK = 7'b0100000, //产生应答状态
    GEN_STO = 7'b1000000; //产生停止信号

 Cmd 的 6 个命令(写请求<WR>、起始位请求<STA>、读请求<RD>、停止位请求<STO>、应答位请求<ACK>、无应答请求<NACK>)

//cmd命令定义
    localparam
    WR =6'b000001,//写请求
    STA =6'b000010,//起始位请求
    RD =6'b000100,//读请求
    STO =6'b001000,//停止位请求
    ACK =6'b010000,//应答位请求
    NACK =6'b100000;//无应答请求

开始状态转移吧:

    reg [4:0] cnt;//用来记录sclk_plus次数(1-4)
    reg [7:0] state;
    
    always@(posedge clk or negedge rst_n)
    if(!rst_n)begin
        rx_data<=0;
        i2c_sdat_oe<=1'b0;
        en_div_cnt<=1'b0;
        i2c_sdat_o<=1'b1;
        trans_done<=1'b0;
        ack_o<=1'b0;
        state<=IDLE;
        cnt<=0;
    end    
    else begin
        case(state)
            IDLE://写器件地址操作的 Cmd 命令里面的条件会首先跳转到 GEN_STA这个状态,没有go则留在IDLE状态,分频计数器不开启
                begin
                    trans_done <= 1'b0;
                    i2c_sdat_oe <= 1'd1;
                    if (go) begin
                        en_div_cnt <= 1'b1;//开始分频序列机计数
                        if (cmd & STA)
                            state <= GEN_STA;
                        else if (cmd & WR)
                            state <= WR_DATA;
                        else if (cmd & RD)
                             state <= RD_DATA;
                        else
                             state <= IDLE;
                        end
                    else begin
                        en_div_cnt <= 1'b0;
                        state <= IDLE;
                    end
                end
                
           GEN_STA://产生开始信号:SCL高电平时,SDA输出模式下由高拉低
                begin
                    if (sclk_plus) begin//传送1bit信号分为四个时刻(SCL_CLOCK一个周期分为4部分)
                        if (cnt == 3)
                            cnt <= 0;
                        else
                            cnt <= cnt + 1'b1;
                        case (cnt)
                            0:begin i2c_sdat_o <= 1; i2c_sdat_oe <= 1'd1;end//拉高数据总线,sda输入模式
                            1:begin i2c_sclk <= 1;end//拉高时钟线
                            2:begin i2c_sdat_o <= 0; i2c_sclk <= 1;end//拉低sda,scl保持高电平
                            3:begin i2c_sclk <= 0;end//拉低scl
                            default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
                        endcase
                        if (cnt == 3) begin//判断开始后是读or写
                            if (cmd & WR)
                                state <= WR_DATA;
                            else if (cmd & RD)
                                state <= RD_DATA;
                        end
                    end
                end  
                
            WR_DATA:
                begin
                    if (sclk_plus) begin
                        if (cnt == 31)
                            cnt <= 0;
                        else
                            cnt <= cnt + 1'b1;
                        case (cnt)
                            0,4,8,12,16,20,24,28://把 7 位器件地址和最低位(方向位)的 0 直接赋值给 Tx_DATA,从最高位开始传给i2c_sdat_o
                            begin
                                i2c_sdat_o <= tx_data[7-cnt[4:2]];//cnt[4:2]即第几个第一时刻
                                i2c_sdat_oe <= 1'd1;
                            end
                            1,5,9,13,17,21,25,29:begin i2c_sclk<=1;end //将总线时钟(SCL,代码里面 i2c_sclk)拉高
                            2,6,10,14,18,22,26,30:begin i2c_sclk<=1;end //将总线时钟继续保持为高电平
                            3,7,11,15,19,23,27,31:begin i2c_sclk <=0;end //将时钟总线 i2c_sclk 拉为低电平  
                            default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
                        endcase
                        if (cnt == 31) begin
                            state <= CHECK_ACK;
                        end
                end
            end
            
            CHECK_ACK:
                begin
                    if(sclk_plus)begin
                        if (cnt == 3)
                            cnt <= 0;
                        else
                            cnt <= cnt + 1'b1;
                        case(cnt)
                            0:begin i2c_sdat_oe <= 1'd0; i2c_sclk <= 0;end//sda数据总线为输入模式
                            1:begin i2c_sclk <= 1;end//将总线时钟 i2c_sclk 拉高
                            2:begin ack_o <= i2c_sdat; i2c_sclk <= 1;end//总线时钟继续保持为高电平,将数据总线数据输入到ack_o,此时判断对方产生是否产生应答只需要判断 ack_o 的值是 0(应答)还是 1(无应答)
                            3:begin i2c_sclk <= 0;end//将时钟总线 i2c_sclk 拉为低电平
                            default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
                        endcase
                        if(cnt == 3)begin
                            if(cmd & STO)
                                state <= GEN_STO;
                            else begin
                                state <= IDLE;//继续写/读
                                trans_done <= 1'b1;
                            end
                        end
                    end
                end
                
            GEN_STO://在时钟(SCL,代码里面是 i2c_sclk)为高电平的时候,数据总线(SDA)由低电平到高电平的跳变就一个停止信号
                begin
                    if(sclk_plus)begin
                        if(cnt == 3)
                            cnt <= 0;
                        else
                            cnt <= cnt + 1'b1;
                        case(cnt)
                            0:begin i2c_sdat_o <= 0; i2c_sdat_oe <= 1'd1;end//将 i2c_sdat_o设置为 0,i2c_sdat_oe 使能
                            1:begin i2c_sclk <= 1;end//将总线时钟 i2c_sclk 拉高
                            2:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end//将 i2c_sdat_o 设置为1,让外部上拉电阻将数据总线 i2c_sdat 拉成高电平
                            3:begin i2c_sclk <= 1;end//将时钟总线 i2c_sclk 拉为高电平
                            default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;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;
                    case (cnt)
                        0,4,8,12,16,20,24,28:
                            begin
                            i2c_sdat_oe <= 1'd0;
                            i2c_sclk <= 0;
                            end //将总线设置为输入,即i2c_sdat_oe 设置为 0,i2c_sclk 拉为低电平
                        1,5,9,13,17,21,25,29:begin i2c_sclk<=1;end //sclkposedge
                        2,6,10,14,18,22,26,30:
                            begin
                            i2c_sclk <= 1;
                            rx_data <= {rx_data[6:0],i2c_sdat};//读取数据总线 i2c_sdat 的值到 Rx_DATA,每次将读取到的数据放到 Rx_DATA 的最低位,读取一次,左移一次
                            end //sclk keep high
                        3,7,11,15,19,23,27,31:begin i2c_sclk<=0;end //sclknegedge
                        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_sdat_oe <= 1'd1;
                        i2c_sclk <= 0;
                        if(cmd & ACK)
                            i2c_sdat_o <= 1'b0;//拉低(应答)
                        else if(cmd & NACK)
                            i2c_sdat_o <= 1'b1;//拉高(不应答)
                    end
                    1:begin i2c_sclk <= 1;end
                    2:begin i2c_sclk <= 1;end
                    3:begin i2c_sclk <= 0;end
                    default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
                endcase
                if(cnt == 3)begin
                    if(cmd & STO)
                        state <= GEN_STO;
                    else begin
                        state <= IDLE;
                        trans_done <= 1'b1;
                    end
                end
            end
        end
        
        default:state<=IDLE; 
    endcase    
end   
        
endmodule

以上代码连起来便是完整设计代码,可直接复制粘贴使用

接下来进行波形仿真:本实验采用的是镁光官网提供的 EEPROM 仿真模型,仿真模型 24LC04B.v 文件,在本篇文章顶部可自行下载

`timescale 1ns / 1ps

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 DUT(
    .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)
);

    always #10 Clk = ~Clk;
    initial begin
        Clk = 1;
        Rst_n = 0;
        Cmd = 6'b000000;
        Go = 0;
        Tx_DATA = 8'd0;
        #2001;
        Rst_n = 1;
        #2000;
        
        //写数据操作,往 EEPROM 器件的 B1 地址写数据
        //第一次:起始位+EEPROM 器件地址(7 位)+写方向(1 位)
        Cmd = STA | WR;//按位或
        Go = 1;
        Tx_DATA = 8'hA0 | 8'd0;//最后一位为0:写操作
        @ (posedge Trans_Done);//说明字段传输完成,进入下一状态
        Go = 0;
        #200;
        
        //第二次:写 EEPROM 的 8 位寄存器地址
        Cmd = WR;
        Go = 1;
        Tx_DATA = 8'hB1;//写地址 B1
        @ (posedge Trans_Done);
         Go = 0;
        #200;
        
        //第三次:写 8 位数据 + 停止位
        Cmd = WR | STO;
        Go = 1;
        Tx_DATA = 8'hda;//写数据 DA
        @ (posedge Trans_Done);
        Go = 0;
        #200;
        
        #5000000; //仿真模型的两次操作时间间隔
        
        //读数据操作,从 EEPROM 器件的 B1 地址读数据
        //第一次:起始位+EEPROM 器件地址(7 位)+写方向(1 位)
        Cmd = STA | WR;
        Go = 1;
        Tx_DATA = 8'hA0 | 8'd0;//写方向 
        @ (posedge Trans_Done);
        Go = 0;
        #200;
        
        //第二次:写 EEPROM 的 8 位寄存器地址
        Cmd = WR;
        Go = 1;
        Tx_DATA = 8'hB1;//写地址 B1
        @ (posedge Trans_Done);
        Go = 0;
        #200;
        
        //第三次:起始位+EEPROM 器件地址(7 位)+读方向(1 位)
        Cmd = STA | WR;
        Go = 1;
        Tx_DATA = 8'hA0 | 8'd1;//读方向
        @ (posedge Trans_Done);
        Go = 0;
        #200;
        
        //第四次:读 8 位数据 + 停止位
        Cmd = RD | STO;
        Go = 1;
        @ (posedge Trans_Done);
        Go = 0;
        #200;
        
        #2000;
        $stop;
    end
endmodule

4.波形分析

可以看到头和尾有两块很密集的波形,前面的是写操作后面的读操作,我们先看写(放大)

这张图上state显示出来的部分只有00000100,但不是只有这一个状态,其他状态放大波形可看到,这里不过多赘述了。

我们再来看看读操作部分吧。

四、I2C顶层模块:I2C_control

对于一个单字节读操作,可以分为四步:

(1)起始位+写数据(7 位器件 ID + 1 位读写控制位(这里是 0,表示写)),等待从
机应答。
(2)写数据(8 位 EEPROM 的寄存器地址),等待从机应答。
(3)起始位+写数据(7 位器件 ID + 1 位读写控制位(这里是 1,表示读)),等待从机应答。
(4)读数据(从 EEPROM 中读出 8 位数据)+ 应答位(根据需要给出应答0或者无应答1)+ 停止位。

总的来说就是写了三次 1 字节的数据,读了一次 1 字节的数据。为了让代码更简洁这里用两个 task 一个是用来写字节(write_byte),另一个是读字节(read_byte)。

写字节的 task 里面就将要写的字节数据准备好(赋值给 i2c_bit_shift 模块的 Tx_DATA 端口),同时涉及到的传给 i2c_bit_shift 模块的 Cmd 命令也准备好,同时触发 Go 信号,这样就可以通过
i2c_bit_shift 模块将要写的数据发送到总线上了。

同理,读字节的 task 里面将涉及到的传给i2c_bit_shift 模块的 Cmd 命令也准备好,同时触发 Go 信号,这样就可以通过 i2c_bit_shift 模块将总线上的数据读取出来了。

1.接口设计

module i2c_control(
    Clk,
    Rst_n,
    
    wrreg_req,
    rdreg_req,
    addr,
    addr_mode,
    wrdata,
    rddata,
    device_id,
    RW_Done,
    
    ack,
    
    i2c_sclk,
    i2c_sdat
    );
    input Clk;
    input Rst_n;
    
    input wrreg_req;//写请求
    input rdreg_req;//读请求
    input [15:0] addr;//寄存器地址16位
    input addr_mode;//单字节地址和双字节寄存器地址
    input [7:0] wrdata;//要写入从机数据,是由Tx_DATA准备好最后再赋值给他
    output reg [7:0] rddata;//存储读到的数据,也是读到Rx_DATA后再传给它
    input [7:0] device_id;//器件地址
    output reg RW_Done;//读写完成信号
    
    output reg ack;//从机应答信号
    
    output i2c_sclk;//时钟总线
    inout i2c_sdat;//数据总线

    wire [15:0] reg_addr;
    
    assign reg_addr = addr_mode?addr:{addr[7:0],addr[15:8]};//双字节地址先传高八位再传低八位,所以若为单字节地址把地址有效部分【7:0】放到高位

2.模块例化

    wire [7:0] Rx_DATA;
    reg [7:0] Tx_DATA;
    wire Trans_Done;
    wire ack_o;
    reg Go;
    reg [5:0] Cmd;

//模块例化
    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)
);

3.6个命令(cmd)和7个状态

//六个命令
localparam
    WR   =6'b000001,//写请求
    STA  =6'b000010,//起始位请求
    RD   =6'b000100,//读请求
    STO  =6'b001000,//停止位请求
    ACK  =6'b010000,//应答位请求
    NACK =6'b100000;//无应答请求

//七个状态
localparam
    IDLE         = 7'b0000001, //空闲状态
    WR_REG       = 7'b0000010, //写寄存器状态
    WAIT_WR_DONE = 7'b0000100, //等待写寄存器完成状态
    WR_REG_DONE  = 7'b0001000, //写寄存器完成状态
    RD_REG       = 7'b0010000, //读寄存器状态
    WAIT_RD_DONE = 7'b0100000, //等待读寄存器完成状态
    RD_REG_DONE  = 7'b1000000; //读寄存器完成状态

4.读和写任务

目的是为了简化后续代码

   task read_byte;
        input [5:0]Ctrl_Cmd;
        begin
            Cmd <= Ctrl_Cmd;
            Go <= 1'b1;
        end
    endtask
    
    task write_byte;
        input [5:0]Ctrl_Cmd;
        input [7:0]Wr_Byte_Data;
        begin
            Cmd <= Ctrl_Cmd;
            Tx_DATA <= Wr_Byte_Data;
            Go <= 1'b1;
        end
    endtask

5.状态机设计

    reg [6:0] state;
    reg [7:0] cnt;
    
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)begin
        Cmd<=6'b0;
        Tx_DATA<=8'b0;
        Go<=1'b0;
        rddata<=0;
        state<=IDLE;
        ack<=0;
    end
    else begin
        case(state)
            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
                
             WR_REG:
                begin
                    state <= WAIT_WR_DONE;//每次进入写请求时都会先进入等待写完成阶段(这一阶段的目的是判断存储器地址字节数并接收从机ACK信号)
                    case(cnt)
                        0:write_byte(WR | STA, device_id);
                        1:write_byte(WR, reg_addr[15:8]);
                        2:write_byte(WR, reg_addr[7:0]);//若为单字节地址应该跳过这个步骤,在状态WAIT_WR_DONE中执行
                        3:write_byte(WR | STO, wrdata);
                        default:;
                    endcase
                end
                
             WAIT_WR_DONE:
                begin
                    Go <= 1'b0;
                    if(Trans_Done)begin
                        ack <= ack | ack_o;
                        case(cnt)
                            0: begin cnt <= 1; state <= WR_REG;end
                            1: begin
                                state <= WR_REG;
                                if(addr_mode)//addr_mode==1即为双字节地址,从2开始继续传输低8位地址,反之为单字节跳过步骤2,从3开始
                                    cnt <= 2;
                                else
                                    cnt <= 3;
                            end
                            2: begin
                                cnt <= 3;
                                state <= WR_REG;
                            end
                            3:state <= WR_REG_DONE;
                            default:state <= IDLE;
                        endcase
                    end
                end 
                
            WR_REG_DONE:
                begin
                    RW_Done <= 1'b1;//完成写操作标志
                    state <= IDLE;
                end
                
           RD_REG:
                begin
                    state <= WAIT_RD_DONE;
                    case(cnt)
                        0:write_byte(WR | STA, device_id);
                        1:write_byte(WR, reg_addr[15:8]);
                        2:write_byte(WR, reg_addr[7:0]);
                        3:write_byte(WR | STA, device_id | 8'd1);//最低位是1,读操作命令(一定不要混淆主机发送读命令和主机的写操作,这一步是主机写操作并告诉从机它需要读数据了)
                        4:read_byte(RD | NACK | STO);
                        default:;
                    endcase
                end
                
           WAIT_RD_DONE:
                begin
                    Go <= 1'b0;
                    if(Trans_Done)begin
                        if(cnt <= 3)
                            ack <= ack | ack_o;
                        case(cnt)
                            0: begin cnt <= 1; state <= RD_REG;end
                            1: begin
                                state <= RD_REG;
                                if(addr_mode)
                                    cnt <= 2;
                                else
                                    cnt <= 3;
                            end
                            2: begin
                                cnt <= 3;
                                state <= RD_REG;
                            end
                            3:begin
                                cnt <= 4;
                                state <= RD_REG;
                            end
                                4:state <= RD_REG_DONE;
                            default:state <= IDLE;
                        endcase
                    end
                end
                
             RD_REG_DONE:
                begin
                    RW_Done <= 1'b1;
                    rddata <= Rx_DATA;
                    state <= IDLE;
                end
                
             default:state<=IDLE;
        endcase
    end
endmodule

以上为所有完整代码。

6.仿真

接下来是波形仿真:依然需要用到仿真模型 24LC04B.v 文件

`timescale 1ns / 1ps

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_control DUT(
    .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)
);

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;
        rdreg_req = 0;
        wrreg_req = 0;
        #2001;
        Rst_n = 1;
        #2000;
        
        write_one_byte(8'hA0,8'h0A,8'hd1);
        write_one_byte(8'hA0,8'h0B,8'hd2);
        write_one_byte(8'hA0,8'h0C,8'hd3);
        write_one_byte(8'hA0,8'h0D,8'hd4);
        
        read_one_byte(8'hA0,8'h0A);
        read_one_byte(8'hA0,8'h0B);
        read_one_byte(8'hA0,8'h0C);
        read_one_byte(8'hA0,8'h0D);
        $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 = 0;
            wrdata = data;
            wrreg_req = 1;
            #20;
            wrreg_req = 0;
            @(posedge RW_Done);
            #5000000;//一定要等够5ms
        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 = 0;
            rdreg_req = 1;
            #20;
            rdreg_req = 0;
            @(posedge RW_Done);
            #5000000;//一定要等够5ms
        end
    endtask
    
endmodule

插播一条:之前做仿真第二次写操作一直收不到从机ACK,找了好久原因才发现24LC04B 需要 5ms 的写周期时间,之前写的仿真代码周期时间设短了,主机在从机未完成内部写周期时发起新操作,从机可能不响应。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值