今天给大侠带来基于 FPGA 的 USB 接口控制器设计(VHDL),由于篇幅较长,分三篇。今天带来第三篇,下篇,FPGA 固件开发、USB驱动和软件开发。话不多说,上货。
这里也给出前两篇的超链接:
基于FPGA的USB接口控制器设计(VHDL)(上)
基于FPGA的USB接口控制器设计(VHDL)(中)
之前有关于 Veriliog HDL 实现的 USB 接口控制器设计,这里放上超链接,仅供各位大侠参考。
源码系列:基于 FPGA 的 USB2.0 设计
导读
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 所示,各个模块的功能如下。
(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_STATE
is ( 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 所示。
实现分频器模块的代码如下:
-- 申明所使用的包
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 所示。输入时钟连接到分频器模块的输出时钟上,使能信号控制沿输出信号,只要在某一个时钟周期内将使能信号保持低电平,就可以得到一个下降沿输出。
沿控制模块的实现代码如下:
--申明所使用的包
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 is
begin
-- 输出信号赋值
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 所示。
由于 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 所示是控制输入的流程图。
从控制输入的流程图可以看出,设备收发器首先需要通过读 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: process
begin
-- 翻转
clk <= not clk;
-- 等待固定时间
wait for 50 ns;
end process;
其次,由于 FPGA 和 PDIUSBD12 之间有数据读写,所以要模拟所有 FPGA 向 PDIUSBD12 读取的数据。模拟数据读写的方法是将所有数据按照顺序写入一个大的测试数据数组中,使用一个变量作为该数组索引,再编写一个对读信号敏感的过程,在每次读信号的下降沿将数据送到总线上,并且将数组索引变量增加 1。测试数据数组以及索引变量的定义方法如下:
-- 测试数据数组定义
signal td : REG256x8 :=
(
-- 第一次获取设备描述符测试数据
X"01", X"00", X"20", X"00", X"08", -- 各寄存器数据以及端点 0 缓存前两个字节
X"80", X"06", X"00", X"01", X"00", X"00", X"40", X"00", -- 获取设备描述符请求
X"00",
-- 设置地址请求测试数据
X"01", X"00", X"20", X"00", X"08", -- 各寄存器数据以及端点 0 缓存前两个字节
X"00", X"05", X"02", X"00", X"00", X"00", X"00", X"00", -- 设置地址请求
X"00",
-- 获取完整设备描述符测试数据
X"01", X"00", X"20", X"00", X"08", -- 各寄存器数据以及端点 0 缓存前两个字节
X"80", X"06", X"00", X"01", X"00", X"00", X"12", X"00", -- 获取配置描述符请求
X"00",
X"02", X"00", X"00", X"00", -- 各寄存器数据
-- 获取配置描述符请求测试数据
X"01", X"00", X"20", X"00", X"08", -- 各寄存器数据以及端点 0 缓存前两个字节
X"80", X"06", X"00", X"02", X"00", X"00", X"09", X"00", --获取配置描述符请求
X"00",
--获取所有配置描述符请求测试数据
X"01", X"00", X"20", X"00", X"08", -- 各寄存器数据以及端点 0 缓存前两个字节
X"80", X"06", X"00", X"02", X"00", X"00", X"FF", X"00", -- 获取配置描述符请求
X"00",
X"02", X"00", X"00", X"00", -- 各寄存器数据
X"02", X"00", X"00", X"00", -- 各寄存器数据
-- 设置配置请求测试数据
X"01", X"00", X"20", X"00", X"08", -- 各寄存器数据以及端点 0 缓存前两个字节
X"00", X"09", X"01", X"00", X"00", X"00", X"00", X"00", -- 设置配置请求
X"00",
others => X"00"
);
-- 数组索引
signal td_index : INTEGER8 := 255;
再次,需要处理好总线双驱动的问题。前面介绍的输入/输出选择模块的功能就是在必要的时候关闭总线输出来避免双驱动的发生,同样道理,在测试平台中也应该做到这一点,即当测试平台向 FPGA 固件系统读取数据时,应该关闭测试平台的总线输出,即将其设置为高阻。实现代码如下:
process(d12_wr, td_index)
begin
-- 当 FPGA 向 PDIUSBD12 些数据时,总线输出变为高阻
if d12_wr = '0' then
data <= "ZZZZZZZZ";
else
data <= td(td_index);
end if;
end process;
最后,还需要编写一个主流程,在主流程中需要进行系统复位和产生中断信号,代码如下:
-- main process
main: process
variable i : INTEGER8;
begin
-- 复位
reset_n <= '0';
wait for 100 ns;
reset_n <= '1';
wait for 100 us;
-- 循环模拟产生 PDIUSBD12 中断
for i in 0 to 10 loop
int_n_in <= '0';
wait for 3200 ns;
int_n_in <= '1';
wait for 300 us;
end loop;
wait;
end process;
七、USB 驱动和软件开发
7.1 USB 驱动编写
以上介绍的是 FPGA 固件的开发过程,由于本例中设计的不是一个类设备,所以要使设备正常工作,还需要编写专门的驱动程序和软件。由于驱动和软件不是本篇的重点,故下面只简要介绍其编写方法。
1)USB 驱动模型
USB 体系的主机软件可分为两层,即 USB 系统软件和客户端驱动程序,如图 48 所示。
USB 系统软件根据功能可以分为 USBD 和 HCD 上下两部分,其中 HCD 为上层提供了主机控制器的抽象以及数据在总线上的传输抽象。USBD 为上层的客户端驱动程序提供了 USB 设备的抽象,并在客户端驱动和所驱动的设备之间提供了数据传输的抽象。
客户端驱动程序从用户的角度来讲相当于传统意义上的驱动程序。不过设备端不同的接口对应不同的驱动程序,如果设备只有一个接口,那么从用户的角度来讲,两者是一样的,客户端驱动程序通过 USB 系统软件提供的接口与设备交互,而不是通过过去的 I/O 地址或者端口进行访问。
2)使用 Driver Studio 开发 USB 驱动
上面介绍的是 USB 软件模型,对于驱动开发人员来说,需要编写的就是客户端驱动程序。编写客户端驱动程序需要安装 DDK,即 Windows Driver Development Kit,通过 DDK 我们就能够访问 USB 系统软件的接口从而实现与设备的交互。但是,如果只使用 DDK 开发驱动程序的话,会比较复杂,所以可以使用一些驱动开发的专用工具,例如 Driver Studio、WinDriver 等。本例选用的是 Driver Studio 2.7 进行开发,下面介绍一下开发的基本步骤。安装完 DDK 以及 Driver Studio 后,运行 Driver Studio 的 Driver Wizard。在第 1 步中输入驱动工程名称和路径,如图 49 所示。单击 Next 按钮进入如图 50 所示对话框。
图 49 Driver Wizard 第 1 步
图 50 Driver Wizard 第 2 步
第 2 步选择工程类型 WDM Driver,单击 Next 按钮进入如图 51 所示对话框。
第 3 步选择驱动类型 WDM Function Driver。单击 Next 按钮进入如图 52 所示对话框。
图 51 Driver Wizard 第 3 步
第 4 步比较重要,是选择驱动总线类型,应该选择 USB(WDM Only),并且注意要在 USB VendorID 和 USB Product ID 中输入和固件中设备描述一致的信息。这里请注意 Vendor ID 一定是0x0471,因为使用的是 Philips 的 PDIUSBD12 芯片,其 Vendor ID 固定为 0x0471。单击 Next按钮,进入如图 53 所示对话框。
第 5 步是端点定义,可以根据需要定义端点的类型(输入输出)、端点号、缓存大小等。
第 6 步到第 9 步是一些开发辅助信息的定义,可以保持为默认值,如图 54~图 57 所示。
图 54 Driver Wizard 第 6 步
图 55 Driver Wizard 第 7 步
图 56 Driver Wizard 第 8 步
图 57 Driver Wizard 第 9 步
第 10 步是设备类的定义,如图 58 所示。定义打开设备的方式,Symbolic Link 表示按照设备名称打开,Interface(WDM Only)表示按照设备的 GUID 打开,这里选择使用设备名称打开。
图 58 Driver Wizard 第 10 步
第 11 步定义的是设备的 IO 控制接口,也就是驱动和应用程序之间的接口,如图 59 所示。单击 Add 按钮可以定义 IO 控制接口,如图 60 所示。
图 59 Driver Wizard 第 11 步
图 60 定义 IO 控制接口
最后,第 12 步进行一些额外的设置,如图 61 所示,可以保持默认值。
图 61 Driver Wizard 第十二步
以上便是使用 Drive Studio 的 Driver Wizard 生成驱动框架的完整过程,现在我们已经有了一个完成了大部分驱动工作的代码框架,只需要增加一些自定义的处理代码即可。
3)使用 Visual C++编译驱动
运行 Visual C++ 6.0 打开 Driver Wizard 生成的工程文件,可看到在***Device 这个类中已经有了很多设备操作的处理函数,例如上电(OnDevicePowerUp)、休眠(OnDeviceSleep)启动(OnDeviceStart)等,可以根据需要修改这些函数,如果没有特殊要求,可以保持默认设置,如图 62 所示。
图 62 设备操作处理函数
另外还需要完成的工作就是对上面定义的 IO 控制接口函数进行处理,其功能就是建立一个厂商请求。由于本次设计的 USB 设备是一个加密设备,它不是类设备,所以会有一些特定的请求(厂商请求)。为了介绍厂商请求的实现方法,本系统用到了两个厂商请求:设置密码和获取密码。由 Driver Wizard 自动生成的驱动一般都已经包括了标准请求的建立,但是不会包括厂商请求的建立。厂商请求是在 IO 控制接口函数中建立的,即 Driver Wizard 第 11 步所定义的两个函数,建立厂商请求的函数主要是 BuildVendorRequest 函数,其格式如下:
PURB BuildVendorRequest(
PUCHAR TransferBuffer,
ULONG TransferBufferLength,
UCHAR RequestTypeReservedBits,
UCHAR Request,
USHORT Value,
BOOLEAN bIn=FALSE,
BOOLEAN bShortOk=FALSE,
PURB Link=NULL
UCHAR Index=0,
USHORT Function=URB_FUNCTION_VENDOR_DEVICE,
PURB pUrb=NULL
);
其中需要开发人员注意的是前 6 个参数,其意义如下:
• PUCHAR TransferBuffe 数据缓冲。如果是数据输入,用于存储接收到的数据;如果是数据输出,则是待发送数据的数据源;如果没有数据传输,此参数可是为空(NULL)。
• ULONG TransferBufferLength 发送或者接收数据的长度。
• UCHAR RequestTypeReservedBit 请求类型的位掩码,一般为零。
• UCHAR Request 请求代码。
• USHORT Value 即 USB 请求中的 wValue 位
• BOOLEAN bIn=FALSE 此参数为 TRUE 表示数据输出,反之则表示数据输入。
其余的参数可以保持默认。下面就从 USBSOFTLOCK_IOCTL_GET_PASSWORD_Handler 处理函数为例介绍一下 BuildVendorRequest 函数的用法,代码如下:
NTSTATUS USBSoftLockDevice::USBSOFTLOCK_IOCTL_GET_PASSWORD_Handler(KIrp I)
{
NTSTATUS status = STATUS_SUCCESS;
// 输出提示信息
t << "Entering USBSoftLockDevice::USBSOFTLOCK_IOCTL_GET_PASSWORD_Handler, "
<< I << EOL;
t << "IOctrlBuffer address is " << (LONG)(I.IoctlBuffer()) << EOL;
t << "BufferedReadDest address is " << (LONG)(I.BufferedReadDest()) << EOL;
t << "BufferedWriteSource address is " << (LONG)(I.BufferedWriteSource()) << EOL;
t << "IoctlOutputBufferSize is " << (LONG)(I.IoctlOutputBufferSize()) << EOL;
// 保存 8 字节密码的缓存
UCHAR buffer[8];
// 创建厂商请求,请求的代码是 REQUEST_GET_PASSWORD,数据长度为 8
PURB pUrb = m_Lower.BuildVendorRequest(
buffer, -- 数据缓冲
PASSWORD_LENGTH, -- 数据长度
0, -- 保留
REQUEST_GET_PASSWORD, -- 请求代码
0, -- 即 USB 请求的 wValue 字段
TRUE -- TRUE 表示数据输入,反之则是数据输出
);
status = m_Lower.SubmitUrb(pUrb, NULL, NULL, OPERATION_TIMEOUT);
// 判断返回值
if (status == STATUS_SUCCESS) {
t << "Received buffer is ";
for (int i=0;i
t << " " << buffer[i];
}
t << EOL;
PUCHAR output_buffer = (PUCHAR)(I.IoctlBuffer());
memcpy(output_buffer, buffer, PASSWORD_LENGTH);
}
else {
}
return status;
}
完成厂商请求的编写之后,就可以进行驱动程序编译了。驱动编译默认有两种版本,即Win32 Checked 和 Win32 Free,其中前者表示调试版本,而后者表示发布版本,发布版本相对调试版本去掉了大部分调试信息,比较简化。
编 译 驱 动 的 方 法 是 在 Visual C++ 中 打 开 Driver Studio 的 工 具 条 CompuwareDriverStudio,如图 63 所示。
选择合适的编译版本,再单击 Compuware DriverStudio 工具条的最后一个按钮即可。请注意不能使用 Visual C++本身的编译按钮进行驱动编译。编译成功,如果是 Win32 Free 版本,则会在工程目录的 sys\objfre\i386 子目录下生成驱动文件 USBSoftLock.sys;如果是 Win32Checked 版本,驱动文件会在工程目录的 sys\objchk\i386 子目录下。成功编译驱动程序之后,将它和 Driver Studio 自动生成的.inf 文件(在工程目录下)放在同一个目录下,在查找驱动的时候指定这个目录就可以了。
7.2 USB 软件编写
最后,再简要介绍一下 USB 软件的编写,即软件对 USB 设备访问的实现方法。
USB 软件通过 USB 驱动实现对 USB 设备的访问,编写 USB 软件必须符合 USB 驱动定义的接口规范。一般来说,使用 Driver Wizard 生成一个驱动工程后,会同时生成一个***ioctl.h的文件,这个文件就是建立软件和驱动之间通信的桥梁,它定义了访问驱动程序的接口,在编写软件的时候需要将其引用进去。
USB 软件的编写一般有下面几个步骤。
1) 打开设备
打开设备主要需要调用 CreateFile 函数,它将设备作为一个文件来处理,代码如下:
BOOL CSoftLock::OpenDevice()
{
if (m_hDevice != INVALID_HANDLE_VALUE)
return TRUE;
const char *sLinkName = "\\\\.\\USBSoftLockDevice0";
m_hDevice = CreateFile(sLinkName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL);
return m_hDevice != INVALID_HANDLE_VALUE;
}
2) 调用设备 IO 接口
调 用 设 备 IO 接 口 使 用 DeviceIoControl 函 数 控 制 设 备 。 这 里 主 要 用 到 两 次DeviceIOControl 函数,即设置密码和获取密码,它们分别对应驱动中已经定义的 IO 控制接口函数。例如,设置密码接口函数的调用方法如下:
BOOL CSoftLock::SetPassword(char* password)
{
// Note that Input and Output are named from the point of view
// of the DEVICE:
// bufInput supplies data to the device
// bufOutput is written by the device to return data to this application
CHAR bufInput[IOCTL_INBUF_SIZE]; // Input to device
CHAR bufOutput[IOCTL_OUTBUF_SIZE]; // Output from device
ULONG nOutput; // Count written to bufOutput
memset(bufInput, 0, BUFFER_LENGTH);
memset(bufOutput, 0, BUFFER_LENGTH);
memcpy(bufInput, password, PASSWORD_LENGTH);
// Call device IO Control interface (USBSOFTLOCK_IOCTL_SET_PASSWORD) in driver
printf("Issuing Ioctl to device - ");
if (!DeviceIoControl( m_hDevice,
USBSOFTLOCK_IOCTL_SET_PASSWORD,
bufInput,
PASSWORD_LENGTH,
bufOutput,
PASSWORD_LENGTH,
&nOutput,
NULL) )
{
printf("ERROR: DeviceIoControl returns %0x.", GetLastError());
return FALSE;
}
else {
printf("input buffer is : %s, output buffer is %s, output buffer size is %d",
bufInput,
bufOutput,
nOutput);
}
return TRUE;
}
3) 关闭设备
和打开设备对应,关闭设备就是调用 CloseHandle 函数关闭设备的句柄就可以了,例如:
void CSoftLock::CloseIfOpen()
{
if (m_hDevice != INVALID_HANDLE_VALUE)
{
// Close the handle to the driver
if (!CloseHandle(m_hDevice))
{
printf("ERROR: CloseHandle returns %0x.\n", GetLastError());
}
m_hDevice = INVALID_HANDLE_VALUE;
}
}
USB软件的详细代码请参考源代码中的cube测试程序,它模拟了一个硬件加密设备的工作过程。cube程序运行后会出现一个立方体,使得立方体转动表示正常的程序运行状态。程序运行需要密码,但是密码不是保存在计算机上,而是保存在USB设备上,并且程序运行时需要及时校验密码,一旦密码校验失败(可能是因为密码不正确或者USB设备被移除),程序都会停止运行。方法是首先选择菜单File—>Open Device打开USB设备(如图64所示),如果打开设备成功,选择File—>Play Cube,在出现的密码输入框内输入密码,如果密码正确,立方体就会开始转动,并且cube程序在不时地和USB设备之间进行密码校验(可以看到PDIUSBD12的GOODLINK灯会不停的闪,这表示有数据传输)。还可以通过选择File—>Set Password设置密码,此密码会通过Set Password请求发送给设备。
总结
本篇首先说明了 USB 系统的体系结构以及 USB 协议相关的内容,之后,详细介绍了一下USB 接口器件 PDIUSBD12 的使用方法,最后,本章通过一个实例描述了使用 FPGA 接口 PDIUSBD12开发 USB 接口的流程。本篇的学习要点可以总结如下:
首先,对 USB 协议的了解是最为重要的。虽然 PDIUSBD12 芯片能够完成很多协议解析工作,但对 USB 协议的了解程度还是对整个开发过程起到了决定性的作用。USB 协议非常的复杂,熟悉 USB 协议的方法应该是由大到小,即首先了解 USB 通信的基本原理,比如控制传输、批量传输的原理和特点;然后再了解各个传输的组成,即每个传输首先发送的是什么数据包,然后接受的是什么数据包;最后再去分析每个数据包的格式、意义等。
其次,需要对 PDIUSBD12 芯片的比较了解,比如它的各个信号引脚的功能、特性,更为重要的是其通信时序和控制命令。
最后,对各种语言以及各种开发工具熟悉也是非常重要的。在本次设计中,需要用到的开发语言很多,包括 VHDL、C++(Visual C++);此外,本次设计还用到了多种开发工具,包括EDA 开发、驱动开发、软件开发等,只有熟悉这些工具才能够快速的进行开发。USB 体系非常庞大,所以编写本章也是为了够帮助读者跨入 USB 开发的大门,希望读者通过本篇的学习,能够设计出更为完善、高效的 USB 接口。
本篇到此结束,各位大侠,有缘再见!
往期精选
FPGA技术江湖广发江湖帖
无广告纯净模式,给技术交流一片净土,从初学小白到行业精英业界大佬等,从军工领域到民用企业等,从通信、图像处理到人工智能等各个方向应有尽有,QQ微信双选,FPGA技术江湖打造最纯净最专业的技术交流学习平台。
FPGA技术江湖微信交流群
加群主微信,备注姓名+学校/公司+专业/岗位进群
FPGA技术江湖QQ交流群
备注姓名+学校/公司+专业/岗位进群