基于Vivado的硬件IIC软硬协同设计



前言

  这几天总算是将I2C通信协议的verilog设计写好了,并成功完成了与EEPROM的数据交互。虽说Vivado中有AXI_IIC模块,但博主认为IIC通信协议是用来训练自己逻辑的再好不过的选择了,因此自己手写一个硬件IIC还是有必要的。在设计过程中遇到了一个非常奇怪的问题,就是Vivado对于模块中内嵌例化的三态门无法综合的问题,简单来说就是无法综合inout定义的信号,故开篇来记录下解决的过程。

一、代码下载链接

  废话不多说,这里贴出该I2C接口的verilog源码及软件驱动函数库链接:
  https://hihii11.github.io/verilog_AXI_IIC.html
  同样这也是我在github上的个人博客,欢迎大家访问!

二、I2C通信协议简介

  在开始之前首先对I2C通信协议进行简要的介绍,有基础的读者可以直接跳过了。

1.I2C协议简介

  I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。需要注意的是I2C是一种半双工通信,也就是说在同一时刻无法同时发送或接收数据。
  在I2C设备通信中,分为主机和从机,主机决定I2C传输的开始与结束,并产生I2C通信的时钟。从机则以主机产生的时钟步调,发送数据或接收由主机发出的数据。
  一次完整的I2C通信大致有以下几个过程。
  1. 主机发送I2C起始信号。 从机在接收到起始信号后,准备接收主机发送的地址。
  2. 主机发送从机地址和读写标志位。 从机在接收到主机发送的数据后与自身地址相比较,一致则响应此次I2C数据传输,并判断此次操作是读还是写。
  3. 从机返回一个应答信号,告知主机已接收到地址数据。
  4. 主机将需要发送的数据一位一位Push到数据线上。
  5. 从机返回一个应答信号,告知主机已接收到数据。
  6. 主机发送I2C结束信号。
  一般来说,现在大部分I2C器件都能进行地址递增的连续数据传输,所以在第4步主机发送数据也可一次性发送多个字节。

2.I2C接口

  I2C接口比较简单,在通信设备之间只需要两根线就可以实现通信,分别是时钟线SCL和数据线SDA。如图2.0所示
  时钟线传输由主机发出的时钟脉冲,是一个有一定频率,固定占空比(通常50%)的方波。在SDA数据线上传输数据。
  这里需要注意的是,由于在该协议中,数据交互是双向的,所以SDA端口的类型应当是inout双向口类型。
图1.0 I2C接口                图2.0 I2C接口

3.I2C通信时序

  这里采用某I2C器件的时序图做下简单的讲解。如图2.1所示。
  我们可以看到这张图还是很复杂的,但实际上,我们对它做下简单的拆分,就会好理解的多。
在这里插入图片描述                图2.1 I2C时序
I2C起始信号与结束信号:
  如图2.2所示,起始信号即在SCL为高电平时,SDA有高电平变为低电平。结束信号为在SCL为高电平时,SDA由低电平变为高电平。
在这里插入图片描述             图2.2 I2C起始信号与结束信号
I2C数据传输与应答信号:
  图2.3展示了I2C数据传输与应答信号。

  在I2C数据传输过程中,要求在SCL为高电平时,SDA上的数据不允许发生改变,必须保持稳定,即只有在SCL为低电平时,SDA数据才允许发生改变。
  应答信号本质和发送数据一样,但它是在发送完一个字节数据后的额外的一比特数据。
  若该比特位为低电平,则发出的是一个响应应答信号,若是高电平,则是非应答信号。
在这里插入图片描述             图2.3 I2C数据传输与应答信号

三、I2C硬件接口设计

1.硬件I2C设计方案

  虽然看上去I2C协议似乎比较复杂,但综合起来看,其实也就几种SCL与SDA数据的组合,下面我将这些不同的组合成为I2C事件,如表3.0所示。
              表3.0 I2C事件划分

I2C事件事件描述
起始事件标志着一次I2C通信的开始
结束事件标志着一次I2C通信的结束
发送事件串行发送一个字节数据
接收事件串行接收一个字节数据
响应事件发送一个应答或非应答信号
检测响应事件检测一个应答或非应答信号

  这样拆分的好处是,可以将任何一次I2C通信,都拆分成若干个I2C事件的组合,在硬件设计上,可以通过配置寄存器来设置本次执行的事件,在加以一个触发信号使得器件执行本次事件,在器件完成后反馈一个结束信号或一个中断信号。
  对于每一个事件的实现,可以通过状态机来实现。
  图3.1为整体接口的架构。
在这里插入图片描述
              图3.1 硬件I2C架构
  从左往右介绍,首先用户在使用时通过AXI总线对接口中寄存器进行读写,可读写的寄存器如下表:
              表3.1 I2C寄存器说明

寄存器名称寄存器功能
Div0 Register分频器的分频系数,控制整个I2C接口的输入时钟
Div1 Register该分频器控制发送一比特数据时,SCL的脉冲宽度
Cmd Register指令寄存器,指示I2C当前事件
Status Register状态寄存器。寄存当前I2C接口状态
Rx Buffer接收缓冲区,寄存接收数据
Tx Buffer发送缓冲区,寄存发送数据

在配置完Cmd Register后,发出一个触发信号,I2C状态机就会根据Cmd Register中的指令来执行相应的事件,在执行完成后返回一个中断信号,同时状态寄存器中的完成标志位会置位。

2.重要程序设计

I2C模块信号:

module IIC_Master_2(
        input                       clk,//system clk input
        input                     iic_en,//enable signal
        input                    iic_rst,//reset signal
        
        input                    iic_sda_i,//IIC SDA port
        output reg              iic_sda_o,
        output reg             iic_sda_out,
        
        output reg               iic_scl,//IIC SCL port
        
        input    [7:0]      iic_send_data,//IIC send data input
        output  reg [7:0]   iic_rec_data,//IIC_receive data output
        
        input                  iic_locked,//IIC div lock signal
        input   [15:0]            clk_div,//IIC clk div
        input   [15:0]             step_t,//IIC step time
        input   [7:0]             iic_sel,//IIC event select
        input             iic_event_start,//to start an iic event
        output reg  [7:0]        iic_IFG,//IIC event finish flag
        output reg              iic_busy,//IIC busy flag
        output reg              iic_ack,//when iic_ack = 1 ,an ack signal was checked ,otherwise an no-ack signal was checked
        output reg              iic_INT,
        output reg              iic_qvld       //IIC event finish signal output
    );

这边需要注意的是,sda信号并不是写成inout的形式,而是写成三态缓冲的形式,这是由于在Vivado中若直接用inout语句,则会出现综合错误,这个错误会在接下来介绍。
I2C状态机:

always @ (posedge iic_qvld , posedge iic_event_start)
    begin
        if(iic_event_start)
        begin
            iic_IFG <= 8'd0;
        end
        else
        begin
            if(iic_en)
            begin
                        case(iic_sel)
                            8'b0000_0000:begin iic_IFG <= 8'b0000_0000;end
                            8'b0000_0001:begin iic_IFG <= 8'b0000_0001;end
                            8'b0000_0010:begin iic_IFG <= 8'b0000_0010;end
                            8'b0000_0100:begin iic_IFG <= 8'b0000_0100;end
                            8'b0000_1000:begin iic_IFG <= 8'b0000_1000;end
                            8'b0001_0000:begin iic_IFG <= 8'b0001_0000;end
                            8'b0010_0000:begin iic_IFG <= 8'b0010_0000;end
                            8'b0100_0000:begin iic_IFG <= 8'b0100_0000;end
                            default:iic_IFG <= 8'd0;
                        endcase
            end
            else
            begin
                iic_IFG <= iic_IFG;
            end
        end
    end 
    
    //IIC State Machine
    always@(posedge IIC_clk , posedge iic_rst)
    begin
            if(iic_rst)
            begin
                iic_state <= IDLE0;
                iic_t_cnt <= 16'd0;
                iic_rec_data <= 8'd0;
                iic_sda_out <= 1'b1;
                iic_sda_o <= 1'b1;
                iic_scl <= 1'b1;
                iic_qvld <= 1'b0;
                iic_ack <= 1'b0;
                iic_start_IFG<=1'b0;
                iic_busy <= 1'b0;
                iic_send_buff <= 8'd0;
                iic_recv_buff <= 8'd0;
                iic_bit_cnt <=  4'd0;
                iic_INT <= 1'b0;
            end
            else
            begin
                if(iic_en)
                begin
                    case(iic_state)
                    IDLE0://wait start signal 
                    begin
                        iic_INT <= 1'b0;
                        if(iic_event_start)
                        begin
                            iic_state <= IDLE1; 
                            iic_qvld <= 1'b0;
                            iic_busy <= 1'b1;
                        end
                        else
                        begin
                            iic_bit_cnt <=  4'd0;
                            iic_busy <= 1'b0;
                            iic_state <= IDLE0;
                            iic_t_cnt <= 16'd0;
                            iic_sda_out <= 1'b0;
                            iic_sda_o <= 1'b1;
                            iic_send_buff <= 8'd0;
                            iic_recv_buff <= 8'd0;
                            if(iic_start_IFG) iic_scl <= 1'b0;
                            else iic_scl <= 1'b1;
                        end
                    end
                    IDLE1://wait start signal finish
                    begin
                        case(iic_sel)
                            8'b0000_0000:begin iic_state <= IDLE0;end
                            8'b0000_0001:begin iic_state <= START;iic_sda_out <= 1'b0;end
                            8'b0000_0010:begin iic_state <= SENDBYTE;iic_sda_out <= 1'b0;
                                        iic_send_buff <= iic_send_data;iic_scl<=1'b0;end
                            8'b0000_0100:begin iic_state <= SENDACK;iic_sda_out <= 1'b0;iic_scl<=1'b0;end
                            8'b0000_1000:begin iic_state <= SENDNACK;iic_sda_out <= 1'b0;iic_scl<=1'b0;end
                            8'b0001_0000:begin iic_state <= STOP;iic_sda_out <= 1'b0;iic_scl<=1'b0;end
                            8'b0010_0000:begin iic_state <= RECVBYTE;iic_sda_out <= 1'b1;iic_scl<=1'b0;end//set sda pin as input
                            8'b0100_0000:begin iic_state <= CHECKACK0;iic_sda_out <= 1'b1;iic_scl<=1'b0;end//set sda pin as input
                            default:begin iic_state <= IDLE0;iic_sda_out <= 1'b0;iic_scl<=1'b0; end
                        endcase
                    end
                    START://IIC send a start signal
                    begin
                        if(iic_t_cnt == 16'd0) begin iic_sda_o <= 1'b1;iic_scl <= 1'b1; end
                        else if(iic_t_cnt == step_time/2) begin iic_sda_o <= 1'b0;iic_scl <= 1'b1; end 
                        else if(iic_t_cnt == step_time) begin iic_scl <= 1'b0;iic_state <= FINISH;iic_start_IFG<=1'b1; end
                        else iic_state <= START;
                        iic_t_cnt <= iic_t_cnt+16'd1;
                    end
                    
                    SENDBYTE://IIC send a byte data
                    begin
                        if(iic_bit_cnt != 4'd8)
                        begin
                            if(iic_t_cnt == 16'd0) begin iic_sda_o <= iic_send_buff[7];iic_scl <= 1'b0; end
                            else if(iic_t_cnt == step_time/2) begin iic_scl <= 1'b1;end//set scl pin high
                            else if(iic_t_cnt == step_time) begin iic_scl <= 1'b0;iic_send_buff <= iic_send_buff<<1;end//set scl pin high
                            
                            if(iic_t_cnt != step_time) begin iic_t_cnt <= iic_t_cnt+16'b1;end
                            else begin iic_t_cnt <= 16'd0;iic_bit_cnt<= iic_bit_cnt+16'd1; end
                        end
                        else
                        begin
                            iic_bit_cnt <= 4'd0;iic_state <= FINISH;iic_start_IFG<=1'b1;
                        end
                    end
                    
                    SENDACK://IIC set an ack signal
                    begin
                        if(iic_t_cnt == 16'd0) begin iic_sda_o <= 1'b0;iic_scl <= 1'b0; end
                        else if(iic_t_cnt == step_time/2) begin iic_sda_o <= 1'b0;iic_scl <= 1'b1;end//set scl pin high
                        else if(iic_t_cnt == step_time) begin iic_sda_o <= 1'b0;iic_scl <= 1'b0;end//set scl pin high
                        
                        if(iic_t_cnt != step_time) begin iic_t_cnt <= iic_t_cnt+16'b1;end
                        else begin iic_t_cnt <= 16'd0;iic_state <= FINISH;iic_start_IFG<=1'b1; end
                    end
                    
                    SENDNACK:
                    begin
                        if(iic_t_cnt == 16'd0) begin iic_sda_o <= 1'b1;iic_scl <= 1'b0; end
                        else if(iic_t_cnt == step_time/2) begin iic_sda_o <= 1'b1;iic_scl <= 1'b1;end//set scl pin high
                        else if(iic_t_cnt == step_time) begin iic_sda_o <= 1'b1;iic_scl <= 1'b0;end//set scl pin high
                        
                        if(iic_t_cnt != step_time) begin iic_t_cnt <= iic_t_cnt+16'b1;end
                        else begin iic_t_cnt <= 16'd0;iic_state <= FINISH;iic_start_IFG<=1'b1; end
                    end
                    
                    
                    STOP:
                    begin
                        if(iic_t_cnt == 16'd0) begin iic_sda_o <= 1'b0;iic_scl <= 1'b0; end
                        else if(iic_t_cnt == step_time/2) begin iic_sda_o <= 1'b0;iic_scl <= 1'b1;end//set scl pin high
                        else if(iic_t_cnt == step_time) begin iic_sda_o <= 1'b1;iic_scl <= 1'b1;end//set scl pin high
                        
                        if(iic_t_cnt != step_time) begin iic_t_cnt <= iic_t_cnt+16'd1;end
                        else begin iic_t_cnt <= 16'd0;iic_state <= FINISH;iic_start_IFG<=1'b0; end
                    end
                    
                    RECVBYTE://IIC receive a byte data
                    begin
                        if(iic_bit_cnt != 4'd8)
                        begin
                            if(iic_t_cnt == 16'd0)
                            begin  
                                iic_scl <= 1'b0; 
                                iic_recv_buff <= iic_recv_buff<<1;
                            end//set iic sel pin low
                            else if(iic_t_cnt == step_time/2) 
                            begin 
                                iic_scl <= 1'b1; 
                            end//set iic sel pin high
                            else if(iic_t_cnt == ((step_time/4)*3))
                            begin 
                                iic_scl <= 1'b1;
                                iic_recv_buff<=iic_recv_buff|iic_sda_i; 
                            end//read iic sda pin
                            else if(iic_t_cnt == step_time)
                            begin
                                iic_scl <= 1'b0;
                            end
                            else
                            begin
                                iic_recv_buff<=iic_recv_buff;
                            end
                            
                            if(iic_t_cnt != step_time)
                            begin 
                                iic_t_cnt<= iic_t_cnt+16'd1;
                            end
                            else
                            begin
                                iic_t_cnt<= 16'd0;iic_bit_cnt<= iic_bit_cnt+16'd1;
                            end
                        end
                        else
                        begin
                            iic_rec_data <= iic_recv_buff;
                            iic_recv_buff <= 8'd0;
                            iic_bit_cnt <= 4'd0;
                            iic_state <= FINISH;
                            iic_start_IFG<=1'b1;
                        end
                    end
                    
                    CHECKACK0://IIC check ack signal from slave
                    begin
                         if(iic_t_cnt == 16'd0) begin iic_scl <= 1'b0; end
                         else if(iic_t_cnt == step_time/2) //set IIC scl pin high
                         begin 
                            iic_scl <= 1'b1; 
                         end
                         
                         if(iic_t_cnt != step_time)begin iic_t_cnt <= iic_t_cnt + 16'd1; end
                         else begin iic_t_cnt <= 16'd0;iic_state <= CHECKACK1;  end
                    end
                    
                    CHECKACK1:
                    begin
                        if(iic_sda_i == 1'b0)//check if sda pin is low
                        begin
                            iic_scl <= 1'b0; 
                            iic_ack <= 1'b1; //has checked an ack signal
                            iic_state <= FINISH;
                            iic_start_IFG<=1'b1;
                        end
                        else
                        begin
                            iic_scl <= 1'b1; 
                            iic_ack <= 1'b0;
                            iic_t_cnt <= iic_t_cnt + 16'd1;
                            if(iic_t_cnt == error_time)//has checked an no-ack signal
                            begin
                                 iic_ack <= 1'b0;
                                 iic_t_cnt <= 16'd0;
                                 iic_state <= FINISH;
                                 iic_start_IFG<=1'b1;
                            end
                        end 
                    end
                    
                    FINISH://set qvld high
                    begin
                        if(iic_event_start)
                        begin
                            iic_state <= FINISH;
                        end
                        else
                        begin
                            iic_state <= IDLE0;
                        end
                        iic_qvld <= 1'b1;
                        iic_INT <= 1'b1;
                    end 
                    default:iic_state <= IDLE0;
                    endcase
            end
            else
            begin
                iic_bit_cnt <=  iic_bit_cnt;
                iic_state   <= iic_state   ;
                iic_t_cnt   <= iic_t_cnt   ;
                iic_rec_data <=iic_rec_data;
                iic_sda_out <= iic_sda_out;
                iic_sda_o <= iic_sda_o;
                iic_scl <= iic_scl;
                iic_qvld <= iic_qvld;
                iic_ack <= iic_ack;
                iic_send_buff <= iic_send_buff;
                iic_recv_buff <= iic_recv_buff;
                iic_INT <= iic_INT;
            end
           end
    end

3.遇到的问题

  在设计中,遇到的最头疼也是最奇怪的问题就是inout端口无法综合的问题,接下来写一下该问题的解决过程。

  IIC属于半双工通信总线,所以对于SDA数据线既要能够作为输出发送数据,又要作为输入接收数据。这就涉及到了输入输出端口的设计,代码如下。

 module tri_io(
        input     tri_i,
        input     tri_t,
        output    tri_o,
        inout     tri_p
 );
    
    assign tri_p = (tri_t == 1'b1) ? tri_i : 1'bz;
    assign tri_o = tri_p;
    
 endmodule

  tri_t为三态门的输入输出方向控制信号,当tri_t=1’b1时,tri_p的值将等于tri_i,此时tri_p作为输出。当tri_t = 1’b0时,tri_p的值为高阻态,即此时tri_p值由外接设备决定,并将该值通过tri_o输出。

  以上代码综合出的电路按理如下。
在这里插入图片描述 但在Vivado综合后,烧录进fpga的电路在设置该端口为输入情况下,无法进行高低电平的识别,甚至将IO口直接拉至电源电压,也无法识别高电平,该端口的读数永远为低电平。

在百思不得其解的情况下,我列出了如下几个可能原因一一排查

1.三态门代码错误。

2.IIC设备SDA总线需要上拉,但在约束时未进行上下拉进行约束。

3.Vivado无法综合inout逻辑。

对于第一种可能性,我将代码在quartus上进行实现,并烧录调试,发现是可以实现输入检测的。说明代码是没有错误的。

对于第二种可能性,我在约束文件中添加如下上拉约束。

set_property PULLUP true [get_ports iic_sda]

通过漫长的编译后下载到zynq中,结果显而易见,仍然为低电平。

那就剩下第三种可能性了,如果Vivado无法综合inout逻辑,那这个三态门综合出的结果是什么呢? 打开RTL原理图后,发现三态门被综合成下面这个样子:
  在这里插入图片描述没错,vivado将三态门综合成了LUT资源,图中的LUT1。

对于这种综合结果,与三态门相差甚远,但也可以解释为什么读数总为0了。为了解决这个综合错误问题,我尝试将三态门的逻辑放置到顶层top中,具体做法是将三态门单独封装成一个IP核,并在bd设计中调用,自以为这种直接连接IO口的逻辑,应该不会综合错误了吧。但结果令人大跌眼镜,Vivado综合出的结果如下。
在这里插入图片描述这种综合结果仅在“形状”上与三态门相似,功能相差了十万八千里。也就是说在输入时,高阻逻辑被综合成了逻辑0。这就基本确定,Vivado无法综合inout逻辑。这让我突然想到前几个月在xilinx开发板上移植CPU时,总线错误问题,大概率和这个是一个原因了。

那既然vivado无法综合三态门,难道真的无法设计输入输出口了吗,答案肯定是行的,如果无法综合inout逻辑,那三态门肯定是在FPGA上的既定的资源。问题是如何去使用。
具体操作方法就是点击页面的加号在Interface Definition中找到gpio_rtl,再将信号一一绑定即可。重新设计完IP核后,进行综合和RTL分析,总算得到了心心念念的三态门。
在这里插入图片描述
在这里插入图片描述至此,三态门综合错误问题解决。总结一下,Vivado是无法综合inout逻辑的,三态门作为既定的资源,也只能对真正存在的物理IO使用,具体方法就是通过Interface进行总线绑定,通过三态缓冲器驱动三态门。最后再放一张修改后的IP核图。
在这里插入图片描述其实除了上述这种方法,还有一种方法是直接例化一个三态门IP核。如下:

    IOBUF#(
        )
    IOBUF_inst7(
        .T(  tri_t[7]),
        .I(  tri_i[7]),
        .O(  tri_o[7]),
        .IO(tri_io[7])
    );

这样也能得到三态门。

四、软件驱动设计

  软件层面实际上就是通过AXI总线向相关寄存器读写数据完成配置的过程,由于篇幅过长,这里就放一个发送字节的函数讲解下读写思路。
I2C接口初始化

/*-------------------------------------------------------------------
 * 【     Name  】: <IIC_Master_send_byte>
 * 【 Describe】: <This function start a processing of sending a byte data.>
 * 【Parameter】:<Base_addr: The base address of IIC_Master device.>
 * 【  Example 】:<IIC_Master_send_byte(IIC_Master_0);
 * 				 The example will let IIC_Master device send a byte data.>
 *------------------------------------------------------------------*/
void IIC_Master_send_byte(AXI_BaseAddress_Data_Type Base_addr,uint8 data)
{
	Xil_Out32(Base_addr|IIC_Master_SEL0, IIC_Master_SEL0_SENDBYTE);
	while(((Xil_In32(Base_addr|IIC_Master_IFG))&(IIC_Master_BUSYIFG))==IIC_Master_BUSYIFG);
	Xil_Out32(Base_addr|IIC_Master_SEDBUFF, data);
	AXI_Register_Data_Type CTL0_status = Xil_In32(Base_addr|IIC_Master_CTL0);
	CTL0_status |= IIC_Master_START;
	Xil_Out32(Base_addr|IIC_Master_CTL0,CTL0_status);
	while(((Xil_In32(Base_addr|IIC_Master_IFG))&(IIC_Master_BUSYIFG))==0x00);
	CTL0_status &= ~IIC_Master_START;
	Xil_Out32(Base_addr|IIC_Master_CTL0,CTL0_status);
	while(((Xil_In32(Base_addr|IIC_Master_IFG))&(IIC_Master_EVENTIFG))==0x00);
}

  首先配置指令寄存器IIC_Master_SEL0为发送数据,然后通过一个while去检测状态寄存器,查看I2C接口此时是否有正在进行的事件,然后向发送缓冲区IIC_Master_SEDBUFF写入发送数据,然后将控制寄存器CTL0中的start控制位拉高,触发一次发送事件。最后等待事件执行完成。

五、测试效果

通过逻辑分析仪显示的I2C事件:
在这里插入图片描述驱动I2C OLED屏幕:
文字:
在这里插入图片描述
图片:
在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PPRAM

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值