一、前情提要
上一节内容介绍了图形化FPGA测量数字信号的频率,广泛用于舵机角度控制、直流电机转速控制等运动控制领域,为此,NI在Motion领域重点推广和发展SoftMotion工具包。此外,USB通信现在应用广泛,因为支持热插拔,即插即用,很多采集卡、移动设备、相机里面都集成了USB通信口,FPGA在通信方面也发挥着重要的作用,本节重点关注FPGA的并行通信,本节内容将手把手带大家编写一个USB通信实验的IP集成节点。
【LabVIEW FPGA图形化】 ngc、edf网表文件的编写:LED流水灯
【LabVIEW FPGA图形化】 IP集成节点:按键控制LED
二、FPGA蔡氏定律
FPGA有两条蔡氏定律:
1、FPGA不仅仅是FPGA。
2、FPGA的最终目的是做出可用的电路。
FPGA的学习脱离不了硬件电路,FPGA功能的实现也需要基于外围电路,在本节实验中,由于USB协议较复杂,需要配置USB报文及端点信息,FPGA实现难度大,占用资源多,因此FPGA需要USB控制器外设实现通信功能,FPGA只需要进行数据处理和并行发送,大大的节约了硬件资源,完成整个通信功能的不仅仅是FPGA本身,还需要结合外围电路来实现可用的功能。
三、USB外围电路
USB是用差分的方法进行传输的,就是在发送端两条信号线上发送幅值一样但是相位相反的信号,然后在接收端将这两个信号进行减法运算,就会得到相位相同幅值翻倍的信号。将两条信号线进行扭在一起,根据电磁学的原理,可以近似认为两条线受到的干扰信号的幅值和相位是相同的,所以在进行减法运算的时候就会将干扰信号给去除掉,提高其抗干扰的能力。
USB采用差分的结构可以抑制线路中的共模干扰,其串行结构使得它的时钟频率可以做到很高,开发板上通过Cypress CY7C68013A USB2.0控制器芯片实现PC与FPGA间的高速数据通信,CY7C68013A控制器完全符合通用串行总线协议2.0版规范,支持全速(12Mbit/s)以及低速(480Mbit/s)模式。用户通过用USB线连接PC的USB口和开发板的MINI型的USB口(J6)就可以进行USB2.0的数据通信。
对于CY68013A这款芯片采用24MHz的外部晶振,内部可通过锁相环倍频至48MHz进行工作,需要说明的是CY68013A里面本质上是8051处理器,需要相应的固件程序以实现通信功能,相关程序将存储与EEPROM中,方便更新。如果用户需要自己做板子的话,可以采用FT232的USB芯片,价格相对便宜。用户可以通过配置 USB 芯片的工作模式,让其工作在 Master 或 Slave 模式。 本节实验模拟的是一个 USB 采集卡,因此,属于 USB 从设备,应该工作在 Slave 模式,PC机工作在 Master 主机模式。
四、LabVIEW FPGA IP集成节点网表文件的编写
USB通信的实现实际上就是控制CY68013A的过程,我们需要针对CY68013A写时序,相关内容可以参考数据手册:
通过原理图可以发现,USB 芯片的外部晶振是 24MHz,而且有一个大小为 4MByte 的EEPROM 芯片 AT24C32 用于保存固件程序。数据接收和发送引脚公用一组,都是并行 16位的,也就是说,用户可以一次性传输 2 个字节数据或者 1 个 16 位数据给 USB 芯片内部的发送缓冲区 FIFO,或者从 USB 芯片内部接收缓冲区 FIFO 里面读取从 PC 下发的数据。
“USB_FLAGA”、“USB_FLAGB”、“USB_FLAGC”这 3 个输出引脚的含义如下:
input usb_flaga, //CY68013 EP2 FIFO empty indication; 1:not empty; 0: empty
input usb_flagb, //CY68013 EP4 FIFO empty indication; 1:not empty; 0: empty
input usb_flagc, //CY68013 EP6 FIFO full indication; 1:not full; 0: full
通过判断USB_FLAGA和USB_FLAGB这两个引脚是否为高电平,就可以知道USB芯片是否接收到了数据;USB_FLAGC判断发送缓冲区是否满了看要不要继续发送数据。
RTL视图:
在该模块中,我们的数据通过data_in输入,通过输入有效(input_vaild)握手,除了时钟clk和复位reset外,其他的信号都是cypress芯片给出的,比较重要的是其FIFO空满标志位,用于读写控制,输出端除了接收数据的data_out和idle信号外,其他都是与芯片连接的接口。
控制这个芯片的逻辑是
- 没有数据输入,将一直保持在读取状态,idles始终空闲1
- 有数据输入,但是输入FIFO没有满,存入数据后仍向上一级反馈idles空闲1
- 有数据输入,输入FIFO满了,不能存入数据,向上一级反馈忙信号0
从上面的这个逻辑可以看出,usb_cypress_little这个模块是写入优先的,通过idles作为握手信号,告诉上一级应该读还是写,当idles为忙信号0,则USB模块不从上一级读数据,此时开放写入和数据处理等请求。
根据RTL的思路,我们可以编写出USB模块的代码:
`timescale 10ns/1ns
//
// Module Name: usb_cypress_little
// Description: If the FIFO of EP2 is not empty and
// the EP6 is not full, Read the 16bit data from EP2 FIFO
// and send to EP6 FIFO.
//
module usb_cypress_little(
input clk, //FPGA Clock Input 50Mhz
input reset, //FPGA Reset input
input usb_flaga, //CY68013 EP2 FIFO empty indication; 1:not empty; 0: empty 外部读信号进来时 查询是否读空
input usb_flagc, //CY68013 EP6 FIFO full indication; 1:not full; 0: full
output reg usb_slcs, //CY68013 Chipset select
output reg usb_sloe, //CY68013 Data output enable
output reg usb_slrd, //CY68013 READ indication
input [15:0] usb_port_in,
output reg usb_slwr, //CY68013 Write indication
output reg [1:0] usb_fifoaddr, //CY68013 FIFO Address
output reg usb_fd_en,
output reg [15:0] usb_port_out,
// inout [15:0] usb_fd, //CY68013 Data 王电令把这个三态门拆成读写 usb_port_in usb_port_out
// //王电令增加读写控制位 数据输入 和输入有效 数据输出和输出有效
// //usb_fd_en 映射到了三态门的方向选择 即新型EIO节点
// input usb_flagb, //不用EP4
input rw,
input [15:0] data_in,
input in_valid,
output reg out_vaild,
output reg [15:0] data_out,
output reg idles //用access_req作为idle信号
);
//reg[15:0] data_reg;
reg[15:0] data_temp;
reg bus_busy;
reg write_req;
reg read_req;
//reg access_req;
reg [4:0] usb_state;
reg [4:0] i;
parameter IDLE=5'd0;
parameter EP2_RD_CMD=5'd1;
parameter EP2_RD_DATA=5'd2;
parameter EP2_RD_OVER=5'd3;
parameter EP6_WR_CMD=5'd4;
parameter EP6_WR_OVER=5'd5;
/* Generate USB read/write access request*/
//always @(negedge clk or posedge reset)
//begin
// if (reset ) begin
// access_req<=1'b0;
// end
// else begin
// if (usb_flaga & usb_flagc & (bus_busy==1'b0)) //a为高电平说明有数据要接收,c为高电平说明数据不急着读取
// access_req<=1'b1; //busy等于0,说明没有在发送或接收过程中,这一位相当于idle
// else //USB读写请求
// access_req<=1'b0; //这样的代码是有问题的 只判断有数据要写 不判断有数据要读
// end //这就是王电令要分读写的原因,有数据就写,没数据不读
//end
always @(negedge clk or posedge reset)
begin
if (reset ) begin
write_req<=1'b0;
read_req<=1'b0;
end
else begin
if (usb_flaga & (rw==1'b0) &(bus_busy==1'b0)) //a为高电平说明有数据要接收,外部状态允许接收,同时总线不忙
read_req<=1'b1; //busy等于0,说明没有在发送或接收过程中,这一位相当于idle
else if(usb_flagc & rw & in_valid & (bus_busy==1'b0)) //发送缓冲区没满 有发送请求 总线不忙
write_req<=1'b1;
else begin //USB读写请求
read_req<=1'b0;
write_req<=1'b0;
end
end
end
always @(negedge clk or posedge reset)
begin
if (reset ) begin
idles<=1'b0;
end
else if((bus_busy==1'b0)& (usb_flagc) & (usb_flaga==1'b0) & (rw==1'b0)) //总线不忙 发送缓冲区没满 没有接收请求 没有发送请求
idles<=1'b1;
else
idles<=1'b0;
end
always @(negedge clk or posedge reset)
begin
if (reset ) begin
data_temp<=16'b0;
end
else if(in_valid==1'b1)
data_temp<=data_in;
else
data_temp<=data_temp;
end
/* Generate USB read and write command*/
always @(posedge clk or posedge reset)
begin
if (reset) begin
usb_fifoaddr<=2'b00; //fifoaddr为0 表示使用EP2
usb_slcs<=1'b0; //使能 一直为有效
usb_sloe<=1'b1; //芯片在读写状态时拉低
usb_slrd<=1'b1; //芯片读的时候拉高,不读的时候拉低
usb_slwr<=1'b1; //读为1,写为0,默认在读状态,空闲也在读状态
usb_fd_en<=1'b0; //控制是否从data——reg中读取数据
usb_state<=IDLE;
end
else begin
case(usb_state)
IDLE:begin
usb_fifoaddr<=2'b00;
i<=0;
usb_fd_en<=1'b0;
if (read_req==1'b1) begin //读请求
usb_state<=EP2_RD_CMD; //开始读USB EP2 FIFO的数据
bus_busy<=1'b1; //状态变忙
end
else if(write_req==1'b1) begin //写请求
usb_fifoaddr<=2'b10;
usb_state<=EP6_WR_CMD;
bus_busy<=1'b1;
end
else begin
bus_busy<=1'b0;
usb_state<=IDLE;
end
end
EP2_RD_CMD:begin //发送EP2端口FIFO的数据读命令,先拉低OE信号,再拉低RD信号
if(i==2) begin //延迟两拍
usb_slrd<=1'b1;
usb_sloe<=1'b0; //OE信号变低
i<=i+1'b1;
end
else if(i==8) begin
usb_slrd<=1'b0; //RD信号变低
usb_sloe<=1'b0;
i<=0;
usb_state<=EP2_RD_DATA;
end
else begin
i<=i+1'b1;
end
end
EP2_RD_DATA:begin //读取EP2中的数据
if(i==8) begin
usb_slrd<=1'b1; //RD信号变高,读取数据
usb_sloe<=1'b0;
i<=0;
usb_state<=EP2_RD_OVER;
data_out<=usb_port_in;
out_vaild<=1'b1;
// data_reg<=usb_fd; //读取数据
end
else begin
usb_slrd<=1'b0;
usb_sloe<=1'b0;
i<=i+1'b1;
end
end
EP2_RD_OVER:begin
if(i==4) begin
usb_slrd<=1'b1; //OE信号变高,读取数据完成
usb_sloe<=1'b1;
i<=0;
// usb_fifoaddr<=2'b10; //切换到EP6 ,应该在发送的时候切换 这个时候回Idle
// usb_state<=EP6_WR_CMD;
usb_state<=IDLE;
end
else begin
usb_slrd<=1'b1;
usb_sloe<=1'b0;
out_vaild<=1'b0;
data_out<=16'b0;
i<=i+1'b1;
end
end
EP6_WR_CMD:begin //EP6端口写入数据,slwr变低后,再变高
if(i==8) begin
usb_slwr<=1'b1;
i<=0;
usb_state<=EP6_WR_OVER;
end
else begin
usb_slwr<=1'b0;
usb_fd_en<=1'b1; //数据总线改为输出
usb_port_out<=data_temp;
i<=i+1'b1;
end
end
EP6_WR_OVER:begin //EP6写完成
if(i==4) begin
usb_fd_en<=1'b0;
usb_port_out<=16'b0;
bus_busy<=1'b0;
i<=0;
usb_state<=IDLE;
end
else begin
i<=i+1'b1;
end
end
default:usb_state<=IDLE;
endcase
end
end
//assign usb_fd = usb_fd_en?data_reg:16'bz; //USB数据总线输入输出改变
endmodule
代码中通过判断总线状态、FIFO缓冲区状态、读写请求信号与上一级通过idle握手,默认设置EP2和EP6作为USB的endpoint(用户可根据需求修改),data_temp用于将data_in的信号同步到当前时钟域下并保持。通过定义6种有限状态使得数据按照时序发送与接收。
parameter IDLE=5'd0;
parameter EP2_RD_CMD=5'd1;
parameter EP2_RD_DATA=5'd2;
parameter EP2_RD_OVER=5'd3;
parameter EP6_WR_CMD=5'd4;
parameter EP6_WR_OVER=5'd5;
用过NI公司的FPGA板卡或者RIO产品的用户应该知道,I/O节点传统FPGA不同,传统FPGA对一些总线共用读写的情况如IIC、单总线,数据传输采用三态门的形式实现,语言形态为 inout。由于 NI FPGA软件提供的 IP Node(类似 DLL 函数调用节点)本身不支持 inout 类型,所以我们将传统的 EIO 节点进行升级改造,将一个 EIO 资源同时拆分为 3 个独立的 I/O 资源:输入、输出、使能。这样做的目的是可以通过“使能”这个开关来控制 I/O 当前是输出状态还是高阻态(输入)。默认情况下,我们将 Xilinx FPGA 芯片的最后一个 BANK 里面的 I/O 引脚,全部封装成 EIO 节点。
在FPGA 程序框图中可以通过拖放各种不同的 I/O 节点来驱动真实的 FPGA 引脚或者外设,这些 I/O 节点称之为 Elemental I/O,简称 EIO。Elemental I/O 是 NI 独创的一种编程模式,最常见的就是网络共享变量。EIO 本质上是把一个对象的全部操作封装到一个节点内部,有点类似 Express VI 的味道。可以通过属性节点的方式对其进行配置。
-
1)传统 EIO 节点:使用方便,操作简单,易于理解,适用于直接读写 IO,输入输出模式切换速度慢(切换回高阻模式需要延时至少 1个CLK),无法与IP Node节点直接对接。
-
2)新型 EIO 节点:适用于 inout 数据类型,可与 IP Node 进行结合,输入输出模式切换速度快,一个时钟周期 CLK 内可完成工作模式切换。
如果用户希望把 FPGA 引脚设置成高阻态,也就是不带输出电流的输入模式,高阻模式最大的优点在于对外部电路不产生任何影响,同时还能读取到引脚外部的电平状态。我们可以利用 FPGA I/O 函数选板里面的 “FPGA I/O 方法节点”和“FPGA I/O 常量”来对 IO 引脚进行动态设置。
将 IO 方法里面的“设置输出启用”设置为假(F),相当于将 IO 设置成了高阻态,然后再读取就可以了。如果设置为真(T),相当于开启了推出输出模式,输出的电流会比较大一些
编写好.v文件后需要修改综合参数,在Xilinx Specific Options中去掉 Add I/O Buffer选项,不添加I/O buffer并综合。
五、FPGA图形化程序编写
将上面程序生成的usb_cypress_little.ngc(要求.v的文件名与模块名字相同) 添加到vi所在的工程目录下,调用IP block节点。
可以按照上图顺序将节点接口排好,方便连线,将外围接口进行连接通过FIFO交换数据,构成USB通信线程,在下图中对于数据位宽较宽的接口可以采用Boolean作为数据类型,便于数据拼接和拆分。
上图为USB通信线程,该线程可通过FIFO节点实现跨时钟域的数据交互,该线程最快可采用80M时钟。FIFO节点超时?用于判断FIFO内有无数据,接输入有效,有数据且IP核空闲的时候发送FIFO数据,idle忙或当前时钟已发送数据情况下,切换到读数据分支(假分支),因此需要一个与条件和一个反馈节点,这样可以实现互锁操作,保证每次数据更新与idle错开。
反馈节点(移位寄存器)对于数字电路来说,非常重要,可以简化编程
USB通信线程用于发送和接收数据,在发送数据时作为消费者线程,我们需要匹配一个信号产生线程作为生产者,下图我们用while循环生成了一个斜坡信号通过FIFO_USB_Write发送到上位机。
为了方便观察结果,我们将发送的数据,接收的数据,以及数据的数量在前面板进行显示。
在上图中,面板上可以通过一个布尔控件控制信号的产生与暂停,生产者速度输入用于调整数据产生线程的分频系数,发送计数显示当前发送数据的数量,接收到的数据表示从上位机接收到的数据(十进制表示),接收计数表示上位机发送数据的个数。
此外,上位机可显示出接收到的斜坡信号。
在进行开发板与计算机连接前,要确保提前安装并配置好Cypress的驱动程序,采用NI-VISA的USB向导安装好驱动
这样才能保证上下位机都能接收到数据。
总结
FPGA图形化编程其优势在于逻辑图形化,若不去考虑底层verliog,在顶层设计上FPGA的图形化编程是有优势的,可以更有效率的完成FPGA的设计,本节实验的Verliog网表文件和LabVIEW的vi均开源在 我的资源 如有需要可以下载学习。