目录
- 一、前情提要
- 二、FPGA蔡氏定律
- 三、LabVIEW FPGA IP集成节点网表文件的编写
- 四、IIC时序仿真
- 五、FPGA图形化程序编写
- 总结
一、前情提要
上一节内容介绍了图形化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引脚与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的通信逻辑体现得淋漓尽致,根据两个函数逻辑所设计出的状态转移图如下:
根据状态转移图我们可以更简便的用有限状态机去实现我们的代码。当然,在编写代码之前,我们还需要设计模块的输入和输出,以下是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) begindevice_addr_d0<=0;add_d0 <=0;dat_d0 <=0;rw_d0 <=0;endelse 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;endelse begindevice_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;
endalways @(posedge clk or posedge reset) begin //状态转移图if(reset) beginscl_out<=1; sda_ena<=1;sda_out<=1;data_out<=0;output_vaild<=0;State<=IDLE;cnt<=0;endelse begincase (State)IDLE: beginif (input_vaild==1) beginState<=IIC_START;cnt<=0;endelse beginState<=IDLE; scl_out<=1; sda_ena<=0;sda_out<=1;endendIIC_START: beginif (cnt==0) beginscl_out<=1'b1;sda_out<=1'b1;sda_ena<=1'b1;cnt<=cnt+1;endelse if(cnt==50) begin //SDA先由高变低sda_out<=1'b0;cnt<=cnt+1;endelse if(cnt==200) beginscl_out<=1'b0; //SCL后由高变低cnt<=cnt+1;endelse if(cnt==249)begincnt<=0;State<=IIC_Send_device_add;i<=7;endelse begincnt<=cnt+1;endendIIC_Send_device_add:beginif (cnt==0) beginscl_out<=1'b0;sda_out<=device_addr_d0[i]; cnt<=cnt+1;endelse if (cnt==50) beginscl_out<=1'b1; //SCL为高电平,保持100时钟cnt<=cnt+1;endelse if(cnt==150) beginscl_out<=1'b0; cnt<=cnt+1; endelse if(cnt==199) begincnt<=0;if(i==0) beginState<=IIC_WaitAck0;cnt<=0;endelse begini<=i-1; State<=State; endendelsecnt<=cnt+1; endIIC_WaitAck0: beginsda_ena<=1'b0;sda_out<=1'b0;if(cnt==50) beginscl_out<=1'b1;cnt<=cnt+1;endelse if(cnt==150) beginscl_out<=1'b0;cnt<=cnt+1;endelse if(cnt==199) begincnt<=0;State<=IIC_Send_add;i<=7;endelse cnt<=cnt+1;endIIC_Send_add:beginif(cnt==0) beginsda_ena<=1'b1;sda_out<=add_d0[i];cnt<=cnt+1;endelse if (cnt==50) beginscl_out<=1'b1; //SCL为高,保持100时钟cnt<=cnt+1;endelse if(cnt==150) beginscl_out<=1'b0; cnt<=cnt+1; endelse if(cnt==199) begincnt<=0;if(i==0) beginState<=IIC_WaitAck1;cnt<=0;endelsei<=i-1;endelsecnt<=cnt+1;endIIC_WaitAck1: beginsda_ena<=1'b0;sda_out<=1'b0;if(cnt==50) beginscl_out<=1'b1;cnt<=cnt+1;endelse if(cnt==150) beginscl_out<=1'b0;cnt<=cnt+1;endelse if(cnt==199) begincnt<=0;if (rw_d0==0) begin //继续写入State<=IIC_Send_dat;sda_out<=1'b1; //保证下一次开始的时序 i<=7; endelseState<=IIC_START1; //准备读取endelse cnt<=cnt+1;endIIC_Send_dat: beginif(cnt==0) beginsda_ena<=1'b1;sda_out<=dat_d0[i];cnt<=cnt+1;endelse if (cnt==50) beginscl_out<=1'b1; //SCL为高,保持100时钟cnt<=cnt+1;endelse if(cnt==150) beginscl_out<=1'b0; cnt<=cnt+1; endelse if(cnt==199) begincnt<=0;if(i==0) beginState<=IIC_WaitAck2;cnt<=0;endelse beginState<=State;i<=i-1; endendelse cnt<=cnt+1; endIIC_WaitAck2: beginsda_ena<=1'b0;sda_out<=1'b0; //保证最后停止的时序output_vaild<=0;if(cnt==50) beginscl_out<=1'b1;cnt<=cnt+1;endelse if(cnt==150) beginscl_out<=1'b0;cnt<=cnt+1;endelse if(cnt==199) begincnt<=0;State<=IIC_stop;i<=7;endelse cnt<=cnt+1; endIIC_stop:beginif(cnt==0) beginsda_ena<=1'b1; cnt<=cnt+1; endelse if (cnt==50) beginscl_out<=1'b1; //SCL先拉高cnt<=cnt+1;endelse if(cnt==150) beginsda_out<=1'b1; //SDA再拉高cnt<=cnt+1;endelse if (cnt==199) begincnt<=0;State<=IDLE;endelsecnt<=cnt+1;endIIC_START1: beginif (cnt==50) beginscl_out<=1'b1;sda_out<=1'b1;sda_ena<=1'b1;cnt<=cnt+1;endelse if(cnt==100) begin //SDA先由高变低 sda_out<=1'b0;cnt<=cnt+1;endelse if(cnt==200) beginscl_out<=1'b0; //SCL后由高变低cnt<=cnt+1;endelse if(cnt==249)begincnt<=0;State<=IIC_Send_device_add1;i<=7;endelse begincnt<=cnt+1;end endIIC_Send_device_add1: beginif (cnt==0) beginscl_out<=1'b0;sda_out<=device_addr1_d0[i]; cnt<=cnt+1;endelse if (cnt==50) beginscl_out<=1'b1; //SCL为高,保持100时钟cnt<=cnt+1;endelse if(cnt==150) beginscl_out<=1'b0; cnt<=cnt+1; endelse if(cnt==199) begincnt<=0;if(i==0) beginState<=IIC_WaitAck3;cnt<=0;endelse begini<=i-1; State<=State; endendelse cnt<=cnt+1; endIIC_WaitAck3: beginsda_ena<=1'b0;sda_out<=1'b0;if(cnt==50) beginscl_out<=1'b1;cnt<=cnt+1;endelse if(cnt==150) beginscl_out<=1'b0;cnt<=cnt+1;endelse if(cnt==199) begincnt<=0;State<=IIC_rec_byte;i<=7;endelse cnt<=cnt+1; endIIC_rec_byte: beginif (cnt==0) beginsda_ena<=1'b0;cnt<=cnt+1; endelse if (cnt==50) beginscl_out<=1'b1;cnt<=cnt+1; endelse if (cnt==100) begindata_recive[i]<=sda_in;cnt<=cnt+1; endelse if (cnt==150) beginscl_out<=1'b0;cnt<=cnt+1; endelse if (cnt==199) begincnt<=0;if (i==0) beginState<=IIC_WaitAck2;data_out<=data_recive;output_vaild<=1;endelse beginState<=State;i<=i-1;endendelsecnt<=cnt+1; enddefault: ;endcaseend
endendmodule
上面的代码没有黑金代码简洁,也没有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 beginsda_in=1;clk=0;reset=1;rw=0;device_addr=0;add=0;dat=0;input_vaild=0;#100reset=0;#60device_addr=8'h90;add=8'h55;dat=8'haa;rw=0;input_vaild=1;#20input_vaild=0;rw=0;device_addr=0;add=0;dat=0;#119000device_addr=8'h90;add=8'h55;dat=0;rw=1;input_vaild=1;#20device_addr=0;add=0;dat=0;rw=0;input_vaild=0;endalways #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均开源在 我的资源 如有需要可以下载学习。