在icestick板子上实现从FPGA到USB Host的数据传输

背景信息
icestick 板载 usb 接口芯片 ft2232h 的端口 a 和端口 b 均与 fpga ice40hx1k 相连。其中,端口 a 处于 mpsse 模式,用于读写 spi flash 以更新 fpga 的 bitfile,而 b 口默认处于 async serial 模式,当作串口使用。
端口 b 都只有一部分引脚连到 fpga,无法支持 245 fifo 或者 245 fifo sync 模式以实现高速数据传输。而在 async serial 模式时,其支持最大 12mbaud 即最高 1.14mbps 的数据传输。
笔者发现 ft2232h 还支持一种称为 fast opto-isolated serial interface 的模式。在 ftdi 文档和软件中,这一模式也被称为 fast serial interface 模式或者 opto isolate 模式。因为引入了时钟引脚,这一模式可以在使用较少引脚的情况实现比 async serial 模式更高带宽的数据传输。
读者可以通过此文了解 opto isolate 模式如何使用并按照说明可以实现最高 2.57mbps 的数据传输。
准备工作
你需要具备以下条件:
一块 icestick 开发板
ice40 fpga 开发工具,开源工具或者 icecube2
ft prog 程序及 d2xx 驱动
ft2232h datasheet
同时,笔者使用以下软件实现 windows 端测试程序:
zadig 软件
libusb 库及开发环境
准备妥当后,我们先尝试修改 ft2232h 芯片端口 b 的模式。
修改模式
首先,我们需要使用 ftdi 公司的 ft prog 程序来修改端口 b 的模式。而 ft prog 程序则需要驱动程序 d2xx 驱动。
打开设备管理器,插入 icestick 后,如果 d2xx 设备驱动程序配置正常,在通用总线控制器下会出现 usb serial converter a 和 usb serial converter b 设备,如下图所示。
打开 ft prog 程序,主菜单上点击 devices 然后点击 scan and parse 子菜单。如果一切正常,读者应该可以看到以下界面:
选中设备,找到 hardware specific 下 port b 的 hardware 项,选择 opto isolate 项,然后在主菜单上点击 devices 然后点击 program 子菜单。
点击 program 按钮就可以将修改后的配置选项写入 ft2232h 的 eerom 中。
烧写完毕后可以重新拔插 icestick,打开 ft prog 再次查看设备的 port b 的属性是否已经修改成 opto isolate。
修改成功后,我们看一下 opto isolate 模式下接口的规格。
opto isolate 模式
在 ft2232h datasheet 章节 3.1.4.6 ft2232h pins used as a fast serial interface 的 table 3.10 中我们可以找到:
由于本文侧重于 fpga 到 ft2232h 的数据传输,我们可以暂时忽略用于 ft2232h 到 fpga 方向数据传输的 fsdo 引脚。
在章节 4.8.2 incoming fast serial data 中 figure 4.15 fast opto-isolated serial interface input data 我们可以看到:
有此时序图我们可以看出:
空闲状态下 fscts 和 fsdi 应为高电平
fsclk 作为数据传输的参考时钟,ft2232h 应在 fsclk 的上升沿采数据
fscts 为高电平时,fpga 可以进行数据传输
一次传输的数据由一个由 0 表示的起始位、lsb 优先的 8 位数据和一个 dest 位组成
在 dest 位发送后,fscts 仍然会保持一段时间的低电平
然后我们在章节 4.8  fast opto-isolated serial interface mode description 的 figure 4.13 fast opto-isolated serial interface signal waveforms 看到具体时序:
以及在同一章节的 table 4.6 fast opto-isolated serial interface signal timings 看到具体时序:
根据这些,我们可以得到:
fsclk 的最小周期是 20ns,即最大频率是 50mhz
fsdi 的建立时间最小为 10ns,保持时间最小为 5 ns
这样我们可以根据这些参数进行 fpga 接口设计了。
接口电路设计
在实现接口电路之前,读者需要检查具体用到的 fpga 的性能。如果 io 反转性能不能达到 50mhz,那么 io 就成为瓶颈;如果 io 性能满足要求,但是接口电路频率不能超过 100mhz 或者在支持支持双沿输出和输入的情况下超过 50mhz,接口电路就会成为瓶颈。
通过查看 ice40hx1k 的文档,我们可以大致确定 ice40hx1k 的普通 io 反转可以做到 50mhz,而且,在此 fpga 上实现一个运行在近 100mhz 的电路并不十分困难。
如果我们使用 100mhz 作为内部时钟,那么数据传输时序图会变成:
需要说明的是
我们使用 100mhz 使用来产生一个 50mhz 的 fsclk 对应 io 的反转
我们在 fsclk 的下降沿变更数据,这样保证 fsdi 的建立时间和保持时间都是 10ns
我们会产生一个持续反转的 fsclk,而不会在不传输数据的时候暂停 fsclk
通过观察时序图,计数状态机来实现此电路较为简单。同时对于慢速接口电路,valid-ready 信号也是不能缺少的。加入这些信息后,数据传输时序图会变成:
有了这样的时序图,我们可以开始实现具体的电路了。
接口电路实现
首先,输入信号 fscts 需要经过跨时钟域处理。我们可以将 i_fscts 连接到 2 个级联的 dff 上来进行处理,从而得到同步的信号 w_fscts。
//    // cdc signals    //    localparam  dff_w = 2;    wire                    w_fscts;    reg     [dff_w - 1 : 0] r_fscts_sync;    always @ (posedge i_clk)        r_fscts_sync <= {r_fscts_sync[dff_w - 2 : 0], i_fscts};    assign w_fscts = r_fscts_sync[dff_w - 1];  
其次,fast opto-isolated serial interface 电路本质是将并行的数据进行串行数据,我们还需要一个移位寄存器和对应的控制信号:
//    // r_data    //    // r_data keeps the data to be transmitted    //    localparam  tx_data_w = data_w + 3;    reg     [tx_data_w - 1 : 0] r_data;    reg     [tx_data_w - 1 : 0] w_data_next;    wire                        w_data_tick;    wire                        w_data_load;    wire                        w_status_tick;    always @ (posedge i_clk, posedge i_arst)        if (i_arst)            r_data <= {tx_data_w{1'b1}};        else            r_data <= w_data_next;    always @(*) begin        w_data_next = r_data;        if (w_data_load)            w_data_next = {i_channel, i_data, 2'b01};        else            if (w_data_tick)                w_data_next = {1'b1, r_data[tx_data_w - 1 : 1]};    end    // w_data_load indicates if r_data could be filled safely    assign w_data_load = o_ready & i_valid;    assign w_data_tick = w_status_tick | w_status_start;  
因为数据的发送需要两个必备条件,我们需要一个 bit 来确保默认情况 fsdi 的数据为高电平。这样,加上起始位和 dest 位后,我们共需要 11 位移位寄存器。
同时,数据发送时 lsb 优先的,我们的 fsdi 自然而然的会连接到移位寄存器的最低位。
复位时,移位寄存器全部填充为 1;复位解除后,如果需要加载数据时,即 w_data_load 为有效时,那么将表示 dest 位的 i_channel、8 位数据 i_data 、表示起始位的 0 和默认电平 1 加载到寄存器中;如果遇到 w_data_tick 有效时,那么就将移位寄存器进行右移,最高位填充 1。
然后,因为数据发送的两个条件并不是同时发生,我们需要一个寄存器来表示 r_data 是否已经填充。
//    // r_data_status    //    // r_data_status decides the valid-ready signals and    // if r_status fsm starts    //    reg     r_data_filled;    reg     r_data_filled_next;    wire    w_data_done;    wire    w_status_done;    always @ (posedge i_clk, posedge i_arst)        if (i_arst)            r_data_filled <= 1'b0;        else            r_data_filled <= r_data_filled_next;    always @(*) begin        r_data_filled_next = r_data_filled;        // if the tx data has been filled        if (r_data_filled) begin            // and the data transmission has been done            if (w_data_done)                r_data_filled_next = 1'b0;        end        // or if the tx data is empty        else begin            // and upstream module's data is ready            if (i_valid)                r_data_filled_next = 1'b1;        end    end    assign w_data_done = w_status_done & i_tick;  
默认的情况下,r_data_filled 为 0 表示 r_data 是没有填充的;如果没有填充过且输入数据有效,那么就将 r_data_filled 设置为 1,表示已经填充;如果已经填充,且表示数据已经完全被发送出去,即 w_data_done 为 1 时将 r_data_filled 设置为 0。
接着,我们需要实现一个计数状态机来控制 r_data 的移位和 r_data_filled 的更新。
//    // r_status    //    // r_status controls tx fsm    //    localparam  status_max  = 11;    localparam  status_w    = $clog2(status_max);    reg     [status_w - 1 : 0]  r_status;    reg     [status_w - 1 : 0]  w_status_next;    wire                        w_status_start;    wire                        w_status_idle;    assign w_status_idle = (r_status == {status_w{1'b0}});    assign w_status_done = (r_status == (status_max[status_w - 1 : 0] - 1'b1));    assign w_status_tick = i_tick & ~w_status_idle;    always @ (posedge i_clk, posedge i_arst)        if (i_arst)            r_status <= {status_w{1'b0}};        else            r_status <= w_status_next;    always @(*) begin        w_status_next = r_status;        if (w_status_start)            w_status_next = {{status_w-1{1'b0}}, 1'b1};        else            if (w_status_tick)                if (w_status_done)                    w_status_next = {status_w{1'b0}};                else                    w_status_next = r_status + 1'b1;    end    // fsm begins counting when fsm is idle    assign w_status_start = w_status_idle &                            // r_data is filled                            r_data_filled &                            // i_fstcs is ready to receive data                            w_fscts &                            // to sync with o_fsclk signal                            i_tick;  
计数状态机的实现并不复杂。默认情况下,计数器为 0 表示接口处于空闲状态;在处于空闲状态下,如果 r_data 已经被填充,fscts 信号为高,在保证与时钟同步的情况下,计数器变成 1,然后每个 w_status_tick 有效时加一,知道计到最大状态 10 之后又变为 0,等待 w_status_start 再次变为 1。
最后,有了上述电路,那么输出信号处理就较为简单。
//    // output signals    //    // r_data is ready when r_data_filled is 0    assign o_ready = ~r_data_filled;    assign o_fsdi = r_data[0];  
对于 o_ready 信号,只要数据发送完毕就可以进行填充;而 fsdi 信号直接取 r_data 的最低位。
为了测试这一接口模块,我们还需要设计另外一个控制模块来产生数据并驱动此接口模块。笔者实现了一个控制模块,支持周期发送和最大带宽发送两种模式选择。限于篇幅,此处不再赘述其实现细节。
控制模块的代码、接口模块的代码、cocotb 仿真代码及 icestick 完整的工程可以在 icestick-oifs 库中 gateware 目录 oifs-tx 子目录下找到。
需要注意的是,由于 icestick 板载晶振频率 12mhz,使用 pll 倍频出最大符合规范的时钟频率为 99mhz,所以,实际的 fsclk 的反转频率是 49.5mhz,而非设计时的 50mhz。
fpga 部分实现完毕后,我们还需要实现 usb host 侧的软件来接收数据。
usb host 侧软件
为了实现跨平台的代码,笔者使用 libusb 库进行 usb host 侧的代码。
在 windows 上,读者需要使用 zadig 软件将端口 b 的 d2xx 驱动替换成 libusb 的驱动。如下图所示:
同时,笔者在 msys2 环境下安装 mingw-w64-x86_64-libftdi 和必要的开发工具,用来构建 usb host 侧的程序。
笔者编写了两个程序,一个程序用来读取数据并计算性能,名为 oifs-rxperf,另外一个程序用来将数据打印到控制台,名为 oifs-rxdump。
usb host 侧代码可以在 icestick-oifs 库中 software 目录下找到。这些代码可以不用修改或者稍加修改运行在 linux 平台上。
测试结果
笔者在 windows 上测试的结果如下:
我们可以看到最高的传输速率可以达到 2.57mbps。笔者在 ubuntu 20.04 和  ubuntu 22.04 中分别进行了测试,测试结果与 windows 平台上得到的结果一致。
在接口逻辑仿真环境中,我们假设了 fscts 信号总高,但是实际情况并非如此:
通过逻辑分析仪抓到信号的波形来看,fscts 在 dest 位传输完毕后保持 140ns 的低电平,那么一次传输需要大概 364ns 到 384ns,则极限带宽则约为 2.61mbps。
总结
按照本文的说明及相应的代码,读者应该可以在 icestick 上实现从 fpga 到 usb host最高 2.57mbps 的数据传输,从而将 icestick 变成一个 usb 数据采集板。


如何理解TN-S供电系统
华为高薪招募人工智能博士
OmniVision宣布推出全新的200万像素传感器
设备运维监控解决方案:雷蒙机可视化数据管理系统
碳化硅功率器件的工作原理详解
在icestick板子上实现从FPGA到USB Host的数据传输
iPhone X被99%的用户误会,问题都是有原因的
基于HarmonyOS运动员智能训练系统开发过程
蔚来汽车于美纽交所上市,未来如何?
赤潮卫星遥感监测与实时预报-莱森光学
光纤放大器的放大自发辐射说明
AMD揭开Zen CPU核心技术Ryzen神秘面纱
开放原子开源基金会联合主办的2023 CCF中国开源大会正式开幕
中控智慧科技考勤机U560介绍
雷军:小米8真的不错,再战两年!
身份证拍照识别技术简介及其特点
IO-Link成为现代智能工厂的核心技术
10个和高速PCB设计相关的重要知识分享
德勤:2025年全球人工智能市场规模将超6万亿美元,复合增长率达30%
元宇宙为何会出现?