基于FPGA的I2C接口控制器(包含单字节和多字节读写)-Anlogic-安路社区-FPGA CPLD-ChipDebug

基于FPGA的I2C接口控制器(包含单字节和多字节读写)

01
概括
前文对IIC的时序做了详细的讲解,还有不懂的可以获取TI的IIC数据手册查看原理。通过手册需要知道的是IIC读、写数据都是以字节为单位,每次操作后接收方都需要进行应答。主机向从机写入数据后,从机接收数据,需要把总线拉低来告知主机,前面发送的数据已经被接收。主机在读取从机数据后,如果还需要继续读取数据,就要对从机做出应答,否则不应答。
另一个需要注意的是数据在时钟的低电平中间进行赋值,数据线在时钟线的高电平期间状态不能发生变化。这是因为在时钟线高电平期间,数据线从高电平变为低电平,从机会认为主机发送了起始位,数据线从低电平变为高电平,从机会认为主机发送停止位。
在起始位和停止位之间,可以存在任意字节长度的操作。也就是说从机寄存器地址和寄存器数据的宽度都没有限制,根据具体的芯片确定。其实很好理解,比如EEPROM支持单字节的读写操作和突发的页读写操作,这就是上述原因的结果。还有部分芯片的寄存器地址可能是3个字节,读写的数据也是几个字节,这也是可以的。
使用FPGA接口实现IIC的难度会比UART和SPI高那么一点,原因在于双向IO的控制。双向IO一般使用三态门实现,当然xilinx这类器件还可以使用IOBUFR这种原语实现,会比使能简单很多,但是本文设计的是通用模块,没有平台限制的代码,所以不会使用原语。
网上关于FPGA的IIC控制器代码还是挺多的,但是基本上对寄存器地址、数据长度都有限制,而且不支持突发读写,如果需要这些功能还是需要独立开发,所以本文就打算设计一个支持寄存器地址长度可变、数据长度可变、支持突发读、写的接口模块,且没有平台限制,一次解决所有问题。
最后在eeprom上验证单字节读写和突发读写。
02
分析设计
首先通过几个时序图来具体分析一下单字节、多字节读写时序和多字节的地址读写时序,进而总结出设计思路。
如下图所示,是eeprom芯片的单字节写时序,该时序每次只写入单个存储单元的单字节数据,所以依次发送起始位、器件地址、写指示位、从机应答、写入地址、从机应答、写入数据、从机应答、停止位即可。

图片[1]-基于FPGA的I2C接口控制器(包含单字节和多字节读写)-Anlogic-安路社区-FPGA CPLD-ChipDebug

图1 某eeprom的单字节写时序
下图是该eeprom实现页写的时序,页写与sdram这些的突发写本质是一样的,就是发送起始位值的地址,后面连续输入后续地址的数据即可。与上图的区别是在第一次写入数据,从机应答之后,主机不发送停止位,而是继续写入数据,便可以向从机的下一个地址写入数据,从机应答之后继续写入数据,直到写入指定个数的数据且从机应答之后,发送停止位结束写入。
页写入表面上看只节省了发送起始位、器件地址、寄存器地址的时间,但其实节省更多的是单字节写入时中间等待的时间。eeprom两次写入间隔有一个时间要求,芯片手册会给出这个数据的最大值,有的芯片是3ms,有的是5ms,有的10ms。这个时间表示芯片接收数据后,把数据存储到内部指定地址所需要的最大时间。eeprom芯片的页写其实节省最大的是这个时间。

图片[2]-基于FPGA的I2C接口控制器(包含单字节和多字节读写)-Anlogic-安路社区-FPGA CPLD-ChipDebug

图2 某eeprom的页写时序
下图是eeprom的单字节读时序,因为可以读取任意存储位置的数据,所以在发送读指令之前,需要告知存储芯片本次读取数据的存储地址是多少。因此下图读时序中会先发送起始位、器件地址、写指示信号、从机应答、寄存器地址、从机应答。
将需要读取数据的存储地址写入芯片之后,接下来就是读取该地址的数据了。先发送重复起始位(不发送停止位的原因是在多主机系统中避免被别的主机抢占总线控制权),然后发送器件地址、读指示位、从机应答,之后从机将该寄存器的数据输出到总线上,主机在时钟高电平中部读取数据总线上的数据即可,从机输出一字节数据后主机不应答从机,最后主机发送停止位结束本次读取操作。
图片[3]-基于FPGA的I2C接口控制器(包含单字节和多字节读写)-Anlogic-安路社区-FPGA CPLD-ChipDebug图片[4]-基于FPGA的I2C接口控制器(包含单字节和多字节读写)-Anlogic-安路社区-FPGA CPLD-ChipDebug图3 某eeprom的单字节读时序
下图是eeprom的页读时序,与上图的区别在于主机接收从机发送的第一字节数据后,主机把数据总线拉低,对从机做出应答,从机就会输出下一个存储地址的数据,从而实现连续地址的数据读取。主机接收到指定个数的数据后,应答时将数据总线拉高,不应答从机,然后发送停止位,结束本次读取。

图片[5]-基于FPGA的I2C接口控制器(包含单字节和多字节读写)-Anlogic-安路社区-FPGA CPLD-ChipDebug

图4 某eeprom的页读时序
最后再来查看一个IIC接口的温湿度传感器的读时序,下图时序中的指令数据其实与上述的存储地址是一致的。下图中包含2字节的命令,在发送寄存器地址时需要传输两次,先传输高字节数据。后面寄存器的数据也是16位的,并且后面还包含一字节的CRC校验码,所以读取数据时,需要连续读取3字节数据。
主机读取前两字节数据时,也需要对从机做出应答,在读完3字节数据后,主机不再对从机做出应答,然后发送停止位结束本次读操作。

图片[6]-基于FPGA的I2C接口控制器(包含单字节和多字节读写)-Anlogic-安路社区-FPGA CPLD-ChipDebug

图5 某温湿度传感器IIC读时序
通过上面对几个时序图的分析可知,页读取(图4)与从同一个寄存器读取多个字节数据(图5)的时序原理是一样,就是读前面字节数据后应答从机,最后一字节数据时不应答从机。
综上,IIC的读写时序中,器件地址的长度一般是固定的,根据不同芯片设计,寄存器地址的长度不固定,读写的数据长度也是不固定的,所以在设计驱动模块时,这两部分需要根据实际情况自动改变。
最简单的想法就是通过一个计数器来对已经发送的寄存器地址字节数和读写数据的字节数计数。写入的寄存器地址字节数达到要求后再跳转到别的状态,而读写数据时,只有读写指定字节数数据时,主机才能发送停止位。
在fpga实现时,寄存器的地址字节数、读写数据字节数可以通过parameter常量进行设置,便于使用时修改,且不会产生多余的电路。
03
设计实现
下表是该模块的端口信号,开始读写信号start必须在模块空闲(rdy为高电平)时才能拉高,拉高一个时钟周期即可。

表1 端口信号列表

信号 位宽 I/O 含义
clk 1 I 系统时钟信号,默认100MHz
rst_n 1 I 系统复位,默认低电平有效;
start 1 I 读、写操作开始信号,高电平有效。
rw_flag 1 I 读、写指示信号,高电平表示读。
reg_addr 可变 I 寄存器地址信号。
wdata 可变 I 写入寄存器的数据。
rdata 可变 O 从寄存器读出的数据。
rdata_vld 1 O 读出数据有效指示信号,高电平有效。
rdy 1 O 模块忙闲指示信号,高电平表示模块空闲。
scl 1 O IIC的串行时钟线。
sda 1 IO IIC的双向串行数据线;

本次设计采用一个状态机嵌套三个计数器作为主体架构实现,状态机包括7个状态。状态转换图如下所示,“将发送1字节加上应答位划分一个状态”,这句话不完全状态。

图片[7]-基于FPGA的I2C接口控制器(包含单字节和多字节读写)-Anlogic-安路社区-FPGA CPLD-ChipDebug

图6 状态机状态转化图
将发送起始位、器件地址、写标志位划分为W_DEVICE_ADDR状态,发送读数据划分为WDATA状态(这个状态可能会读取多个字节数据,根据设置跳转),发送读、写寄存器地址划分为W_REG_ADDR状态(这个状态依旧可能发送多个字节的数据),发送重复起始位、器件地址、读标志位划分为R_DEVIC E_ADDR状态,接收数据线的数据划分为RDATA状态(这个状态依旧可能发送多个字节的数据),最后STOP状态发送停止位。
分频计数器div_cnt在状态机不处于空闲状态时,对系统时钟进行计数,从而产生IIC时钟信号scl,同时将scl的低电平、高电平的中间分别生成wr_flag和rd_flag标志信号,wr_flag位高电平表示可以对IIC数据线赋值,rd_flag高电平表示可以在此时读取IIC数据线上的数据。
计数器bit_cnt用于记录每次读写数据的位数,当分频计数器计数结束时加1。状态机在不同的状态,bit_cnt计数器的最大值不一样,当状态机处于W_DEVICE_A DDR或R_DEVICE_ADDR状态时,需要发送起始位、器件地址、读写标志位、应答位,所以bit_cnt计数器最大值为10-1,而状态机位于WDATA,W_REG_ADDR,RDATA状态时,每次读写的单位都是1字节数据、应答位,所以bit_cnt计数器最大值为9-1。
计数器byte_cnt用于记录状态机处于WDATA,W_REG_ADDR,RDATA状态时,接收或者发送的数据字节数。当状态机处于上述三个状态且计数器bit_cnt计数结束时加1,根据需要读写的寄存器地址字节数和读写数据字节数,确定该计数器在各个状态下的最大值。
状态机的跳转与三个计数器的结束条件有效,比较简单,此处不做过多介绍,看代码即可。
只需要注意一下下面几个信号的变化即可,首先注意模块有几个parameter常量,包括系统时钟的频率、IIC时钟的频率、IIC的从机器件地址、读写寄存器的字节数、读写数据的字节数,对应代码如下所示。

module iic_drive #(
    parameter           FCLK                    =   100_000_000         ,//系统时钟频率,默认100MHz。
    parameter           FSCL                    =   400_000             ,//IIC时钟频率,默认400KHz。
    parameter           REG_ADDR_BYTE_NUM       =   1                   ,//寄存器地址字节数;
    parameter      DATA_BYTE_NUM           =   1                ,//读写数据字节数。
    parameter      DEVICE_ADDR             =   7'b1010000           //器件地址。
)(
    input                          clk                ,//系统时钟信号;
    input                          rst_n              ,//系统复位信号,低电平有效;

    input                                           start               ,//开始进行读写操作;
    input                                           rw_flag             ,//读写标志信号,高电平表示读操作,低电平表示写操作;
    input               [REG_ADDR_BYTE_NUM*8-1 : 0] reg_addr            ,//寄存器地址,读写操作时共用的地址信号;
    input               [DATA_BYTE_NUM*8 - 1 : 0]   wdata               ,//写数据;

    output  reg         [DATA_BYTE_NUM*8 - 1 : 0]   rdata               ,//读数据信号;
    output  reg                                     rdata_vld           ,//读数据输出使能信号,高电平有效;
    output  reg                                     rdy                 ,//模块忙闲指示信号,位高电平时可以接收上游模块的读写使能信号;

    output  reg                                     scl                 ,//IIC的时钟信号;
    inout                                           sda                 ,//IIC的双向数据信号;
    output  reg                                     ack_flag             //高电平表示应答失败;
);

 

当接收到上游模块的读写开始信号(start为高电平)时,将寄存器地址、写数据、读写状态信号暂存,便于后续读写过程中使用,读写寄存器的地址和数据信号全部采用参数化设计,不需要人为修改信号位宽。将器件地址和起始位、写指示位拼接,便于后续使用。对应代码如下所示:

//暂存器件地址和起始位还有写指示位。
    assign device_addr = {1'b0,DEVICE_ADDR,1'b0};

    //开始信号有效时,把待发送的信号暂存。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            wdata_r <= 0;
            rw_flag_r <= 1'b0;
            reg_addr_r <= 0;
        end
        else if(start)begin
            wdata_r <= wdata;
            rw_flag_r <= rw_flag;
            reg_addr_r <= reg_addr;
        end
    end

 

下面就是状态机的跳转了,状态机采用三段式,下面代码包含其次态到现态的转换以及次态变化最重要的两段,跳转很简单,也有注释,基本上就是对应数据读写完毕后就跳转,不做多余讲解。

//状态机,次态到现态的转换;
    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(start)begin//开始信号有效时,跳转到发送起始位和器件地址的状态;
                    state_n = W_DEVICE_ADDR;
                end
                else begin
                    state_n = state_c;
                end
            end
            W_DEVICE_ADDR : begin
                if(end_bit_cnt)begin//器件地址发送完成后,跳转到写寄存器地址状态;
                    state_n = W_REG_ADDR;
                end
                else begin
                    state_n = state_c;
                end
            end
            W_REG_ADDR : begin
                if(end_byte_cnt)begin//寄存器地址写入完成后,
                    if(rw_flag_r)//如果是读操作,则跳转到重复起始位和写器件地址状态;
                        state_n = R_DEVICE_ADDR;
                    else//如果是写操作,跳转到写数据状态;
                        state_n = WDATA;
                end
                else begin
                    state_n = state_c;
                end
            end
            WDATA : begin
                if(end_byte_cnt)begin//如果数据全部写入完成,则跳转到停止状态;
                    state_n = STOP;
                end
                else begin
                    state_n = state_c;
                end
            end
            R_DEVICE_ADDR : begin
                if(end_bit_cnt)begin//如果重复起始位、器件地址、读指示位写入完毕,则跳转到读数据状态;
                    state_n = RDATA;
                end
                else begin
                    state_n = state_c;
                end
            end
            RDATA : begin
                if(end_byte_cnt)begin//读出一次需要读出的所有数据后,跳转到停止状态;
                    state_n = STOP;
                end
                else begin
                    state_n = state_c;
                end
            end
            STOP : begin
                if(end_div_cnt)begin//停止位发送完毕后,跳转到空闲状态;
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            default:begin//
                state_n = IDLE;
            end
        endcase
    end

 

然后就是分频计数器div_cnt,当状态机不处于空闲状态时,对系统时钟进行计数,从而生成scl时钟信号,对应代码如下所示。

生成该计数器相关的四个信号,计数器计数到一半时,需要把IIC时钟线scl拉低,所以生成标志信号l2h_flag表示scl下降沿。同理生成h2l_flag表示scl上升沿,主机在scl低电平中间驱动数据线SDA,所以分频计数器计数到1/4时,把wr_flag拉高,表示主机可以写入数据。主机在高电平中间读取SDA数据,所以分频计数器计数到3/4时,把rd_flag拉高,表示主机可以读取从机的数据。

//分频计数器,用于生成SCL信号。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//
            div_cnt <= 0;
        end
        else if(state_c != IDLE)begin
            if(end_div_cnt)//状态机不处于空闲状态时,对系统时钟进行计数;
                div_cnt <= 0;
            else
                div_cnt <= div_cnt + 1;
        end
    end

    //根据clk_cnt生成各种标志信号,由于计数器从零开始计数,并且下面为时序电路,所以产生条件是为对应值减2。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            l2h_flag <= 1'b0;
            h2l_flag <= 1'b0;
            wr_flag  <= 1'b0;
            rd_flag  <= 1'b0;
            end_div_cnt <= 1'b0;
        end
        else begin
            l2h_flag <= (div_cnt == CLK_DIV / 2);//在计数器div_cnt计数到一半时scl拉高;
            h2l_flag <= (div_cnt == 0);//在计数器div_cnt计数0时scl拉低;
            end_div_cnt <= (div_cnt == CLK_DIV - 2);//在计数器div_cnt计数结束时scl拉低;
            wr_flag <= (div_cnt == CLK_DIV / 4);//在计数器div_cnt计数四分之一处SDA写入数据;
            rd_flag <= (div_cnt == CLK_DIV*3 / 4);//在计数器div_cnt计数四分之三处从SDA读取数据;
        end
    end

 

接下来是用来记录发送字节数的计数器bit_cnt,对应代码如下所示:当分频计数器计数结束时该计数器加1,表示经过了发送1位数据的时间。根据状态机所处状态不同,每次需要发送或者读取的数据位数不同,使用bit_cnt_num去控制该计数器在状态机不同状态的最大值。

状态机在W_DEVICE_ADDR和R_DEVICE_ADDR需要发送起始位、7位器件地址、1位读写指示位、1位应答位,需要持续10个时钟周期,而写寄存器地址、读写数据状态都是8位数据加1位应答位,所以最大值为9。特别注意该计数器在状态机处于空闲状态时需要清零。

//数据位计数器bit_cnt,初始值为0,当分频计数器计数结束的时候加一。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            bit_cnt <= 0;
        end
        else if(state_c == IDLE)begin//状态机处于空闲状态时清零;
            bit_cnt <= 0;
        end
        else if(add_bit_cnt)begin
            if(end_bit_cnt)
                bit_cnt <= 0;
            else
                bit_cnt <= bit_cnt + 1;
        end
    end
    
    assign add_bit_cnt = end_div_cnt;//计数器加一条件,当分频计数器计数结束时有效;
    assign end_bit_cnt = add_bit_cnt && (bit_cnt == bit_cnt_num - 1);

    //用于表示每个状态每次发送的数据位数,发送器件地址之前需要发送起始位,在加上应答位,需要是个SCL时钟。
    //其余状态每次发送一字节数据后需要发送应答位,所以计数器最大值为9。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            bit_cnt_num <= 4'd9;
        end//写器件地址和起始位、读写指示位,总共是10位数据,所以计数器的最大值为10-1;
        else if((state_c == W_DEVICE_ADDR) || (state_c == R_DEVICE_ADDR))begin
            bit_cnt_num <= 4'd10;
        end
        else begin//其余状态下计数器最大值为9。
            bit_cnt_num <= 4'd9;
        end
    end

 

然后是用来记录状态机在写寄存器地址、读写数据阶段读写数据字节数的计数器byte_cnt,对应代码如下图所示。

当状态机处于这几个状态下,计数器bit_cnt计数结束时加1,读写数据的最大值在状态机不同状态页不相同,与前文设置的parameter参数有关,通过byte_cnt_num信号的值控制计数器byte_cnt的最大值。

//发送字节数的计数器,用于计数发送数据的字节数据。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0。
            byte_cnt <= 0;
        end
        else if(state_c == IDLE)begin//状态机处于空闲状态时清零;
            byte_cnt <= 0;
        end
        else if(add_byte_cnt)begin
            if(end_byte_cnt)
                byte_cnt <= 0;
            else
                byte_cnt <= byte_cnt + 1;
        end
    end
    //当状态机处于写寄存器地址或写数据或读数据状态且发送数据位计数器计数结束时加1。
    assign add_byte_cnt = ((state_c == W_REG_ADDR) || (state_c == WDATA) || (state_c == RDATA)) && end_bit_cnt;
    assign end_byte_cnt = add_byte_cnt && (byte_cnt == byte_cnt_num);//当计数到指定数值时清零。

    //字节计数器的最大值,初始值为写寄存器地址的长度;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            byte_cnt_num <= REG_ADDR_BYTE_NUM - 1;
        end
        else if(state_c == W_REG_ADDR)begin
            byte_cnt_num <= REG_ADDR_BYTE_NUM - 1;
        end
        else if((state_c == WDATA) || (state_c == RDATA))begin
            byte_cnt_num <= DATA_BYTE_NUM - 1;
        end
    end

 

前文就将状态机和三个计数器的主体架构搭建好了,后面就根据这个架构去生成本文需要的输出信号了,是不是也很简单。

首先生成IIC的时钟信号scl,当状态机不处于空闲状态且l2h_flag有效时拉高。在产生起始位时,时钟信号需要保持一段时间高电平,状态机在W_REG_ADDR状态下,发送第一位数据时,时钟信号需要一直保持高电平,否则只要h2l_flag有效,就把scl拉低,对应代码如下所示。

//生成串行时钟信号;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            scl <= 1'b1;
        end//当拉高条件有效或者状态机处于空闲状态时拉高。
        else if(l2h_flag || state_c == IDLE)begin
            scl <= 1'b1;
        end//只有在初始发送起始位时满足拉低条件时不拉低,其余情况下满足条件均要拉低;
        else if((((state_c == W_DEVICE_ADDR) && bit_cnt > 0) || (state_c != W_DEVICE_ADDR)) && h2l_flag)begin
            scl <= 1'b0;
        end
    end

 

然后生成串行数据输出信号sda_out,初始时该信号为高电平,状态机在不同状态输出不同数据即可。状态机处于R_REG_ADDR、RDATA、STOP需要特别注意,重复起始位的产生需要在写数据(bit_cnt==0 && wr_flag)时拉高,在scl的高电平中间(bit_cnt==0 && rd_flag)拉低。读指示位(bit_cnt == bit_cnt_num-2 && wr_flag)需要输出高电平。

读数据阶段主机需要在读取完最后一字节数据后输出高电平,表示不应答从机,如果读取的数据不是最后一字节数据,则输出低电平应答从机,继续接收从机输出的数据。
然后在发送停止位时需要先在scl为低电平时把sda拉低,在scl为高电平时拉高sda,从而表示出停止位。

//赋值输出信号;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            sda_out <= 1'b1;
        end
        else begin
            case (state_c)
                W_DEVICE_ADDR : begin
                    if((~bit_cnt[3]) && wr_flag)//输出器件地址和写指示位;
                        sda_out <= device_addr[8 - bit_cnt];
                end
                W_REG_ADDR : begin
                    if((~bit_cnt[3]) && wr_flag)//输出需要写入的寄存器地址;
                        sda_out <= reg_addr_r[REG_ADDR_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt];//reg_addr_r[7 - bit_cnt];
                end
                WDATA : begin//输出写数据,先输出高字节数据;
                    if((~bit_cnt[3]) && wr_flag)
                        sda_out <= wdata_r[DATA_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt];
                end
                R_DEVICE_ADDR : begin//输出重复开始信号,器件地址和读指示位;
                    if(wr_flag)//当SCL低电平时把SDA拉低,便于后续产生起始位;
                        if(bit_cnt == 0 || bit_cnt == bit_cnt_num - 2)
                            sda_out <= 1'b1;
                        else//产生起始位之后,在SCL低电平中间发送器件地址;
                            sda_out <= device_addr[8 - bit_cnt];
                    else if(rd_flag && bit_cnt == 0)//在SCL高电平的时候拉低SDA,发送重复起始位;
                        sda_out <= 1'b0;
                end
                RDATA : begin
                    if(bit_cnt == bit_cnt_num - 1 && wr_flag)
                        if(byte_cnt == DATA_BYTE_NUM - 1)//如果是读取的最后一字节数据,则不应答。
                            sda_out <= 1'b1;
                        else//如果不是最后一字节数据,则进行应答。
                            sda_out <= 1'b0;
                end
                STOP : begin
                    if(wr_flag)//停止信号需要先拉低;
                        sda_out <= 1'b0;
                    else if(rd_flag)//在SCL高电平的时候拉高,表示停止位;
                        sda_out <= 1'b1;
                end
                default : sda_out <= sda_out;
            endcase
        end
    end

 

上述生成了串行数据的输出信号,接下来就需要生成三态门使能信号sda_out_en,把上面生成的数据输出。如果使用iobufr则可以省略该信号。因为一般系统中只会存在一个主机,所以主机除了需要从机做出应答的状态,其余时间主机全程驱动数据线。

//赋值输出使能信号,除了从机应答之外,其余全为高电平;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为1;
            sda_out_en <= 1'b1;
        end
        else if(wr_flag)begin
            case (state_c)//在写器件地址、写寄存器地址、写数据、读过程的写器件地址的从机应答状态,都需要释放总线;
                W_DEVICE_ADDR,WDATA,R_DEVICE_ADDR,W_REG_ADDR : begin
                    if(bit_cnt == 0)//当计数器为0时,总线拉高,开始写入下一字节数据;
                        sda_out_en <= 1'b1;
                    else if(bit_cnt == bit_cnt_num - 1)//当写入最后一位数据后,将使能信号拉低,释放总线;
                        sda_out_en <= 1'b0;
                end
                STOP : begin
                    if(bit_cnt == 0)//当计数器为0时,总线拉高,开始写入下一字节数据;
                        sda_out_en <= 1'b1;
                end
                RDATA : begin//在读数据阶段,主机应答时需要控制总线,其余时间释放总线;
                    if(bit_cnt == 0)
                        sda_out_en <= 1'b0;
                    else if(bit_cnt == bit_cnt_num - 1)
                        sda_out_en <= 1'b1;
                end
                default: ;
            endcase
        end
    end

 

因此使能信号初始为高电平,状态机处于W_DEVICE_ADDR、WDATA、R_DEVICE_ADDR、W_REG_ADDR、STOP状态时,在从机应答的时候释放总线。而读数据状态RDATA只有在应答时主机才控制总线,所以与其他状态的控制状态刚好相反。STOP状态下bit_cnt==bit_cnt_num-1不会满足,所以使能在该状态下不会被拉低。

然后就是主机接收从机的数据了,如下所示,为了该用户接口的信号保持稳定,则将接收完成的数据打一拍后输出,在SCL的中部接收数据,先接收的数据位于高字节的高位。

//在读数据阶段,读取总线上的数据;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            rdata_r <= 0;
        end//当处于读数据阶段时,在SCL高电平中间读取数据总线上的数据;
        else if(state_c == RDATA && rd_flag)begin
            rdata_r[DATA_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt] <= sda_in;
        end
    end

    //数据输出有效指示信号,该信号为高电平时,表示读取的数据rdata有效;
    always@(posedge clk)begin
        rdata_vld_r <= (state_c == RDATA) && rd_flag && (bit_cnt == bit_cnt_num - 2) && (byte_cnt == byte_cnt_num);
    end

    //将读取的数据输出。
    always@(posedge clk)begin
        rdata <= rdata_vld_r ? rdata_r : rdata;
        rdata_vld <= rdata_vld_r;
    end

 

最后就是模块忙闲指示信号和应答失败的指示信号,模块接收到开始信号或状态机不处于空闲状态时,模块处于忙碌状态,rdy拉低,其余时间拉高,表示模块空闲,注意该信号只能采用组合逻辑生成。

//模块忙闲指示信号,当模块接收到开始信号或者状态机不处于空闲状态时拉低,表示模块处于工作状态;
    always@(*)begin
        if(start || (state_c != IDLE))
            rdy = 1'b0;
        else//其余时间拉高,表示模块处于空闲状态,上游模块可以发起写或者读操作;
            rdy = 1'b1;
    end

    //从机应答失败标志信号,高电平表示应答失败,每次开始读写操作时清零;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            ack_flag <= 1'b0;
        end
        else if(start)begin//接收到开始读写请求信号时拉低;
            ack_flag <= 1'b0;
        end//在从机应答状态下,将接收到的应答信号输出,高电平表示应答失败,低电平表示应答成功。
        else if(((state_c == WDATA) || (state_c == W_DEVICE_ADDR) || (state_c == W_REG_ADDR) || (state_c == R_DEVICE_ADDR)) && rd_flag && (bit_cnt == bit_cnt_num - 1))begin
            ack_flag <= sda_in;
        end
    end

 

最后应答失败指示信号,在各个状态的应答位中部读取串行数据线sda的状态,低电平表示应答,高电平表示从机不应答。

04
参考代码
上述就是该模块的设计,是不是也很简单?总共加注释也就402行代码,没有采用任何缩写,后文通过驱动eeprom对该模块的设计进行仿真和验证,最后可以看综合结果,消耗的资源也是比较少的,寄存器地址和读写数据均为1字节时,也只需要消耗八十多个LUT和触发器资源。
该模块的完整参考代码如下所示:

module iic_drive #(
    parameter           FCLK                    =   100_000_000         ,//系统时钟频率,默认100MHz。
    parameter           FSCL                    =   400_000             ,//IIC时钟频率,默认400KHz。
    parameter           REG_ADDR_BYTE_NUM       =   1                   ,//寄存器地址字节数;
    parameter      DATA_BYTE_NUM           =   1                ,//读写数据字节数。
    parameter      DEVICE_ADDR             =   7'b1010000           //器件地址。
)(
    input                          clk                ,//系统时钟信号;
    input                          rst_n              ,//系统复位信号,低电平有效;

    input                                           start               ,//开始进行读写操作;
    input                                           rw_flag             ,//读写标志信号,高电平表示读操作,低电平表示写操作;
    input               [REG_ADDR_BYTE_NUM*8-1 : 0] reg_addr            ,//寄存器地址,读写操作时共用的地址信号;
    input               [DATA_BYTE_NUM*8 - 1 : 0]   wdata               ,//写数据;

    output  reg         [DATA_BYTE_NUM*8 - 1 : 0]   rdata               ,//读数据信号;
    output  reg                                     rdata_vld           ,//读数据输出使能信号,高电平有效;
    output  reg                                     rdy                 ,//模块忙闲指示信号,位高电平时可以接收上游模块的读写使能信号;

    output  reg                                     scl                 ,//IIC的时钟信号;
    inout                                           sda                 ,//IIC的双向数据信号;
    output  reg                                     ack_flag             //高电平表示应答失败;
);
    localparam          CLK_DIV             =       FCLK / FSCL         ;//计算计数器div_cnt的结束值;
    localparam          CLK_DIV_W           =       clogb2(CLK_DIV - 1) ;//计算计数器div_cnt的位宽;
    //根据比较寄存器地址和读写数据的大小,然后自动计算处byte_cnt计数器位宽。
    localparam          BYTE_CNT_W          =       (REG_ADDR_BYTE_NUM  > DATA_BYTE_NUM) ? clogb2(REG_ADDR_BYTE_NUM-1) : clogb2(DATA_BYTE_NUM-1);
    //Four-stage state machine;
    localparam          IDLE                =       7'b0000001          ;//状态机空闲状态;
    localparam          W_DEVICE_ADDR       =       7'b0000010          ;//状态机写器件地址状态;
    localparam          W_REG_ADDR          =       7'b0000100          ;//状态机写寄存器地址状态;
    localparam          WDATA               =       7'b0001000          ;//状态机写数据状态;
    localparam          R_DEVICE_ADDR       =       7'b0010000          ;//状态机发送读器件地址状态;
    localparam          RDATA              =       7'b0100000          ;//状态机读数据状态;
    localparam          STOP              =       7'b1000000          ;//状态机停止状态;

    reg                                             l2h_flag            ;
    reg                                             h2l_flag            ;
    reg                                             wr_flag             ;
    reg                                             rd_flag             ;
    reg                                             end_div_cnt         ;
    reg                                             rw_flag_r           ;//
    reg                                             sda_out             ;
    reg                                             sda_out_en          ;
    reg                 [6 : 0]                      state_n             ;
    reg                 [6 : 0]                      state_c             ;
    reg                 [3 : 0]                   bit_cnt             ;//
    reg                 [3 : 0]                     bit_cnt_num         ;//
    reg                 [CLK_DIV_W - 1 : 0]       div_cnt             ;//
    reg                 [BYTE_CNT_W - 1 : 0]       byte_cnt            ;//
    reg                 [BYTE_CNT_W - 1 : 0]        byte_cnt_num        ;//
    reg                 [DATA_BYTE_NUM*8 - 1 : 0]   wdata_r             ;
    reg                 [REG_ADDR_BYTE_NUM*8 - 1 : 0] reg_addr_r        ;
    reg                 [DATA_BYTE_NUM*8 - 1 : 0]   rdata_r             ;
    reg                                             rdata_vld_r         ;

    wire                                         add_byte_cnt        ;
    wire                                         end_byte_cnt        ;
    wire                [8 : 0]                     device_addr         ;
    wire                                            sda_in              ;
    wire                                           add_bit_cnt         ;
    wire                                           end_bit_cnt         ;

    // Pullup output (connect directly to top-level port)
    //PULLUP PULLUP_inst (.O(sda));

    //双向IO控制;
    assign sda_in = sda;
    assign sda = sda_out_en ? sda_out : 1'bz;

    //自动计算位宽函数;
    function integer clogb2(input integer depth);
        begin
            if(depth == 0)
                clogb2 = 1;
            else if(depth != 0)
                for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)
                    depth=depth >> 1;
        end
    endfunction

    //暂存器件地址和起始位还有写指示位。
    assign device_addr = {1'b0,DEVICE_ADDR,1'b0};

    //开始信号有效时,把待发送的信号暂存。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            wdata_r <= 0;
            rw_flag_r <= 1'b0;
            reg_addr_r <= 0;
        end
        else if(start)begin
            wdata_r <= wdata;
            rw_flag_r <= rw_flag;
            reg_addr_r <= reg_addr;
        end
    end

    //状态机,次态到现态的转换;
    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(start)begin//开始信号有效时,跳转到发送起始位和器件地址的状态;
                    state_n = W_DEVICE_ADDR;
                end
                else begin
                    state_n = state_c;
                end
            end
            W_DEVICE_ADDR : begin
                if(end_bit_cnt)begin//器件地址发送完成后,跳转到写寄存器地址状态;
                    state_n = W_REG_ADDR;
                end
                else begin
                    state_n = state_c;
                end
            end
            W_REG_ADDR : begin
                if(end_byte_cnt)begin//寄存器地址写入完成后,
                    if(rw_flag_r)//如果是读操作,则跳转到重复起始位和写器件地址状态;
                        state_n = R_DEVICE_ADDR;
                    else//如果是写操作,跳转到写数据状态;
                        state_n = WDATA;
                end
                else begin
                    state_n = state_c;
                end
            end
            WDATA : begin
                if(end_byte_cnt)begin//如果数据全部写入完成,则跳转到停止状态;
                    state_n = STOP;
                end
                else begin
                    state_n = state_c;
                end
            end
            R_DEVICE_ADDR : begin
                if(end_bit_cnt)begin//如果重复起始位、器件地址、读指示位写入完毕,则跳转到读数据状态;
                    state_n = RDATA;
                end
                else begin
                    state_n = state_c;
                end
            end
            RDATA : begin
                if(end_byte_cnt)begin//读出一次需要读出的所有数据后,跳转到停止状态;
                    state_n = STOP;
                end
                else begin
                    state_n = state_c;
                end
            end
            STOP : begin
                if(end_div_cnt)begin//停止位发送完毕后,跳转到空闲状态;
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            default:begin//
                state_n = IDLE;
            end
        endcase
    end

    //分频计数器,用于生成SCL信号。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//
            div_cnt <= 0;
        end
        else if(state_c != IDLE)begin
            if(end_div_cnt)//状态机不处于空闲状态时,对系统时钟进行计数;
                div_cnt <= 0;
            else
                div_cnt <= div_cnt + 1;
        end
    end

    //根据clk_cnt生成各种标志信号,由于计数器从零开始计数,并且下面为时序电路,所以产生条件是为对应值减2。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            l2h_flag <= 1'b0;
            h2l_flag <= 1'b0;
            wr_flag  <= 1'b0;
            rd_flag  <= 1'b0;
            end_div_cnt <= 1'b0;
        end
        else begin
            l2h_flag <= (div_cnt == CLK_DIV / 2);//在计数器div_cnt计数到一半时scl拉高;
            h2l_flag <= (div_cnt == 0);//在计数器div_cnt计数0时scl拉低;
            end_div_cnt <= (div_cnt == CLK_DIV - 2);//在计数器div_cnt计数结束时scl拉低;
            wr_flag <= (div_cnt == CLK_DIV / 4);//在计数器div_cnt计数四分之一处SDA写入数据;
            rd_flag <= (div_cnt == CLK_DIV*3 / 4);//在计数器div_cnt计数四分之三处从SDA读取数据;
        end
    end

    //数据位计数器bit_cnt,初始值为0,当分频计数器计数结束的时候加一。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            bit_cnt <= 0;
        end
        else if(state_c == IDLE)begin//状态机处于空闲状态时清零;
            bit_cnt <= 0;
        end
        else if(add_bit_cnt)begin
            if(end_bit_cnt)
                bit_cnt <= 0;
            else
                bit_cnt <= bit_cnt + 1;
        end
    end
    
    assign add_bit_cnt = end_div_cnt;//计数器加一条件,当分频计数器计数结束时有效;
    assign end_bit_cnt = add_bit_cnt && (bit_cnt == bit_cnt_num - 1);

    //用于表示每个状态每次发送的数据位数,发送器件地址之前需要发送起始位,在加上应答位,需要是个SCL时钟。
    //其余状态每次发送一字节数据后需要发送应答位,所以计数器最大值为9。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            bit_cnt_num <= 4'd9;
        end//写器件地址和起始位、读写指示位,总共是10位数据,所以计数器的最大值为10-1;
        else if((state_c == W_DEVICE_ADDR) || (state_c == R_DEVICE_ADDR))begin
            bit_cnt_num <= 4'd10;
        end
        else begin//其余状态下计数器最大值为9。
            bit_cnt_num <= 4'd9;
        end
    end

    //发送字节数的计数器,用于计数发送数据的字节数据。
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0。
            byte_cnt <= 0;
        end
        else if(state_c == IDLE)begin//状态机处于空闲状态时清零;
            byte_cnt <= 0;
        end
        else if(add_byte_cnt)begin
            if(end_byte_cnt)
                byte_cnt <= 0;
            else
                byte_cnt <= byte_cnt + 1;
        end
    end
    //当状态机处于写寄存器地址或写数据或读数据状态且发送数据位计数器计数结束时加1。
    assign add_byte_cnt = ((state_c == W_REG_ADDR) || (state_c == WDATA) || (state_c == RDATA)) && end_bit_cnt;
    assign end_byte_cnt = add_byte_cnt && (byte_cnt == byte_cnt_num);//当计数到指定数值时清零。

    //字节计数器的最大值,初始值为写寄存器地址的长度;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            byte_cnt_num <= REG_ADDR_BYTE_NUM - 1;
        end
        else if(state_c == W_REG_ADDR)begin
            byte_cnt_num <= REG_ADDR_BYTE_NUM - 1;
        end
        else if((state_c == WDATA) || (state_c == RDATA))begin
            byte_cnt_num <= DATA_BYTE_NUM - 1;
        end
    end

    //生成串行时钟信号;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            scl <= 1'b1;
        end//当拉高条件有效或者状态机处于空闲状态时拉高。
        else if(l2h_flag || state_c == IDLE)begin
            scl <= 1'b1;
        end//只有在初始发送起始位时满足拉低条件时不拉低,其余情况下满足条件均要拉低;
        else if((((state_c == W_DEVICE_ADDR) && bit_cnt > 0) || (state_c != W_DEVICE_ADDR)) && h2l_flag)begin
            scl <= 1'b0;
        end
    end

    //赋值输出信号;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            sda_out <= 1'b1;
        end
        else begin
            case (state_c)
                W_DEVICE_ADDR : begin
                    if((~bit_cnt[3]) && wr_flag)//输出器件地址和写指示位;
                        sda_out <= device_addr[8 - bit_cnt];
                end
                W_REG_ADDR : begin
                    if((~bit_cnt[3]) && wr_flag)//输出需要写入的寄存器地址;
                        sda_out <= reg_addr_r[REG_ADDR_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt];//reg_addr_r[7 - bit_cnt];
                end
                WDATA : begin//输出写数据,先输出高字节数据;
                    if((~bit_cnt[3]) && wr_flag)
                        sda_out <= wdata_r[DATA_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt];
                end
                R_DEVICE_ADDR : begin//输出重复开始信号,器件地址和读指示位;
                    if(wr_flag)//当SCL低电平时把SDA拉低,便于后续产生起始位;
                        if(bit_cnt == 0 || bit_cnt == bit_cnt_num - 2)
                            sda_out <= 1'b1;
                        else//产生起始位之后,在SCL低电平中间发送器件地址;
                            sda_out <= device_addr[8 - bit_cnt];
                    else if(rd_flag && bit_cnt == 0)//在SCL高电平的时候拉低SDA,发送重复起始位;
                        sda_out <= 1'b0;
                end
                RDATA : begin
                    if(bit_cnt == bit_cnt_num - 1 && wr_flag)
                        if(byte_cnt == DATA_BYTE_NUM - 1)//如果是读取的最后一字节数据,则不应答。
                            sda_out <= 1'b1;
                        else//如果不是最后一字节数据,则进行应答。
                            sda_out <= 1'b0;
                end
                STOP : begin
                    if(wr_flag)//停止信号需要先拉低;
                        sda_out <= 1'b0;
                    else if(rd_flag)//在SCL高电平的时候拉高,表示停止位;
                        sda_out <= 1'b1;
                end
                default : sda_out <= sda_out;
            endcase
        end
    end

    //赋值输出使能信号,除了从机应答之外,其余全为高电平;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为1;
            sda_out_en <= 1'b1;
        end
        else if(wr_flag)begin
            case (state_c)//在写器件地址、写寄存器地址、写数据、读过程的写器件地址的从机应答状态,都需要释放总线;
                W_DEVICE_ADDR,WDATA,R_DEVICE_ADDR,W_REG_ADDR : begin
                    if(bit_cnt == 0)//当计数器为0时,总线拉高,开始写入下一字节数据;
                        sda_out_en <= 1'b1;
                    else if(bit_cnt == bit_cnt_num - 1)//当写入最后一位数据后,将使能信号拉低,释放总线;
                        sda_out_en <= 1'b0;
                end
                STOP : begin
                    if(bit_cnt == 0)//当计数器为0时,总线拉高,开始写入下一字节数据;
                        sda_out_en <= 1'b1;
                end
                RDATA : begin//在读数据阶段,主机应答时需要控制总线,其余时间释放总线;
                    if(bit_cnt == 0)
                        sda_out_en <= 1'b0;
                    else if(bit_cnt == bit_cnt_num - 1)
                        sda_out_en <= 1'b1;
                end
                default: ;
            endcase
        end
    end

    //在读数据阶段,读取总线上的数据;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            rdata_r <= 0;
        end//当处于读数据阶段时,在SCL高电平中间读取数据总线上的数据;
        else if(state_c == RDATA && rd_flag)begin
            rdata_r[DATA_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt] <= sda_in;
        end
    end

    //数据输出有效指示信号,该信号为高电平时,表示读取的数据rdata有效;
    always@(posedge clk)begin
        rdata_vld_r <= (state_c == RDATA) && rd_flag && (bit_cnt == bit_cnt_num - 2) && (byte_cnt == byte_cnt_num);
    end

    //将读取的数据输出。
    always@(posedge clk)begin
        rdata <= rdata_vld_r ? rdata_r : rdata;
        rdata_vld <= rdata_vld_r;
    end

    //模块忙闲指示信号,当模块接收到开始信号或者状态机不处于空闲状态时拉低,表示模块处于工作状态;
    always@(*)begin
        if(start || (state_c != IDLE))
            rdy = 1'b0;
        else//其余时间拉高,表示模块处于空闲状态,上游模块可以发起写或者读操作;
            rdy = 1'b1;
    end

    //从机应答失败标志信号,高电平表示应答失败,每次开始读写操作时清零;
    always@(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin//初始值为0;
            ack_flag <= 1'b0;
        end
        else if(start)begin//接收到开始读写请求信号时拉低;
            ack_flag <= 1'b0;
        end//在从机应答状态下,将接收到的应答信号输出,高电平表示应答失败,低电平表示应答成功。
        else if(((state_c == WDATA) || (state_c == W_DEVICE_ADDR) || (state_c == W_REG_ADDR) || (state_c == R_DEVICE_ADDR)) && rd_flag && (bit_cnt == bit_cnt_num - 1))begin
            ack_flag <= sda_in;
        end
    end

endmodule

 

本文就这么多吧,后文对该模块进行仿真和上板验证,不是说还没有验证,是本文篇幅已经过长了,仿真也包括单字节读、写,页写和页读,还有eeprom自己的内容,涉及的东西也不会少。
 

请登录后发表评论

    没有回复内容