今天給大俠帶來基于 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_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 所示。
圖 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 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 所示。
圖 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: 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 所示。
圖 48 USB 接口軟件模型
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 步?
圖 52 Driver Wizard 第 4 步
第 4 步比較重要,是選擇驅動總線類型,應該選擇 USB(WDM Only),并且注意要在 USB VendorID 和 USB Product ID 中輸入和固件中設備描述一致的信息。這里請注意 Vendor ID 一定是0x0471,因為使用的是 Philips 的 PDIUSBD12 芯片,其 Vendor ID 固定為 0x0471。單擊 Next按鈕,進入如圖 53 所示對話框。
圖?53?Driver Wizard 第 5 步
第 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?
?
完成廠商請求的編寫之后,就可以進行驅動程序編譯了。驅動編譯默認有兩種版本,即Win32 Checked 和 Win32 Free,其中前者表示調試版本,而后者表示發布版本,發布版本相對調試版本去掉了大部分調試信息,比較簡化。
編 譯 驅 動 的 方 法 是 在 Visual C++ 中 打 開 Driver Studio 的 工 具 條 CompuwareDriverStudio,如圖 63 所示。
圖 63 Compuware DriverStudio 工具條
選擇合適的編譯版本,再單擊 Compuware DriverStudio 工具條的最后一個按鈕即可。請注意不能使用 Visual C++本身的編譯按鈕進行驅動編譯。編譯成功,如果是 Win32 Free 版本,則會在工程目錄的 sysobjfrei386 子目錄下生成驅動文件 USBSoftLock.sys;如果是 Win32Checked 版本,驅動文件會在工程目錄的 sysobjchki386 子目錄下。成功編譯驅動程序之后,將它和 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. ", 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請求發送給設備。
圖 64 cube 程序運行界面
?
總結
?
本篇首先說明了 USB 系統的體系結構以及 USB 協議相關的內容,之后,詳細介紹了一下USB 接口器件 PDIUSBD12 的使用方法,最后,本章通過一個實例描述了使用 FPGA 接口 PDIUSBD12開發 USB 接口的流程。本篇的學習要點可以總結如下:
首先,對 USB 協議的了解是最為重要的。雖然 PDIUSBD12 芯片能夠完成很多協議解析工作,但對 USB 協議的了解程度還是對整個開發過程起到了決定性的作用。USB 協議非常的復雜,熟悉 USB 協議的方法應該是由大到小,即首先了解 USB 通信的基本原理,比如控制傳輸、批量傳輸的原理和特點;然后再了解各個傳輸的組成,即每個傳輸首先發送的是什么數據包,然后接受的是什么數據包;最后再去分析每個數據包的格式、意義等。
其次,需要對 PDIUSBD12 芯片的比較了解,比如它的各個信號引腳的功能、特性,更為重要的是其通信時序和控制命令。
最后,對各種語言以及各種開發工具熟悉也是非常重要的。在本次設計中,需要用到的開發語言很多,包括 VHDL、C++(Visual C++);此外,本次設計還用到了多種開發工具,包括EDA 開發、驅動開發、軟件開發等,只有熟悉這些工具才能夠快速的進行開發。USB 體系非常龐大,所以編寫本章也是為了夠幫助讀者跨入 USB 開發的大門,希望讀者通過本篇的學習,能夠設計出更為完善、高效的 USB 接口。
?
審核編輯:黃飛
?
評論