一、前情提要
上一节内容介绍了图形化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
二、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) 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均开源在 我的资源 如有需要可以下载学习。