【FPGA】十一、I2C通信回环

文章目录

前言

一、I2C简介

二、I2C原理

2.1、I2C物理层

2.2、I2C协议层

2.2.1、I2C协议

2.2.2、I2C数据传输格式

2.2.3、I2C写操作

2.2.4、I2C读操作

三、项目设计

3.1、任务需求

3.2、状态机设计

3.3、程序代码

3.4、仿真验证

总结


前言

        在前面的文章内容中我们提到常用的三个低速串行通信总线,即uart、I2C和SPI,uart串口协议前面我们已经对它做了一个详细的说明了,相信大家也都理解了它的原理,还是比较简单的。今天我们就来对I2C协议作一些简单的说明与介绍,并采用I2C协议实现通信回环功能,深入理解I2C主机与从机的时序以及其中的判断逻辑。


一、I2C简介

        I2C的英文全称为Inter Integrated Circuit,即集成电路总线,是由Philips半导体公司在八十年代初设计出来的一种简单、双向、二线制总线标准,多用于主机和从机在数据量不大且传输距离短的情况下的主从通信。

        I2C总线是一种两线式串行总线,由时钟线SCL与数据线SDA构成通信线路,即可用来发送数据,也可接收数据, 是一种半双工通信协议,主要用于连接微控制器及其外围设备。主机启动总线,并产生时钟用于传输数据,此时任何接收数据的器件均被认为是从机。I2C器件一般采用开漏结构与总线相连,所以SCL和SDA均需要上拉电阻,也正因如此,当总线空闲时,这两条线都处于高电平状态,当连接到总线上的任何器件输出低电平,都会使总线拉低,表明占用总线进行通信。


二、I2C原理

2.1、I2C物理层

         如上图所示,I2C物理层有两条总线,一条是串行时钟线IIC_SCL,一条是双向传输的串行数据线IIC_SDA。所有连接到I2C总线设备上的串行数据SDA都连接到IIC_SDA上,各个设备的时钟线SCL都连接到总线的IIC_SCL上,I2C总线上的每个设备都有自己唯一的设备地址,来确保不同设备之间访问的准确性。

        I2C总线支持多主机和注册两种工作方式,通信工作在主从工作方式。在主从工作方式下,系统中只有一个主机,其他器件都是具有I2C总线的外围从机。在主场工作方式中,书记启动数据的发送(发送起始信号)并产生时钟信号,数据发送完成后,发出停止信号结束通信。

2.2、I2C协议层

2.2.1、I2C协议

        I2C总线在传输过程中也是需要遵循一定的协议进行通信的,在I2C协议数据的传输工程中有三个类型的信号:开始信号、结束信号与应答信号。

        ① 开始信号:当I2C总线处于空闲状态时,串行时钟线SCL和串行数据线SDA由于上拉电阻的原因都处于高电平状态,如果此时主机想要发起一次通信,就需要在SCL为高电平时将SDA数据线拉低,产生一个起始信号,表明通信的开始。

        ② 结束信号:当主机的一次写完成或者一次读完成后,主机在SCL为高电平时,将SDA数据线由低电平跳变为高电平,产生一个停止信号,表明一次通信的结束。

        ③ 应答信号:接收数据的I2C从机在接收到8bit数据后,向发送数据的I2C主机发送特定的低电平脉冲,表示已经收到数据并且数据正确。如果收到的数据不正确,那么就不会发送应答信号给主机,表示通信故障。

         上图为I2C协议整体时序图,在起始信号之前为空闲状态,起始信号到停止信号之间为数据传输状态,主机可以向从机写数据,也可以读取从机输出的数据,数据的传输是由双向数据线SDA完成,当停止信号产生以后,总线再次回到空闲状态。

        在I2C总线上进行数据传输时,数据的传输也是按照一定的规律进行的,否则就会导致数据混乱,数据错误等现象发生。数据的传输规律为:我们在起始信号之后,主机开始发生传输的数据,在串行时钟线SCL为低电平状态时,SDA允许改变传输的数据位(1为高电平,0为低电平),在SCL为高电平状态时,SDA要求保持稳定,相当与一个时钟周期传输1bit数据,经过8个时钟周期以后,传输8bit数据,即一个字节。如果第9个时钟周期SCL为高电平时,SDA未被检测到低电平,视为非应答,表明此次数据传输失败。第9个时钟周期末,从机释放SDA以使主机继续传输数据,如果主机发送停止信号,表明此次传输结束。这里我们需要注意的是,数据是以一个字节为单位进行传输的,其最先发送的是最高位。

2.2.2、I2C数据传输格式

         当使用I2C协议进行数据传输时,首先需要在SCL时钟线为高电平时将SDA数据线拉低,来产生一个起始信号,然后按照从高到低的位序发送器件地址,一般为7bit,第8bit位为读写控制位R/W,该位为0表示主机对从机进行写操作,当该位为1时表示主机对从机进行读操作,然后接收从机响应。

        当发送完第一个字节(7位器件地址+读/写控制位)并收到从机正确的应答信号后就开始发送字地址。一般而言,每个兼容I2C协议的器件,内部总会有可供读写的寄存器或者存储器,当我们对一器件中的存储单元进行读写时,首先要指定存储单元的地址(字地址),然后再向该地址写入数据或者读取数据。字地址的长度一般为1个或者2个字节,这主要取决于器件内部存储单元的数量,在本次实验中我们使用的字地址是1个字节。

        主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位为0,则表明主机要向从机写入数据,从机此时就处于接收数据状态,等待主机的数据写入。主机写数据也分为单字节写和页写。顾名思义,单字节写就是一次通信只向从机写入一个字节数据,页写就是一次通信向从机写入多个字节的数据。如果一个字节写入完成后发送停止命令,就是单字节写,如果继续发送下一字节数据,就是页写。

        如果读写控制位为1,表明主机要向从机读取数据,此时主机作为接收方,从机作为发送方发送数据给主机。主机读取数据也有三种情况:当前地址读、随机地址读和连续地址读。当前地址读是指在一次读或写操作后发起读操作,由于I2C器件在读写操作后,其内部的地址指针自动加一,因此当前地址读可以读取下一字地址的数据。由于当前地址读极不方便读取任意地址单元的数据,所以后面也就有了随机地址读(本次实验所采用的就是随机地址读,时序见后文)。至于连续地址读,也就是在随机地址读的基础上继续读取数据罢了。

        最后就是主机来结束本次通信了,由主机发送停止位(在SCL时钟线高电平时,将SDA数据线拉高),从机检测到停止位后释放总线,使之处于空闲状态,这一次的I2C通信也就结束了。

2.2.3、I2C写操作

        上图为I2C的单字节写操作,具体的时序为:起始信号→从机设备地址+写控制字→应答信号→字地址→应答信号→数据→应答信号→停止信号。

2.2.4、I2C读操作

         上图为I2C随机地址读操作,具体的时序为:起始信号→从机设备地址+写控制字→应答信号→字地址→应答信号→开始信号→从机设备地址+读控制字→应答信号→数据→应答信号→停止信号。

        注:I2C随机地址读操作中有两次起始信号,两次设备地址,第一次设备地址+写控制字叫做虚写,这是因为我们并不是要真的写数据,而是通过这种续写操作使地址指针指向虚写操作中字地址的位置,等待从机应答后,我们就可以从中读取数据了。


三、项目设计

3.1、任务需求

        通过FPGA开发板,模拟I2C回环时序传输数据,写操作采用单字节写模式,读操作采用随机地址读模式,主机发送数给从机,然后再从从机读取数据回来,实现主从机回环的功能。

        在这里我将工程分为了两个模块,一个是I2C主机模块,用来产生I2C时序进行控制I2C总线,一个是I2C从机模块,用来检测I2C总线变化以及接收或者发送数据给主机进行通信。

3.2、状态机设计

        这里我采用的是三段式状态机,这样理解起来容易一点,后期也方便维护和修改。I2C主机和I2C从机模块都采用了状态机的编写方法,当然这种方法也不是唯一的,状态机的划分也不是唯一的,根据自己的理解来就可以了。

① I2C_master状态机:

IDLE:空闲状态

START:开始状态,产生开始信号

WRITE:写状态,写控制字、字地址、数据等

READ:读状态,读应答信号,数据

RACK:接收应答状态,接收从机返回的应答信号

SACK:发送应答状态,读数据完成后发送应答给从机

STOP:停止状态,产生停止位

② I2C_slave状态机:

IDLE:空闲状态

START:开始状态,检测到开始信号,进入开始状态

CTRL_BYTE:控制字节状态,接收从机设备地址+读/写控制字,并发送应答信号给主机

WORD_ADDR:字节地址状态,接收主机发送的字节地址,并发送应答信号给主机

RECE_DATA:接收数据状态,接收主机发送过来的数据,并发送应答信号给主机

SEND_DATA:发送数据状态,当主机读数据时,发送数据给主机

STOP:停止状态,检测到停止信号,进入停止状态

3.3、程序代码

① I2C_master程序代码:

/*========================================*\
    filename    : i2c_master.v
    description : i2c主机模块
    time        : 2022-12-05 
    author      : 卡夫卡与海
\*========================================*/

module i2c_master(
    input               clk         ,
    input               rst_n       ,

    input               req         ,//请求
    input       [3:0]   cmd         ,//命令
    input       [7:0]   din         ,//数据(给从机)

    output      [7:0]   dout        ,//数据(读从机的数据)
    output              done        ,
    output              slave_ack   ,//从机应答

    output              i2c_scl     ,//i2c时钟线
    input               i2c_sda_i   ,//i2c数据线(输入)
    output              i2c_sda_o   ,//i2c数据线(输出)
    output              i2c_sda_oe   //i2c输出使能
    );

//参数定义
//i2c时钟参数,速率200Kbit/s
parameter   SCL_PERIOD = 250,//i2c时钟周期
            SCL_HALF   = 125,//时钟周期的一半
            LOW_HLAF   = 65 ,//低电平中间点
            HIGH_HALF  = 190;//高电平中间点

//i2c命令参数
parameter   CMD_START = 4'b0001,//开始命令
            CMD_WRITE = 4'b0010,//写命令
            CMD_READ  = 4'b0100,//读命令
            CMD_STOP  = 4'b1000;//停止命令

//状态机参数
localparam  IDLE  = 7'b000_0001,//空闲
            START = 7'b000_0010,//开始(产生起始位)
            WRITE = 7'b000_0100,//写(写控制字节,字节地址,数据)
            RACK  = 7'b000_1000,//接收应答
            READ  = 7'b001_0000,//读(读从机数据)
            SACK  = 7'b010_0000,//发送应答
            STOP  = 7'b100_0000;//停止(产生停止位)

//信号定义

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

    reg     [8:0]       cnt_scl     ;//产生i2c时钟
    wire                add_cnt_scl ;
    wire                end_cnt_scl ;
    reg     [3:0]       cnt_bit     ;//传输数据 bit计数器
    wire                add_cnt_bit ;
    wire                end_cnt_bit ;
    reg     [3:0]       bit_num     ;
    
    reg                 scl         ;//输出寄存器
    reg                 sda_out     ;
    reg                 sda_out_en  ;

    reg     [7:0]       rx_data     ;//接收数据
    reg     [7:0]       m_dout      ;//数据

    reg                 rx_ack      ;//接收应答
    reg     [3:0]       command     ;
    reg     [7:0]       tx_data     ;//发送数据

    wire                idle2start  ;//状态跳转规律
    wire                idle2write  ; 
    wire                idle2read   ; 
    wire                start2write ; 
    wire                start2read  ; 
    wire                write2rack  ; 
    wire                read2sack   ; 
    wire                rack2stop   ; 
    wire                sack2stop   ; 
    wire                rack2idle   ; 
    wire                sack2idle   ; 
    wire                stop2idle   ; 

//状态机
    always @(posedge clk or negedge rst_n) begin 
        if (rst_n==0) begin
            state_c <= IDLE ;
        end
        else begin
            state_c <= state_n;
        end
    end
    
    always @(*) begin 
        case(state_c)  
            IDLE :begin
                if(idle2start)
                    state_n = START ;
                else if(idle2write)
                    state_n = WRITE ;
                else if(idle2read)
                    state_n = READ ;
                else 
                    state_n = state_c ;
            end
            START :begin
                if(start2write)
                    state_n = WRITE ;
                else if(start2read)
                    state_n = READ ;
                else 
                    state_n = state_c ;
            end
            WRITE :begin
                if(write2rack)
                    state_n = RACK ;
                else 
                    state_n = state_c ;
            end
            RACK :begin
                if(rack2stop)
                    state_n = STOP ;
                else if(rack2idle)
                    state_n = IDLE ;
                else 
                    state_n = state_c ;
            end
            READ :begin
                if(read2sack)
                    state_n = SACK ;
                else 
                    state_n = state_c ;
            end
            SACK :begin
                if(sack2stop)
                    state_n = STOP ;
                else if(sack2idle)
                    state_n = IDLE ;
                else 
                    state_n = state_c ;
            end
            STOP :begin
                if(stop2idle)
                    state_n = IDLE ;
                else 
                    state_n = state_c ;
            end
            default : state_n = IDLE ;
        endcase
    end
    
    assign idle2start  = state_c==IDLE  && (req && (cmd&CMD_START));
    assign idle2write  = state_c==IDLE  && (req && (cmd&CMD_WRITE));
    assign idle2read   = state_c==IDLE  && (req && (cmd&CMD_READ ));
    assign start2write = state_c==START && (end_cnt_bit && (command&CMD_WRITE));
    assign start2read  = state_c==START && (end_cnt_bit && (command&CMD_READ ));
    assign write2rack  = state_c==WRITE && (end_cnt_bit);
    assign read2sack   = state_c==READ  && (end_cnt_bit);
    assign rack2stop   = state_c==RACK  && (end_cnt_bit && (command&CMD_STOP ));
    assign sack2stop   = state_c==SACK  && (end_cnt_bit && (command&CMD_STOP ));
    assign rack2idle   = state_c==RACK  && (end_cnt_bit && (command&CMD_STOP ) == 0);
    assign sack2idle   = state_c==SACK  && (end_cnt_bit && (command&CMD_STOP ) == 0);
    assign stop2idle   = state_c==STOP  && (end_cnt_bit);
    
//计数器
    always @(posedge clk or negedge rst_n) begin 
        if (rst_n==0) begin
            cnt_scl <= 0; 
        end
        else if(add_cnt_scl) begin
            if(end_cnt_scl)
                cnt_scl <= 0; 
            else
                cnt_scl <= cnt_scl+1 ;
        end
    end
    assign add_cnt_scl = (state_c != IDLE);
    assign end_cnt_scl = add_cnt_scl  && cnt_scl == (SCL_PERIOD)-1 ;

    always @(posedge clk or negedge rst_n) begin 
        if (rst_n==0) begin
            cnt_bit <= 0; 
        end
        else if(add_cnt_bit) begin
            if(end_cnt_bit)
                cnt_bit <= 0; 
            else
                cnt_bit <= cnt_bit+1 ;
        end
    end
    assign add_cnt_bit = (end_cnt_scl);
    assign end_cnt_bit = add_cnt_bit  && cnt_bit == (bit_num)-1 ;

    always  @(*)begin
        if(state_c == WRITE | state_c == READ) begin
            bit_num = 8;
        end
        else begin 
            bit_num = 1;
        end 
    end
//command
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            command <= 0;
        end
        else if(req)begin
            command <= cmd;
        end
    end

//tx_data
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            tx_data <= 0;
        end
        else if(req)begin
            tx_data <= din;
        end
    end

//scl
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            scl <= 1'b1;
        end
        else if(idle2start | idle2write | idle2read)begin//开始发送时,拉低
            scl <= 1'b0;
        end
        else if(add_cnt_scl && cnt_scl == SCL_HALF-1)begin 
            scl <= 1'b1;
        end 
        else if(end_cnt_scl && ~stop2idle)begin 
            scl <= 1'b0;
        end 
    end

//sda_out
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            sda_out <= 1'b1;
        end
        else if(state_c == START)begin          //发起始位
            if(cnt_scl == LOW_HLAF)begin       //时钟低电平时拉高sda总线
                sda_out <= 1'b1;
            end
            else if(cnt_scl == HIGH_HALF)begin    //时钟高电平时拉低sda总线 
                sda_out <= 1'b0;                //保证从机能检测到起始位
            end 
        end 
        else if(state_c == WRITE && cnt_scl == LOW_HLAF)begin  //scl低电平时发送数据   并串转换
            sda_out <= tx_data[7-cnt_bit];      
        end 
        else if(state_c == SACK && cnt_scl == LOW_HLAF)begin  //发应答位
            sda_out <= (command&CMD_STOP)?1'b1:1'b0;
        end 
        else if(state_c == STOP)begin //发停止位
            if(cnt_scl == LOW_HLAF)begin       //时钟低电平时拉低sda总线
                sda_out <= 1'b0;
            end
            else if(cnt_scl == HIGH_HALF)begin    //时钟高电平时拉高sda总线 
                sda_out <= 1'b1;                //保证从机能检测到停止位
            end 
        end 
    end

//sda_out_en  总线输出数据使能
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            sda_out_en <= 1'b0;
        end
        else if(idle2start | idle2write | read2sack | rack2stop)begin
            sda_out_en <= 1'b1;
        end
        else if(idle2read | start2read | write2rack | stop2idle)begin 
            sda_out_en <= 1'b0;
        end 
    end

//rx_data       接收读入的数据
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            rx_data <= 0;
        end
        else if(state_c == READ && cnt_scl == HIGH_HALF)begin
            rx_data[7-cnt_bit] <= i2c_sda_i;    //串并转换
        end
        else begin
            rx_data <= rx_data;
        end
    end

//m_dout  
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        m_dout <= 0;
    end
    else if(state_c == READ && end_cnt_bit)begin
        m_dout <= rx_data;
    end
    else begin
        m_dout <= m_dout;
    end
end

//rx_ack
    always  @(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            rx_ack <= 1'b1;
        end
        else if(state_c == RACK && cnt_scl == HIGH_HALF)begin
            rx_ack <= i2c_sda_i;
        end
        else begin
            rx_ack <= 1'b1;
        end
    end

//输出信号

    assign i2c_scl    = scl         ;
    assign i2c_sda_o  = sda_out     ;
    assign i2c_sda_oe = sda_out_en  ;

    assign dout = m_dout;
    assign done = rack2idle | sack2idle | stop2idle;
    assign slave_ack = rx_ack;

endmodule

② I2C_slave程序代码:

/*========================================*\
    filename    : i2c_slave.v
    description : i2c从机模块
    time        : 2023-01-02
    author      : 卡夫卡与海
\*========================================*/

module i2c_slave(
    input               clk         ,//系统时钟  50MHZ
    input               rst_n       ,//系统复位

    input   [7:0]       data_in     ,//数据(发送给主机)

    input               i2c_scl     ,//i2c时钟
    input               sda_in      ,//数据输入
    output  reg         sda_out     ,//数据输出
    output  reg         sda_oen     ,//数据输出使能

    output  reg  [7:0]  word_addr   ,//寄存器地址
    output  reg  [7:0]  data_out     //数据(从机接收的数据)
);
//参数定义
parameter   SLAVE_ADDR = 7'h3C;//从机地址

parameter   CLOCK = 20;//20ns  50MHZ

parameter   SCL_TIME      = 2500/CLOCK,//接收速率400Kbit/s
            SCL_HIGH_HOUD = 1200/CLOCK,//高电平保持时间
            SCL_LOW_HOUD  = 1300/CLOCK,//低电平保持时间
            SAMPLE_TIME   = 800/CLOCK ,//采样时间
            CHANGE_TIME   = 200/CLOCK ;//跳变时间

//状态机
parameter   IDLE      = 7'b000_0001,//空闲
            START     = 7'b000_0010,//开始
            CTRL_BYTE = 7'b000_0100,//控制字节
            WORD_BYTE = 7'b000_1000,//字节地址
            SEND_DATA = 7'b001_0000,//发送数据
            RECE_DATA = 7'b010_0000,//接收数据
            STOP      = 7'b100_0000;//停止

//信号定义
reg     [6:0]   state_c         ;//现态
reg     [6:0]   state_n         ;//次态

reg             scl_0           ;//对i2c_scl打拍
reg             scl_1           ;
wire            scl_podge       ;//scl上升沿
wire            scl_nedge       ;//scl下降沿

reg             sda_in_0        ;//sda_in打拍
reg             sda_in_1        ;
wire            sda_in_podge    ;//sda_in上升沿
wire            sda_in_nedge    ;//sda_in下降沿

wire            i2c_start       ;//起始信号
wire            i2c_stop        ;//停止信号

reg     [6:0]   cnt_scl_low     ;//计数scl低电平
wire            add_cnt_scl_low ;
wire            end_cnt_scl_low ;

reg     [3:0]   cnt_bit         ;//bit计数器
wire            add_cnt_bit     ;
wire            end_cnt_bit     ;

reg             wr_or_rd        ;//判断当前读写操作 (0:写  1:读)

reg     [7:0]   receive_buff    ;//数据缓存

reg     [6:0]   chir_addr       ;//缓存从机地址

reg     [7:0]   tx_data         ;//发送给主机的数据缓存

wire            idle2start      ;//状态跳转规律
wire            start2ctrl      ;
wire            ctrl2word       ;
wire            ctrl2send       ;
wire            ctrl2idle       ;
wire            word2rece       ;
wire            send2stop       ;
wire            rece2start      ;
wire            rece2stop       ;
wire            stop2idle       ;

//对SCL打拍,检测上升沿、下降沿
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        scl_0 <= 1'b1;
        scl_1 <= 1'b1;
    end
    else begin
        scl_0 <= i2c_scl;
        scl_1 <= scl_0;
    end
end
assign scl_podge = ~scl_1 && scl_0;//SCL上升沿
assign scl_nedge = scl_1 && ~scl_0;//SCL下降沿

//对SDA_in打拍,检测上升沿、下降沿
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        sda_in_0 <= 1'b1;
        sda_in_1 <= 1'b1;
    end
    else begin
        sda_in_0 <= sda_in;
        sda_in_1 <= sda_in_0;
    end
end
assign sda_in_podge = sda_in_0 && ~sda_in_1;//sda_in上升沿
assign sda_in_nedge = ~sda_in_0 && sda_in_1;//sda_in下降沿

assign i2c_start = (i2c_scl && sda_in_nedge) ? 1'b1 : 1'b0;//起始信号
assign i2c_stop = (i2c_scl && sda_in_podge) ? 1'b1 : 1'b0;//停止信号

//FSM
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        state_c <= IDLE;
    end
    else begin
        state_c <= state_n;
    end
end

always @(*)begin
    case(state_c)
        IDLE      : begin
            if(idle2start)begin
                state_n = START;
            end
            else begin
                state_n = state_c;
            end
        end
        START     : begin
            if(start2ctrl)begin
                state_n = CTRL_BYTE;
            end
            else begin
                state_n = state_c;
            end
        end
        CTRL_BYTE : begin
            if(ctrl2word)begin
                state_n = WORD_BYTE;
            end
            else if(ctrl2send)begin
                state_n = SEND_DATA;
            end
            else if(ctrl2idle)begin
                state_n = IDLE;
            end
            else begin
                state_n = state_c;
            end
        end
        WORD_BYTE : begin
            if(word2rece)begin
                state_n = RECE_DATA;
            end
            else begin
                state_n = state_c;
            end
        end
        SEND_DATA : begin
            if(send2stop)begin
                state_n = STOP;
            end
            else begin
                state_n = state_c;
            end
        end
        RECE_DATA : begin
            if(rece2start)begin
                state_n = START;
            end
            else if(rece2stop)begin
                state_n = STOP;
            end
            else begin
                state_n = state_c;
            end
        end
        STOP      : begin
            if(stop2idle)begin
                state_n = IDLE;
            end
            else begin
                state_n = state_c;
            end
        end
        default: begin
            state_n = IDLE;
        end
    endcase
end

assign idle2start = (state_c ==IDLE     ) && (i2c_start);
assign start2ctrl = (state_c ==START    ) && (scl_nedge);
assign ctrl2word  = (state_c ==CTRL_BYTE) && (end_cnt_bit && chir_addr==SLAVE_ADDR && ~wr_or_rd);
assign ctrl2send  = (state_c ==CTRL_BYTE) && (end_cnt_bit && chir_addr==SLAVE_ADDR && wr_or_rd);
assign ctrl2idle  = (state_c ==CTRL_BYTE) && (end_cnt_bit && chir_addr!=SLAVE_ADDR && ~wr_or_rd);
assign word2rece  = (state_c ==WORD_BYTE) && (end_cnt_bit);
assign send2stop  = (state_c ==SEND_DATA) && (cnt_bit=='d8 && scl_0 && sda_in_0);
assign rece2start = (state_c ==RECE_DATA) && (i2c_start);
assign rece2stop  = (state_c ==RECE_DATA) && (i2c_stop);
assign stop2idle  = (state_c ==STOP     ) && ((~wr_or_rd) || (wr_or_rd && i2c_stop));

//cnt_scl_low   计数SCL低电平数
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cnt_scl_low <= 0;
    end
    else if(add_cnt_scl_low)begin
        if(end_cnt_scl_low)begin
            cnt_scl_low <= 0;
        end
        else begin
            cnt_scl_low <= cnt_scl_low + 1'b1;
        end
    end
end
assign add_cnt_scl_low = state_c >= CTRL_BYTE && ~scl_0;
assign end_cnt_scl_low = add_cnt_scl_low && cnt_scl_low == (SCL_LOW_HOUD - 1);

//cnt_bit 
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cnt_bit <= 0;
    end
    else if(add_cnt_bit)begin
        if(end_cnt_bit)begin
            cnt_bit <= 0;
        end
        else begin
            cnt_bit <= cnt_bit + 1'b1;
        end
    end
end
assign add_cnt_bit = state_c >= CTRL_BYTE && state_c < STOP && (scl_nedge || send2stop);
assign end_cnt_bit = add_cnt_bit && cnt_bit == 'd8;

//wr_or_rd   判断当前是读写状态
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        wr_or_rd <= 1'b0;
    end
    else if(stop2idle)begin
        wr_or_rd <= 1'b0;
    end
    else if(state_c==CTRL_BYTE && cnt_bit == 7 && scl_0)begin
        wr_or_rd <= sda_in_0;
    end
    else begin
        wr_or_rd <= wr_or_rd;
    end
end

//receive_buff  数据缓存
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        receive_buff <= 0;
    end
    else if(state_c == CTRL_BYTE && scl_0)begin
        receive_buff[7 - cnt_bit] <= sda_in_0; //缓存控制字节
    end
    else if(state_c == WORD_BYTE && scl_0)begin
        receive_buff[7 - cnt_bit] <= sda_in_0; //缓存寄存器地址
    end
    else if(state_c == RECE_DATA && scl_0)begin
        receive_buff[7 - cnt_bit] <= sda_in_0; //缓存数据
    end
    else begin
        receive_buff <= receive_buff;
    end
end

//chir_addr  从机地址缓存
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        chir_addr <= 0;
    end
    else if(state_c == CTRL_BYTE && cnt_bit=='d8)begin
        chir_addr <= receive_buff[7:1];//高7位是从机地址,最后一位是读写位
    end
    else begin
        chir_addr <= chir_addr;
    end
end

//word_addr  寄存器地址缓存
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        word_addr <= 0;
    end
    else if(state_c == WORD_BYTE && cnt_bit=='d8)begin
        word_addr <= receive_buff;
    end
    else begin
        word_addr <= word_addr;
    end
end

//data_out  数据缓存
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        data_out <= 0;
    end
    else if(state_c == RECE_DATA && cnt_bit=='d8)begin
        data_out <= receive_buff;
    end
    else begin
        data_out <= data_out;
    end
end

//tx_data  发送给主机的数据缓存
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        tx_data <= 0;
    end
    else if(state_c == SEND_DATA)begin
        tx_data <= data_in;
    end
end

//sda_out     sda_oen
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        sda_out <= 1'bz;
        sda_oen <= 1'b0;
    end
    else if(state_c==IDLE && ~i2c_scl)begin
        sda_out <= 1'bz;
        sda_oen <= 1'b0;
    end
    else if(state_c == SEND_DATA)begin
        if(cnt_scl_low == CHANGE_TIME)begin
            if(cnt_bit <= 'd7)begin
                sda_out <= tx_data[7 - cnt_bit];
                sda_oen <= 1'b1;
            end
            else begin
                sda_out <= 1'bz;
                sda_oen <= 1'b0;
            end
        end
        else begin
            sda_out <= sda_out;
            sda_oen <= sda_oen;
        end
    end
    else begin
        if((cnt_scl_low==CHANGE_TIME) && (chir_addr==SLAVE_ADDR))begin
            case(cnt_bit)
            'd0 : begin
                sda_out <= 1'bz;
                sda_oen <= 1'b0;
            end
            'd8 : begin
                sda_out <= 1'b0;
                sda_oen <= 1'b1;
            end
            default : begin
                sda_out <= 1'bz;
                sda_oen <= 1'b0;
            end
            endcase
        end
    end
end

endmodule

③ 仿真代码:

`timescale 1ns/1ns
/*========================================*\
    filename    : i2c_master_tb.v
    description : i2c仿真模块
    time        : 2023-01-02 
    author      : 卡夫卡与海
\*========================================*/

module i2c_master_tb();

//时钟复位输入
    reg             clk       ;
    reg             rst_n     ;

//激励输入
    reg             req       ;
    reg     [3:0]   cmd       ;
    reg     [7:0]   din       ;

    //输出
    wire    [7:0]   dout      ;
    wire            done      ;
    wire            ack       ;
    wire            scl       ;
    wire            sda_i     ;
    wire            sda_o     ;
    wire            sda_oe    ;

    wire            sda_oen   ;
    wire   [7:0]    word_addr ; 
    wire   [7:0]    data_out  ; 

//时钟周期定义
parameter CYCLE    = 20;

//复位时间定义
parameter RST_TIME = 3 ;

//i2c命令参数
parameter   CMD_START = 4'b0001,//开始命令
            CMD_WRITE = 4'b0010,//写命令
            CMD_READ  = 4'b0100,//读命令
            CMD_STOP  = 4'b1000;//停止命令

//模块例化
i2c_master u_i2c_master(
    /*input               */.clk         (clk       ),
    /*input               */.rst_n       (rst_n     ),

    /*input               */.req         (req       ),
    /*input       [3:0]   */.cmd         (cmd       ),
    /*input       [7:0]   */.din         (din       ),

    /*output      [7:0]   */.dout        (dout      ),
    /*output              */.done        (done      ),
    /*output              */.slave_ack   (ack       ),
    /*output              */.i2c_scl     (scl       ),
    /*input               */.i2c_sda_i   (sda_i     ),
    /*output              */.i2c_sda_o   (sda_o     ),
    /*output              */.i2c_sda_oe  (sda_oe    )   
);

i2c_slave u_i2c_slave(
    /*input               */.clk         (clk),//系统时钟
    /*input               */.rst_n       (rst_n),//系统复位

    /*input      [7:0]    */.data_in     (data_out),//外部数据输入(给主机读取的数据)

    /*input               */.i2c_scl     (scl),//I2C时钟输入
    /*input               */.sda_in      (sda_o),//I2C数据线(输入)
    /*output              */.sda_out     (sda_i),//I2C数据线(输出)
    /*output              */.sda_oen     (sda_oen),//使能

    /*output     [7:0]    */.word_addr   (word_addr),//寄存器地址
    /*output     [7:0]    */.data_out    (data_out)//主机发送的数据

);

task traffic_gen;   
    input       [7:0]       data    ;
    input       [3:0]       command ;
    begin 
        #2;
        req = 1'b1;
        din = data;
        cmd = command;
        #(CYCLE*1);
        req = 1'b0;
        @(negedge done);
        #(CYCLE*1);
    end 
endtask 

//产生时钟
initial begin
    clk = 1;
    forever
    #(CYCLE/2)
    clk=~clk;
end

//产生复位
initial begin
    rst_n = 0;
    #(CYCLE*RST_TIME);
    rst_n = 1;
end

//激励
initial begin
    #1;
    req = 0 ;
    cmd = 0 ;
    din = 0 ;
    #(10*CYCLE);

/****************************
        字节写
****************************/

    traffic_gen(8'b1100_0110,{CMD_START | CMD_WRITE});//发起始位 + 写控制字
    traffic_gen(8'hB2,CMD_WRITE);                  //写字地址
    traffic_gen(8'hb2,{CMD_WRITE |CMD_STOP});     //发数据 + 停止位
    #(50*CYCLE);


    traffic_gen(8'b0111_1000,{CMD_START | CMD_WRITE});//发起始位 + 写控制字
    traffic_gen(8'hB3,CMD_WRITE);                  //写字地址
    traffic_gen(8'hc9,{CMD_WRITE |CMD_STOP});     //发数据 + 停止位
    #(50*CYCLE);

/****************************
        随机地址读
****************************/

    traffic_gen(8'b0111_1000,{CMD_START | CMD_WRITE});//发起始位 + 写控制字
    traffic_gen(8'hB3,CMD_WRITE);              //写字地址
    traffic_gen(8'b0111_1001,{CMD_START | CMD_WRITE});//发起始位 + 发读控制字
    traffic_gen(8'h00,{CMD_READ | CMD_STOP}); //读数据 + 发停止位

    #(100*CYCLE);
    $stop;

end


endmodule

        这里为了使I2C从机模块更加符合I2C总线协议,我在仿真的时候首先发送了一个错误的设备地址,此时I2C从机模块不能够作出任何相应,不能够对总线做任何操作,不然就会占用总线,导致挂载在该总线上的其他设备不工作,这也是我在工作当中发现的问题,希望对大家有帮助。

3.4、仿真验证

① I2C_master仿真波形:

 ② I2C_slave仿真波形:

        从仿真波形来看,I2C主机向从机发起通信时,首先发送的设备地址是63,但是我这里定义的从机设备地址是3C,所以从机并未做出任何响应,也没有对I2C总线做出任何操作,当后面检测到主机发送的设备地址是3C时,从机拉低SDA数据线,发送一个应答信号给主机,表示与I2C主机建立了通信,紧接着主机发送字节地址B3,然后发送数据C9给从机,从机收到寄存器地址与数据后将其缓存下来并输出出去,然后主机再读取字节地址B3中的数据,最后主机收到的数据是C9,表示此次I2C主从机回环实验成功,数据收发正确。

        这里还要注意主从机的采样速率,如果主机发送数据的速率比从机采样数据的速率快,那么很可能采样到的数据就会丢帧,我前面也实验过,会导致采样的数据不正确。我这里从机从采样速率在400Kbit/s左右,主机的发送速率在200Kbit/s左右,采样的数据是正确的,如果将主机的速率降低到100Kbit/s,仍然能够采样到正确的数据。


总结

        I2C协议虽然是两线协议,其实它的时序还是有点复杂的,主机的逻辑还好,特别是从机,千万不能一直占用总线不释放,这样的话就导致总线上的其他从设备无法工作,这个原因我调试了好几天,希望各位小伙伴们别踩这个坑。

        我这里的从机模块的设备地址是直接定义在模块里面的,这就有一点限制的模块的移植性,感兴趣的小伙伴也可以改写由外部输入,这样就可以随时进行更改了,但是原理都是一样的,理解才是最重要的。

        详细的协议还是得看手册,我这里是根据EEPROM的手册写的,只不过没有做读写EEPROM的程序项目,搞这个回环的目的就是很多时候我们FPGA是要做I2C从机的,I2C从机的资料并不是很多,很多都是主机的,索性我就手撕一个回环,主从机都有,以后可以拿来直接用,哈哈!!!

  • 4
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FPGA中实现I2C通信,需要使用FPGA的IO资源和相应的I2C控制器。下面是一个示例代码,展示了如何在FPGA中使用Verilog语言实现基本的I2C通信。 ```verilog module I2C_Master ( input wire clk, input wire reset, output wire sda, output wire scl ); reg [7:0] slaveAddress; reg [7:0] regAddress; reg [7:0] data; reg [7:0] readData; reg write; reg read; reg start; reg stop; reg done; // I2C状态机 reg [2:0] state; localparam IDLE = 3'b000; localparam START = 3'b001; localparam ADDR_SEND = 3'b010; localparam DATA_SEND = 3'b011; localparam RESTART = 3'b100; localparam ADDR_READ = 3'b101; localparam DATA_READ = 3'b110; localparam STOP = 3'b111; always @(posedge clk or posedge reset) begin if (reset) begin state <= IDLE; sda <= 1'b1; scl <= 1'b1; done <= 1'b0; end else begin case(state) IDLE: begin if (start) state <= START; end START: begin state <= ADDR_SEND; sda <= 1'b0; scl <= 1'b1; end ADDR_SEND: begin state <= DATA_SEND; sda <= slaveAddress[7]; end DATA_SEND: begin if (write) begin state <= STOP; sda <= data[7]; end else if (read) begin state <= RESTART; sda <= 1'b1; // SDA变为输入,准备接收数据 end end RESTART: begin state <= ADDR_READ; sda <= 1'b0; scl <= 1'b1; end ADDR_READ: begin state <= DATA_READ; sda <= slaveAddress[7]; end DATA_READ: begin state <= STOP; readData[7] <= sda; // 读取数据 end STOP: begin state <= IDLE; sda <= 1'b1; scl <= 1'b0; done <= 1'b1; end endcase end end endmodule ``` 以上是一个基本的I2C主机模块,它使用`clk`时钟信号、`reset`复位信号以及其他控制和数据信号来实现I2C通信。你可以根据具体的FPGA平台和外设的要求进行适当的修改和扩展。 需要注意的是,上述代码仅演示了I2C主机的发送和接收过程,具体的寄存器地址、数据和设备地址需要根据实际情况进行配置和处理。 此外,还需要根据FPGA开发工具的特定语法和约束文件进行综合、布局和时序约束等操作,以生成对应的比特流文件并在FPGA上实现I2C通信功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值