【LabVIEW FPGA图形化】IP集成节点:IIC通信实验

一、前情提要

上一节内容介绍了图形化FPGA的USB通信,USB通信主要用于FPGA与上位机之间的通信,对于器件(芯片)与器件之间的数据交互,往往采用串口、SPI、IIC、IIS等约定协议进行通信。IIC作为一种很常用的通信协议广泛用于ADC、EEPROM、陀螺仪、倾角传感器等各类传感器和存储芯片之间的通信,FPGA作为可编程的硬件电路,自然也可以用过硬件电路实现硬件IIC,硬件IIC相较于软件IIC有一定的优势,两者之间的关系网上资料很多本文不再赘述,想了解更多可以问ChatGPT,本节重点关注FPGA的IIC通信(常规250Mbps,快速400Mbps),本节内容将手把手带大家编写一个IIC通信实验的IP集成节点。

【LabVIEW FPGA图形化】 ngc、edf网表文件的编写:LED流水灯

【LabVIEW FPGA图形化】 IP集成节点:按键控制LED

【LabVIEW FPGA图形化】IP集成节点:串口通信

【LabVIEW FPGA图形化】IP集成节点:频率计

【LabVIEW FPGA图形化】IP集成节点:USB通信

二、FPGA蔡氏定律

FPGA有两条蔡氏定律:
1、FPGA不仅仅是FPGA。
2、FPGA的最终目的是做出可用的电路。

FPGA的学习脱离不了硬件电路,FPGA实现IIC通信也需要相应的目标器件,常见的IIC器件有24C02、PCF8591、OLED、PCF8574、MPU6050等。黑金AX516上提供了一片支持IIC通信的EEPROM,型号为24LC04,由2个block组成,每块容量为2K,用作一些关键参数的存储,掉电不丢失。这种芯片操作简单,具有极高性价比。

EEPROM原理图部分

EEPROM引脚与FPGA连接:SDA–>N5 SCL–>P6。

三、LabVIEW FPGA IP集成节点网表文件的编写

了解了EEPROM的硬件以及连接方式,接下来我们需要根据IIC的时序编写网表。在单片机上,常用软件模拟IIC的方式实现IIC的通信协议。

#include "iic.h"

#define DELAY_TIME 5

//I2C总线内部延时函数
void IIC_Delay(unsigned char i)
{
    do{_nop_();}
    while(i--);        
}

//I2C总线启动信号
void IIC_Start(void)
{
    SDA = 1;
    SCL = 1;
    IIC_Delay(DELAY_TIME);
    SDA = 0;
    IIC_Delay(DELAY_TIME);
    SCL = 0;	
}

//I2C总线停止信号
void IIC_Stop(void)
{
    SDA = 0;
    SCL = 1;
    IIC_Delay(DELAY_TIME);
    SDA = 1;
    IIC_Delay(DELAY_TIME);
}

//发送应答或非应答信号
void IIC_SendAck(bit ackbit)
{
    SCL = 0;
    SDA = ackbit;  					
    IIC_Delay(DELAY_TIME);
    SCL = 1;
    IIC_Delay(DELAY_TIME);
    SCL = 0; 
    SDA = 1;
    IIC_Delay(DELAY_TIME);
}

//等待应答
bit IIC_WaitAck(void)
{
    bit ackbit;
	
    SCL  = 1;
    IIC_Delay(DELAY_TIME);
    ackbit = SDA;
    SCL = 0;
    IIC_Delay(DELAY_TIME);
    return ackbit;
}

//I2C总线发送一个字节数据
void IIC_SendByte(unsigned char byt)
{
    unsigned char i;

    for(i=0; i<8; i++)
    {
        SCL  = 0;
        IIC_Delay(DELAY_TIME);
        if(byt & 0x80) SDA  = 1;
        else SDA  = 0;
        IIC_Delay(DELAY_TIME);
        SCL = 1;
        byt <<= 1;
        IIC_Delay(DELAY_TIME);
    }
    SCL  = 0;  
}

//I2C总线接收一个字节数据
unsigned char IIC_RecByte(void)
{
    unsigned char i, da;
    for(i=0; i<8; i++)
    {   
    	SCL = 1;
	IIC_Delay(DELAY_TIME);
	da <<= 1;
	if(SDA) da |= 1;
	SCL = 0;
	IIC_Delay(DELAY_TIME);
    }
    return da;    
}
void write_eeprom(unsigned char add,unsigned char dat)
{
		IIC_Start();
		IIC_SendByte(0xA0);
		IIC_WaitAck();
		IIC_SendByte(add);
		IIC_WaitAck();
		IIC_SendByte(dat);
		IIC_WaitAck();
		IIC_Stop();
}

unsigned char read_eeprom(unsigned char add)
{
	unsigned char temp;
		IIC_Start();
		IIC_SendByte(0xA0);
		IIC_WaitAck();
		IIC_SendByte(add);
		IIC_WaitAck();

		IIC_Start();
		IIC_SendByte(0xA1);
		IIC_WaitAck();
		temp=IIC_RecByte();
		IIC_SendAck();
		IIC_Stop();
}

以上是软件模拟IIC的C语言代码,相较于底层verliog,C语言作为顶层更易于理解,如上面write_eeprom()、read_eeprom()两个函数,将IIC的通信逻辑体现得淋漓尽致,根据两个函数逻辑所设计出的状态转移图如下:

IIC通信状态转移图
根据状态转移图我们可以更简便的用有限状态机去实现我们的代码。当然,在编写代码之前,我们还需要设计模块的输入和输出,以下是RTL视图:

在这里插入图片描述

根据RTL视图我们可以确定模块的输入与输出,定义相关的寄存器,结合状态转移图就可以构建出我们的代码结构。

以下为IIC通信的Verliog代码

module IIC_Generate (
    input               clk,
    input               reset,
    input               sda_in,
    input               rw,
    input       [7:0]   device_addr,
    input       [7:0]   add,
    input       [7:0]   dat,
    input               input_vaild,
    output reg          scl_out,
    output reg          sda_ena,
    output reg          sda_out,    //SCL为低电平时,SDA数据才允许改变
    output reg  [7:0]   data_out,
    output reg          output_vaild, 
    output reg          idle

);

parameter IDLE         =        8'd0;
parameter IIC_START    =        8'd1;
parameter IIC_Send_device_add = 8'd2;
parameter IIC_WaitAck0 =        8'd3;
parameter IIC_Send_add =        8'd4;
parameter IIC_WaitAck1 =        8'd5;
parameter IIC_Send_dat =        8'd6;
parameter IIC_WaitAck2 =        8'd7;
parameter IIC_stop     =        8'd8;
parameter IIC_START1   =        8'd9;
parameter IIC_Send_device_add1 =8'd10;
parameter IIC_WaitAck3 =        8'd11;
parameter IIC_rec_byte =        8'd12;

reg [7:0]   State=0;
reg [7:0]   device_addr_d0;
reg [7:0]   device_addr1_d0;
reg [7:0]   add_d0;
reg [7:0]   dat_d0;
reg         rw_d0;
reg [15:0]  cnt;
reg [7:0]   i;
reg [7:0]   data_recive=0;
always @(posedge clk or posedge reset) begin  //缓存有效数据的状态
    if(reset) begin
    device_addr_d0<=0;
    add_d0  <=0;
    dat_d0  <=0;
    rw_d0   <=0;
    end
    else if(input_vaild==1'b1&&idle==1'b1) begin 
    device_addr_d0<=device_addr;        //先把这些值寄存一下
    device_addr1_d0<=device_addr+1'b1;
    add_d0  <=add;
    dat_d0  <=dat;
    rw_d0   <=rw;
    end
    else begin
    device_addr_d0<=device_addr_d0;
    add_d0  <=add_d0;
    dat_d0  <=dat_d0;
    rw_d0   <=rw_d0;    
    end
end  
    
always @(posedge clk ) begin   //准备好输入
    if(reset)
    idle<=0;
    else if(State==IDLE)
    idle<=1;
    else 
    idle<=0;
end

always @(posedge clk or posedge reset) begin //状态转移图
    if(reset) begin
    scl_out<=1;   
    sda_ena<=1;
    sda_out<=1;
    data_out<=0;
    output_vaild<=0;
	State<=IDLE;
	cnt<=0;
    end
    else begin
        case (State)
            IDLE: begin
                if (input_vaild==1) begin
                    State<=IIC_START;
                    cnt<=0;
                end
                else begin
                    State<=IDLE;  
                    scl_out<=1;   
                    sda_ena<=0;
                    sda_out<=1;
                end
            end
            IIC_START: begin
                if (cnt==0) begin
                    scl_out<=1'b1;
                    sda_out<=1'b1;
                    sda_ena<=1'b1;
                    cnt<=cnt+1;
                end
                else if(cnt==50) begin  //SDA先由高变低
                    sda_out<=1'b0;
                    cnt<=cnt+1;
                end
                else if(cnt==200) begin
                    scl_out<=1'b0;    //SCL后由高变低
                    cnt<=cnt+1;
                end
                else if(cnt==249)begin
                    cnt<=0;
                    State<=IIC_Send_device_add;
                    i<=7;
                end
                else begin
                    cnt<=cnt+1;
                end
                 
            end
            IIC_Send_device_add:begin

                if (cnt==0) begin
                    scl_out<=1'b0;
                    sda_out<=device_addr_d0[i]; 
                    cnt<=cnt+1;
                end
                else if (cnt==50) begin
                    scl_out<=1'b1;      //SCL为高电平,保持100时钟
                    cnt<=cnt+1;
                end
                else if(cnt==150) begin
                    scl_out<=1'b0;  
                    cnt<=cnt+1;  
                end
                else if(cnt==199) begin
                    cnt<=0;
                    if(i==0) begin
                       State<=IIC_WaitAck0;
                       cnt<=0;
                    end
                    else begin
                       i<=i-1;  
                       State<=State;  
                    end
                end
                else
                cnt<=cnt+1; 
            end
            IIC_WaitAck0: begin
                sda_ena<=1'b0;
				sda_out<=1'b0;
                if(cnt==50) begin
                    scl_out<=1'b1;
                    cnt<=cnt+1;
                end
                else if(cnt==150) begin
                    scl_out<=1'b0;
                    cnt<=cnt+1;
                end
                else if(cnt==199) begin
                    cnt<=0;
                    State<=IIC_Send_add;
                    i<=7;
                end
                else 
                    cnt<=cnt+1;
            end
            IIC_Send_add:begin
                if(cnt==0) begin
                    sda_ena<=1'b1;
                    sda_out<=add_d0[i];
                    cnt<=cnt+1;
                end
                else if (cnt==50) begin
                    scl_out<=1'b1;      //SCL为高,保持100时钟
                    cnt<=cnt+1;
                end
                else if(cnt==150) begin
                    scl_out<=1'b0;  
                    cnt<=cnt+1;  
                end
                else if(cnt==199) begin
                    cnt<=0;
                    if(i==0) begin
                       State<=IIC_WaitAck1;
                       cnt<=0;
                    end
                    else
                    i<=i-1;
                end
                else
                     cnt<=cnt+1;
            end
            IIC_WaitAck1: begin
                sda_ena<=1'b0;
				sda_out<=1'b0;
                if(cnt==50) begin
                    scl_out<=1'b1;
                    cnt<=cnt+1;
                end
                else if(cnt==150) begin
                    scl_out<=1'b0;
                    cnt<=cnt+1;
                end
                else if(cnt==199) begin
                    cnt<=0;
                    if (rw_d0==0) begin     //继续写入
                    State<=IIC_Send_dat;
					sda_out<=1'b1;	//保证下一次开始的时序					
                    i<=7;  
                    end
                    else
                    State<=IIC_START1;    //准备读取
                end
                else 
                    cnt<=cnt+1;
            end
            IIC_Send_dat: begin
                if(cnt==0) begin
                    sda_ena<=1'b1;
                    sda_out<=dat_d0[i];
                    cnt<=cnt+1;
                end
                else if (cnt==50) begin
                    scl_out<=1'b1;      //SCL为高,保持100时钟
                    cnt<=cnt+1;
                end
                else if(cnt==150) begin
                    scl_out<=1'b0;  
                    cnt<=cnt+1;  
                end
                else if(cnt==199) begin
                    cnt<=0;
                    if(i==0) begin
                       State<=IIC_WaitAck2;
                       cnt<=0;
                    end
                    else begin
                        State<=State;
                        i<=i-1;  
                    end
                end
                else 
                cnt<=cnt+1; 
            end
            IIC_WaitAck2: begin
             sda_ena<=1'b0;
			 sda_out<=1'b0;		//保证最后停止的时序
             output_vaild<=0;
                if(cnt==50) begin
                    scl_out<=1'b1;
                    cnt<=cnt+1;
                end
                else if(cnt==150) begin
                    scl_out<=1'b0;
                    cnt<=cnt+1;
                end
                else if(cnt==199) begin
                    cnt<=0;
                    State<=IIC_stop;
                    i<=7;
                end
                else 
                    cnt<=cnt+1;            
                end
            IIC_stop:begin
                if(cnt==0) begin
                 sda_ena<=1'b1; 
                  cnt<=cnt+1;  
                end
                else if (cnt==50) begin
                  scl_out<=1'b1;            //SCL先拉高
                   cnt<=cnt+1;
                end
                else if(cnt==150) begin
                  sda_out<=1'b1;        //SDA再拉高
                   cnt<=cnt+1;
                end
                else if (cnt==199) begin
                    cnt<=0;
                    State<=IDLE;
                end
                else
                cnt<=cnt+1;
            end
            IIC_START1: begin
                 if (cnt==50) begin
                    scl_out<=1'b1;
                    sda_out<=1'b1;
                    sda_ena<=1'b1;
                    cnt<=cnt+1;
                end
                else if(cnt==100) begin  //SDA先由高变低 
                    sda_out<=1'b0;
                    cnt<=cnt+1;
                end
                else if(cnt==200) begin
                    scl_out<=1'b0;    //SCL后由高变低
                    cnt<=cnt+1;
                end
                else if(cnt==249)begin
                    cnt<=0;
                    State<=IIC_Send_device_add1;
                    i<=7;
                end
                else begin
                    cnt<=cnt+1;
                end  
            end
            IIC_Send_device_add1: begin
                if (cnt==0) begin
                    scl_out<=1'b0;
                    sda_out<=device_addr1_d0[i]; 
                    cnt<=cnt+1;
                end
                else if (cnt==50) begin
                    scl_out<=1'b1;      //SCL为高,保持100时钟
                    cnt<=cnt+1;
                end
                else if(cnt==150) begin
                    scl_out<=1'b0;  
                    cnt<=cnt+1;  
                end
                else if(cnt==199) begin
                    cnt<=0;
                    if(i==0) begin
                       State<=IIC_WaitAck3;
                       cnt<=0;
                    end
                    else begin
                       i<=i-1;  
                       State<=State;  
                    end
                end
                else 
                cnt<=cnt+1; 
            end
            IIC_WaitAck3: begin
                    sda_ena<=1'b0;
					sda_out<=1'b0;
                if(cnt==50) begin
                    scl_out<=1'b1;
                    cnt<=cnt+1;
                end
                else if(cnt==150) begin
                    scl_out<=1'b0;
                    cnt<=cnt+1;
                end
                else if(cnt==199) begin
                    cnt<=0;
                    State<=IIC_rec_byte;
                    i<=7;
                end
                else 
                    cnt<=cnt+1;   
            end
            IIC_rec_byte: begin
                if (cnt==0) begin
                    sda_ena<=1'b0;
                    cnt<=cnt+1; 
                end
                else if (cnt==50) begin
                    scl_out<=1'b1;
                    cnt<=cnt+1; 
                end
                else if (cnt==100) begin
                    data_recive[i]<=sda_in;
                    cnt<=cnt+1; 
                end
                else if (cnt==150) begin
                    scl_out<=1'b0;
                    cnt<=cnt+1; 
                end
                else if (cnt==199) begin
                    cnt<=0;
                    if (i==0) begin
                        State<=IIC_WaitAck2;
                        data_out<=data_recive;
                        output_vaild<=1;
                    end
                    else begin
                        State<=State;
                        i<=i-1;
                    end
                end
                else
                cnt<=cnt+1; 
            end
            default: ;
        endcase
    end
end

endmodule 

上面的代码没有黑金代码简洁,也没有C语言代码简洁,原因就是保留了所有状态,没有将wait_ack,write_data状态合并导致相当多的代码重复,不过代码易于理解,文中cnt=199(50M/200=250k),若提高系统时钟为80M,IIC协议可以以快速模式运行(80M/200=400k),当然,也可以修改计数值实现速度调整。

编写好顶层文件后需要修改综合参数,在Xilinx Specific Options中去掉 Add I/O Buffer选项,不添加I/O buffer并综合。

四、IIC时序仿真

在仿真前我们先看一下IIC的表情包。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

以上表情包均是IIC时序描述,说明了当时序(应答)不正确的时候,数据传输无效。我在查找IIC资料时,发现网上鲜有一次通信完整的IIC时序,在数据手册里也只有起始位、停止位、单个字节数据的描述,现在我们通过仿真方式了解一下底层IIC通信的过程。
首先我们需要编写testbench仿真文件:

`timescale 1ns / 1ps
module IIC_General_tb(

    );

    reg         clk             ;
    reg         reset           ;
    reg          rw             ;
    reg  [7:0]   device_addr    ;
    reg  [7:0]   add            ;
    reg  [7:0]    dat           ;
    reg      input_vaild        ;
    wire     scl_out            ;
    wire     sda_ena            ;
    wire     sda_out            ;
    wire     data_out           ;
    wire     output_vaild       ;
    wire     idle               ;   
    reg      sda_in             ;
    
    IIC_Generate IIC_Generate_tb (
    .clk               (clk         ),
    .reset             (reset       ),
    .sda_in            (sda_in      ),
    .rw                (rw          ),
    .device_addr       (device_addr ),
    .add               (add         ),
    .dat               (dat         ),
    .input_vaild       (input_vaild ),
    .scl_out           (scl_out     ),
    .sda_ena           (sda_ena     ),
    .sda_out           (sda_out     ),
    .data_out          (data_out    ),
    .output_vaild      (output_vaild),
    .idle              (idle        )

);

    initial begin
        sda_in=1;
        clk=0;
        reset=1;
        rw=0;
        device_addr=0;
        add=0;
        dat=0;
        input_vaild=0;
        #100
        reset=0;
        #60
        device_addr=8'h90;
        add=8'h55;
        dat=8'haa;
        rw=0;
        input_vaild=1;
        #20
        input_vaild=0;
        rw=0;
        device_addr=0;
        add=0;
        dat=0;
        #119000
        device_addr=8'h90;
        add=8'h55;
        dat=0;
        rw=1;
        input_vaild=1;
        #20
      
        device_addr=0;
        add=0;
        dat=0;
        rw=0;
        input_vaild=0;
    end

    always #10 clk = ~ clk;
endmodule


激励将器件地址设置为0x90,地址设置为0x55,数据设置为0xaa,分别进行读写操作。
在写数据阶段完整IIC时序如下图:
在这里插入图片描述
主要看sda上的数据随着scl的变化,注意在起始状态,等待应答状态和停止状态下sda和scl的变化顺序。
同样,我们观察在读数据阶段下sda和scl的变化顺序。
在这里插入图片描述
在读数据时,有两次起始的过程,要注意提前把信号线拉高才能满足开始的时序。

五、FPGA图形化程序编写

将上面程序生成的IIC_General.ngc 添加到vi所在的工程目录下,调用IP节点,对其各个端口进行连线。为了实现快速模式下的IIC通信(400kbit/s),FPGA通过锁相环生成一个80M的衍生时钟(并不是所有器件都支持快速模式的通信),进行单次读写的测试。

在上图中,时钟为80M,输入数据和读写操作都通过前面板人为控制发送,通过一个计数器显示数据有效次数以及读出的数据。

在这里插入图片描述

编译好程序后我们在前面板上进行了数据读取,一共是读取了3次,写入了2次,可以看到输出的显示数据得到的值与我们写入的值一致,单次数据读写成功。若后续有连续写入的需要,可以在数据输入和输出之间加FIFO,将该IP作为器件的数据交互线程,在其他线程中产生/读取数据。

总结

FPGA图形化编程其优势在于逻辑图形化,若不去考虑底层verliog,在顶层设计上FPGA的图形化编程是有优势的,在工程上,可以直接调用开源的网表文件,可以更有效率的完成FPGA的设计。如果后续需要进一步提高IIC的速率(取决于器件),采用更高速的IIC通信,可以通过修改底层cnt的值和IP节点的输入时钟改变SCL时钟线的跳变时间,从而实现任意速率下的IIC通信。本节实验的Verliog网表文件和LabVIEW的vi均开源在 我的资源 如有需要可以下载学习。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值