基于FPGA的USB接口控制器设计(VHDL)


基于FPGA的USB接口控制器设计(VHDL)

时间:2025-03-11  作者:Diven  阅读:0

今天给大侠带来基于 FPGA 的 USB 接口控制器设计(VHDL),由于篇幅较长,分三篇。今天带来第三篇,下篇,FPGA 固件开发、USB驱动和软件开发。话不多说,上货。

 

导读

2019年9月4日,USB-IF终于正式公布USB 4规范。它引入了Intel此前捐献给USB推广组织的Thunderbolt雷电协议规范,双链路运行(Two-lane),传输带宽因此提升,与雷电3持平,都是40Gbps。需要注意的是,你想要体验最高传输速度,就必须使用经过认证的全新数据线。USB4保留了良好的兼容性,可向下兼容USB 3.2/3.1/3.0、雷电3。除此之外,USB4将只有USB Type-C一种接口,并支持多种数据、显示协议,包括DisplayPort,可以一起充分利用高速带宽,也支持USB PD供电。

比较遗憾的是,USB4的发布时间至今暂未公布。值得注意的是,此次发布的USB4是规范,而并非USB4.0。在此之前,USB Implementers Forum(USB-IF)计划取消USB 3.0/3.1命名,统一划归为USB 3.2。其中USB 3.0更名USB 3.2 Gen 1(5Gbps),USB 3.1更名USB 3.2 Gen 2(10Gbps),USB 3.2更名为USB 3.2 Gen 2x2(20Gbps)。以上就是关于USB标准以及命名的讯息。

现在大部分USB设备(比如USB接口的鼠标、键盘、闪存、U盘等等)都是采用了USB通用驱动,而你的系统有USB通用驱动的话(比如XP就内建了USB通用驱动)就能用。而有些USB设备是需要特殊驱动的,比如某些手机,连接到电脑的USB口,是需要安装驱动才能使用的。下面我们一起动手做一做USB接口控制器设计,了解一下如何设计。

第三篇内容摘要:本篇会介绍FPGA 固件开发,包括固件模块划分、自定义包编写、分频器模块的实现、沿控制模块的实现、输入/输出切换模块的实现、请求处理模块的实现、设备收发器模块的实现、测试平台的编写;USB 驱动和软件开发,包括USB 驱动编写、USB 软件编写以及总结等相关内容。

 

六、FPGA 固件开发

 

6.1 固件模块划分

在本例中,固件开发指的就是 FPGA 开发,也就是使用硬件描述语言(VHDL 或者 VerilogHDL)编写 FPGA 内部程序。FPGA 的作用就是和 PDIUSBD12 进行通信,从 PDIUSBD12 中获取数据并且根据主机的要求发送数据。PDIUSBD12 和 FPGA 之间的通信就是 8 位数据总线加上若干控制信号(A0、WR_N、RD_N 等),只要控制 FPGA 产生符合 PDIUSBD12 输入/输出时序的脉冲,即可实现两者之间的通信。

FPGA 固件的模块图如图 34 所示,各个模块的功能如下。

图 34 硬件加密系统设计方案

(1)分频器模块

由于 PDIUSBD12 在读写时序上有时间限制,例如每次读写操作之间的间隔不能小于 500ns,而 FPGA 的系统时钟一般频率都比较高,所以不能直接使用系统时钟控制 PDIUSBD12,必须进行分频。分频器模块的功能就是按照要求由系统时钟生成所需频率的时钟信号。

(2)沿控制器模块

PDIUSBD12 的读写操作都各自有一个读写控制信号 WR_N 和 RD_N,每次读写操作都在对应的控制信号的下降沿触发,沿控制模块的功能就是可控地产生一个下降沿信号,用于控制读写操作。

(3)输入/输出切换模块

输入/输出切换模块在整个系统中非常重要,因为 FPGA 芯片和 PDIUSBD12 芯片之间的数据总线是双向的总线,所以当读写操作之一在进行的时候另一个操作的信号源必须关闭,否则就会造成双驱动,这不但不能得到正确的数据还会损害芯片。输入/输出切换模块的功能就是根据当前的读写状况控制信号源,保证在一个时刻只有一个信号源在驱动总线。

(4)设备收发器模块

这个模块是整个固件的核心模块,它完成的工作包括配置 PDIUSBD12 芯片、处理 PDIUSBD12产生的中断、完成从缓存读取数据,并且根据需要将数据通过 PDIUSBD12 发送。设备收发器模块完成对每个主机请求的解析工作,此外,还要将解析完成的请求数据传递给请求处理模块。

(5)请求处理模块

请求处理模块的作用是接收设备收发器模块解析完成的主机请求,并且决定如何处理此请求。

模块划分完毕之后就可以使用 ISE 创建工程了,然后就各个模块分别编写实现代码和测试平台,最后将所有模块整合起来作为一个实体并且对其进行仿真、测试,这样就是一次完整的FPGA 开发过程。

ISE 的一些基本使用方法在前面的文章已有详细介绍,这里放超链接,在此不详细说明。下面详细介绍一下各个模块的实现方法。

ISE 14.7 安装教程及详细说明

 

6.2 自定义包编写

在实际实现各个模块功能之前,首先需要编写两个自定义包,分别是 USB 包和 PDIUSBD12包。

USB 包定义了 USB 协议以及 USB 设备相关的数据类型、常量等内容,比如自定义数据类型、设备类型代码值、请求代码值、设备描述符、设备的工作状态机等。设备的工作状态机定义如下:

- 定义设备的工作状态机type TRANSEIVER_STATEis ( TS_DISCONNECTED, -- 未连接TS_CONNECTING, -- 正在连接TS_IDLE, -- 闲置TS_END_REQUESTHANDLER, -- 请求处理完成TS_READ_IR, -- 读取中断寄存器TS_READ_LTS, -- 读取最后处理状态TS_BUSRESET, -- 总线复位TS_SUSPENDCHANGE, -- 挂起改变TS_EP0_RECEIVE, -- 端点 0 接收完成TS_EP0_TRANSMIT, -- 端点 0 发送完成TS_EP2_RECEIVE, -- 端点 2 接收完成TS_EP2_TRANSMIT, -- 端点 2 发送完成TS_END_RECEIVE, -- 从 PDIUSBD12 读取数据完成TS_END_TRANSMIT, -- 向 PDIUSBD12 写数据完成TS_SEND_DESCRIPTOR_1ST, -- 首次发送设备描述符TS_SEND_DESCRIPTOR, -- 发送设备描述符TS_SET_ADDRESS, -- 设置地址TS_SET_CONFIGURATION, -- 设置配置TS_GET_CONFIGURATION, -- 获取配置TS_GET_INTERFACE, -- 获取接口TS_SEND_STATUS, -- 发送状态TS_CLEAR_FEATURE, -- 清除特性TS_SET_FEATURE, -- 启用特性TS_SET_INTERFACE, -- 设置接口TS_READ_ENDPOINT, -- 从端点读取数据TS_WRITE_ENDPOINT, -- 向端点写入数据TS_SEND_PASSWORD, -- 发送密码TS_SET_PASSWORD_HIGH, -- 设置密码低位TS_SET_PASSWORD_LOW, -- 设置密码高位TS_SEND_EMPTY_PACKET, -- 发送空包TS_STALL, -- 禁止TS_ERROR); -- 错误

请求类型以及请求的代码定义如下:

-- 描述符类型constant TYPE_DEVICE_DESCRIPTOR: STD_LOGIC_VECTOR(7 downto 0) := X"01";constant TYPE_CONFIGURATION_DESCRIPTOR: STD_LOGIC_VECTOR(7 downto 0) := X"02";constant TYPE_STRING_DESCRIPTOR: STD_LOGIC_VECTOR(7 downto 0) := X"03";constant TYPE_INTERFACE_DESCRIPTOR: STD_LOGIC_VECTOR(7 downto 0) := X"04";constant TYPE_ENDPOINT_DESCRIPTOR: STD_LOGIC_VECTOR(7 downto 0) := X"05";constant TYPE_POWER_DESCRIPTOR: STD_LOGIC_VECTOR(7 downto 0) := X"06";-- 设备描述符相关的代码、索引值等constant CODE_DEVICE_CLASS: STD_LOGIC_VECTOR(7 downto 0) := X"DC";constant CODE_BCD_USB_HIGH: STD_LOGIC_VECTOR(7 downto 0) := X"00";constant CODE_BCD_USB_LOW: STD_LOGIC_VECTOR(7 downto 0) := X"01";constant CODE_ID_VENDOR_HIGH: STD_LOGIC_VECTOR(7 downto 0) := X"71";constant CODE_ID_VENDOR_LOW: STD_LOGIC_VECTOR(7 downto 0) := X"04";constant CODE_ID_PRODUCT_HIGH: STD_LOGIC_VECTOR(7 downto 0) := X"66";constant CODE_ID_PRODUCT_LOW: STD_LOGIC_VECTOR(7 downto 0) := X"06";constant CODE_BCD_DEVICE_HIGH: STD_LOGIC_VECTOR(7 downto 0) := X"00";constant CODE_BCD_DEVICE_LOW: STD_LOGIC_VECTOR(7 downto 0) := X"01";constant CODE_NUMBER_CONFIGURATIONS: STD_LOGIC_VECTOR(7 downto 0) := X"19";

另一个包是 PDIUSBD12 包,它定义的则是和 PDIUSBD12 相关的内容,比如 PDIUSBD12 的命令代码值、中断代码值等内容。对 PDIUSBD12 控制命令的定义如下:

-- PDIUSBD12 控制命令constant D12_COMMAND_ENABLE_ADDRESS: STD_LOGIC_VECTOR(7 downto 0) := X"D0";constant D12_COMMAND_ENABLE_ENDPOINT: STD_LOGIC_VECTOR(7 downto 0) := X"D8";constant D12_COMMAND_SET_MODE: STD_LOGIC_VECTOR(7 downto 0) := X"F3";constant D12_COMMAND_SET_DMA: STD_LOGIC_VECTOR(7 downto 0) := X"FB";constant D12_COMMAND_READ_IR: STD_LOGIC_VECTOR(7 downto 0) := X"F4";constant D12_COMMAND_SEL_EP0_OUT: STD_LOGIC_VECTOR(7 downto 0) := X"00";constant D12_COMMAND_SEL_EP0_IN: STD_LOGIC_VECTOR(7 downto 0) := X"01";constant D12_COMMAND_SEL_EP1_OUT: STD_LOGIC_VECTOR(7 downto 0) := X"02";constant D12_COMMAND_SEL_EP1_IN: STD_LOGIC_VECTOR(7 downto 0) := X"03";constant D12_COMMAND_SEL_EP2_OUT: STD_LOGIC_VECTOR(7 downto 0) := X"04";constant D12_COMMAND_SEL_EP2_IN: STD_LOGIC_VECTOR(7 downto 0) := X"05";constant D12_COMMAND_READ_LTS_EP0_OUT: STD_LOGIC_VECTOR(7 downto 0) := X"40";constant D12_COMMAND_READ_LTS_EP0_IN: STD_LOGIC_VECTOR(7 downto 0) := X"41";constant D12_COMMAND_READ_LTS_EP1_OUT: STD_LOGIC_VECTOR(7 downto 0) := X"42";constant D12_COMMAND_READ_LTS_EP1_IN: STD_LOGIC_VECTOR(7 downto 0) := X"43";constant D12_COMMAND_READ_LTS_EP2_OUT: STD_LOGIC_VECTOR(7 downto 0) := X"44";constant D12_COMMAND_READ_LTS_EP2_IN: STD_LOGIC_VECTOR(7 downto 0) := X"45";constant D12_COMMAND_RW_BUFFER: STD_LOGIC_VECTOR(7 downto 0) := X"F0";constant D12_COMMAND_ACK_SETUP: STD_LOGIC_VECTOR(7 downto 0) := X"F1";constant D12_COMMAND_CLEAR_EP_BUFFER: STD_LOGIC_VECTOR(7 downto 0) := X"F2";constant D12_COMMAND_ENABLE_BUFFER: STD_LOGIC_VECTOR(7 downto 0) := X"FA";

鉴于篇幅以及其他原因,以上仅仅介绍 USB 包和 PDIUSBD12 包的部分内容作为参考。

 

6.3 分频器模块的实现

分频器模块实现的基本原理就是设计一个工作在系统时钟下的计数器,循环地递减或者递加计数,在某个计数的固定值将输出翻转,即可实现时钟分频的功能。

例如,实验板上的系统时钟是 50MHz,而所需的读写周期间隔要求大于 500ns,即读写的时钟频率不能高于 2MHz,需要将原系统时钟进行至少 25 倍分频。所以,我们设定一个计数器,工作在系统时钟下,每个系统时钟周期计数减一,减到零后恢复到 13,这样,每经过 13×2=26个系统时钟周期,计数器的输出会是一个完整的周期。

分频器模块的示意图如图 35 所示。

图 35 分频器模块的示意图

实现分频器模块的代码如下:

-- 申明所使用的包library IEEE;use IEEE.STD_LOGIC_1164.all;use WORK.USB_PACKAGE.all;-- 申明实体entity FrequencyDivider is generic( div_factor : INTEGER8 := 0 -- 分频系数属性 ); port( reset_n : in STD_LOGIC; -- 复位端口 clk_origin : in STD_LOGIC; -- 输入时钟端口 clk : out STD_LOGIC -- 输出时钟端口 );end FrequencyDivider;architecture FrequencyDivider of FrequencyDivider is-- 内部信号,在内部随时改变同时又输出给输出时钟端口signal clk_tmp: STD_LOGIC;begin -- 信号连接 clk <= clk_tmp; -- 主过程 main_process: process( reset_n, clk_origin ) variable count: INTEGER8; begin if reset_n = '0' then count := 0; clk_tmp <= '0'; elsif rising_edge(clk_origin) then -- 计数到达分频系数时翻转输出,并且重置计数 if count = div_factor then clk_tmp <= not clk_tmp; count := 0; else count := count+1; end if; end if; end process;end FrequencyDivider;

6.4 沿控制模块的实现

沿控制模块的功能是提供可控的下降沿输出,实现的方案如下:用一个使能信号 CE_N 控制输出。输入为分频后的时钟,当 CE_N 输入为高的时候,输出保持高电平,而当 CE_N 输入变为低的时候,将时钟接到输出上,这样就能得到连续的下降沿信号(和时钟的下降沿同步)。只要对 CE_N 进行适当的控制,就能得到需要的下降沿。

沿控制模块的示意图和时序图如图 36 所示。输入时钟连接到分频器模块的输出时钟上,使能信号控制沿输出信号,只要在某一个时钟周期内将使能信号保持低电平,就可以得到一个下降沿输出。

图 36 沿控制模块的示意图和时序图

沿控制模块的实现代码如下:

--申明所使用的包library IEEE;use IEEE.STD_LOGIC_1164.all;-- 申明实体entity EdgeController is port( clk : in STD_LOGIC; -- 输入时钟端口 ce_n : in STD_LOGIC; -- 使能端口 edge : out STD_LOGIC -- 沿信号输出端口 );end EdgeController;architecture EdgeController of EdgeController isbegin -- 输出信号赋值 edge <= clk when ce_n = '0' else '1';end EdgeController;6.5 输入/输出切换模块的实现

由于 PDIUSBD12 的 8 位数据线是双向总线,所以当进行读写操作的时候,应该注意避免双驱动。双驱动的意思就是在总线两边同时往总线上加输出信号,这样总线数据就处于一种不定态(用 X 表示),并且还容易损坏器件。例如,没有处理好双驱动的仿真波形就会如图 37 所示,这种情况下无法得到正确的数据的。

图 37 仿真不定态时序图

信号的 4 种基本状态是高电平(1)、低电平(0)、不定态(X)和高阻态(Z),当一个总线上同时加有两个信号时,组合起来的结果如表 35 所示。

表 35 信号状态表

可见,当一个总线上同时有两个驱动的时候,很有可能产生不定态 X,但是如果其中一个信号为高阻态 Z 的话,则是一个确定的状态(即另一个信号的状态)。所以,避免双驱动的基本思想就是根据目前的读写状态关闭某一个驱动源,也就是说将其另一个驱动源输出设置为高阻态。由于读写操作是由各自的控制信号(WR_N、RD_N)控制的,所以可以将这两个信号作为互斥关系的信号来控制总线数据的信号源。例如,当 RD_N 为低时,要从 PDIUSBD12 读取数据,就应该关闭 FPGA 对总线的输出,即将 FPGA 的总线输出信号变为高阻态 Z。反过来也一样,当 WR_N 为低时,要向 PDIUSBD12 发送数据,此时 PDIUSBD12 也会自动关闭它在总线上的输出。以上思想可用公式表示为:

输入/输出切换模块的示意图如图 6-38 所示。其中左边的总线表示连接到 PDIUSBD12 的总线,右边的输入、输出总线是在 FPGA 内部的总线信号,表示在 FPGA 内部将总线的输入和输出区分开来;RD_N 和 WR_N 信号分别用于读、写控制。

图 38 输入/输出切换模块的示意图

输入/输出切换模块的实现代码如下:

--申明所使用的包library IEEE;use IEEE.STD_LOGIC_1164.all;-- 申明实体entity IOSwitch is port( data : inout STD_LOGIC_VECTOR(7 downto 0); -- 8 位双向数据总线,和 PDIUSBD12 相连 din : in STD_LOGIC_VECTOR(7 downto 0); -- 8 位输入数据总线,仅用于输入 dout : out STD_LOGIC_VECTOR(7 downto 0); -- 8 位输出数据总线,仅用于输出 sel_in_n : in STD_LOGIC; -- 总线输入控制信号 sel_out_n : in STD_LOGIC -- 总线输出控制信号 );end IOSwitch;architecture IOSwitch of IOSwitch is-- 创建一个内部信号,用作数据传递signal data_tmp : STD_LOGIC_VECTOR(7 downto 0);begin -- 信号连接 data <= data_tmp; dout <= data; -- 主进程 process(sel_in_n, sel_out_n, data, din) begin -- 当输出控制信号有效时,将 data_tmp 赋值高阻 if sel_out_n = '0' then data_tmp <= "ZZZZZZZZ"; -- 当输入控制信号有效时,将输入的信号赋值给 data_tmp elsif sel_in_n = '0' then data_tmp <= din; else data_tmp <= "ZZZZZZZZ"; end if; end process;end IOSwitch;

6.6 请求处理模块的实现

请求处理模块的功能是根据主机的请求控制设备收发器模块的处理状态。在本例中,请求处理模块实际的功能就是根据目前接收到的主机请求控制设备收发器模块发送数据,所以请求处理模块的实现就是一个简单的状态机。

请求处理模块的示意图如图 39 所示。时钟信号是由分频器的输出时钟提供;请求类型输入是一个 8 位端口,它和接收事件输入协同工作,当设备收发器接收到一个请求时,就会将请求代码发送到请求类型输入端口,在接收事件输入端口输出一个时钟周期的低电平,表示一次新的请求处理;命令输出端口和命令中断端口则用于控制设备收发器模块的操作状态。

图 39 请求处理模块的示意图

请求处理模块的实现代码如下:

-- 申明要使用的库library IEEE;use IEEE.STD_LOGIC_1164.all;use WORK.USB_PACKAGE.all;use WORK.PDIUSBD12_PACKAGE.all;-- 申明实体entity RequestHandler is port( reset_n : in STD_LOGIC; -- 复位端口 clk : in STD_LOGIC; -- 输入时钟 recv_n : in STD_LOGIC; -- 接收事件输入端口 req_type : in STD_LOGIC_VECTOR(7 downto 0); -- 请求类型输入端口 cmd : out STD_LOGIC_VECTOR(7 downto 0); -- 命令输出端口 exec_n : out STD_LOGIC -- 命令中断端口 );end RequestHandler;architecture RequestHandler of RequestHandler is-- 状态机,已在 USB 包中有定义signal rh_state: REQUEST_HANDLER_STATE := RH_IDLE;-- 寄存器,用于标示是否已分配地址signal address_set: STD_LOGIC := '0';begin -- 主进程 main_process: process( reset_n, clk ) begin if reset_n = '0' then -- reset output signals cmd <= X"00"; exec_n <= '1'; address_set <= '0'; -- reset state machine rh_state <= RH_IDLE;        elsif falling_edge(clk) then            case rh_state is            when RH_IDLE => -- recv_n 为低时候表示需要进行请求处理 if recv_n = '0' then -- req_type 就是请求的代码 case req_type is -- 获取描述符请求 when REQUEST_GET_DESCRIPTOR => if address_set = '0' then cmd <= RH_SEND_DESCRIPTOR_1ST; else cmd <= RH_SEND_DESCRIPTOR; end if; exec_n <= '0';                    -- 获取状态请求                    when REQUEST_GET_STATUS => cmd <= RH_SEND_STATUS; exec_n <= '0';                    -- 设置地址状态                    when REQUEST_SET_ADDRESS => address_set <= '1'; cmd <= RH_SET_ADDRESS; exec_n <= '0';                    -- 启用特性请求                    when REQUEST_SET_FEATURE => cmd <= RH_SET_FEATURE; exec_n <= '0';                    -- 清除特性请求                    when REQUEST_CLEAR_FEATURE => cmd <= RH_CLEAR_FEATURE; exec_n <= '0';                    -- 设置配置请求和设置描述符请求                    when                        REQUEST_SET_CONFIGURATION | REQUEST_SET_DESCRIPTOR => cmd <= RH_SET_CONFIGURATION; exec_n <= '0';                    -- 获取配置请求                    when REQUEST_GET_CONFIGURATION => cmd <= RH_SEND_CONFIGURATION; exec_n <= '0';                    -- 设置接口请求                    when REQUEST_SET_INTERFACE => cmd <= RH_SET_INTERFACE; exec_n <= '0';                    -- 获取密码请求                    when REQUEST_GET_PASSWORD => cmd <= RH_SEND_PASSWORD; exec_n <= '0';                    -- 获取密码高位请求                    when REQUEST_SET_PASSWORD_HIGH => cmd <= RH_SET_PASSWORD_HIGH; exec_n <= '0';                    -- 获取密码低位请求                    when REQUEST_SET_PASSWORD_LOW => cmd <= RH_SET_PASSWORD_LOW; exec_n <= '0';                    when others => NULL; end case; else exec_n <= '1'; cmd <= RH_INVALID_COMMAND;                end if;            when others => NULL; end case; end if; end process;end RequestHandler;

6.7 设备收发器模块的实现

设备收发器模块是整个固件系统的核心,实现的基本思想是创建一个状态机,将各个处理操作都作为一个状态处理,在每个状态中按照 PDIUSBD12 的时序要求对其进行数据访问和控制。

设备收发器模块的示意图如图 40 所示。

图 40 设备收发器模块的示意图

由于 USB 协议很复杂并且 PDIUSBD12 的控制也比较复杂,所以设备收发器状态机的状态量会较多。根据设备收发器的功能,可以将状态机各个状态的功能分为 3 类。

• 初始化器件:初始化器件就是对 PDIUSBD12 器件进行配置的状态,需要配置的内容包括设置地址/使能、设置 DMA 以及设置模式等。

• 数据访问:数据访问即实现 PDIUSBD12 和 FPGA 之间的数据读写,包括读取中断寄存器、读取前次传输状态、由端点读取数据、由端点发送数据等。

• 请求回复:请求回复是指根据各种类型请求的数据格式提取所需要的数据,并且在解析完成后通知请求处理模块。下面详细介绍一下以上 3 种状态的实现。

1)初始化器件

初始化器件相关的状态主要是 TS_DISCONNECTED 和 TS_CONNECTING(状态的定义见USB_Package.vhd 文件),其中 TS_DISCONNECTED 是系统复位后的状态,TS_CONNECTING 是配置PDIUSBD12 寄存器的状态。需要注意的是 PDIUSBD12 器件在复位后应该等待至少 3 ms 后再访问其寄存器,这样可让晶振稳定下来。

由于对寄存器配置的命令以及时序都是确定的,所以可以在自定义包中将配置数据定义为常数,例如:

constant D12_CONNECT_DATA: REG8x8:=( D12_COMMAND_SET_DMA, D12_DMA, D12_COMMAND_SET_MODE, D12_MODE_CONFIG, D12_MODE_CLOCK_DIV, others => X"00"                                    );                                    constant D12_CONNECT_DATA_TYPE: REG8x1:=( D12_COMMAND, D12_DATA, D12_COMMAND, D12_DATA, D12_DATA, others => '0'                                         );constant D12_CONNECT_DATA_LENGTH: INTEGER8 := 5;

上面定义的就是 PDIUSBD12 的配置参数,第一个常数数组是配置命令和数据,第二个数组表示命令、数据的顺序,最后一个参数是配置参数的总长度。定义的过程是首先向 PDIUSBD12发送命令 D12_COMMAND_SET_DMA(设置 DMA 命令),然后发送此命令的数据 D12_DMA(D12_DMA定义为 0xC0,其意义请参考图 23);之后发送设置模式命令和此命令的两个数据。D12_COMMAND_SET_DMA、D12_DMA、D12_COMMAND、D12_DATA 等都是已定义的常数,例如:

constant D12_COMMAND: STD_LOGIC := '1';constant D12_DATA: STD_LOGIC := '0';--constant D12_COMMAND_SET_DMA: STD_LOGIC_VECTOR(7 downto 0) := X"FB";constant D12_DMA:STD_LOGIC_VECTOR(7 downto 0) := X"C0";

详细的常数定义请参考 PDIUSBD12 包的定义文件。这样定义虽然显得复杂,但是便于将数据与格式分离,也便于代码阅读。此外,在调用配置数据时也较为方便,只需要使用一个循环索引变量,依次读取 D12_CONNECT_DATA 数组和D12_CONNECT_DATA 数组的数值,发送给 PDIUSBD12 即可,代码如下:

-- TS_CONNECT 状态,对 PDIUSBD12 进行配置when TS_CONNECTING => -- handle_step 作为循环变量 if handle_step = D12_CONNECT_DATA_LENGTH then ts_state <= TS_IDLE; else data_out <= D12ConnectData(handle_step); a0 <= D12ConnectDataType(handle_step); wr_n_var := '0'; -- wr_n_var 置为低表示向 PDIUSBD12 输出 end if; handle_step := handle_step+1;

以上代码运行的结果就是经过 5 个时钟周期,FPGA 完成向 PDIUSBD12 输出的一系列命令以及数据,通过编写测试平台仿真可以看到运行的结果(测试平台的编写将会在下面专门介绍),如图 41 所示。

图 41 器件配置仿真时序图

通过上面的时序图可以看出,8 位总线上传输的是 D12_CONNECT_DATA 定义的配置命令和数据,而 a0 位表明了总线上的是命令还是数据,通过一个下降沿的写信号可以将命令或者数据发送给 PDIUSBD12。

2)数据访问状态

数据访问状态的功能简单地说就是中断监测和数据收发。每次系统复位后 FPGA 会自动配置 PDIUSBD12 器件,配置完成之后设备收发器模块会处于空闲状态(TS_IDLE)。PDIUSBD12 器件在接收到数据包时会通过中断来通知设备收发器,此外,请求处理模块也会通过命令中断信号控制设备收发器模块。所以,中断监测就是在每个时钟周期读取一次 PDIUSBD12 的中断信号和请求处理模块的命令中断信号,如果发现其中的一个中断信号为低,则转为其他状态。

中断监测的代码如下:

-- 空闲状态,监测中断信号when TS_IDLE => data_out <= X"00"; recv_n <= '1'; ih_state <= IH_START; -- 判断 PDIUSBD12 的中断信号 if int_n = '0' then handle_step := 0; ts_state <= TS_READ_IR; -- 判断请求处理模块的命令中断信号 elsif exec_n = '0' then ts_state <= GetCommandHandler(cmd); handle_step := 0; end if;

当监测到 PDIUSBD12 的中断时,设备收发器首先读取中断寄存器,然后就会进入数据收发状态,如果监测到的是请求处理模块的命令中断,则进入的是请求回复状态。请求回复状态包括了发送描述符、发送配置信息等,这些内容将在下面一个小节介绍。数据收发状态包括读取中断寄存器、控制端点数据收发等。读取中断寄存器的流程图如图42 所示。

图 42 中断处理流程图

读取中断寄存器的代码如下:

-- 读取中断寄存器状态when TS_READ_IR => -- 第一步,发送读取中断寄存器命令 if handle_step = 0 then a0 <= D12_COMMAND; data_out <= D12_COMMAND_READ_IR; wr_n_var := '0'; -- 第二步,设置读信号为低,读取第一个返回参数,即中断寄存器第一个字节 elsif handle_step = 1 then a0 <= D12_DATA; rd_n_var := '0'; -- 第三步,保存中断寄存器第一个字节并读取第二个返回参数(中断寄存器第二个字节) elsif handle_step = 2 then -- 保存中断寄存器第一个字节 ir_0 := data_in; -- 读取第二个参数 a0 <= D12_DATA; rd_n_var := '0'; -- 最后,保存第二个参数,进入下一处理状态 else -- 保存中断寄存器第二个字节 ir_1 := data_in(0); -- 根据中断寄存器选择进入下一处理状态 ts_state <= GetInterruptHandler(ir_0, ir_1); ih_state <= IH_START; end if; handle_step := handle_step+1;

下面介绍一下控制输出的处理流程。控制输出的输出是相对主机来说的,所以相对于设备来说,就是接收主机的数据。当一次控制输出发生时,设备首先会判断接收到的是不是建立包(Setup Packet),如果是则开始接收下面的数据,否则,接收前次传输所剩余的数据。控制传输的处理流程图如图 43 所示。

图 43 控制输出流程图

从上面的流程图可以看出,设备收发器首先要选择控制输出端点,提取建立包的内容,再进行端点是为满还是空的判断。如果控制端点不为空,设备收发器将从缓冲区读出内容并将其保存。之后,它将判断设备请求的有效性,如果是一个有效的请求,设备收发器必须向控制输出端点发送应答建立命令以重新使能下一个建立阶段。

接下来,设备收发器需要证实控制传输是控制读还是写。这可以通过读建立包中bmRequestType 的第 8 位来判断。如果控制传输是一个控制读类型,那就是说器件需要在下一个数据阶段向主机发回数据包。设备收发器会设置一个标志以指示设备现在正处于传输模式,即准备在主机发送请求时进入传输状态(TS_EP0_TRANSMIT)向主机发送数据。

处理流程的各个步骤在设备收发器模块中被划分在两个状态中实现,其中选择端点和读取、保存数据的操作在 TS_READ_ENDPOINT 状态中实现,其他的内容在 TS_EP0_RECEIVE 状态中实现。下面是从端点(PDIUSBD12 的缓冲)数据读取的实现代码,即 TS_READ_ENDPOINT 状态的代码,由于篇幅原因,这里只提供部分参考代码。

-- 读取端点数据状态when TS_READ_ENDPOINT => -- handle_step 表示操作步骤 case handle_step is -- 首先,发送选择端点命令,选择端点 when 0 => a0 <= D12_COMMAND; data_out <= active_ep;        wr_n_var := '0';        handle_step := handle_step+1;    -- 发送读取端点数据的命令,准备接收数据    when 1 => a0 <= D12_COMMAND; data_out <= D12_COMMAND_RW_BUFFER;        wr_n_var := '0';        handle_step := handle_step+1;    -- 读取缓冲数据的前两个字节,第一个字节为保留数据,第二个字节表示数据长度    when 2 | 3 => a0 <= D12_DATA;        rd_n_var := '0';        handle_step := handle_step+1;    -- 保存第二个字节(数据长度),准备接收有效数据    when 4 => -- 保留第二个字节 read_in := conv_integer(data_in); -- 判断数据长度是否为零 if read_in = 0 then handle_step := 7; else -- 获取剩余的数据 handle_step := handle_step+1; a0 <= D12_DATA;            rd_n_var := '0';        end if;    -- 依次读取数据并且保存数据    when 5 => -- 保存前一个周期要求获取的数据 ts_data(ram_address) <= data_in; ram_address := ram_address+1; read_count := read_count+1; -- 判断全部数据是否已经获取 if read_count = read_in then handle_step := 6; else -- 继续要求获取下一个数据 a0 <= D12_DATA;            rd_n_var := '0';        end if;    -- 最后,发送清除端点缓冲的命令    when 6 => a0 <= D12_COMMAND; data_out <= D12_COMMAND_CLEAR_EP_BUFFER;        wr_n_var := '0';        handle_step := 7;    -- 恢复到原始处理状态    when others => handle_step := 0; ts_state <= last_ts_state; end case;

下面介绍一下控制输入的处理过程。控制输入就是设备向主机发送数据,最为典型的就是设备向主机发送描述符,图 44 所示是控制输入的流程图。

图 44 控制输入流程图

从控制输入的流程图可以看出,设备收发器首先需要通过读 PDIUSBD12 的最后处理状态寄存器清零中断标志位。接着设备收发器在确认 PDIUSBD12 处于传输模式后进行数据包的发送。PDIUSBD12 的控制端点只有 16 字节 FIFO,如果传输的长度大于 16 字节,设备收发器在传输阶段就必须控制数据的数量。设备收发器必须检查要发送到主机的当前和剩余的数据大小,如果剩下的字节数大于 16,设备收发器将先发送 16 字节并继续等待下一次发送。

当下一个数据发送中断来到时,设备收发器将确定剩余的字节是否为零。如果已经没有数据要发送,设备收发器需要发送一个空的包以指示主机数据已经发送完毕。

控制输入是在 TS_EP0_TRANSMIT 和 TS_WRITE_ENDPOINT 两个状态中实现的。其中,TS_EP0_TRANSMIT 实 现 的 是 控 制 输 入 流 程 控 制 , 而 TS_WRITE_ENDPOINT 的 实 现 和TS_READ_ENDPOINT 很类似,只不过是将读取数据换为发送数据。TS_WRITE_ENDPOINT 状态的实现代码如下,由于篇幅原因,这里只提供部分参考代码。

-- 写端点缓存数据的状态when TS_WRITE_ENDPOINT => case handle_step is -- 首先,发送选择端点的命令,选择端点 0 when 0 => a0 <= D12_COMMAND; data_out <= active_ep;        wr_n_var := '0';        handle_step := handle_step+1;    -- 读取选择端点命令的一个返回参数(可选)    when 1 => a0 <= D12_DATA;        rd_n_var := '0';        handle_step := handle_step+1;    -- 发送读写端点的命令    when 2 => a0 <= D12_COMMAND; data_out <= D12_COMMAND_RW_BUFFER;        wr_n_var := '0';        handle_step := handle_step+1;    -- 写入端点缓存第一个字节,为保留字节,值为 0    when 3 => a0 <= D12_DATA; data_out <= X"00";        wr_n_var := '0';        handle_step := handle_step+1;    -- 写入端点缓存第二个字节,为有效数据的长度    when 4 => a0 <= D12_DATA; data_out <= conv_std_logic_vector(to_write, 8);        wr_n_var := '0';        write_count := 0;        handle_step := handle_step+1;    -- 顺序写入有效数据    when 5 => if to_write = 0 then -- send comnand: enable buffer a0 <= D12_COMMAND; data_out <= D12_COMMAND_ENABLE_BUFFER;            wr_n_var := '0';            handle_step := 7;        else            handle_step := handle_step+1;        end if;    -- 发送缓冲区有效命令,允许 PDIUSBD12 发送数据    when 6 => -- 判断是否所有数据已经被写入 if write_count = to_write then --发送缓冲区有效命令 a0 <= D12_COMMAND; data_out <= D12_COMMAND_ENABLE_BUFFER; wr_n_var := '0'; handle_step := 7; else -- 写入数据 a0 <= D12_DATA; data_out <= ts_data(ram_address);            ram_address := ram_address+1;            wr_n_var := '0';            write_count := write_count+1;        end if;    -- 恢复到原始处理状态    when 7 => handle_step := 0; ts_state <= last_ts_state;        when others => NULL; end case;

以上便是数据访问状态的实现方法,在测试平台中可以对以上代码进行测试,测试时的输入数据应该由测试平台产生(测试平台的编写将在下面的章节进行专门介绍)。如第一次发送设备描述符的仿真波形。此仿真过程可以分为两个部分,第一部分(如图 45 所示)是接收建立包(Setup Packet)以及读取 PDIUSBD12 请求数据的过程;第二部分(如图 46 所示)是将设备描述符数据写入 PDIUSBD12 端点缓存并且使缓冲区有效。

图 45 发送设备描述符仿真波形 1

图 46 发送设备描述符仿真波形 2

3)请求回复状态

请求回复状态的功能就是对各个请求作出响应。USB 的标准请求已经在前面做了介绍,下面就以获取描述符请求为例介绍一下请求响应的实现方法,其他的标准请求以及厂商请求(获取、设置密码)相对来说比较简单,实现的方法请读者参考源代码。

获取描述符请求是最为重要的请求,因为这在设备枚举过程中是必需的,它是主机了解设备的第一个步。获取描述符请求的处理流程如图 47 所示。

图 47 获取描述符处理流程

获取设备描述符请求响应的实现代码如下:

 

 

-- 获取描述符请求响应状态when TS_SEND_DESCRIPTOR => handle_step := 0; active_ep := X"01"; -- 判断是否是设备请求 if ts_data(ADDRESS_DESCRIPTOR_TYPE) = TYPE_DEVICE_DESCRIPTOR then -- LED 输出,提示作用 led(0) <= '0';            -- 检查数据长度是否符合要求            if data_length > LENGTH_DEVICE_DESCRIPTOR then data_length := LENGTH_DEVICE_DESCRIPTOR; end if; -- 判断描述符长度是否超过端点 0 的缓存大小 if data_length > LENGTH_ENDPOINT0_BUFFER then to_write := LENGTH_ENDPOINT0_BUFFER; is_transmit := '1'; else to_write := data_length; end if; -- 设置传输状态标志位,设置传输数据源(描述符)以及数据长度 data_count := to_write; ram_address := ADDRESS_DEVICE_DESCRIPTOR; -- 准备转入进入控制输入状态(TS_WRITE_ENDPOINT),发送数据 ts_state <= TS_WRITE_ENDPOINT;        elsif ts_data(ADDRESS_DESCRIPTOR_TYPE) = TYPE_CONFIGURATION_DESCRIPTOR then            -- 检查数据长度,LED 输出,提示作用            if data_length > LENGTH_CONFIGURATION_DESCRIPTOR then data_length := LENGTH_CONFIGURATION_DESCRIPTOR; led(2) <= '0'; else led(1) <= '0';            end if;            -- 判断描述符长度是否超过端点 0 的缓存大小            if data_length > LENGTH_ENDPOINT0_BUFFER then to_write := LENGTH_ENDPOINT0_BUFFER; is_transmit := '1'; else to_write := data_length; end if; -- 设置传输状态标志位,设置传输数据源(描述符)以及数据长度 data_count := to_write; ram_address := ADDRESS_CONFIGURATION_DESCRIPTOR; -- 设置传输状态标志位,设置传输数据源(描述符)以及数据长度 ts_state <= TS_WRITE_ENDPOINT; else ts_state <= TS_IDLE; end if; last_ts_state := TS_END_REQUESTHANDLER;

6.8 测试平台的编写

上面介绍的是整个 FPGA 固件系统的实现方法,为了验证设计的正确性,还需要编写一个测试平台对整个系统进行仿真。由于实际情况下 FPGA 是和 PDIUSBD12 进行通信,所以在测试平台中需要虚拟一个 PDIUSBD12,来实现仿真的目的。

首先,在测试平台中需要产生一个虚拟的时钟信号,产生的方法就是使用 wait for 语句等待固定时间后将信号值翻转。时钟信号的实现代码如下:

-- 时钟信号生成代码clk_gen: processbegin -- 翻转 clk <= not clk; -- 等待固定时间 wa