FPGA IIC的状态机实现

IIC协议的简单介绍

1.IIC通讯设备的链接图

在这里插入图片描述
注:一个IIC总线可以挂载多个设备,一个IIC总线有两条线,一个是数据线,一个是时钟线。主机通过访问不同的从机地址来进行不同设备之间的通信。细节请自己百度,这里不做过多介绍。

2.IIC协议的时序

2.1整体时序图
在这里插入图片描述
注:图片纯手画,有些丑,不喜勿喷。
由图中可以看出,整体的时序图由A,B,C,D分割。下面我将详细介绍这四部分。
A:表示空闲状态,此时SCL和SDA都为高电平。
B:表示开始状态,当SCL为高电平时,SDA出现下降沿之后,表示进入了开始状态,数据将要发送或者接受。
C:表示数据读写状态,其中的一段时序波形如下图所示:
在这里插入图片描述
D:表示结束状态,当SCL为高电平时,SDA由低电平变为高电平,表示结束。

3.IIC的读写操作以及状态机的实现

3.1 写操作时序

IIC单字节写操作时序
在这里插入图片描述

IIC双字节写操作时序
在这里插入图片描述

注:这个只介绍2字节写操作,因为掌握了2字节的读写操作,一个字节的读写操作也是轻而易举的。这里不在做过多的解释。

3.1.1状态机的实现

在这里插入图片描述在这里插入图片描述图片太长我分两部分,由上面的写操作的时序,我抽象出不同的状态,状态1,空闲状态,状态2,开始状态,状态3,发送器件地址状态(send_device_addr),状态4,发送存储地址高位(send_storage_addr_H),状态5,发送存储地址高位(send_storage_addr_L),
状态6,写数据(wr_data),状态7,停止状态(stop)。

3.2读操作时序

在这里插入图片描述

3.2.1状态机的实现

在这里插入图片描述在这里插入图片描述在这里插入图片描述
图片太长我分三部分,由上面的读操作的时序,我抽象出不同的状态,状态1,空闲状态,状态2,开始状态,状态3,发送器件地址状态(send_device_addr),状态4,发送存储地址高位(send_storage_addr_H),
状态5,发送存储地址高位(send_storage_addr_L),
状态6,开始二状态(start_2),状态7,读数据状态(RD-DATA),状态8,停止状态(STOP).
由于二者之间有相同的部分,故我在一个状态转移图中表示所有状态的转移,整体的状态转移图如下图所示:
在这里插入图片描述下面开始介绍如何实现这些状态机,在第一节中我们知道,状态跳转最重要的知道跳转的条件是什么,同时也要对系统的输入输出有清晰的认识。再此之前我先将状态跳转的程序附上。

/***************************组合逻辑表示状态转移*********************************/
always @(*) begin
     case(current_state)
        IDLE:  
            if(i2c_start == 1'b1)
               next_state = START;
            else 
               next_state = IDLE;
        START:
            if(cnt_driver_clk == 2'd3)
                next_state = SEND_DEVICE_ADDR;
            else 
                next_state = START;
        SEND_DEVICE_ADDR:
                if(addr_num == 1'b1) begin 
                        if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
                            next_state = SEND_STORAGE_ADDR_H;
                        else 
                            next_state = SEND_DEVICE_ADDR;
                    end
                else begin
                        if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
                            next_state = SEND_STORAGE_ADDR_L;
                        else 
                            next_state = SEND_DEVICE_ADDR;
                end
        SEND_STORAGE_ADDR_H:
            if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
                next_state = SEND_STORAGE_ADDR_L;
            else 
                next_state = SEND_STORAGE_ADDR_H;
        SEND_STORAGE_ADDR_L:
            if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8)&&(i2c_rh_wl == 1'b0))
                next_state = WR_DATA;
            else if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8)&&(i2c_rh_wl == 1'b1))
                next_state = START_2;
            else 
                next_state = SEND_STORAGE_ADDR_L;
        WR_DATA:
            if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
                next_state = STOP;
            else 
                next_state = WR_DATA;
        STOP:
            if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd3))
                next_state = IDLE ;
            else 
                next_state = STOP ;
        START_2:
            if(i2c_rh_wl == 1'b1) begin 
                if(cnt_driver_clk == 2'd3)
                    next_state = SEND_DEVICE_ADDR_RD ;
                else 
                    next_state =  START_2;
            end
            else 
                next_state = START_2 ;
        SEND_DEVICE_ADDR_RD:
            if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
                next_state = RD_DATA ;
            else 
                next_state =  SEND_DEVICE_ADDR_RD;
            
        RD_DATA:
            if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
                next_state = STOP ;
            else 
                next_state = RD_DATA ;
        default:next_state = IDLE;
      endcase 
end
/*****************************************************************************
模块说明:状态机中的时序逻辑表示输出
输出参数:i2c_scl
*****************************************************************************/
always @(posedge driver_clk or negedge sys_rst) begin
    if (!sys_rst)
        i2c_scl <= 1'b1;
    else if ((current_state == IDLE)|| (current_state == STOP))
        i2c_scl <= 1'b1;
    else
        i2c_scl <= (cnt_driver_clk == 2'd2) ? 1'b0 : 
                   (cnt_driver_clk == 2'd0) ? 1'b1 : i2c_scl;
end

解释:在IDLE状态下,当出现一个有效的下降沿时,也就是开始信号(i2c_start),状态跳转到START状态。当经过一个iic的时钟周期之后(cnt_driver_clk == 2‘d3),表示START状态持续了一个时钟周期,此时跳转到SEND_DEVICE_ADDR状态,在这里我参考的野火和原子的教程,对存储地址的字节数进行了判断,当addr_num=1时,表示是两个字节,当(cnt_driver_clk=3,cnt_bit=8,sda_in=0时),表示器件地址写入完成,此时进入下一个状态SEND_STORAGE_ADDR_H。相反 addr_num = 0时为一个字节的存储器地址,此时直接跳到SEND_STORAGE_ADDR_L状态。在SEND_STORAGE_ADDR_H状态时,当写入8个字节的高八位存储器地址时,跳入SEND_STORAGE_ADDR_L状态,在SEND_STORAGE_ADDR_L状态下,在写入的八位低存储器地址,此时若i2c_rh_wl= 0则进入写数据状态WR_DATA,若此时i2c_rh_wl = 1则进入读状态START_2。在WR_DATA状态的完成时,进入STOP状态,在时钟的上升沿的同时数据拉高,状态结束。在START_2状态下,再次经过一个IIc时钟周期,进入SEND_DEVICE_ADDR_RD状态,在这个状态下,当发送完第八个字节,切iic从机的相应为0时,进入读数据状态。无论是在WR_DATA还是RD_DATA状态他们的状态转移条件都是,写或者读满8个字节,之后进行状态的跳转。
下面附上不同状态的输入输出程序:

/*****************************************************************************
模块说明:状态机中的组合逻辑表示输出
输出参数:sda_out
*****************************************************************************/
always @(*) begin 
    if(! sys_rst ) begin 
        sda_out  = 1'b1 ;
        out_flag = 1'b1 ;
    end 
    else  begin
        case(current_state)
            IDLE:begin
                	sda_out  = 1'b1 ;
                	out_flag = 1'b1 ;
                end
            START:
                if(cnt_driver_clk <= 2'b0)
                    sda_out  = 1'b1 ;
                else 
                    sda_out  = 1'b0 ;
            SEND_DEVICE_ADDR:  begin
                out_flag = 1'b1;
                if(cnt_bit <= 4'd6 )
                    sda_out = DIVICE_ADDR[6-cnt_bit];
                else if(cnt_bit == 4'd7)
                    sda_out = 1'b0;      //写数据
                else  begin//if(cnt_bit == 3'd8)
                    	out_flag = 1'b0;
                    	sda_out  = 1'b1;
                    end
               end
            SEND_STORAGE_ADDR_H: begin
                out_flag = 1'b1 ;
                if(cnt_bit <= 4'd7)
                    sda_out = byte_addr[15-cnt_bit];
                else  begin//if(cnt_bit == 3'd8)
                    	out_flag = 1'b0;
                    	sda_out  = 1'b1;
                    end
                end
            SEND_STORAGE_ADDR_L: begin
                out_flag = 1'b1 ;
                if(cnt_bit <= 4'd7)
                    sda_out = byte_addr[7-cnt_bit];
                else  begin//if(cnt_bit == 3'd8)
                    	out_flag = 1'b0;
                    	sda_out  = 1'b1;
                     end
                end 
            WR_DATA: begin 
                out_flag = 1'b1 ;
                if(cnt_bit <= 4'd7)
                    sda_out = wr_data[7-cnt_bit];
                else begin //if(cnt_bit == 3'd8)
                    	sda_out = 1'b1;
                    	out_flag = 1'b0;
                    end
                end
            STOP: begin
                out_flag = 1'b1 ;
                if((cnt_bit == 3'd0)&&(cnt_driver_clk == 2'b0))
                    sda_out = 1'b0;
                else if((cnt_bit == 3'd0)&&(cnt_driver_clk == 2'd3))
                    sda_out = 1'b1;
                else 
                    sda_out = sda_out ;
               end
            START_2:begin 
                out_flag = 1'b1 ;
                if(cnt_driver_clk < 2'd2)
                    sda_out  = 1'b1 ;
                else 
                    sda_out  = 1'b0 ;
                end
            SEND_DEVICE_ADDR_RD: begin
                out_flag = 1'b1 ;
                if(cnt_bit <= 4'd6)
                    sda_out = DIVICE_ADDR[6-cnt_bit];
                else if(cnt_bit == 4'd7)
                    sda_out = 1'b1;
                else begin //if(cnt_bit == 3'd8)
                    	sda_out  = 1'b1;
                    	out_flag = 1'b0;   
                    end
             end
            RD_DATA: begin
                out_flag = 1'b0;
                if((cnt_bit <= 4'd7)&&(cnt_driver_clk=='d2))
                        rd_data_reg[7-cnt_bit] = sda_in ; 
                else   if(cnt_bit == 4'd8) begin
                    	out_flag = 1'b1;
                    	sda_out  = 1'b1;    //非应答信号
                end
              end
            default:;
        endcase
    end
end

再次之前我先将一部分波形图附上:
1.写操作的波形图
在这里插入图片描述在这里插入图片描述
2.读操作的波形图
在这里插入图片描述在这里插入图片描述在这里插入图片描述

cnt_driver_clk 先解释一下,这个变量的意思:本实验输出的IIC频率为250k,fpga的时钟输入时钟为50mhz,先进行分频将1mhz时钟driver_clk,这时采用一个cnt_driver_clk 计数器,计数到3的时候就说明iic经过一个时钟周期。cnt_bit,字节计数器。
大家这里可以参考波形图,理解代码,这里要写的太多了,如果有需要建议私信,我们好友联系。
最后一个知识点:数据io可以是双向的,这个可以将io口设计为三态模式的。

/************************三态门的使用**************************************/

assign  i2c_sda = out_flag ? sda_out : 1'bz ;     //当out_flag输出为高时,表示输出   
assign  sda_in  = i2c_sda                   ;

这段代码的解释就是,当out_flag为高电平时,i2c_sda = sda_out,当为低电平时,i2c_sda为高阻态。out_flag和sda_out的赋值,详细请看上面的代码,里面详细写了不同状态下,sda_out和out_flag是如何赋值的。当out_flag为低电平时,i2c_sda为输入作用。sda_in的赋值也在上面的代码中有详细的介绍。
IIC的介绍就到这里,如果需要工程源码,私信即可,欢迎大家交流沟通。
关于应答信号的一些问题,题主在这里卡了好久,通过ila抓取,实际上的应答信号不是持续iic_scl的一个完整时钟周期的,这里我用的板子是黑金zynq7020+24lc04。为了程序的严谨性,我同样将ack采集出来了。这里的iic是单字节写入和读出,所以在理论上一个完成的iic协议应该有4个(寄存器地址为8位)或者5个ack(寄存器地址为16位)回应。故通过这样的分析我采集的逻辑如下:

assign  ack = (current_state == SEND_DEVICE_ADDR || current_state == SEND_DEVICE_ADDR_RD 
                || current_state == WR_DATA || current_state == SEND_STORAGE_ADDR_H
                    ||current_state == SEND_STORAGE_ADDR_L) 
                    ? ((cnt_bit == 'd8&&cnt_driver_clk<= 'd2) ? sda_in:1'b1):1'b1;
always @(posedge driver_clk or negedge sys_rst) begin
    if(!sys_rst)
        ack_d1 <= 'd1;
    else 
        ack_d1 <= ack;
end
assign ack_pos = (~ack_d1)&ack;
always @(posedge driver_clk or negedge sys_rst) begin
    if(!sys_rst)
        i2c_ack <= 1'b1;
    else if(ack_num == 'd4 && addr_num == 'd1)
        i2c_ack <= 1'b0;
    else if(ack_num == 'd3 && addr_num == 'd0)
        i2c_ack <= 1'b0;
    else 
        i2c_ack <= i2c_ack;
end
always @(posedge driver_clk or negedge sys_rst) begin
    if(!sys_rst)
        ack_num <= 'd0;
    else if(ack_num == 'd4 && addr_num == 'd1)
        ack_num <= 'd0;
    else if(ack_num == 'd3 && addr_num == 'd0)   
        ack_num <= 'd0;
    else if(ack_pos)
        ack_num <= ack_num + 1'b1;
    else 
        ack_num <= ack_num;
end

初始i2c_ack 信号为高电平,当ack_num满足条件时,拉低。程序可以通过这样的逻辑来检测ack应答信号是否正确。
过段时间更新以太网的状态机实现,至此状态机将不再做更新。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值