FPGA之Usb数据传输
Usb 通信
你也许会有疑问,明明有这么多通信方式和数据传输(SPI、I2C、UART、以太网)为什么偏偏使用USB呢?
原因有很多,如下:
1. 高速数据传输能力
- 高带宽:USB接口提供了较高的数据传输速率,尤其是随着USB版本的升级(如USB 3.0及更高版本),其理论速度可达5 Gbps甚至更高。这对于需要高速数据传输的应用(如视频处理、实时数据采集等)尤为重要。
- 低延迟:相比一些其他接口(如UART),USB的延迟更低,能够满足实时性要求较高的场景。
2. 通用性和兼容性
- 广泛的硬件支持:几乎所有现代计算机和嵌入式系统都配备了USB接口,这意味着使用USB进行通信可以轻松实现跨平台支持,无需额外的硬件适配。
- 标准化接口:USB是一种标准化的接口,遵循统一的协议和规范。这不仅简化了开发过程,还确保了不同设备之间的互操作性。
3. 开发便利性
- 丰富的工具支持:大多数FPGA开发工具(如Xilinx Vivado、Altera Quartus等)都提供了对USB接口的支持,简化了设计和验证过程。
- 成熟的驱动和库资源:大量的现成驱动程序和库资源可以轻松集成到项目中,减少了软件开发的工作量。
4. 灵活的通信模式
- 全双工通信:USB支持全双工通信模式,允许同时进行数据的上传和下载,提高了通信效率。
- 多种数据传输类型:USB支持控制传输、批量传输、中断传输和同步传输等多种数据传输类型,能够适应不同应用场景的需求。
成本效益
- 低成本解决方案:相比于一些高端接口(如PCIe),USB的成本较低,适合预算有限的项目。
- 减少外部组件需求:由于USB的标准化和广泛支持,可以减少对外部组件的需求,从而降低整体硬件成本。
也正是因为如此,usb广泛应用于数据的采集和处理、视频和音频传输、嵌入式系统开发等。
而我们今天即将要学习的,就是FPGA的USB传输,以FX2芯片为例
FX2
USB是一种通用的数据传输协议和接口标准,定义了设备与主机(如电脑)之间的通信规则(如协议、电气特性、数据传输模式等);FX系列芯片(FX2, FX3)是Cypress(现英飞凌)推出的USB控制芯片,用于实现高速USB设备的功能。说的再简单,直白一点:USB是协议标准,FX芯片是实现这一标准的硬件载体
FX芯片可以
- 自动处理USB复杂协议,无需开发者手动实现,
- 支持高速传输(FX2:支持USB2.0高速传输, 480Mbps; FX3则为 5Gbps)
- 提供灵活的接口(GPIF,Slave FIFO)方便直接连接外设,
- 内置微控制器,可以通过固件配置USB功能
FX2控制器内部结构图如下
FX2可以通过两种方式到FPGA,一个是(通用可编程接口)GPIF模式和从设备FIFO模式
GPIF:FX2是总线的主控者,用户自定义时序,灵活但开发复杂
Slave FIFO: FX2是被动的FIFO从设备,外部主控直接控制,简单但灵活性受限
在实际项目中,Slave FIFO模式更常用(尤其是FPGA做为主控的场景),而GPIF模式需要更精确控制总线的特殊需求
回环测试
介绍
我们此处就以简单的回环测试为例,实现FPGA的Usb数据传输。
所谓回环测试,就是说由 PC 发送数据到 FX2 芯片的 OUT 端点 2,然后再由主机将端点 2 中的数据读出,拷贝到IN 端点 6。使用 FPGA 设计 SlaveFIFO 读取和写入接口逻辑,将端点 2 中的数据读出,然后写入端点 6 中,再由电脑上位机从端点 6 中将数据读回,从而实现数据的回环。
代码
FIFO
module FiFo #(
Depth = 512,
Width = 16
)
(
input fifo_clk,
input rst_n,
input write_busy,
input read_busy,
input fifo_flush,
input [Width-1:0]din,
output reg fifo_full,
output reg fifo_empty,
output reg [Width-1:0] dout
);
localparam ADDR_Width =$clog2(Depth);
//计数多加一位,防止溢出
reg [ADDR_Width:0] write_occupancy;
reg [ADDR_Width:0] read_occupancy;
wire [ADDR_Width:0] next_write_occupancy;
wire [ADDR_Width:0] next_read_occupancy;
//fifo 地址索引
wire [ADDR_Width-1:0] next_write_ptr;
reg [ADDR_Width-1:0] write_ptr;
wire [ADDR_Width-1:0] next_read_ptr;
reg [ADDR_Width-1:0] read_ptr;
reg [Width-1:0] data_array[Depth-1:0];
wire write_enable;
wire read_enable;
// 写使能和读使能逻辑
assign write_enable = !write_busy && !fifo_full;
assign read_enable = !read_busy && !fifo_empty;
// 下一个指针和计数器计算
assign next_write_ptr = (write_enable) ? (write_ptr + 1) : write_ptr;
assign next_read_ptr = (read_enable) ? (read_ptr + 1) : read_ptr;
assign next_write_occupancy = fifo_flush ? 10'd0 : (write_enable ? (write_occupancy + 1) : write_occupancy);
assign next_read_occupancy = fifo_flush ? 10'd0 : (read_enable ? (read_occupancy + 1) : read_occupancy);
// 满/空状态判断(基于下一个计数器值)
wire [ADDR_Width:0] next_occupancy_diff = next_write_occupancy - next_read_occupancy;
wire next_fifo_full = (next_occupancy_diff >= Depth);
wire next_fifo_empty = (next_occupancy_diff == 0);
//更新指针
always @(posedge fifo_clk or negedge rst_n)begin
if(!rst_n)begin
write_ptr<=0;
read_ptr<=0;
end else if(fifo_flush)begin
write_ptr<=0;
read_ptr<=0;
end else begin
write_ptr<=next_write_ptr;
read_ptr<=next_read_ptr;
end//else
end//always
// 更行空/满信号
always @(posedge fifo_clk or negedge rst_n)begin
if(!rst_n)begin
fifo_full<=0;
fifo_empty<=1;
end else if (fifo_flush)begin
fifo_full<=0;
fifo_empty<=1;
end else begin
fifo_full<=next_fifo_full;
fifo_empty<=next_fifo_empty;
end
end//always
// 读/写计数
always @(posedge fifo_clk or negedge rst_n)begin
if(!rst_n)begin
write_occupancy<=0;
read_occupancy<=0;
end else if(fifo_flush)begin
write_occupancy<=0;
read_occupancy<=0;
end else begin
write_occupancy<=next_write_occupancy;
read_occupancy<=next_read_occupancy;
end//else
end//always
//输出数据
always @(posedge fifo_clk or negedge rst_n)begin
if(!rst_n)dout<=0;
else if(fifo_flush) dout<=0;
else dout<=data_array[read_ptr];
end//always
//数据写入存储阵列
always @(posedge fifo_clk)begin
if(write_enable)
data_array[write_ptr]<=din;
end
// 溢出警告
always @(posedge fifo_clk) begin
if (fifo_full && write_busy) begin
$display("ERROR: %m: Fifo overflow at time %t", $time);
$finish;
end
end // always
// 下溢警告
always @(posedge fifo_clk) begin
if (fifo_empty && read_busy) begin
$display("ERROR: %m: Fifo underflow at time %t", $time);
$finish;
end
end // always
// synthesis translate_on
endmodule
FX2_SF
module FX2_SF(
input clk,
input reset_n,
inout [15:0] fx2_fdata, // 双向数据总线
output [1:0] fx2_faddr, // FIFO地址选择
output fx2_slrd, // 读使能(低有效)
output fx2_slwr, // 写使能(低有效)
output fx2_sloe, // 输出使能(低有效)
input ep6_full_flag, // EP6满标志(可写)
input ep2_empty_flag, // EP2空标志(可读)
input fx2_ifclk, // 接口时钟(60MHz)
output fx2_pkt_end, // 包结束脉冲
output fx2_clear, // 复位信号
output fx2_slcs // 片选(常低)
);
//------------------------ 参数优化 ------------------------//
localparam [1:0]
LOOPBACK_IDLE = 2'd0,
LOOPBACK_READ = 2'd1,
LOOPBACK_WAIT_ep6_full = 2'd2,
LOOPBACK_WRITE = 2'd3;
localparam [1:0]
FIFO_ADDR_READ = 2'b00, // EP2
FIFO_ADDR_WRITE = 2'b10; // EP6
//------------------------ 信号声明 ------------------------//
reg [1:0] current_state, next_state;
// FIFO控制信号
wire fifo_wr_en;
wire fifo_rd_en;
reg [15:0] fifo_din;
wire [15:0] fifo_dout;
// FX2接口信号
reg slrd_n;
reg slwr_n;
reg sloe_n;
reg [1:0] faddr_n;
reg pkt_end_n;
//------------------------ 接口分配 ------------------------//
assign fx2_slwr = slwr_n;
assign fx2_slrd = slrd_n;
assign fx2_sloe = sloe_n;
assign fx2_faddr = faddr_n;
assign fx2_pkt_end= pkt_end_n;
assign fx2_slcs = 1'b0; // 常使能
assign fx2_clear = 1'b0; // 未使用
// 三态总线控制
assign fx2_fdata = (slwr_n == 1'b0) ? fifo_dout : 16'hzzzz;
//------------------------ 状态机 ------------------------//
always @(posedge fx2_ifclk or negedge reset_n) begin
if(!reset_n) current_state <= LOOPBACK_IDLE;
else current_state <= next_state;
end
always @(*) begin
next_state = current_state;
case(current_state)
LOOPBACK_IDLE: //ep2为空, 上位机可传输数据
if(ep2_empty_flag) next_state = LOOPBACK_READ;
LOOPBACK_READ:
if(!ep2_empty_flag) next_state = LOOPBACK_WAIT_ep6_full;
LOOPBACK_WAIT_ep6_full:
if(ep6_full_flag) next_state = LOOPBACK_WRITE;
LOOPBACK_WRITE: begin
if(!ep6_full_flag || fifo_empty)
next_state = LOOPBACK_IDLE;
end
default: next_state = LOOPBACK_IDLE;
endcase
end
//------------------------ 控制信号生成 ------------------------//
always @(*) begin
// 默认值
slrd_n = 1'b1;
sloe_n = 1'b1;
slwr_n = 1'b1;
faddr_n = FIFO_ADDR_READ;
pkt_end_n = 1'b1;
case(current_state)
LOOPBACK_READ: begin
faddr_n = FIFO_ADDR_READ;
slrd_n = !ep2_empty_flag; // 有数据时持续读取
sloe_n = !ep2_empty_flag;
end
LOOPBACK_WRITE: begin
faddr_n = FIFO_ADDR_WRITE;
slwr_n = !(ep6_full_flag && !fifo_empty);
// 在最后一次写入后生成包结束脉冲
pkt_end_n = (slwr_n == 1'b0) ? 1'b0 : 1'b1;
end
endcase
end
//------------------------ FIFO接口 ------------------------//
assign fifo_wr_en = (current_state == LOOPBACK_READ) && !slrd_n;
assign fifo_rd_en = (current_state == LOOPBACK_WRITE) && !slwr_n;
// 数据输入寄存器
always @(posedge fx2_ifclk) begin
if(fifo_wr_en)
fifo_din <= fx2_fdata;
end
FiFo #(
.Depth(512),
.Width(16)
) u_fifo (
.fifo_clk (fx2_ifclk),
.rst_n (reset_n),
.write_busy (1'b0), // 外部无写阻塞
.read_busy (1'b0), // 外部无读阻塞
.fifo_flush (1'b0), // 禁用自动flush
.din (fifo_din),
.fifo_full (fifo_full),
.fifo_empty (fifo_empty),
.dout (fifo_dout)
);
wire clk_96m;//生成96M时钟用于ILA采样
pll pll_inst(
.clk_out1(clk_96m),
.clk_in1(clk)
);
//------------------------ 调试模块注释 ------------------------//
ila_0 ila_0_inst(
.clk(clk_96m), // input wire clk
.probe0(fx2_fdata), // input wire [15:0] probe0
.probe1(fx2_faddr), // input wire [1:0] probe1
.probe2(ep2_empty_flag), // input wire [0:0] probe2
.probe3(ep6_full_flag), // input wire [0:0] probe3
.probe4(fx2_sloe), // input wire [0:0] probe4
.probe5(fx2_slwr), // input wire [0:0] probe5
.probe6(fx2_slrd), // input wire [0:0] probe6
.probe7(fifo_empty), // input wire [0:0] probe7
.probe8(fifo_full), // input wire [0:0] probe8
.probe9(fifo_flush) // input wire [0:0] probe9
);
endmodule
注:实现该项目时需要使用到 Cypress提供的基本开发包(名为CySuiteUsb)和安装对应的驱动,可以到官网上去下载。