Verilog -- IIC总线协议

Verilog – IIC总线协议

文章目录

  • Verilog -- IIC总线协议
    • 简介
    • 读写时序
      • 写时序
      • 读时序
    • verilog代码设计
      • IIC发送模块的接口定义与整体设计
      • SCL标志位创建逻辑
      • 发送逻辑
      • 读逻辑
    • IIC设备多字节连续读写操作

参考自 https://www.cnblogs.com/liujinggang/p/9656358.html
上面的博文写的很好,下面只是摘录一些重要知识点以及自己的理解。

简介

IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。I2C总线产生于在80年代,最初为音频和视频设备开发,如今主要在服务器管理中使用,其中包括单个组件状态的通信。例如管理员可对各个组件进行查询,以管理系统的配置或掌握组件的功能状态,如电源和系统风扇。可随时监控内存、硬盘、网络、系统温度等多个参数,增加了系统的安全性,方便了管理。IIC数据传输速率有标准模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些变种实现了低速模式(10 kbps)和快速+模式(1 Mbps)。
需要注意的是IIC总线的数据采集是scl高电平期间,再0电平期间允许sda数据变化。因此是电平敏感的,不是边沿触发类型

Verilog -- IIC总线协议_第1张图片

下图是一个嵌入式系统中处理器仅通过2根线的IIC总线控制多个IIC外设的典型应用图
  

Verilog -- IIC总线协议_第2张图片

读写时序

写时序

主机通过IIC总线往从机中写数据的时候,主机首先会发送一个起始信号,接着把IIC从机的7位设备地址后面添一个0(设备地址后面的0表示主机向从机写数据,1表示主机从从机中读数据)组成一个8位的数据,把这个8位的数据发给从机,发完这8位的数据以后主机马上释放SDA信号线等待从机的应答,如果从机正确收到这个数据,从机就会发送一个有效应答位0给主机告诉主机自己已经收到了数据,主机收到从机的有效应答位以后 ,接下来主机会发送想要写入的寄存器地址,寄存器发送完毕以后主机同样会释放SDA信号线等待从机的应答,从机如果正确收到了主机发过来的寄存器地址,从机会再次发送一个有效应答位给主机,主机收到从机的有效应答位0以后,接下来主机就会给从机发送想要写入从机的数据,从机正确收到这个数据以后仍然像之前两次一样会给主机发送一个有效应答位,主机收到这个有效应答位以后给从机发送一个停止信号,整个传输过程就结束了。下图是整个传输过程的示意图:

Verilog -- IIC总线协议_第3张图片

读时序

主机通过IIC总线从从机中读数据的过程与写数据的过程有相似之处,但是读数据的过程还多了一些额外的步骤。主机从从机读数据时主机首先会发送一个起始信号,接着把IIC从机的7位设备地址后面添一个0(设备地址后面的0表示主机向从机写数据,1表示主机从从机中读数据),把这个8位的数据发给从机,发完这8位的数据以后主机马上释放SDA信号线等待从机的应答,如果从机正确收到这个数据,从机就会发送一个有效应答位0给主机告诉主机自己已经收到了数据,主机收到从机的有效应答位以后 ,接下来主机会发送想要读的寄存器地址,寄存器发送完毕以后主机同样会释放SDA信号线等待从机的应答,从机如果正确收到了主机发过来的寄存器地址,从机会再次发送一个有效应答位给主机,主机收到从机的有效应答位0以后,主机会给从机再次发送一次起始信号,接着把IIC从机的7位设备地址后面添一个1(设备地址后面的0表示主机向从机写数据,1表示主机从从机中读数据),注意,第一次是在设备地址后面添0,这一次是在设备地址后面添1,把这个8位的数据发给从机,发完这8位的数据以后主机马上释放SDA信号线等待从机的应答,如果从机正确收到这个数据,从机就会发送一个有效应答位0给主机告诉主机自己已经收到了数据,接着从机继续占用SDA信号线给主机发送寄存器中的数据,发送完毕以后,主机再次占用SDA信号线发送一个非应答信号1给从机,主机发送一个停止信号给从机结束整个读数据的过程。下图是整个读数据过程的示意图:

Verilog -- IIC总线协议_第4张图片

verilog代码设计

IIC发送模块的接口定义与整体设计

Verilog编写的IIC发送模块除了进行IIC通信的两根信号线(SCL和SDA)以外还要包括一些时钟、复位、使能、并行的输入输出以及完成标志位。其框图如下所示:
  

Verilog -- IIC总线协议_第5张图片

  其中:

  • I_clk是系统时钟;
  • I_rst_n是系统复位;
  • I_iic_send_en发送使能信号,当I_iic_send_en为1时IIC主机(FPGA)才能给IIC从机发送数据;
  • I_dev_addr[6:0]是IIC从机的设备地址;
  • I_word_addr[7:0]是字地址,也就是我们想要操作的IIC设备的内部存储地址;
  • I_write_data[7:0]是主机(FPGA)要往IIC字地址中写入的数据;
  • O_done_flag是主机(FPGA)发送一个字节完成标志位,发送完成后会产生一个高脉冲;
  • O_scl是IIC总线的串行时钟线;
  • IO_sda是IIC总线的串行数据线;

通过观察上面的时序图可以看出,发送一个字节的数据之前必须要先发送起始位,然后发送控制字节,接着等待应答,然后在发送字地址,接着在等待应答。数据发送完毕以后,在等待最后一个应答,应答成功后发送停止信号结束整个过程。所以,根据这个流程,可以归纳出如下几个状态:

  • 状态0:空闲状态,用来初始化各个寄存器的值
  • 状态1:加载IIC设备的物理地址
  • 状态2:加载IIC设备的字地址
  • 状态3:加载要发送的数据
  • 状态4:发送起始信号
  • 状态5:发送一个字节,从高位开始发送
  • 状态6:接收应答状态的应答位
  • 状态7:校验应答位
  • 状态8:发送停止信号
  • 状态9:IIC写操作结束

需要注意的是上面的各个状态并不是按照顺序执行的,有些状态要复用多次,比如状态5发送字节的状态就需要复用三次用来发送三个8-bit的数据;同样,状态6和状态7也要复用多次。
下面上代码:

SCL标志位创建逻辑

由于IIC时序要求数据线SDA在串行时钟线的高电平保持不变,在串行时钟线的低电平才能变化,所以代码里面必须在串行时钟线低电平的正中间产生一个标志位,写代码的时候在这个标志位处改变SDA的值,这样就可以保证SDA在SCL的高电平期间保持稳定了。同时,必须在SCL高电平期间的正中间判断应答信号是否满足条件(0为有效应答,1为无效应答),因此代码里面还必须在串行时钟线高电平的正中间产生一个标志位,在这个标志下接收应答位并进行校验。

parameter   C_DIV_SELECT        =   10'd500 ; // 分频系数选择

parameter   C_DIV_SELECT0       =   (C_DIV_SELECT >> 2)  -  1           , // 用来产生IIC总线SCL高电平最中间的标志位
            C_DIV_SELECT1       =   (C_DIV_SELECT >> 1)  -  1           ,
            C_DIV_SELECT2       =   (C_DIV_SELECT0 + C_DIV_SELECT1) + 1 , // 用来产生IIC总线SCL低电平最中间的标志位
            C_DIV_SELECT3       =   (C_DIV_SELECT >> 1)  +  1           ; // 用来产生IIC总线SCL下降沿标志位

always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        R_scl_cnt   <=  10'd0 ; 
    else if(R_scl_en)   
        begin
            if(R_scl_cnt == C_DIV_SELECT - 1'b1)
                R_scl_cnt <= 10'd0 ;
            else
                R_scl_cnt <= R_scl_cnt + 1'b1 ;     
        end
    else
        R_scl_cnt   <= 10'd0 ;
end

assign O_scl          = (R_scl_cnt <= C_DIV_SELECT1) ? 1'b1 : 1'b0 ; // 产生串行时钟信号O_scl
assign W_scl_low_mid  = (R_scl_cnt == C_DIV_SELECT2) ? 1'b1 : 1'b0 ; // 产生scl高低平正中间标志位
assign W_scl_high_mid = (R_scl_cnt == C_DIV_SELECT0) ? 1'b1 : 1'b0 ; // 产生scl高电平正中间标志位

发送逻辑

发送8-bit数据的整个过程如下:加载8-bit数据->发送8-bit数据->接收应答位->校验应答位->加载第二个8-bit数据………。所以为了复用中间标红的这几个状态,必须在加载8-bit数据这个状态提前设置好校验应答位状态执行完毕以后的后一个状态的位置,这在代码里面通过R_jump_state这个变量来完成

module iic_send
(
    input                I_clk           , // 系统50MHz时钟
    input                I_rst_n         , // 系统全局复位
    input                 I_iic_send_en   , // IIC发送使能位
    
    input        [6:0]   I_dev_addr      , // IIC设备的物理地址
    input        [7:0]   I_word_addr     , // IIC设备的字地址,即我们想操作的IIC的内部地址
    input        [7:0]   I_write_data    , // 往IIC设备的字地址写入的数据
    output  reg          O_done_flag     , // 读或写IIC设备结束标志位
    
    // 标准的IIC设备总线
    output               O_scl           , // IIC总线的串行时钟线
    inout                IO_sda            // IIC总线的双向数据线
);          

parameter   C_DIV_SELECT        =   10'd500 ; // 分频系数选择

parameter   C_DIV_SELECT0       =   (C_DIV_SELECT >> 2)  -  1           , // 用来产生IIC总线SCL低电平最中间的标志位
            C_DIV_SELECT1       =   (C_DIV_SELECT >> 1)  -  1           ,
            C_DIV_SELECT2       =   (C_DIV_SELECT0 + C_DIV_SELECT1) + 1 , // 用来产生IIC总线SCL高电平最中间的标志位
            C_DIV_SELECT3       =   (C_DIV_SELECT >> 1)  +  1           ; // 用来产生IIC总线SCL下降沿标志位
 

reg     [9:0]   R_scl_cnt       ; // 用来产生IIC总线SCL时钟线的计数器   
reg             R_scl_en        ; // IIC总线SCL时钟线使能信号
reg     [3:0]   R_state         ; 
reg             R_sda_mode      ; // 设置SDA模式,1位输出,0为输入
reg             R_sda_reg       ; // SDA寄存器
reg     [7:0]   R_load_data     ; // 发送/接收过程中加载的数据,比如设备物理地址,字地址和数据等
reg     [3:0]   R_bit_cnt       ; // 发送字节状态中bit个数计数
reg             R_ack_flag      ; // 应答标志
reg     [3:0]   R_jump_state    ; // 跳转状态,传输一个字节成功并应答以后通过这个变量跳转到导入下一个数据的状态

wire            W_scl_low_mid   ; // SCL的低电平中间标志位
wire            W_scl_high_mid  ; // SCL的高电平中间标志位
wire            W_scl_neg        ; // SCL的下降沿标志位

assign IO_sda  =  (R_sda_mode == 1'b1) ? R_sda_reg : 1'bz ;

always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        R_scl_cnt   <=  10'd0 ; 
    else if(R_scl_en)   
        begin
            if(R_scl_cnt == C_DIV_SELECT - 1'b1)
                R_scl_cnt <= 10'd0 ;
            else
                R_scl_cnt <= R_scl_cnt + 1'b1 ;     
        end
    else
        R_scl_cnt     <= 10'd0 ;
end

assign O_scl           = (R_scl_cnt <= C_DIV_SELECT1) ? 1'b1 : 1'b0 ; // 产生串行时钟信号O_scl
assign W_scl_low_mid  = (R_scl_cnt == C_DIV_SELECT2) ? 1'b1 : 1'b0 ; // 产生scl低电平正中间标志位
assign W_scl_high_mid = (R_scl_cnt == C_DIV_SELECT0) ? 1'b1 : 1'b0 ; // 产生scl高电平正中间标志位
assign W_scl_neg       = (R_scl_cnt == C_DIV_SELECT3) ? 1'b1 : 1'b0 ; // 产生scl下降沿标志位

always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        begin
            R_state         <=  4'd0 ;
            R_sda_mode      <=  1'b1 ;
            R_sda_reg       <=  1'b1 ;
            R_bit_cnt       <=  4'd0 ;
            O_done_flag     <=  1'b0 ;
            R_jump_state    <=  4'd0 ;
            R_ack_flag        <=    1'b0 ;
        end
    else if(I_iic_send_en) // 往IIC设备发送数据
        begin
            case(R_state)
                4'd0   : // 空闲状态设置SCL与SDA均为高
                    begin
                        R_sda_mode      <=  1'b1 ; // 设置SDA为输出
                        R_sda_reg       <=  1'b1 ; // 设置SDA为高电平
                        R_scl_en        <=  1'b0 ; // 关闭SCL时钟线
                        R_state         <=  4'd1 ; // 下一个状态是加载设备物理地址状态
                        R_bit_cnt       <=  4'd0 ; // 发送字节状态中bit个数计数清零
                        O_done_flag     <=  1'b0 ;
                        R_jump_state    <=  4'd0 ;
                    end                               
                4'd1   :  // 加载IIC设备物理地址              
                    begin                             
                        R_load_data     <=  {I_dev_addr, 1'b0}  ;
                        R_state         <=  4'd4                ;
                        R_jump_state    <=  4'd2                ;
                    end                                     
                4'd2   :   // 加载IIC设备字地址                     
                    begin                                   
                        R_load_data     <=  I_word_addr         ; 
                        R_state         <=  4'd5                ;
                        R_jump_state    <=  4'd3                ;
                    end                                     
                4'd3   :    // 加载要发送的数据                    
                    begin                                   
                        R_load_data     <=  I_write_data        ; 
                        R_state         <=  4'd5                ;
                        R_jump_state    <=  4'd8                ;
                    end                                                         
                4'd4   :    // 发送起始信号                   
                    begin                                   
                        R_scl_en    <=  1'b1                ; // 打开SCL时钟线
                        R_sda_mode  <=  1'b1                ; // 设置SDA为输出
                        if(W_scl_high_mid)                  
                            begin                           
                                R_sda_reg   <=  1'b0        ; // 在SCL高电平中间把SDA信号拉低,产生起始信号
                                R_state     <=  4'd5        ; 
                            end
                        else
                            R_state <=  4'd4                ; // 如果SCL高电平中间标志没出现就一直在这个状态等着                          
                    end
                4'd5   :    // 发送1个字节,从高位开始发
                    begin
                        R_scl_en    <=  1'b1                ; // 打开SCL时钟线
                        R_sda_mode  <=  1'b1                ; // 设置SDA为输出
                        if(W_scl_low_mid)
                            begin
                                if(R_bit_cnt == 4'd8)
                                    begin
                                        R_bit_cnt   <=  4'd0            ;
                                        R_state     <=  4'd6            ; // 字节发完以后进入应答状态
                                    end
                                else
                                    begin                                 
                                        R_sda_reg   <=  R_load_data[7-R_bit_cnt] ; // 先发送高位
                                        R_bit_cnt   <=  R_bit_cnt + 1'b1         ; 
                                    end
                            end
                        else
                            R_state <=  4'd5 ; // 字节没发完时在这个状态一直等待 
                    end 
                4'd6   :    // 接收应答状态的应答位
                    begin
                        R_scl_en    <=  1'b1  ; // 打开SCL时钟线
                        R_sda_mode  <=  1'b0  ; // 设置SDA为输入
                        if(W_scl_high_mid)
                            begin
                                R_ack_flag  <=  IO_sda  ; 
                                R_state     <=  4'd7    ; 
                            end                            
                        else
                            R_state <=  4'd6  ;     
                    end
                4'd7  :    // 校验应答位
                    begin
                        R_scl_en    <=  1'b1  ; // 打开SCL时钟线                        
                        if(R_ack_flag == 1'b0)    // 校验通过
                            begin
                                if(W_scl_neg == 1'b1) 
                                    begin
                                        R_state <=  R_jump_state ;
                                        R_sda_mode  <=  1'b1 ; // 设置SDA的模式为输出
                                        R_sda_reg   <=  1'b0 ; // 读取完应答信号以后要把SDA信号设置成输出并拉低,因为如果这个状
                                                               // 态后面是停止状态的话,需要SDA信号的上升沿,所以这里提前拉低它
                                    end
                                else
                                    R_state <= 4'd7    ;
                            end
                        else
                            R_state <=  4'd0 ;      
                    end
                4'd8   : // 发送停止信号
                    begin
                        R_scl_en    <=  1'b1        ; // 打开SCL时钟线
                        R_sda_mode  <=  1'b1        ; // 设置SDA为输出
                        if(W_scl_high_mid)
                            begin
                                R_sda_reg   <=  1'b1 ;
                                R_state     <=  4'd9 ;
                            end
                    end
                4'd9    :   // IIC写操作结束
                    begin
                        R_scl_en    <=  1'b0 ; // 关闭SCL时钟线
                        R_sda_mode  <=  1'b1 ; // 设置SDA为输出
                        R_sda_reg   <=  1'b1 ; // 拉高SDA保持空闲状态情况
                        O_done_flag <=  1'b1 ;
                        R_state     <=  4'd0 ; 
                        R_ack_flag  <=  1'b0 ;
                    end  
                default    : R_state     <=  4'd0 ; 
            endcase
        end 
    else
        begin
            R_state         <=  4'd0 ;
            R_sda_mode      <=  1'b1 ;
            R_sda_reg       <=  1'b1 ;
            R_bit_cnt       <=  4'd0 ;
            O_done_flag     <=  1'b0 ;
            R_jump_state    <=  4'd0 ;
            R_ack_flag      <=  1'b0 ;
        end
end


读逻辑

Verilog -- IIC总线协议_第6张图片

其中:

I_clk是系统时钟;

I_rst_n是系统复位;

I_iic_recv_en接收使能信号,当I_iic_recv_en为1时IIC主机(FPGA)才能从IIC从机接收数据;

I_dev_addr[6:0]是IIC从机的设备地址;

I_word_addr[7:0]是字地址,也就是我们想要读取的IIC设备的内部存储地址;

O_read_data[7:0]是主机(FPGA)从IIC设备字地址中读取的数据;

O_done_flag是主机(FPGA)接收一个字节完成标志位,接收完成后会产生一个高脉冲;

O_scl是IIC总线的串行时钟线;

IO_sda是IIC总线的串行数据线;

module iic_recv
(
    input                I_clk           , // 系统50MHz时钟
    input                I_rst_n         , // 系统全局复位
    input                I_iic_recv_en   , // IIC发送使能位
    
    input        [6:0]   I_dev_addr      , // IIC设备的物理地址
    input        [7:0]   I_word_addr     , // IIC设备的字地址,即我们想操作的IIC的内部地址
    output  reg  [7:0]   O_read_data     , // 从IIC设备的字地址读出来的数据   
    output  reg          O_done_flag     , // 读或写IIC设备结束标志位
    
    // 标准的IIC设备总线
    output               O_scl           , // IIC总线的串行时钟线
    inout                IO_sda            // IIC总线的双向数据线
);          

parameter   C_DIV_SELECT        =   10'd500 ; // 分频系数选择

parameter   C_DIV_SELECT0       =   (C_DIV_SELECT >> 2)  -  1           , // 用来产生IIC总线SCL高电平最中间的标志位
            C_DIV_SELECT1       =   (C_DIV_SELECT >> 1)  -  1           , // 用来产生IIC串行时钟线
            C_DIV_SELECT2       =   (C_DIV_SELECT0 + C_DIV_SELECT1) + 1 , // 用来产生IIC总线SCL低电平最中间的标志位
            C_DIV_SELECT3       =   (C_DIV_SELECT >> 1) + 1             ; // 用来产生IIC总线SCL下降沿标志位
 

reg     [9:0]   R_scl_cnt       ; // 用来产生IIC总线SCL时钟线的计数器   
reg             R_scl_en        ; // IIC总线SCL时钟线使能信号
reg     [3:0]   R_state         ; 
reg             R_sda_mode      ; // 设置SDA模式,1位输出,0为输入
reg             R_sda_reg       ; // SDA寄存器
reg     [7:0]   R_load_data     ; // 发送/接收过程中加载的数据,比如设备物理地址,字地址和数据等
reg     [3:0]   R_bit_cnt       ; // 发送字节状态中bit个数计数
reg             R_ack_flag      ; // 应答标志
reg     [3:0]   R_jump_state    ; // 跳转状态,传输一个字节成功并应答以后通过这个变量跳转到导入下一个数据的状态
reg     [7:0]   R_read_data_reg ;

wire            W_scl_low_mid   ; // SCL的低电平中间标志位
wire            W_scl_high_mid  ; // SCL的高电平中间标志位

assign IO_sda  =  (R_sda_mode == 1'b1) ? R_sda_reg : 1'bz ;

always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        R_scl_cnt   <=  10'd0 ; 
    else if(R_scl_en)   
        begin
            if(R_scl_cnt == C_DIV_SELECT - 1'b1)
                R_scl_cnt <= 10'd0 ;
            else
                R_scl_cnt <= R_scl_cnt + 1'b1 ;     
        end
    else
        R_scl_cnt     <= 10'd0 ;
end

assign O_scl           = (R_scl_cnt <= C_DIV_SELECT1) ? 1'b1 : 1'b0 ; // 产生串行时钟信号O_scl
assign W_scl_low_mid  = (R_scl_cnt == C_DIV_SELECT2) ? 1'b1 : 1'b0 ; // 产生scl低电平正中间标志位
assign W_scl_high_mid = (R_scl_cnt == C_DIV_SELECT0) ? 1'b1 : 1'b0 ; // 产生scl高电平正中间标志位
assign W_scl_neg       = (R_scl_cnt == C_DIV_SELECT3) ? 1'b1 : 1'b0 ; // 产生scl下降沿标志位

always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        begin
            R_state         <=  4'd0 ;
            R_sda_mode      <=  1'b1 ;
            R_sda_reg       <=  1'b1 ;
            R_bit_cnt       <=  4'd0 ;
            O_done_flag     <=  1'b0 ;
            R_jump_state    <=  4'd0 ;
            R_read_data_reg <=  8'd0 ;
            R_ack_flag        <=    1'b0 ;
            O_read_data        <=    8'd0 ;
        end
    else if(I_iic_recv_en) // 往IIC设备发送数据
        begin
            case(R_state)
                4'd0    :   // 空闲状态,用来初始化相关所有信号
                    begin
                        R_sda_mode      <=  1'b1 ; // 设置SDA为输出
                        R_sda_reg       <=  1'b1 ; // 设置SDA为高电平
                        R_scl_en        <=  1'b0 ; // 关闭SCL时钟线
                        R_state         <=  4'd1 ; // 下一个状态是加载设备物理地址状态
                        R_bit_cnt       <=  4'd0 ;
                        O_done_flag     <=  1'b0 ;
                        R_jump_state    <=  5'd0 ;
                        R_read_data_reg <=  8'd0 ;
                    end
                4'd1    :   // 加载IIC设备物理地址 
                    begin
                        R_load_data <=  {I_dev_addr, 1'b0}  ;
                        R_state     <=  4'd3                ; // 加载完设备物理地址以后进入起始状态
                        R_jump_state <=  R_state + 1'b1     ; 
                    end
                4'd2   :   // 加载IIC设备字地址                     
                    begin                                   
                        R_load_data <=  I_word_addr         ; 
                        R_state     <=  4'd4                ;
                        R_jump_state <=  R_state + 5'd5      ; // 设置这里是为了这一轮发送并应答后跳到第二次启始位
                    end 
                4'd3    :   // 发送第一个起始信号
                    begin
                        R_scl_en    <=  1'b1                ; // 打开时钟
                        R_sda_mode  <=  1'b1                ; // 设置SDA的模式为输出
                        if(W_scl_high_mid)
                            begin
                                R_sda_reg   <=  1'b0        ; // 在SCL高电平的正中间把SDA引脚拉低产生一个下降沿
                                R_state     <=  4'd4        ; // 下一个状态是发送一个字节数据(IIC设备的物理地址) 
                            end
                        else
                            R_state <=  4'd3                ;    
                    end                      
                4'd4    :   // 发送一个字节
                    begin
                        R_scl_en    <=  1'b1                ; // 打开时钟
                        R_sda_mode  <=  1'b1                ; // 设置SDA的模式为输出
                        if(W_scl_low_mid)                     // 在SCL低电平的最中间改变数据
                            begin
                                if(R_bit_cnt == 4'd8)
                                    begin
                                        R_bit_cnt  <=  4'd0 ;  
                                        R_state    <=  4'd5 ;
                                    end 
                                else
                                    begin
                                        R_sda_reg  <=  R_load_data[7-R_bit_cnt] ;
                                        R_bit_cnt  <=  R_bit_cnt + 1'b1        ; 
                                    end     
                            end
                        else
                            R_state <=  4'd4    ;    
                    end
                4'd5    :   // 接收应答状态应答位
                    begin
                        R_scl_en    <=  1'b1 ; // 打开时钟
                        R_sda_reg   <=  1'b0 ;
                        R_sda_mode  <=  1'b0 ; // 设置SDA的模式为输入
                        if(W_scl_high_mid)  
                            begin
                                R_ack_flag  <=  IO_sda  ;
                                R_state     <=  4'd6    ;                                   
                            end
                        else
                            R_state <=  4'd5    ;    
                    end                      
                4'd6    :   // 校验应答位
                    begin
                        R_scl_en    <=  1'b1 ; // 打开时钟 
                        if(R_ack_flag   == 1'b0)    // 校验通过
                            begin
                                if(W_scl_neg == 1'b1)
                                    begin
                                        R_state <=  R_jump_state ;
                                        R_sda_mode  <=  1'b1 ; // 设置SDA的模式为输出
                                        R_sda_reg   <=  1'b1 ; // 设置SDA的引脚电平拉高,方便后面产生第二次起始位
                                    end
                                else
                                    R_state <= 4'd6    ;
                            end
                        else
                            R_state <=  4'd0 ;    
                    end 
                4'd7    :   // 第二次起始位(IIC读操作要求有2次起始位) 
                    begin
                        R_scl_en    <=  1'b1 ; // 打开时钟
                        R_sda_mode  <=  1'b1 ; // 设置SDA的模式为输出 
                        if(W_scl_high_mid)
                            begin
                                R_sda_reg   <=  1'b0  ;
                                R_state     <=  4'd8  ; 
                            end
                        else
                            R_state <=  4'd7 ;    
                    end 
                4'd8   :   // 再次加载IIC设备物理地址 ,但这次地址最后一位应该为1,表示读操作                    
                    begin                                   
                        R_load_data     <=  {I_dev_addr, 1'b1}  ; // 前7bit是设备物理地址,最后一位1表示读操作
                        R_state         <=  4'd4            ;
                        R_jump_state    <=  4'd9            ; // 设置这里是为了这一轮发送并应答后跳到第二次启始位
                    end
                4'd9    :   // 读一个字节数据
                    begin
                        R_scl_en    <=  1'b1 ; // 打开时钟
                        R_sda_mode  <=  1'b0 ; // 设置SDA的模式为输入
                        if(W_scl_high_mid)
                            begin
                                if(R_bit_cnt == 4'd7)
                                    begin
                                        R_bit_cnt    <=  4'd0    ;
                                        R_state      <=  4'd10   ;  
                                        O_read_data  <=  {R_read_data_reg[6:0],IO_sda}  ;                                           
                                    end
                                else
                                    begin
                                        R_read_data_reg  <=  {R_read_data_reg[6:0],IO_sda}  ;
                                        R_bit_cnt        <=  R_bit_cnt   +   1'b1            ;  
                                    end    
                            end 
                        else
                            R_state <=  4'd9 ;    
                    end 
                4'd10    :  // 读完一个字节数据以后进入10,主机发送一个非应答信号1
                    begin
                        R_scl_en    <=  1'b1 ; // 打开时钟
                        R_sda_mode  <=  1'b1 ; // 设置SDA的模式为输入
                        if(W_scl_low_mid)
                            begin
                                R_state     <=  4'd11   ; 
                                R_sda_reg    <=    1'b1     ;
                            end 
                        else
                            R_state <=  4'd10 ;    
                    end 
                4'd11   :  
                    begin
                        R_scl_en    <=  1'b1 ; // 打开时钟
                        R_sda_mode  <=  1'b1 ; // 设置SDA的模式为输入
                        if(W_scl_low_mid)
                            begin
                                R_state     <=  4'd12   ; 
                                R_sda_reg    <=    1'b0     ;
                            end
                        else
                            R_state <=  4'd11   ;    
                    end 
                4'd12   : //停止位Stop
                    begin
                        R_scl_en    <=  1'b1 ; // 打开时钟
                        R_sda_mode  <=  1'b1 ; // 设置SDA的模式为输出
                        if(W_scl_high_mid)
                            begin
                                R_sda_reg   <=  1'b1    ;
                                R_state     <=  4'd13   ; 
                            end
                        else
                            R_state <=  4'd12   ;    
                    end 
                4'd13   :
                    begin
                        R_scl_en        <=  1'b0 ; // 关闭SCL时钟线
                        R_sda_mode      <=  1'b1 ; // 设置SDA为输出
                        R_sda_reg       <=  1'b1 ; // 拉高SDA保持空闲状态情况
                        O_done_flag     <=  1'b1 ;
                        R_state         <=  4'd0 ;
                        R_read_data_reg <=  8'd0 ;
                    end 
                default: R_state         <=  4'd0 ;
            endcase
        end 
    else
        begin
            R_state         <=  4'd0 ;
            R_sda_mode      <=  1'b1 ;
            R_sda_reg       <=  1'b1 ;
            R_bit_cnt       <=  4'd0 ;
            O_done_flag     <=  1'b0 ;
            R_jump_state    <=  4'd0 ;
            R_read_data_reg <=  8'd0 ;
            R_ack_flag         <=  1'b0 ;
        end
end

endmodule

IIC设备多字节连续读写操作

Verilog -- IIC总线协议_第7张图片
Verilog -- IIC总线协议_第8张图片

有了上面接收模块的基础,实现这段时序不算困难。只要多增加几个加载数据的状态就可以了。

你可能感兴趣的:(Verilog,总线协议)