基于FPGA的IIC主机数据接收发送控制器

绪论:

IIC总线是一种常用的片级总线,它集成于很多器件当中。在构造一个FPGA系统框架中,往往会使用IIC总线对存储器等一些重要的外围器件进行读写。而在这种应用场景之下,IIC主机数据接收发送控制器就成为FPGA工程师所必须掌握的模块。本文设计了一种可进行读写双向操作的IIC主机控制器,但由于未根据传输数据量大小设立足够的缓冲区,因此通过了指示信号来进行信号传输沟通。当实际应用时,使用者可根据实际数据大小标定缓冲区,从而减少控制信号标定带来的复杂性。另外,IIC总线控制器实际上是一个低速设备,由于当中必须使用到计数器,因此会造成关键路径上的大延时。如果需要使整个系统运行在更高的时钟频率下,请使用者使用锁相环为该模块分频出一个小频率时钟,并通过FIFO连接到FPGA内的其它模块。

一、IIC总线

1.概论

IIC总线介绍:https://blog.csdn.net/qq_38410730/article/details/80312357       

       I²C(Inter-Integrated Circuit)即集成电路总线,它是一种串行通信总线,使用多主从架构,实现白双工通信。IIC总线最主要的优点是其简单性和有效性。由于接口直接在组件之上,因此IIC总线占用的空间非常小,减少了电路板的空间和芯片管脚的数量,降低了互联成本。总线的长度可高达25英尺,并且能够以10Kbps的最大传输速率支持40个组件。IIC总线支持多主控, 其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。

       IIC串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL,其时钟信号是由主控器件产生。所有接到IIC总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。对于并联在一条总线上的每个IC都有唯一的地址。

       通常,数据线SDA和时钟线SCL都是处于上拉电阻状态。因为:在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。IIC上,SDA传输的有效数据只能在SCL低电平时改变。在SCL为高时,SDA上的电平代表着数据的高低。

                                                                              

2.起始信号与终止信号时序图

  • 起始信号:当时钟线SCL为高期间,数据线SDA由高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平信号;
  • 停止信号:当时钟线SCL为高期间,数据线SDA由低到高的跳变;停止信号也是一种电平跳变时序信号,而不是一个电平信号。

                                                                  

3.主机读写时序图

                                     

       如图所示,IIC数据传输为大端模式。其中DEVICE ADDRESS为所要交互的从机地址,地址的最低位表示着读写操作(0写1读)。图上所示的即为主机写从机操作,主机从START开始状态出发,逐位传送一字节数据,等待从机应答信号ACK(ACK为0则应答,为1则一边表示接收有误)。之后,WORD ADDRESS可以为数据,也可以为地址。图上标注为地址是因为它是用于写存储器,故而将第一字节数据作为地址,第二字节作为实际需写入的数据。

       同理可得,读操作即为DEVICE ADDRESS传输完毕后,从机应答。主机将SDA总线交给从机控制,主机自己控制SCL时钟信号,并在接收到从机一字节数据后向其提供ACK应答。当主机不想继续从从机读取数据(突发),主机在ACK应答1,然后发出STOP信号。当为正常约定时,主机ACK回复0,然后主机拉STOP信号。

二、FPGA程序设计

    IIC总线主机控制器是以状态机的形式进行数据读写。由于采用了简化性操作,状态机的状态分量比较少,在这里就不予给出。读者可自行阅读注释完备的代码,自己绘制出状态转移图。读者有兴趣可以对下列代码进行仿真,并注意inout口的问题。

1.程序代码

module IIC(
    clk,rst_n,sda,scl,data_rd,data_in,data_v,wr,rd
);
///
//定位:主机控制模块
//发送设计目标:可整体发送一帧数据码,数据帧由地址信息作为起始,每节数据依赖于data_v进行识别采样;
//接收设计目标:可连续接收数据,并利用rd脉冲标记接收缓冲数据可被读出
//硬件时钟为50MHz,SCL通讯时钟100KHz,可通过宏定义修改
//不对传输结果有效性进行判断、不对总线冲突进行仲裁
//IIC总线通信应当提前约定好接收/发送数据每帧的数据长度
//本设计通过宏定义规定:
//      发送数据:操作地址(非设备地址)为8位,数据量为16bit分两次发送
//      接受数据:操作地址为8位,数据量为16bit分两次接收
//由于IIC为低速总线,建议使用PLL分频较低时钟控制,并在末端配置FIFO以降低其对整体设计性能的影响
//IIC总线数据传递形式为大端,请注意读写顺序
///
input clk,rst_n;

inout sda;                  //数据接收发送口
output scl;                 //IIC数据时钟线

output[7:0]data_rd;         //数据接收后并行输出
output rd;                  //接收数据可读出脉冲

input[7:0]data_in;          //发送数据输入
input data_v;               //发送数据输入指示脉冲
output wr;                  //发送数据缓冲区可写入数据
///
//IIC模块输出信号定义
reg scl;                    
reg rd;
reg[7:0]data_rd;
reg wr;
wire sda;
//
reg SDA;                    //sda信号的中介信号,inout综合为三台门,不应直接用寄存器存z值
reg link;                   //sda传输方向控制信号
                            //0:写
                            //1:读
reg[7:0]data_t;             //寄存需要向外发送的数据
reg[7:0]data_r;             //接收缓冲寄存器
///
parameter END_FRAME     =   8'hff;              //发送数据停止帧,IIC接收到该帧后停止数据发送
                                                 //也可以提前约定好数据帧的长度,利用计数器判定。
                                                 //在一般使用中应当提前约定好一次发送的数据长度
reg SDA_link;                                   //SDA读/写方向控制
reg[3:0]cnt;                                    //发送/接收一次的8位数据计数
reg[3:0]cnt_frame;                              //发送/接收数据帧数计数,如提前约定好可以使用状态机分量实现,避免计数器带来的时钟瓶颈
parameter FRAME_R   =   4'd1;                   //主机一次接收一帧数据,第一帧为从机地址,第二帧为从机发送的数据
parameter FRAME_T   =   4'd2;                   //主机一次发送两帧数据,包含第一帧的设备地址
//reg[7:0]add_dev;                                //本次发送的从机地址:使用发送数据寄存器替代

///
//状态机参数定义
//如需操作存储器,添加已注释的状态可使整个过程更清晰
reg[3:0]STATE;                                   //状态机
parameter IDLE          =   4'b0000;            //空闲状态
parameter START         =   4'b0001;            //开始信号生成状态
parameter ADD_DEV       =   4'b0010;            //从机地址数据发送状态:一次发送8位地址
parameter ACK_DEV_ADD   =   4'b0011;            //从机设备地址应答识别状态
//parameter ADD_WORD      =   4'b0100;            //操作字地址发送状态:可以根据实际地址长度进行修改,本设计以8位地址为例
//parameter ACK_WORD_ADD  =   4'b0101;            //操作字地址应答与读写识别状态
parameter DATA_T        =   4'b0110;            //数据发送状态
parameter ACK_DATA_T    =   4'b0111;            //数据发送应答接收状态
//parameter DEV_ADD_R     =   4'b1000;            //从机设备地址接收状态
//parameter ACK_DEV_R     =   4'b1001;            //应答从机设备地址状态
parameter DATA_R        =   4'b1010;            //数据接收状态
parameter ACK_DATA_R    =   4'b1011;            //数据接收应答状态
parameter STOP          =   4'b1100;            //发送停止信号生成状态
///
///
//SCL时钟线状态定义与延时控制模块
//由于设定中时钟频率为50MHz,通信频率100KHz,因此分频计数500,4个阶段各分配125
`define SCL_POS     (cnt_delay == 9'd499)   //延时计数器计数值为499时,SCL拉高
`define SCL_HIG     (cnt_delay == 9'd124)   //延时计数器计数值为124时,SCL为稳定高电平,此时可以读取SDA上的数据
`define SCL_NEG     (cnt_delay == 9'd249)   //延时计数器计数值为249时,SCL为拉低
`define SCL_LOW     (cnt_delay == 9'd374)   //延时计数器计数值为374时,SCL为稳定低电平,此时可以改变SDA上发送的数据
reg[8:0]cnt_delay;                           //延时计数器
//延时计数器模块
always@(posedge clk or negedge rst_n)
    if(!rst_n)  cnt_delay <= 9'd0;
    else if(cnt_delay == 9'd499)   cnt_delay <= 9'd0; 
    else    cnt_delay <= cnt_delay + 9'd1;
//scl电平控制
always@(posedge clk or negedge rst_n)
    if(!rst_n)  scl <= 1'b1;
    else   
        case(STATE)
        IDLE : begin
                    scl <= 1'b1;                                        //空闲状态,scl拉高            
                end
        START : begin
                    if(`SCL_HIG)    scl <= 1'b1;                        //开始状态,scl拉高,等待计数器计数到sdr拉低,通讯开始
                end
        STOP : begin
                    if(`SCL_POS)    scl <= 1'b1;                        //通讯结束状态,scl拉高
                end
        default : begin
                    if(`SCL_POS)    scl <= 1'b1;                        //其余状态遵循计数器宏定义状态改变
                    else if(`SCL_NEG)    scl <= 1'b0;
                    else    scl <= scl;
                    end
        endcase
///
//输入数据监测模块:生成数据有效标志,通过检测外部输入数据有效标志上升沿
reg flag_data_in;                               //并行输入数据有效标志
reg data_v_1;                                   //外部输入数据有效标志寄存
always@(posedge clk or negedge rst_n)
    if(!rst_n)
        begin
            flag_data_in <= 1'b0;
            data_v_1 <= 1'b0;
        end
    else
        begin
            data_v_1 <= data_v;
            flag_data_in <= ~data_v_1 & data_v;
        end
///
//模块控制状态机转移控制模块
always@(posedge clk or negedge rst_n)
    if(!rst_n)  STATE <= 4'b0000;
    else if(STATE == IDLE)                                              //空闲状态
        begin
            if(flag_data_in)    STATE <= START;                         //如果检测到有数据需要通过IIC模块发出,状态转移为START                  
            else    STATE <= IDLE;                                      //否则继续保持空闲状态
        end
    else if(STATE == START)                                             //开始信号生成状态
        begin
            if(`SCL_HIG)    STATE <= ADD_DEV;                           //如果scl下降沿已生成,状态转移至ADD_DEV
            else STATE <= START;
        end
    else if(STATE ==  ADD_DEV)                                          //设备地址发送状态
        begin
            if(cnt == 4'd8&&`SCL_LOW) STATE <= ACK_DEV_ADD;             //8位地址(7位设备地址加一位读写标志)发送完毕,转入ACK_DEV_ADD状态
            else STATE <= ADD_DEV;                                      //未发送完毕,继续保持
        end
    else if(STATE ==  ACK_DEV_ADD)                                      //从机设备地址接收应当状态
        begin
            if(`SCL_NEG)                                                //在SCL下降沿改变状态
            begin
                if(data_t[0])                                          //设备地址最低位位读写标志,1位读,0为写
                    STATE <= DATA_R;                                    //转移至DATA_R
                else
                    STATE <= DATA_T;                                    //转移至DATA_T
            end
            else    
                STATE <= ACK_DEV_ADD;
        end
    else if(STATE ==  DATA_T)                                           //主机数据发送状态
        begin
            if(cnt == 4'd8 && `SCL_LOW)                                 //8位数据发送完毕且处于SCL低电平
                STATE <= ACK_DATA_T;                                    //状态转移至ACK_DATA_T
            else    STATE <= DATA_T;
        end
    else if(STATE ==  ACK_DATA_T)                                       //主机数据发送应答状态
        begin
            if(`SCL_NEG)                                                //在SCL下降沿时检测
            begin
                if(cnt_frame == (FRAME_T-1))                                //当已发送指定帧数的数据
                    STATE <= STOP;                                      //状态转移至STOP
                 else    STATE <= DATA_T;                               //否则,状态转移至DATA_T,继续进行数据发送
            end
            else    STATE <= ACK_DATA_T;
        end
    else if(STATE ==  DATA_R)                                           //主机数据接收状态
        begin
            if(cnt == 4'd8 && `SCL_LOW)                                 //8位数据接收完毕且处于SCL低电平
                STATE <= ACK_DATA_R;                                    //状态转移至ACK_DATA_R
            else    STATE <= DATA_R;                                    
        end
    else if(STATE ==  ACK_DATA_R)                                       //主机接收数据应答状态
        begin
            if(`SCL_NEG)                                                //SCL下降沿检测
                begin
                    if(cnt_frame == (FRAME_R-1))                            //当已接收指定帧数数据
                        STATE <= STOP;                                  //状态转移至STOP
                    else    STATE <= DATA_R;                            //否则继续接收下一阵数据
                end
            else    STATE <= ACK_DATA_R;
        end
    else if(STATE ==  STOP)                                             //数据发送/接收终止状态
        begin
            if(`SCL_HIG)                                                //当SCL抵达高电平时
                STATE <= IDLE;                                          //状态转移至IDLE
            else STATE <= STOP;
        end
    else    STATE <= IDLE;                                              //默认状态为IDLE

//状态机状态操作模块
always@(posedge clk or negedge rst_n)
    if(!rst_n)
        begin
            cnt <= 4'd0;                                                //发送/接收一次的8位数据计数
            cnt_frame <= 4'd0;                                          //发送/接收数据帧数计数
            SDA <= 1'b1;                                                //写加载串行数据
            link <= 1'b0;                                               //sda传输方向控制信号,0写1读
            wr <= 1'b1;                                                 //发送数据缓冲区可写入数据
            rd <= 1'b0;                                                 //接收数据可读出脉冲
            data_t <= 8'd0;                                             //寄存需要向外部发送的数据
            data_r <= 8'd0;                                             //读取接收的数据
            data_rd <= 8'd0;                                            //接收的数据寄存
        end
    else
        case(STATE)
        IDLE :          begin                                          //空闲状态,SDA拉高,IIC等待内部数据到来
                            cnt <= 4'd0;
                            cnt_frame <= 4'd0;
                            SDA <= 1'b1;
                            link <= 1'b0;
                            rd <= 1'b0;
                            data_t <= 8'd0;
                            if(flag_data_in) wr <= 1'b0;
                        end
        START :         begin                                           //开始状态:向外发送起始信息
                            if(`SCL_HIG)                                //当计数器计数到scl为高时,sdr上的数据拉低,通讯开始
                                begin
                                    link <= 1'b0;                       //sda传输向外写
                                    SDA <= 1'b0;                        //写数据为0
                                    data_t <= data_in;                  //发送数据寄存
                                    wr <= 1'b0;                         //不接受新的数据进入缓冲
                                                                        //在约定数据深度情况下,可以一步到位锁存,降低对FPGA内部其它模块的干扰
                                    rd <= 1'b0;                         //不接受数据被读取
                                end
                        end
        ADD_DEV :       begin                                           //设备地址发送状态:IIC通讯必须以此开始确定通讯对象及读写操作
                            if(`SCL_LOW)                                //SCL低电平时改变数据
                                begin
                                    if(cnt == 4'd8)                     //8bit发送完毕后
                                        begin
                                            cnt <= 4'd0;                //8位数据计数清零
                                            SDA <= 1'b1;                //SDR输出数据赋1,但此时为输入高阻状态
                                            link <= 1'b1;               //SDR输入状态
                                            if(data_t[0]) wr <= 1'b0;
                                            else wr <= 1'b1;            //发送缓冲区可写入数据
                                            rd <= 1'b0;                 //接收缓冲区不可被读
                                        end
                                    else
                                        begin
                                            cnt <= cnt + 4'd1;          //位计数器计数
                                            link <= 1'b0;               //SDR输出状态
                                            case(cnt)
                                            4'd0 : SDA <= data_t[7];
                                            4'd1 : SDA <= data_t[6];
                                            4'd2 : SDA <= data_t[5];
                                            4'd3 : SDA <= data_t[4];
                                            4'd4 : SDA <= data_t[3];
                                            4'd5 : SDA <= data_t[2];
                                            4'd6 : SDA <= data_t[1];
                                            4'd7 : SDA <= data_t[0];
                                            default : SDA <= 1'b1;
                                            endcase
                                        end
                                end 
                        end
        ACK_DEV_ADD :   begin                                                       //设备地址接收应答状态 
                            cnt <= 4'd0;                                            //位发送接收计数器清零                                         
                            if(flag_data_in)                                        //有数据需要发送,则赋值到寄存器内
                                begin
                                    data_t <= data_in;                              //由于下一个状态也可能是数据接收,因此本步不一定运行
                                    cnt_frame <= 4'd1;                              //接收到一帧发送数据,计数器赋1
                                    wr <= 1'b0;                                     //数据不可写入发送缓冲寄存器
                                    rd <= 1'b0;                                     //接收缓冲寄存器不可读
                                end
                        end
        DATA_T :        begin                                           //数据发送状态
                            if(`SCL_LOW)                                //SCL低电平时改变数据
                                begin
                                    if(cnt == 4'd8)                     //8bit发送完毕后
                                        begin
                                            cnt <= 4'd0;                //8位数据计数清零
                                            SDA <= 1'b1;                //SDR输出数据赋1,但此时为输入高阻状态
                                            link <= 1'b1;               //SDR输入状态
                                            wr <= 1'b1;                 //发送缓冲区可写入数据
                                            rd <= 1'b0;                 //接收缓冲区不可被读
                                        end
                                    else
                                        begin
                                            cnt <= cnt + 4'd1;          //位计数器计数
                                            link <= 1'b0;               //SDR输出状态
                                            case(cnt)
                                            4'd0 : SDA <= data_t[7];
                                            4'd1 : SDA <= data_t[6];
                                            4'd2 : SDA <= data_t[5];
                                            4'd3 : SDA <= data_t[4];
                                            4'd4 : SDA <= data_t[3];
                                            4'd5 : SDA <= data_t[2];
                                            4'd6 : SDA <= data_t[1];
                                            4'd7 : SDA <= data_t[0];
                                            default : SDA <= 1'b1;
                                            endcase
                                        end
                                end 
                        end
        ACK_DATA_T :    begin                                                       //数据发送应答状态
                            cnt <= 4'd0;                                            //位发送接收计数器清零  
                            if(flag_data_in)                                        //有数据需要发送,则赋值到寄存器内
                                begin
                                    data_t <= data_in;                              //由于下一个状态也可能是数据接收,因此本步不一定运行
                                    cnt_frame <= cnt_frame + 4'd1;                  //接收到一帧发送数据,计数器加1
                                    wr <= 1'b0;                                     //数据发送缓冲区不可写入
                                    rd <= 1'b0;                                     //数据接收缓冲
                                end
                            else
                                begin
                                    cnt_frame <= cnt_frame;                  //接收到一帧发送数据,计数器加1
                                    wr <= wr;                                     //数据发送缓冲区不可写入
                                    rd <= rd;
                                end
                        end
        DATA_R :        begin                                                       //数据接收状态  
                            if(`SCL_HIG)                                            //SCL高电平读取数据 
                            begin  
                                if(cnt == 4'd8)
                                    begin
                                       cnt <= 4'd0;                                     //8位数据计数清零
                                       SDA <= 1'b1;                                     //SDR输出数据赋1,但此时为输入高阻状态
                                       link <= 1'b1;                                    //SDR接收状态,下一步需要应答,此时不可在SCL高电平输出应答信号
                                       wr <= 1'b0;                                      //发送缓冲区可写入数据
                                       rd <= 1'b1;                                      //接收缓冲区不可被读
                                       data_rd <= data_r;                               //接受数据放入可读接口中 
                                    end
                                else
                                    begin
                                        wr <= 1'b0;                                     //数据发送缓冲区不可写
                                        rd <= 1'b0;                                     //数据接收缓冲区不可读                                    
                                        cnt <= cnt + 1'b1;                              //位计数加1
                                        case(cnt)
                                        4'd0 : data_r[0] <= sda;
                                        4'd1 : data_r[1] <= sda;
                                        4'd2 : data_r[2] <= sda;
                                        4'd3 : data_r[3] <= sda;
                                        4'd4 : data_r[4] <= sda;
                                        4'd5 : data_r[5] <= sda;
                                        4'd6 : data_r[6] <= sda;
                                        4'd7 : data_r[7] <= sda;
                                        default : data_r <= 4'd0;
                                        endcase   
                                    end
                            end
                        end
        ACK_DATA_R :    begin                                           //数据接收应答状态
                            cnt <= 4'd0;                                //8位数据计数器清零
                            link <= 1'b0;                               //SDA向外写
                            SDA <=  1'b0;                               //SDA拉低
                            wr <= 1'b0;                                 //数据发送缓冲区不可写
                            rd <= 1'b1;                                 //数据接收缓冲区可读
                            if(`SCL_NEG)                                //SCL低电平
                                begin
                                if(cnt_frame == FRAME_R)                //SCL下降沿
                                    begin
                                        cnt_frame <= 4'd0;              //帧计数器清零
                                    end
                                else
                                    begin
                                        cnt_frame <= cnt_frame + 4'd1;  //帧计数器加1
                                    end
                            end
                        end
        STOP :          begin                                           //停止发送/接收状态
                            wr <= 1'b0;                                 //不可写入数据
                            rd <= 1'b0;                                 //不可读出数据
                            if(`SCL_LOW)
                                begin
                                    link <= 0;                          //向外写数据
                                    SDA <= 1'b0;                        //拉高SDA
                                end
                            else if(`SCL_HIG)                                //SCL高电平状态
                                begin
                                    link <= 0;                          //向外写数据
                                    SDA <= 1'b1;                        //拉高SDA
                                end
                                
                        end
        default : begin
                       cnt <= 4'd0;                                                //发送/接收一次的8位数据计数
                       cnt_frame <= 4'd0;                                          //发送/接收数据帧数计数
                       SDA <= 1'b0;                                                //写加载串行数据
                       link <= 1'b0;                                               //sda传输方向控制信号,0写1读
                       wr <= 1'b1;                                                 //发送数据缓冲区可写入数据
                       rd <= 1'b0;                                                 //接收数据可读出脉冲
                       data_t <= 8'd0;                                             //待发送数据缓冲区           
                    end
        endcase
        
        

///
assign sda = link ? 1'bz : SDA;        //读时sda高阻,写时为SDA加载数据
///
endmodule

2.主机写仿真波形

3.主机读仿真波形

 

  • 7
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值