大俠好,歡迎來到FPGA技術(shù)江湖,江湖偌大,相見即是緣分。大俠可以關(guān)注FPGA技術(shù)江湖,在“闖蕩江湖”、"行俠仗義"欄里獲取其他感興趣的資源,或者一起煮酒言歡。
今天給大俠帶來基于 FPGA 的 USB 接口控制器設(shè)計(VHDL),由于篇幅較長,分三篇。今天帶來第三篇,下篇,F(xiàn)PGA 固件開發(fā)、USB驅(qū)動和軟件開發(fā)。話不多說,上貨。
2019年9月4日,USB-IF終于正式公布USB 4規(guī)范。它引入了Intel此前捐獻(xiàn)給USB推廣組織的Thunderbolt雷電協(xié)議規(guī)范,雙鏈路運(yùn)行(Two-lane),傳輸帶寬因此提升,與雷電3持平,都是40Gbps。需要注意的是,你想要體驗最高傳輸速度,就必須使用經(jīng)過認(rèn)證的全新數(shù)據(jù)線。USB4保留了良好的兼容性,可向下兼容USB 3.2/3.1/3.0、雷電3。除此之外,USB4將只有USB Type-C一種接口,并支持多種數(shù)據(jù)、顯示協(xié)議,包括DisplayPort,可以一起充分利用高速帶寬,也支持USB PD供電。
比較遺憾的是,USB4的發(fā)布時間至今暫未公布。值得注意的是,此次發(fā)布的USB4是規(guī)范,而并非USB4.0。在此之前,USB Implementers Forum(USB-IF)計劃取消USB 3.0/3.1命名,統(tǒng)一劃歸為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)。以上就是關(guān)于USB標(biāo)準(zhǔn)以及命名的訊息。
現(xiàn)在大部分USB設(shè)備(比如USB接口的鼠標(biāo)、鍵盤、閃存、U盤等等)都是采用了USB通用驅(qū)動,而你的系統(tǒng)有USB通用驅(qū)動的話(比如XP就內(nèi)建了USB通用驅(qū)動)就能用。而有些USB設(shè)備是需要特殊驅(qū)動的,比如某些手機(jī),連接到電腦的USB口,是需要安裝驅(qū)動才能使用的。下面我們一起動手做一做USB接口控制器設(shè)計,了解一下如何設(shè)計。
第三篇內(nèi)容摘要:本篇會介紹FPGA 固件開發(fā),包括固件模塊劃分、自定義包編寫、分頻器模塊的實現(xiàn)、沿控制模塊的實現(xiàn)、輸入/輸出切換模塊的實現(xiàn)、請求處理模塊的實現(xiàn)、設(shè)備收發(fā)器模塊的實現(xiàn)、測試平臺的編寫;USB 驅(qū)動和軟件開發(fā),包括USB 驅(qū)動編寫、USB 軟件編寫以及總結(jié)等相關(guān)內(nèi)容。
六、FPGA 固件開發(fā)
6.1 固件模塊劃分
在本例中,固件開發(fā)指的就是 FPGA 開發(fā),也就是使用硬件描述語言(VHDL 或者 VerilogHDL)編寫 FPGA 內(nèi)部程序。FPGA 的作用就是和 PDIUSBD12 進(jìn)行通信,從 PDIUSBD12 中獲取數(shù)據(jù)并且根據(jù)主機(jī)的要求發(fā)送數(shù)據(jù)。PDIUSBD12 和 FPGA 之間的通信就是 8 位數(shù)據(jù)總線加上若干控制信號(A0、WR_N、RD_N 等),只要控制 FPGA 產(chǎn)生符合 PDIUSBD12 輸入/輸出時序的脈沖,即可實現(xiàn)兩者之間的通信。
FPGA 固件的模塊圖如圖 34 所示,各個模塊的功能如下。
圖 34 硬件加密系統(tǒng)設(shè)計方案
(1)分頻器模塊
由于 PDIUSBD12 在讀寫時序上有時間限制,例如每次讀寫操作之間的間隔不能小于 500ns,而 FPGA 的系統(tǒng)時鐘一般頻率都比較高,所以不能直接使用系統(tǒng)時鐘控制 PDIUSBD12,必須進(jìn)行分頻。分頻器模塊的功能就是按照要求由系統(tǒng)時鐘生成所需頻率的時鐘信號。
(2)沿控制器模塊
PDIUSBD12 的讀寫操作都各自有一個讀寫控制信號 WR_N 和 RD_N,每次讀寫操作都在對應(yīng)的控制信號的下降沿觸發(fā),沿控制模塊的功能就是可控地產(chǎn)生一個下降沿信號,用于控制讀寫操作。
(3)輸入/輸出切換模塊
輸入/輸出切換模塊在整個系統(tǒng)中非常重要,因為 FPGA 芯片和 PDIUSBD12 芯片之間的數(shù)據(jù)總線是雙向的總線,所以當(dāng)讀寫操作之一在進(jìn)行的時候另一個操作的信號源必須關(guān)閉,否則就會造成雙驅(qū)動,這不但不能得到正確的數(shù)據(jù)還會損害芯片。輸入/輸出切換模塊的功能就是根據(jù)當(dāng)前的讀寫狀況控制信號源,保證在一個時刻只有一個信號源在驅(qū)動總線。
(4)設(shè)備收發(fā)器模塊
這個模塊是整個固件的核心模塊,它完成的工作包括配置 PDIUSBD12 芯片、處理 PDIUSBD12產(chǎn)生的中斷、完成從緩存讀取數(shù)據(jù),并且根據(jù)需要將數(shù)據(jù)通過 PDIUSBD12 發(fā)送。設(shè)備收發(fā)器模塊完成對每個主機(jī)請求的解析工作,此外,還要將解析完成的請求數(shù)據(jù)傳遞給請求處理模塊。
(5)請求處理模塊
請求處理模塊的作用是接收設(shè)備收發(fā)器模塊解析完成的主機(jī)請求,并且決定如何處理此請求。
模塊劃分完畢之后就可以使用 ISE 創(chuàng)建工程了,然后就各個模塊分別編寫實現(xiàn)代碼和測試平臺,最后將所有模塊整合起來作為一個實體并且對其進(jìn)行仿真、測試,這樣就是一次完整的FPGA 開發(fā)過程。
ISE 的一些基本使用方法在前面的文章已有詳細(xì)介紹,這里放超鏈接,在此不詳細(xì)說明。下面詳細(xì)介紹一下各個模塊的實現(xiàn)方法。
ISE 14.7 安裝教程及詳細(xì)說明
6.2 自定義包編寫
在實際實現(xiàn)各個模塊功能之前,首先需要編寫兩個自定義包,分別是 USB 包和 PDIUSBD12包。
USB 包定義了 USB 協(xié)議以及 USB 設(shè)備相關(guān)的數(shù)據(jù)類型、常量等內(nèi)容,比如自定義數(shù)據(jù)類型、設(shè)備類型代碼值、請求代碼值、設(shè)備描述符、設(shè)備的工作狀態(tài)機(jī)等。設(shè)備的工作狀態(tài)機(jī)定義如下:
- 定義設(shè)備的工作狀態(tài)機(jī)
type TRANSEIVER_STATE
is ( TS_DISCONNECTED, -- 未連接
TS_CONNECTING, -- 正在連接
TS_IDLE, -- 閑置
TS_END_REQUESTHANDLER, -- 請求處理完成
TS_READ_IR, -- 讀取中斷寄存器
TS_READ_LTS, -- 讀取最后處理狀態(tài)
TS_BUSRESET, -- 總線復(fù)位
TS_SUSPENDCHANGE, -- 掛起改變
TS_EP0_RECEIVE, -- 端點(diǎn) 0 接收完成
TS_EP0_TRANSMIT, -- 端點(diǎn) 0 發(fā)送完成
TS_EP2_RECEIVE, -- 端點(diǎn) 2 接收完成
TS_EP2_TRANSMIT, -- 端點(diǎn) 2 發(fā)送完成
TS_END_RECEIVE, -- 從 PDIUSBD12 讀取數(shù)據(jù)完成
TS_END_TRANSMIT, -- 向 PDIUSBD12 寫數(shù)據(jù)完成
TS_SEND_DESCRIPTOR_1ST, -- 首次發(fā)送設(shè)備描述符
TS_SEND_DESCRIPTOR, -- 發(fā)送設(shè)備描述符
TS_SET_ADDRESS, -- 設(shè)置地址
TS_SET_CONFIGURATION, -- 設(shè)置配置
TS_GET_CONFIGURATION, -- 獲取配置
TS_GET_INTERFACE, -- 獲取接口
TS_SEND_STATUS, -- 發(fā)送狀態(tài)
TS_CLEAR_FEATURE, -- 清除特性
TS_SET_FEATURE, -- 啟用特性
TS_SET_INTERFACE, -- 設(shè)置接口
TS_READ_ENDPOINT, -- 從端點(diǎn)讀取數(shù)據(jù)
TS_WRITE_ENDPOINT, -- 向端點(diǎn)寫入數(shù)據(jù)
TS_SEND_PASSWORD, -- 發(fā)送密碼
TS_SET_PASSWORD_HIGH, -- 設(shè)置密碼低位
TS_SET_PASSWORD_LOW, -- 設(shè)置密碼高位
TS_SEND_EMPTY_PACKET, -- 發(fā)送空包
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";
-- 設(shè)備描述符相關(guān)的代碼、索引值等
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 相關(guān)的內(nèi)容,比如 PDIUSBD12 的命令代碼值、中斷代碼值等內(nèi)容。對 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 包的部分內(nèi)容作為參考。
6.3?分頻器模塊的實現(xiàn)
分頻器模塊實現(xiàn)的基本原理就是設(shè)計一個工作在系統(tǒng)時鐘下的計數(shù)器,循環(huán)地遞減或者遞加計數(shù),在某個計數(shù)的固定值將輸出翻轉(zhuǎn),即可實現(xiàn)時鐘分頻的功能。
例如,實驗板上的系統(tǒng)時鐘是 50MHz,而所需的讀寫周期間隔要求大于 500ns,即讀寫的時鐘頻率不能高于 2MHz,需要將原系統(tǒng)時鐘進(jìn)行至少 25 倍分頻。所以,我們設(shè)定一個計數(shù)器,工作在系統(tǒng)時鐘下,每個系統(tǒng)時鐘周期計數(shù)減一,減到零后恢復(fù)到 13,這樣,每經(jīng)過 13×2=26個系統(tǒng)時鐘周期,計數(shù)器的輸出會是一個完整的周期。
分頻器模塊的示意圖如圖 35 所示。
圖 35 分頻器模塊的示意圖
實現(xiàn)分頻器模塊的代碼如下:
-- 申明所使用的包
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use WORK.USB_PACKAGE.all;
-- 申明實體
entity FrequencyDivider is
generic(
div_factor : INTEGER8 := 0 -- 分頻系數(shù)屬性
);
port(
reset_n : in STD_LOGIC; -- 復(fù)位端口
clk_origin : in STD_LOGIC; -- 輸入時鐘端口
clk : out STD_LOGIC -- 輸出時鐘端口
);
end FrequencyDivider;
architecture FrequencyDivider of FrequencyDivider is
-- 內(nèi)部信號,在內(nèi)部隨時改變同時又輸出給輸出時鐘端口
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
-- 計數(shù)到達(dá)分頻系數(shù)時翻轉(zhuǎn)輸出,并且重置計數(shù)
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 沿控制模塊的實現(xiàn)
沿控制模塊的功能是提供可控的下降沿輸出,實現(xiàn)的方案如下:用一個使能信號 CE_N 控制輸出。輸入為分頻后的時鐘,當(dāng) CE_N 輸入為高的時候,輸出保持高電平,而當(dāng) CE_N 輸入變?yōu)榈偷臅r候,將時鐘接到輸出上,這樣就能得到連續(xù)的下降沿信號(和時鐘的下降沿同步)。只要對 CE_N 進(jìn)行適當(dāng)?shù)目刂疲湍艿玫叫枰南陆笛亍?/p>
沿控制模塊的示意圖和時序圖如圖 36 所示。輸入時鐘連接到分頻器模塊的輸出時鐘上,使能信號控制沿輸出信號,只要在某一個時鐘周期內(nèi)將使能信號保持低電平,就可以得到一個下降沿輸出。
圖 36 沿控制模塊的示意圖和時序圖
沿控制模塊的實現(xiàn)代碼如下:
--申明所使用的包
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 輸入/輸出切換模塊的實現(xiàn)
由于 PDIUSBD12 的 8 位數(shù)據(jù)線是雙向總線,所以當(dāng)進(jìn)行讀寫操作的時候,應(yīng)該注意避免雙驅(qū)動。雙驅(qū)動的意思就是在總線兩邊同時往總線上加輸出信號,這樣總線數(shù)據(jù)就處于一種不定態(tài)(用 X 表示),并且還容易損壞器件。例如,沒有處理好雙驅(qū)動的仿真波形就會如圖 37 所示,這種情況下無法得到正確的數(shù)據(jù)的。
圖 37 仿真不定態(tài)時序圖
信號的 4 種基本狀態(tài)是高電平(1)、低電平(0)、不定態(tài)(X)和高阻態(tài)(Z),當(dāng)一個總線上同時加有兩個信號時,組合起來的結(jié)果如表 35 所示。
表 35 信號狀態(tài)表
可見,當(dāng)一個總線上同時有兩個驅(qū)動的時候,很有可能產(chǎn)生不定態(tài) X,但是如果其中一個信號為高阻態(tài) Z 的話,則是一個確定的狀態(tài)(即另一個信號的狀態(tài))。所以,避免雙驅(qū)動的基本思想就是根據(jù)目前的讀寫狀態(tài)關(guān)閉某一個驅(qū)動源,也就是說將其另一個驅(qū)動源輸出設(shè)置為高阻態(tài)。由于讀寫操作是由各自的控制信號(WR_N、RD_N)控制的,所以可以將這兩個信號作為互斥關(guān)系的信號來控制總線數(shù)據(jù)的信號源。例如,當(dāng) RD_N 為低時,要從 PDIUSBD12 讀取數(shù)據(jù),就應(yīng)該關(guān)閉 FPGA 對總線的輸出,即將 FPGA 的總線輸出信號變?yōu)楦咦钁B(tài) Z。反過來也一樣,當(dāng) WR_N 為低時,要向 PDIUSBD12 發(fā)送數(shù)據(jù),此時 PDIUSBD12 也會自動關(guān)閉它在總線上的輸出。以上思想可用公式表示為:
輸入/輸出切換模塊的示意圖如圖 6-38 所示。其中左邊的總線表示連接到 PDIUSBD12 的總線,右邊的輸入、輸出總線是在 FPGA 內(nèi)部的總線信號,表示在 FPGA 內(nèi)部將總線的輸入和輸出區(qū)分開來;RD_N 和 WR_N 信號分別用于讀、寫控制。
圖 38 輸入/輸出切換模塊的示意圖
輸入/輸出切換模塊的實現(xiàn)代碼如下:
--申明所使用的包
library IEEE;
use IEEE.STD_LOGIC_1164.all;
-- 申明實體
entity IOSwitch is
port(
data : inout STD_LOGIC_VECTOR(7 downto 0); -- 8 位雙向數(shù)據(jù)總線,和 PDIUSBD12 相連
din : in STD_LOGIC_VECTOR(7 downto 0); -- 8 位輸入數(shù)據(jù)總線,僅用于輸入
dout : out STD_LOGIC_VECTOR(7 downto 0); -- 8 位輸出數(shù)據(jù)總線,僅用于輸出
sel_in_n : in STD_LOGIC; -- 總線輸入控制信號
sel_out_n : in STD_LOGIC -- 總線輸出控制信號
);
end IOSwitch;
architecture IOSwitch of IOSwitch is
-- 創(chuàng)建一個內(nèi)部信號,用作數(shù)據(jù)傳遞
signal data_tmp : STD_LOGIC_VECTOR(7 downto 0);
begin
-- 信號連接
data <= data_tmp;
dout <= data;
-- 主進(jìn)程
process(sel_in_n, sel_out_n, data, din)
begin
-- 當(dāng)輸出控制信號有效時,將 data_tmp 賦值高阻
if sel_out_n = '0' then
data_tmp <= "ZZZZZZZZ";
-- 當(dāng)輸入控制信號有效時,將輸入的信號賦值給 data_tmp
elsif sel_in_n = '0' then
data_tmp <= din;
else
data_tmp <= "ZZZZZZZZ";
end if;
end process;
end IOSwitch;
6.6 請求處理模塊的實現(xiàn)
請求處理模塊的功能是根據(jù)主機(jī)的請求控制設(shè)備收發(fā)器模塊的處理狀態(tài)。在本例中,請求處理模塊實際的功能就是根據(jù)目前接收到的主機(jī)請求控制設(shè)備收發(fā)器模塊發(fā)送數(shù)據(jù),所以請求處理模塊的實現(xiàn)就是一個簡單的狀態(tài)機(jī)。
請求處理模塊的示意圖如圖 39 所示。時鐘信號是由分頻器的輸出時鐘提供;請求類型輸入是一個 8 位端口,它和接收事件輸入?yún)f(xié)同工作,當(dāng)設(shè)備收發(fā)器接收到一個請求時,就會將請求代碼發(fā)送到請求類型輸入端口,在接收事件輸入端口輸出一個時鐘周期的低電平,表示一次新的請求處理;命令輸出端口和命令中斷端口則用于控制設(shè)備收發(fā)器模塊的操作狀態(tài)。
圖 39 請求處理模塊的示意圖
請求處理模塊的實現(xiàn)代碼如下:
-- 申明要使用的庫
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; -- 復(fù)位端口
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
-- 狀態(tài)機(jī),已在 USB 包中有定義
signal rh_state: REQUEST_HANDLER_STATE := RH_IDLE;
-- 寄存器,用于標(biāo)示是否已分配地址
signal address_set: STD_LOGIC := '0';
begin
-- 主進(jìn)程
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 為低時候表示需要進(jì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';
-- 獲取狀態(tài)請求
when REQUEST_GET_STATUS =>
cmd <= RH_SEND_STATUS;
exec_n <= '0';
-- 設(shè)置地址狀態(tài)
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';
-- 設(shè)置配置請求和設(shè)置描述符請求
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';
-- 設(shè)置接口請求
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 設(shè)備收發(fā)器模塊的實現(xiàn)
設(shè)備收發(fā)器模塊是整個固件系統(tǒng)的核心,實現(xiàn)的基本思想是創(chuàng)建一個狀態(tài)機(jī),將各個處理操作都作為一個狀態(tài)處理,在每個狀態(tài)中按照 PDIUSBD12 的時序要求對其進(jìn)行數(shù)據(jù)訪問和控制。
設(shè)備收發(fā)器模塊的示意圖如圖 40 所示。
圖 40 設(shè)備收發(fā)器模塊的示意圖
由于 USB 協(xié)議很復(fù)雜并且 PDIUSBD12 的控制也比較復(fù)雜,所以設(shè)備收發(fā)器狀態(tài)機(jī)的狀態(tài)量會較多。根據(jù)設(shè)備收發(fā)器的功能,可以將狀態(tài)機(jī)各個狀態(tài)的功能分為 3 類。
? 初始化器件:初始化器件就是對 PDIUSBD12 器件進(jìn)行配置的狀態(tài),需要配置的內(nèi)容包括設(shè)置地址/使能、設(shè)置 DMA 以及設(shè)置模式等。
? 數(shù)據(jù)訪問:數(shù)據(jù)訪問即實現(xiàn) PDIUSBD12 和 FPGA 之間的數(shù)據(jù)讀寫,包括讀取中斷寄存器、讀取前次傳輸狀態(tài)、由端點(diǎn)讀取數(shù)據(jù)、由端點(diǎn)發(fā)送數(shù)據(jù)等。
? 請求回復(fù):請求回復(fù)是指根據(jù)各種類型請求的數(shù)據(jù)格式提取所需要的數(shù)據(jù),并且在解析完成后通知請求處理模塊。下面詳細(xì)介紹一下以上 3 種狀態(tài)的實現(xiàn)。
1)初始化器件
初始化器件相關(guān)的狀態(tài)主要是 TS_DISCONNECTED 和 TS_CONNECTING(狀態(tài)的定義見USB_Package.vhd 文件),其中 TS_DISCONNECTED 是系統(tǒng)復(fù)位后的狀態(tài),TS_CONNECTING 是配置PDIUSBD12 寄存器的狀態(tài)。需要注意的是 PDIUSBD12 器件在復(fù)位后應(yīng)該等待至少 3 ms 后再訪問其寄存器,這樣可讓晶振穩(wěn)定下來。
由于對寄存器配置的命令以及時序都是確定的,所以可以在自定義包中將配置數(shù)據(jù)定義為常數(shù),例如:
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 的配置參數(shù),第一個常數(shù)數(shù)組是配置命令和數(shù)據(jù),第二個數(shù)組表示命令、數(shù)據(jù)的順序,最后一個參數(shù)是配置參數(shù)的總長度。定義的過程是首先向 PDIUSBD12發(fā)送命令 D12_COMMAND_SET_DMA(設(shè)置 DMA 命令),然后發(fā)送此命令的數(shù)據(jù) D12_DMA(D12_DMA定義為 0xC0,其意義請參考圖 23);之后發(fā)送設(shè)置模式命令和此命令的兩個數(shù)據(jù)。D12_COMMAND_SET_DMA、D12_DMA、D12_COMMAND、D12_DATA 等都是已定義的常數(shù),例如:
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";
詳細(xì)的常數(shù)定義請參考 PDIUSBD12 包的定義文件。這樣定義雖然顯得復(fù)雜,但是便于將數(shù)據(jù)與格式分離,也便于代碼閱讀。此外,在調(diào)用配置數(shù)據(jù)時也較為方便,只需要使用一個循環(huán)索引變量,依次讀取 D12_CONNECT_DATA 數(shù)組和D12_CONNECT_DATA 數(shù)組的數(shù)值,發(fā)送給 PDIUSBD12 即可,代碼如下:
-- TS_CONNECT 狀態(tài),對 PDIUSBD12 進(jìn)行配置
when TS_CONNECTING =>
-- handle_step 作為循環(huán)變量
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;
以上代碼運(yùn)行的結(jié)果就是經(jīng)過 5 個時鐘周期,F(xiàn)PGA 完成向 PDIUSBD12 輸出的一系列命令以及數(shù)據(jù),通過編寫測試平臺仿真可以看到運(yùn)行的結(jié)果(測試平臺的編寫將會在下面專門介紹),如圖 41 所示。
圖 41 器件配置仿真時序圖
通過上面的時序圖可以看出,8 位總線上傳輸?shù)氖?D12_CONNECT_DATA 定義的配置命令和數(shù)據(jù),而 a0 位表明了總線上的是命令還是數(shù)據(jù),通過一個下降沿的寫信號可以將命令或者數(shù)據(jù)發(fā)送給 PDIUSBD12。
2)數(shù)據(jù)訪問狀態(tài)
數(shù)據(jù)訪問狀態(tài)的功能簡單地說就是中斷監(jiān)測和數(shù)據(jù)收發(fā)。每次系統(tǒng)復(fù)位后 FPGA 會自動配置 PDIUSBD12 器件,配置完成之后設(shè)備收發(fā)器模塊會處于空閑狀態(tài)(TS_IDLE)。PDIUSBD12 器件在接收到數(shù)據(jù)包時會通過中斷來通知設(shè)備收發(fā)器,此外,請求處理模塊也會通過命令中斷信號控制設(shè)備收發(fā)器模塊。所以,中斷監(jiān)測就是在每個時鐘周期讀取一次 PDIUSBD12 的中斷信號和請求處理模塊的命令中斷信號,如果發(fā)現(xiàn)其中的一個中斷信號為低,則轉(zhuǎn)為其他狀態(tài)。
中斷監(jiān)測的代碼如下:
-- 空閑狀態(tài),監(jiān)測中斷信號
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;
當(dāng)監(jiān)測到 PDIUSBD12 的中斷時,設(shè)備收發(fā)器首先讀取中斷寄存器,然后就會進(jìn)入數(shù)據(jù)收發(fā)狀態(tài),如果監(jiān)測到的是請求處理模塊的命令中斷,則進(jìn)入的是請求回復(fù)狀態(tài)。請求回復(fù)狀態(tài)包括了發(fā)送描述符、發(fā)送配置信息等,這些內(nèi)容將在下面一個小節(jié)介紹。數(shù)據(jù)收發(fā)狀態(tài)包括讀取中斷寄存器、控制端點(diǎn)數(shù)據(jù)收發(fā)等。讀取中斷寄存器的流程圖如圖42 所示。
圖 42 中斷處理流程圖
讀取中斷寄存器的代碼如下:
-- 讀取中斷寄存器狀態(tài)
when TS_READ_IR =>
-- 第一步,發(fā)送讀取中斷寄存器命令
if handle_step = 0 then
a0 <= D12_COMMAND;
data_out <= D12_COMMAND_READ_IR;
wr_n_var := '0';
-- 第二步,設(shè)置讀信號為低,讀取第一個返回參數(shù),即中斷寄存器第一個字節(jié)
elsif handle_step = 1 then
a0 <= D12_DATA;
rd_n_var := '0';
-- 第三步,保存中斷寄存器第一個字節(jié)并讀取第二個返回參數(shù)(中斷寄存器第二個字節(jié))
elsif handle_step = 2 then
-- 保存中斷寄存器第一個字節(jié)
ir_0 := data_in;
-- 讀取第二個參數(shù)
a0 <= D12_DATA;
rd_n_var := '0';
-- 最后,保存第二個參數(shù),進(jìn)入下一處理狀態(tài)
else
-- 保存中斷寄存器第二個字節(jié)
ir_1 := data_in(0);
-- 根據(jù)中斷寄存器選擇進(jìn)入下一處理狀態(tài)
ts_state <= GetInterruptHandler(ir_0, ir_1);
ih_state <= IH_START;
end if;
handle_step := handle_step+1;
下面介紹一下控制輸出的處理流程??刂戚敵龅妮敵鍪窍鄬χ鳈C(jī)來說的,所以相對于設(shè)備來說,就是接收主機(jī)的數(shù)據(jù)。當(dāng)一次控制輸出發(fā)生時,設(shè)備首先會判斷接收到的是不是建立包(Setup Packet),如果是則開始接收下面的數(shù)據(jù),否則,接收前次傳輸所剩余的數(shù)據(jù)??刂苽鬏?shù)奶幚砹鞒虉D如圖 43 所示。
圖 43 控制輸出流程圖
從上面的流程圖可以看出,設(shè)備收發(fā)器首先要選擇控制輸出端點(diǎn),提取建立包的內(nèi)容,再進(jìn)行端點(diǎn)是為滿還是空的判斷。如果控制端點(diǎn)不為空,設(shè)備收發(fā)器將從緩沖區(qū)讀出內(nèi)容并將其保存。之后,它將判斷設(shè)備請求的有效性,如果是一個有效的請求,設(shè)備收發(fā)器必須向控制輸出端點(diǎn)發(fā)送應(yīng)答建立命令以重新使能下一個建立階段。
接下來,設(shè)備收發(fā)器需要證實控制傳輸是控制讀還是寫。這可以通過讀建立包中bmRequestType 的第 8 位來判斷。如果控制傳輸是一個控制讀類型,那就是說器件需要在下一個數(shù)據(jù)階段向主機(jī)發(fā)回數(shù)據(jù)包。設(shè)備收發(fā)器會設(shè)置一個標(biāo)志以指示設(shè)備現(xiàn)在正處于傳輸模式,即準(zhǔn)備在主機(jī)發(fā)送請求時進(jìn)入傳輸狀態(tài)(TS_EP0_TRANSMIT)向主機(jī)發(fā)送數(shù)據(jù)。
處理流程的各個步驟在設(shè)備收發(fā)器模塊中被劃分在兩個狀態(tài)中實現(xiàn),其中選擇端點(diǎn)和讀取、保存數(shù)據(jù)的操作在 TS_READ_ENDPOINT 狀態(tài)中實現(xiàn),其他的內(nèi)容在 TS_EP0_RECEIVE 狀態(tài)中實現(xiàn)。下面是從端點(diǎn)(PDIUSBD12 的緩沖)數(shù)據(jù)讀取的實現(xiàn)代碼,即 TS_READ_ENDPOINT 狀態(tài)的代碼,由于篇幅原因,這里只提供部分參考代碼。
-- 讀取端點(diǎn)數(shù)據(jù)狀態(tài)
when TS_READ_ENDPOINT =>
-- handle_step 表示操作步驟
case handle_step is
-- 首先,發(fā)送選擇端點(diǎn)命令,選擇端點(diǎn)
when 0 =>
a0 <= D12_COMMAND;
data_out <= active_ep;
wr_n_var := '0';
handle_step := handle_step+1;
-- 發(fā)送讀取端點(diǎn)數(shù)據(jù)的命令,準(zhǔn)備接收數(shù)據(jù)
when 1 =>
a0 <= D12_COMMAND;
data_out <= D12_COMMAND_RW_BUFFER;
wr_n_var := '0';
handle_step := handle_step+1;
-- 讀取緩沖數(shù)據(jù)的前兩個字節(jié),第一個字節(jié)為保留數(shù)據(jù),第二個字節(jié)表示數(shù)據(jù)長度
when 2 | 3 =>
a0 <= D12_DATA;
rd_n_var := '0';
handle_step := handle_step+1;
-- 保存第二個字節(jié)(數(shù)據(jù)長度),準(zhǔn)備接收有效數(shù)據(jù)
when 4 =>
-- 保留第二個字節(jié)
read_in := conv_integer(data_in);
-- 判斷數(shù)據(jù)長度是否為零
if read_in = 0 then
handle_step := 7;
else
-- 獲取剩余的數(shù)據(jù)
handle_step := handle_step+1;
a0 <= D12_DATA;
rd_n_var := '0';
end if;
-- 依次讀取數(shù)據(jù)并且保存數(shù)據(jù)
when 5 =>
-- 保存前一個周期要求獲取的數(shù)據(jù)
ts_data(ram_address) <= data_in;
ram_address := ram_address+1;
read_count := read_count+1;
-- 判斷全部數(shù)據(jù)是否已經(jīng)獲取
if read_count = read_in then
handle_step := 6;
else
-- 繼續(xù)要求獲取下一個數(shù)據(jù)
a0 <= D12_DATA;
rd_n_var := '0';
end if;
-- 最后,發(fā)送清除端點(diǎn)緩沖的命令
when 6 =>
a0 <= D12_COMMAND;
data_out <= D12_COMMAND_CLEAR_EP_BUFFER;
wr_n_var := '0';
handle_step := 7;
-- 恢復(fù)到原始處理狀態(tài)
when others =>
handle_step := 0;
ts_state <= last_ts_state;
end case;
下面介紹一下控制輸入的處理過程??刂戚斎刖褪窃O(shè)備向主機(jī)發(fā)送數(shù)據(jù),最為典型的就是設(shè)備向主機(jī)發(fā)送描述符,圖 44 所示是控制輸入的流程圖。
圖 44 控制輸入流程圖
從控制輸入的流程圖可以看出,設(shè)備收發(fā)器首先需要通過讀 PDIUSBD12 的最后處理狀態(tài)寄存器清零中斷標(biāo)志位。接著設(shè)備收發(fā)器在確認(rèn) PDIUSBD12 處于傳輸模式后進(jìn)行數(shù)據(jù)包的發(fā)送。PDIUSBD12 的控制端點(diǎn)只有 16 字節(jié) FIFO,如果傳輸?shù)拈L度大于 16 字節(jié),設(shè)備收發(fā)器在傳輸階段就必須控制數(shù)據(jù)的數(shù)量。設(shè)備收發(fā)器必須檢查要發(fā)送到主機(jī)的當(dāng)前和剩余的數(shù)據(jù)大小,如果剩下的字節(jié)數(shù)大于 16,設(shè)備收發(fā)器將先發(fā)送 16 字節(jié)并繼續(xù)等待下一次發(fā)送。
當(dāng)下一個數(shù)據(jù)發(fā)送中斷來到時,設(shè)備收發(fā)器將確定剩余的字節(jié)是否為零。如果已經(jīng)沒有數(shù)據(jù)要發(fā)送,設(shè)備收發(fā)器需要發(fā)送一個空的包以指示主機(jī)數(shù)據(jù)已經(jīng)發(fā)送完畢。
控制輸入是在 TS_EP0_TRANSMIT 和 TS_WRITE_ENDPOINT 兩個狀態(tài)中實現(xiàn)的。其中,TS_EP0_TRANSMIT 實 現(xiàn) 的 是 控 制 輸 入 流 程 控 制 , 而 TS_WRITE_ENDPOINT 的 實 現(xiàn) 和TS_READ_ENDPOINT 很類似,只不過是將讀取數(shù)據(jù)換為發(fā)送數(shù)據(jù)。TS_WRITE_ENDPOINT 狀態(tài)的實現(xiàn)代碼如下,由于篇幅原因,這里只提供部分參考代碼。
-- 寫端點(diǎn)緩存數(shù)據(jù)的狀態(tài)
when TS_WRITE_ENDPOINT =>
case handle_step is
-- 首先,發(fā)送選擇端點(diǎn)的命令,選擇端點(diǎn) 0
when 0 =>
a0 <= D12_COMMAND;
data_out <= active_ep;
wr_n_var := '0';
handle_step := handle_step+1;
-- 讀取選擇端點(diǎn)命令的一個返回參數(shù)(可選)
when 1 =>
a0 <= D12_DATA;
rd_n_var := '0';
handle_step := handle_step+1;
-- 發(fā)送讀寫端點(diǎn)的命令
when 2 =>
a0 <= D12_COMMAND;
data_out <= D12_COMMAND_RW_BUFFER;
wr_n_var := '0';
handle_step := handle_step+1;
-- 寫入端點(diǎn)緩存第一個字節(jié),為保留字節(jié),值為 0
when 3 =>
a0 <= D12_DATA;
data_out <= X"00";
wr_n_var := '0';
handle_step := handle_step+1;
-- 寫入端點(diǎn)緩存第二個字節(jié),為有效數(shù)據(jù)的長度
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;
-- 順序?qū)懭胗行?shù)據(jù)
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;
-- 發(fā)送緩沖區(qū)有效命令,允許 PDIUSBD12 發(fā)送數(shù)據(jù)
when 6 =>
-- 判斷是否所有數(shù)據(jù)已經(jīng)被寫入
if write_count = to_write then
--發(fā)送緩沖區(qū)有效命令
a0 <= D12_COMMAND;
data_out <= D12_COMMAND_ENABLE_BUFFER;
wr_n_var := '0';
handle_step := 7;
else
-- 寫入數(shù)據(jù)
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;
-- 恢復(fù)到原始處理狀態(tài)
when 7 =>
handle_step := 0;
ts_state <= last_ts_state;
when others =>
NULL;
end case;
以上便是數(shù)據(jù)訪問狀態(tài)的實現(xiàn)方法,在測試平臺中可以對以上代碼進(jìn)行測試,測試時的輸入數(shù)據(jù)應(yīng)該由測試平臺產(chǎn)生(測試平臺的編寫將在下面的章節(jié)進(jìn)行專門介紹)。如第一次發(fā)送設(shè)備描述符的仿真波形。此仿真過程可以分為兩個部分,第一部分(如圖 45 所示)是接收建立包(Setup Packet)以及讀取 PDIUSBD12 請求數(shù)據(jù)的過程;第二部分(如圖 46 所示)是將設(shè)備描述符數(shù)據(jù)寫入 PDIUSBD12 端點(diǎn)緩存并且使緩沖區(qū)有效。
圖 45 發(fā)送設(shè)備描述符仿真波形 1
圖 46 發(fā)送設(shè)備描述符仿真波形 2
3)請求回復(fù)狀態(tài)
請求回復(fù)狀態(tài)的功能就是對各個請求作出響應(yīng)。USB 的標(biāo)準(zhǔn)請求已經(jīng)在前面做了介紹,下面就以獲取描述符請求為例介紹一下請求響應(yīng)的實現(xiàn)方法,其他的標(biāo)準(zhǔn)請求以及廠商請求(獲取、設(shè)置密碼)相對來說比較簡單,實現(xiàn)的方法請讀者參考源代碼。
獲取描述符請求是最為重要的請求,因為這在設(shè)備枚舉過程中是必需的,它是主機(jī)了解設(shè)備的第一個步。獲取描述符請求的處理流程如圖 47 所示。
圖 47 獲取描述符處理流程
獲取設(shè)備描述符請求響應(yīng)的實現(xiàn)代碼如下:
-- 獲取描述符請求響應(yīng)狀態(tài)
when TS_SEND_DESCRIPTOR =>
handle_step := 0;
active_ep := X"01";
-- 判斷是否是設(shè)備請求
if ts_data(ADDRESS_DESCRIPTOR_TYPE) = TYPE_DEVICE_DESCRIPTOR then
-- LED 輸出,提示作用
led(0) <= '0';
-- 檢查數(shù)據(jù)長度是否符合要求
if data_length > LENGTH_DEVICE_DESCRIPTOR then
data_length := LENGTH_DEVICE_DESCRIPTOR;
end if;
-- 判斷描述符長度是否超過端點(diǎn) 0 的緩存大小
if data_length > LENGTH_ENDPOINT0_BUFFER then
to_write := LENGTH_ENDPOINT0_BUFFER;
is_transmit := '1';
else
to_write := data_length;
end if;
-- 設(shè)置傳輸狀態(tài)標(biāo)志位,設(shè)置傳輸數(shù)據(jù)源(描述符)以及數(shù)據(jù)長度
data_count := to_write;
ram_address := ADDRESS_DEVICE_DESCRIPTOR;
-- 準(zhǔn)備轉(zhuǎn)入進(jìn)入控制輸入狀態(tài)(TS_WRITE_ENDPOINT),發(fā)送數(shù)據(jù)
ts_state <= TS_WRITE_ENDPOINT;
elsif ts_data(ADDRESS_DESCRIPTOR_TYPE) = TYPE_CONFIGURATION_DESCRIPTOR then
-- 檢查數(shù)據(jù)長度,LED 輸出,提示作用
if data_length > LENGTH_CONFIGURATION_DESCRIPTOR then
data_length := LENGTH_CONFIGURATION_DESCRIPTOR;
led(2) <= '0';
else
led(1) <= '0';
end if;
-- 判斷描述符長度是否超過端點(diǎn) 0 的緩存大小
if data_length > LENGTH_ENDPOINT0_BUFFER then
to_write := LENGTH_ENDPOINT0_BUFFER;
is_transmit := '1';
else
to_write := data_length;
end if;
-- 設(shè)置傳輸狀態(tài)標(biāo)志位,設(shè)置傳輸數(shù)據(jù)源(描述符)以及數(shù)據(jù)長度
data_count := to_write;
ram_address := ADDRESS_CONFIGURATION_DESCRIPTOR;
-- 設(shè)置傳輸狀態(tài)標(biāo)志位,設(shè)置傳輸數(shù)據(jù)源(描述符)以及數(shù)據(jù)長度
ts_state <= TS_WRITE_ENDPOINT;
else
ts_state <= TS_IDLE;
end if;
last_ts_state := TS_END_REQUESTHANDLER;
6.8 測試平臺的編寫
上面介紹的是整個 FPGA 固件系統(tǒng)的實現(xiàn)方法,為了驗證設(shè)計的正確性,還需要編寫一個測試平臺對整個系統(tǒng)進(jìn)行仿真。由于實際情況下 FPGA 是和 PDIUSBD12 進(jìn)行通信,所以在測試平臺中需要虛擬一個 PDIUSBD12,來實現(xiàn)仿真的目的。
首先,在測試平臺中需要產(chǎn)生一個虛擬的時鐘信號,產(chǎn)生的方法就是使用 wait for 語句等待固定時間后將信號值翻轉(zhuǎn)。時鐘信號的實現(xiàn)代碼如下:
-- 時鐘信號生成代碼
clk_gen: process
begin
-- 翻轉(zhuǎn)
clk <= not clk;
-- 等待固定時間
wait for 50 ns;
end process;
其次,由于 FPGA 和 PDIUSBD12 之間有數(shù)據(jù)讀寫,所以要模擬所有 FPGA 向 PDIUSBD12 讀取的數(shù)據(jù)。模擬數(shù)據(jù)讀寫的方法是將所有數(shù)據(jù)按照順序?qū)懭胍粋€大的測試數(shù)據(jù)數(shù)組中,使用一個變量作為該數(shù)組索引,再編寫一個對讀信號敏感的過程,在每次讀信號的下降沿將數(shù)據(jù)送到總線上,并且將數(shù)組索引變量增加 1。測試數(shù)據(jù)數(shù)組以及索引變量的定義方法如下:
-- 測試數(shù)據(jù)數(shù)組定義
signal td : REG256x8 :=
(
-- 第一次獲取設(shè)備描述符測試數(shù)據(jù)
X"01", X"00", X"20", X"00", X"08", -- 各寄存器數(shù)據(jù)以及端點(diǎn) 0 緩存前兩個字節(jié)
X"80", X"06", X"00", X"01", X"00", X"00", X"40", X"00", -- 獲取設(shè)備描述符請求
X"00",
-- 設(shè)置地址請求測試數(shù)據(jù)
X"01", X"00", X"20", X"00", X"08", -- 各寄存器數(shù)據(jù)以及端點(diǎn) 0 緩存前兩個字節(jié)
X"00", X"05", X"02", X"00", X"00", X"00", X"00", X"00", -- 設(shè)置地址請求
X"00",
-- 獲取完整設(shè)備描述符測試數(shù)據(jù)
X"01", X"00", X"20", X"00", X"08", -- 各寄存器數(shù)據(jù)以及端點(diǎn) 0 緩存前兩個字節(jié)
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", -- 各寄存器數(shù)據(jù)
-- 獲取配置描述符請求測試數(shù)據(jù)
X"01", X"00", X"20", X"00", X"08", -- 各寄存器數(shù)據(jù)以及端點(diǎn) 0 緩存前兩個字節(jié)
X"80", X"06", X"00", X"02", X"00", X"00", X"09", X"00", --獲取配置描述符請求
X"00",
--獲取所有配置描述符請求測試數(shù)據(jù)
X"01", X"00", X"20", X"00", X"08", -- 各寄存器數(shù)據(jù)以及端點(diǎn) 0 緩存前兩個字節(jié)
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", -- 各寄存器數(shù)據(jù)
X"02", X"00", X"00", X"00", -- 各寄存器數(shù)據(jù)
-- 設(shè)置配置請求測試數(shù)據(jù)
X"01", X"00", X"20", X"00", X"08", -- 各寄存器數(shù)據(jù)以及端點(diǎn) 0 緩存前兩個字節(jié)
X"00", X"09", X"01", X"00", X"00", X"00", X"00", X"00", -- 設(shè)置配置請求
X"00",
others => X"00"
);
-- 數(shù)組索引
signal td_index : INTEGER8 := 255;
再次,需要處理好總線雙驅(qū)動的問題。前面介紹的輸入/輸出選擇模塊的功能就是在必要的時候關(guān)閉總線輸出來避免雙驅(qū)動的發(fā)生,同樣道理,在測試平臺中也應(yīng)該做到這一點(diǎn),即當(dāng)測試平臺向 FPGA 固件系統(tǒng)讀取數(shù)據(jù)時,應(yīng)該關(guān)閉測試平臺的總線輸出,即將其設(shè)置為高阻。實現(xiàn)代碼如下:
process(d12_wr, td_index)
begin
-- 當(dāng) FPGA 向 PDIUSBD12 些數(shù)據(jù)時,總線輸出變?yōu)楦咦?/code>
if d12_wr = '0' then
data <= "ZZZZZZZZ";
else
data <= td(td_index);
end if;
end process;
最后,還需要編寫一個主流程,在主流程中需要進(jìn)行系統(tǒng)復(fù)位和產(chǎn)生中斷信號,代碼如下:
-- main process
main: process
variable i : INTEGER8;
begin
-- 復(fù)位
reset_n <= '0';
wait for 100 ns;
reset_n <= '1';
wait for 100 us;
-- 循環(huán)模擬產(chǎn)生 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 驅(qū)動和軟件開發(fā)
7.1 USB 驅(qū)動編寫
以上介紹的是 FPGA 固件的開發(fā)過程,由于本例中設(shè)計的不是一個類設(shè)備,所以要使設(shè)備正常工作,還需要編寫專門的驅(qū)動程序和軟件。由于驅(qū)動和軟件不是本篇的重點(diǎn),故下面只簡要介紹其編寫方法。
1)USB 驅(qū)動模型
USB 體系的主機(jī)軟件可分為兩層,即 USB 系統(tǒng)軟件和客戶端驅(qū)動程序,如圖 48 所示。
圖 48 USB 接口軟件模型
USB 系統(tǒng)軟件根據(jù)功能可以分為 USBD 和 HCD 上下兩部分,其中 HCD 為上層提供了主機(jī)控制器的抽象以及數(shù)據(jù)在總線上的傳輸抽象。USBD 為上層的客戶端驅(qū)動程序提供了 USB 設(shè)備的抽象,并在客戶端驅(qū)動和所驅(qū)動的設(shè)備之間提供了數(shù)據(jù)傳輸的抽象。
客戶端驅(qū)動程序從用戶的角度來講相當(dāng)于傳統(tǒng)意義上的驅(qū)動程序。不過設(shè)備端不同的接口對應(yīng)不同的驅(qū)動程序,如果設(shè)備只有一個接口,那么從用戶的角度來講,兩者是一樣的,客戶端驅(qū)動程序通過 USB 系統(tǒng)軟件提供的接口與設(shè)備交互,而不是通過過去的 I/O 地址或者端口進(jìn)行訪問。
2)使用 Driver Studio 開發(fā) USB 驅(qū)動
上面介紹的是 USB 軟件模型,對于驅(qū)動開發(fā)人員來說,需要編寫的就是客戶端驅(qū)動程序。編寫客戶端驅(qū)動程序需要安裝 DDK,即 Windows Driver Development Kit,通過 DDK 我們就能夠訪問 USB 系統(tǒng)軟件的接口從而實現(xiàn)與設(shè)備的交互。但是,如果只使用 DDK 開發(fā)驅(qū)動程序的話,會比較復(fù)雜,所以可以使用一些驅(qū)動開發(fā)的專用工具,例如 Driver Studio、WinDriver 等。本例選用的是 Driver Studio 2.7 進(jìn)行開發(fā),下面介紹一下開發(fā)的基本步驟。安裝完 DDK 以及 Driver Studio 后,運(yùn)行 Driver Studio 的 Driver Wizard。在第 1 步中輸入驅(qū)動工程名稱和路徑,如圖 49 所示。單擊 Next 按鈕進(jìn)入如圖 50 所示對話框。
圖 49 Driver Wizard 第 1 步?
圖 50 Driver Wizard 第 2 步
第 2 步選擇工程類型 WDM Driver,單擊 Next 按鈕進(jìn)入如圖 51 所示對話框。
第 3 步選擇驅(qū)動類型 WDM Function Driver。單擊 Next 按鈕進(jìn)入如圖 52 所示對話框。
圖 51 Driver Wizard 第 3 步?
圖 52 Driver Wizard 第 4 步
第 4 步比較重要,是選擇驅(qū)動總線類型,應(yīng)該選擇 USB(WDM Only),并且注意要在 USB VendorID 和 USB Product ID 中輸入和固件中設(shè)備描述一致的信息。這里請注意 Vendor ID 一定是0x0471,因為使用的是 Philips 的 PDIUSBD12 芯片,其 Vendor ID 固定為 0x0471。單擊 Next按鈕,進(jìn)入如圖 53 所示對話框。
圖?53?Driver Wizard 第 5 步
第 5 步是端點(diǎn)定義,可以根據(jù)需要定義端點(diǎn)的類型(輸入輸出)、端點(diǎn)號、緩存大小等。
第 6 步到第 9 步是一些開發(fā)輔助信息的定義,可以保持為默認(rèn)值,如圖 54~圖 57 所示。
圖 54 Driver Wizard 第 6 步?
圖 55 Driver Wizard 第 7 步
圖 56 Driver Wizard 第 8 步?
圖 57 Driver Wizard 第 9 步
第 10 步是設(shè)備類的定義,如圖 58 所示。定義打開設(shè)備的方式,Symbolic Link 表示按照設(shè)備名稱打開,Interface(WDM Only)表示按照設(shè)備的 GUID 打開,這里選擇使用設(shè)備名稱打開。
圖 58 Driver Wizard 第 10 步
第 11 步定義的是設(shè)備的 IO 控制接口,也就是驅(qū)動和應(yīng)用程序之間的接口,如圖 59 所示。單擊 Add 按鈕可以定義 IO 控制接口,如圖 60 所示。
圖 59 Driver Wizard 第 11 步?
圖 60 定義 IO 控制接口
最后,第 12 步進(jìn)行一些額外的設(shè)置,如圖 61 所示,可以保持默認(rèn)值。
圖 61 Driver Wizard 第十二步
以上便是使用 Drive Studio 的 Driver Wizard 生成驅(qū)動框架的完整過程,現(xiàn)在我們已經(jīng)有了一個完成了大部分驅(qū)動工作的代碼框架,只需要增加一些自定義的處理代碼即可。
3)使用 Visual C++編譯驅(qū)動
運(yùn)行 Visual C++ 6.0 打開 Driver Wizard 生成的工程文件,可看到在***Device 這個類中已經(jīng)有了很多設(shè)備操作的處理函數(shù),例如上電(OnDevicePowerUp)、休眠(OnDeviceSleep)啟動(OnDeviceStart)等,可以根據(jù)需要修改這些函數(shù),如果沒有特殊要求,可以保持默認(rèn)設(shè)置,如圖 62 所示。
圖 62 設(shè)備操作處理函數(shù)
另外還需要完成的工作就是對上面定義的 IO 控制接口函數(shù)進(jìn)行處理,其功能就是建立一個廠商請求。由于本次設(shè)計的 USB 設(shè)備是一個加密設(shè)備,它不是類設(shè)備,所以會有一些特定的請求(廠商請求)。為了介紹廠商請求的實現(xiàn)方法,本系統(tǒng)用到了兩個廠商請求:設(shè)置密碼和獲取密碼。由 Driver Wizard 自動生成的驅(qū)動一般都已經(jīng)包括了標(biāo)準(zhǔn)請求的建立,但是不會包括廠商請求的建立。廠商請求是在 IO 控制接口函數(shù)中建立的,即 Driver Wizard 第 11 步所定義的兩個函數(shù),建立廠商請求的函數(shù)主要是 BuildVendorRequest 函數(shù),其格式如下:
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
??);
其中需要開發(fā)人員注意的是前 6 個參數(shù),其意義如下:
? PUCHAR TransferBuffe 數(shù)據(jù)緩沖。如果是數(shù)據(jù)輸入,用于存儲接收到的數(shù)據(jù);如果是數(shù)據(jù)輸出,則是待發(fā)送數(shù)據(jù)的數(shù)據(jù)源;如果沒有數(shù)據(jù)傳輸,此參數(shù)可是為空(NULL)。
? ULONG TransferBufferLength 發(fā)送或者接收數(shù)據(jù)的長度。
? UCHAR RequestTypeReservedBit 請求類型的位掩碼,一般為零。
? UCHAR Request 請求代碼。
? USHORT Value 即 USB 請求中的 wValue 位
? BOOLEAN bIn=FALSE 此參數(shù)為 TRUE 表示數(shù)據(jù)輸出,反之則表示數(shù)據(jù)輸入。
其余的參數(shù)可以保持默認(rèn)。下面就從 USBSOFTLOCK_IOCTL_GET_PASSWORD_Handler 處理函數(shù)為例介紹一下 BuildVendorRequest 函數(shù)的用法,代碼如下:
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 字節(jié)密碼的緩存
UCHAR buffer[8];
// 創(chuàng)建廠商請求,請求的代碼是 REQUEST_GET_PASSWORD,數(shù)據(jù)長度為 8
PURB pUrb = m_Lower.BuildVendorRequest(
buffer, -- 數(shù)據(jù)緩沖
PASSWORD_LENGTH, -- 數(shù)據(jù)長度
0, -- 保留
REQUEST_GET_PASSWORD, -- 請求代碼
0, -- 即 USB 請求的 wValue 字段
TRUE -- TRUE 表示數(shù)據(jù)輸入,反之則是數(shù)據(jù)輸出
);
status = m_Lower.SubmitUrb(pUrb, NULL, NULL, OPERATION_TIMEOUT);
// 判斷返回值
if (status == STATUS_SUCCESS) {
t << "Received buffer is ";
for (int i=0;i<PASSWORD_LENGTH;i++) {
t << " " << buffer[i];
}
t << EOL;
PUCHAR output_buffer = (PUCHAR)(I.IoctlBuffer());
memcpy(output_buffer, buffer, PASSWORD_LENGTH);
}
else {
}
return status;
}
完成廠商請求的編寫之后,就可以進(jìn)行驅(qū)動程序編譯了。驅(qū)動編譯默認(rèn)有兩種版本,即Win32 Checked 和 Win32 Free,其中前者表示調(diào)試版本,而后者表示發(fā)布版本,發(fā)布版本相對調(diào)試版本去掉了大部分調(diào)試信息,比較簡化。
編 譯 驅(qū) 動 的 方 法 是 在 Visual C++ 中 打 開 Driver Studio 的 工 具 條 CompuwareDriverStudio,如圖 63 所示。
圖 63 Compuware DriverStudio 工具條
選擇合適的編譯版本,再單擊 Compuware DriverStudio 工具條的最后一個按鈕即可。請注意不能使用 Visual C++本身的編譯按鈕進(jìn)行驅(qū)動編譯。編譯成功,如果是 Win32 Free 版本,則會在工程目錄的 sysobjfrei386 子目錄下生成驅(qū)動文件 USBSoftLock.sys;如果是 Win32Checked 版本,驅(qū)動文件會在工程目錄的 sysobjchki386 子目錄下。成功編譯驅(qū)動程序之后,將它和 Driver Studio 自動生成的.inf 文件(在工程目錄下)放在同一個目錄下,在查找驅(qū)動的時候指定這個目錄就可以了。
7.2 USB 軟件編寫
最后,再簡要介紹一下 USB 軟件的編寫,即軟件對 USB 設(shè)備訪問的實現(xiàn)方法。
USB 軟件通過 USB 驅(qū)動實現(xiàn)對 USB 設(shè)備的訪問,編寫 USB 軟件必須符合 USB 驅(qū)動定義的接口規(guī)范。一般來說,使用 Driver Wizard 生成一個驅(qū)動工程后,會同時生成一個***ioctl.h的文件,這個文件就是建立軟件和驅(qū)動之間通信的橋梁,它定義了訪問驅(qū)動程序的接口,在編寫軟件的時候需要將其引用進(jìn)去。
USB 軟件的編寫一般有下面幾個步驟。
1) 打開設(shè)備
打開設(shè)備主要需要調(diào)用 CreateFile 函數(shù),它將設(shè)備作為一個文件來處理,代碼如下:
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) 調(diào)用設(shè)備 IO 接口
調(diào) 用 設(shè) 備 IO 接 口 使 用 DeviceIoControl 函 數(shù) 控 制 設(shè) 備 。 這 里 主 要 用 到 兩 次DeviceIOControl 函數(shù),即設(shè)置密碼和獲取密碼,它們分別對應(yīng)驅(qū)動中已經(jīng)定義的 IO 控制接口函數(shù)。例如,設(shè)置密碼接口函數(shù)的調(diào)用方法如下:
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) 關(guān)閉設(shè)備
和打開設(shè)備對應(yīng),關(guān)閉設(shè)備就是調(diào)用 CloseHandle 函數(shù)關(guān)閉設(shè)備的句柄就可以了,例如:
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軟件的詳細(xì)代碼請參考源代碼中的cube測試程序,它模擬了一個硬件加密設(shè)備的工作過程。cube程序運(yùn)行后會出現(xiàn)一個立方體,使得立方體轉(zhuǎn)動表示正常的程序運(yùn)行狀態(tài)。程序運(yùn)行需要密碼,但是密碼不是保存在計算機(jī)上,而是保存在USB設(shè)備上,并且程序運(yùn)行時需要及時校驗密碼,一旦密碼校驗失敗(可能是因為密碼不正確或者USB設(shè)備被移除),程序都會停止運(yùn)行。方法是首先選擇菜單File—>Open Device打開USB設(shè)備(如圖64所示),如果打開設(shè)備成功,選擇File—>Play Cube,在出現(xiàn)的密碼輸入框內(nèi)輸入密碼,如果密碼正確,立方體就會開始轉(zhuǎn)動,并且cube程序在不時地和USB設(shè)備之間進(jìn)行密碼校驗(可以看到PDIUSBD12的GOODLINK燈會不停的閃,這表示有數(shù)據(jù)傳輸)。還可以通過選擇File—>Set Password設(shè)置密碼,此密碼會通過Set Password請求發(fā)送給設(shè)備。
圖 64 cube 程序運(yùn)行界面
總結(jié)
本篇首先說明了 USB 系統(tǒng)的體系結(jié)構(gòu)以及 USB 協(xié)議相關(guān)的內(nèi)容,之后,詳細(xì)介紹了一下USB 接口器件 PDIUSBD12 的使用方法,最后,本章通過一個實例描述了使用 FPGA 接口 PDIUSBD12開發(fā) USB 接口的流程。本篇的學(xué)習(xí)要點(diǎn)可以總結(jié)如下:
首先,對 USB 協(xié)議的了解是最為重要的。雖然 PDIUSBD12 芯片能夠完成很多協(xié)議解析工作,但對 USB 協(xié)議的了解程度還是對整個開發(fā)過程起到了決定性的作用。USB 協(xié)議非常的復(fù)雜,熟悉 USB 協(xié)議的方法應(yīng)該是由大到小,即首先了解 USB 通信的基本原理,比如控制傳輸、批量傳輸?shù)脑砗吞攸c(diǎn);然后再了解各個傳輸?shù)慕M成,即每個傳輸首先發(fā)送的是什么數(shù)據(jù)包,然后接受的是什么數(shù)據(jù)包;最后再去分析每個數(shù)據(jù)包的格式、意義等。
其次,需要對 PDIUSBD12 芯片的比較了解,比如它的各個信號引腳的功能、特性,更為重要的是其通信時序和控制命令。
最后,對各種語言以及各種開發(fā)工具熟悉也是非常重要的。在本次設(shè)計中,需要用到的開發(fā)語言很多,包括 VHDL、C++(Visual C++);此外,本次設(shè)計還用到了多種開發(fā)工具,包括EDA 開發(fā)、驅(qū)動開發(fā)、軟件開發(fā)等,只有熟悉這些工具才能夠快速的進(jìn)行開發(fā)。USB 體系非常龐大,所以編寫本章也是為了夠幫助讀者跨入 USB 開發(fā)的大門,希望讀者通過本篇的學(xué)習(xí),能夠設(shè)計出更為完善、高效的 USB 接口。
本篇到此結(jié)束,各位大俠,有緣再見!