2013年5月6日 星期一

用 SL811HS 學習 USB 介面 - Part01

     通用序列匯流排 (USB) 是有史以來最成功的通訊介面,目前有數億計的 USB 設備存在於
這個世界上,許多晶片設計公司都已經紛紛推出針對不同裝置設計的 USB 橋接器晶片,像
常見的裝置如:鍵盤、滑鼠、掃描器介面、影像、印表機、磁碟控制器、網路介面、序列埠、並列埠甚至是一些特殊介面 CAN ( Controller Area Network )、GPIB (標準化後又稱 IEEE
 488),吾人碰巧有用過 USB-GPIB 的有趣經驗,這是因為以前有一台示波器後面有一個
USB 埠在好奇心的驅使下接上電腦發現電腦識別為 TEK-USB-488 設備,才知道原來這是
有內建了 USB-GPIB 轉接晶片的示波器,透過這個介面,可以直接用 C/C++ 程式下 SCPI
(Standard Commands for Programmable Instruments) 指令直接控制示波器的設定,像是
螢幕亮度,語系,調整時間刻度,電壓刻度,擷取示波器上的數值。



    相對於原始 GPIB 連接頭而言,這顯示出 USB 許多好的特點,USB 連接頭簡單巧小,
成本極低,USB 連接線到處都可以購得,連接電腦安裝驅動程式即可使用,而採用原始 GPIB
連接頭不僅占空間,它的連接線更不好買而且又貴,電腦端還得要有一張 GPIB PCI 介面卡
才能與儀器連接,實在不方便。這邊就開始點出本篇主題了,因為 USB 的方便是有代價的
付出換來,這是甚麼意思 ? 因為 USB 系統為了能夠支援任意設備,引入了一種稱為
描述子 ( Descriptor ) 的機制,這個需要驅動程式去配合韌體程式搭配,還要有應用程式
介面 (API) 去呼叫驅動程式提供的服務,這個正是許多傳統韌體工程師難以面對的窘境,
也就是說 "通訊介面" 不在是一堆接腳透過不同排列組合來表達,而是被畫分至抽象層
用 描述子 的方式,報告給驅動程式,這樣的機制給了 USB 無窮的擴充能力,也就是說
只要有 驅動程式韌體程式 搭配,USB 就可以 "偽裝" 任何其他的通訊介面,或是講得更
廣義就是指可以偽裝出任何設備,當然也可以直接走原生USB通訊而不偽裝。簡單的講,
就是 USB 的便利是建築在開發人員的痛苦上,USB 對開發人員來說相當複雜,所以最終造就
了一個很廣的 USB 產業鏈,物理層晶片開發,韌體與驅動撰寫,USB 設備的測試,為什麼要
測試呢 ? 也許你開發一個 USB 設備連接 Windows 上沒有問題,但是接到 Mac 上不見得就可以
,還有就是主機控制器是用 UHCI 還是 OHCI 都要找來測試,要確定整個韌體與驅動在
作業系統上運作沒有問題才行。

   因此開發 USB 通訊介面看起來就有一層神秘的面紗,吾人過來經驗認為並非 USB 不好學
,而是 USB 的相關知識又多又雜,往往造成阻礙,因此,學習 USB 最好的方法 就是挑一顆
跟 MCU 無關的 獨立 USB 控制器來學習,根據 Application Note 與 Data Sheet 一步一步往上
有系統的建構韌體程式。通常 獨立 USB 控制器 都會有專門與 MCU 連接的介面,該介面
可以讓 MCU 存取獨立 USB 控制器內的暫存器,要有系統的建構程式,很明顯第一步就是
應該先不理會 USB 介面,把控制器的記憶體通訊介面先抽象化,這個一般是控制器最底層
也是最重要的工作。吾人認為要踏入學習 USB 通訊領域,最佳的獨立 USB 控制器正是
SL811HS,這是一顆具有雙角色能力 ( dual-role capable ) 的獨立 USB 控制器,也就是說
可以做為 Master 也可以當作 Slave,另外它擁有非常簡單易用的標準記憶體介面,可以與
任意 MCU 結合,可以學習到怎麼處理MCU與控制器在記憶體介面如何通訊,剩下的特色
我列在下面 (只列出重要的特色):

1. 支援 全速(12Mbps) 與 低速(1.5Mbps) USB 傳輸
2. 完全符合 USB Sepecification 1.1
3. 控制器上有 256 bytes SRAM 緩衝區
4. 支援 Ping-pong 緩衝區 ( 例如 EP1-A 正在傳輸的時候,EP1-B 已經準備好下次傳輸的資料 )
5. 自動產生 SOF 與 CRC5/16 ( 這很重要 表示這三種欄位是由硬體產生 寫韌體不必理會 )
6. 自動推進式指標 ( 類似 C 語言中的 *p++,可以減少 讀寫 SL811HS的次數 )
7. 有內建 DPLL ( 表示使用 12MHz 石英震盪器,SL811HS內部會升頻至 48MHz )
8. 8 位元雙向 IO 埠 (位址與資料共用),可以用 A0 來選擇

接著來看看整個 SL811HS 內部方塊圖:

圖 1. SL811HS 內部方塊圖

根據前面所述,要有系統的開發韌體程式,今天 Part01 先了解怎麼有系統的抽象化
記憶體介面,並且實做一般通訊介面都要有的四條函式:

1. 讀取某一個位址上的一個位元組
2. 寫入一個位元組到某一個位址上
3. 以某個位址為開頭連續讀取 N 個位元組
4. 以某個位址為開頭連續寫入 N 個位元組

我選擇的 MCU 是 P89C61X2 換其他 MCU 觀念一樣
先對記憶體存取介面與控制器其他訊號的相關腳位用 #define 做抽象化的動作

#define SL811_CS      P2_1 // 晶片選擇訊號, Active Low
#define SL811_A0      P2_0 // 0 表示資料匯流排為位址, 1 表示資料匯流排為資料
#define SL811_RD      P3_7 // 讀取訊號, Active Low
#define SL811_WR      P3_6 // 寫入訊號, Active Low
#define SL811_MS      P2_4 // 1 表示 USB Slave, 0 表示 USB Master
#define SL811_RST     P2_2 // 重置訊號, 1 表示硬體重置
#define SL811_INTRQ   P2_3 // 中斷訊號, Active High
#define SL811_PORT    P0   // 資料匯流排 D[7:0]


SL811HS 的 A0 腳位 可以控制目前的 8-bit Data 是位址還是資料,一樣做抽象化

#define DATABUS_IS_ADDRESS 0
#define DATABUS_IS_DATA    1



至於控制 SL811HS 的主僕控制腳位 M/S,可以讓我們抽象化出第一條 HAL

#define SLAVEMODE 1 // set to usb slave mode
#define HOSTMODE  0 // set to usb host mode
#define SL811SetMS(X) SL811_MS = (X)


HAL 函式一般就是指巨集型式的低階函式,一般用來封裝存取暫存器的動作,使得
韌體程式只需要呼叫 HAL 函式即可,不需要理會底層的暫存器如何作動。

然後參考 Interfacing an External Processor to the SL811HS 裡面所講述的記憶體讀寫時序
用程式的方式寫出來,首先給出讀寫時序圖:

SL811HS 記憶體讀取時序:


SL811HS 記憶體寫入時序:

根據讀寫時序圖就可知道,nCS腳位要拉低,就相當於對記憶體介面做 "打開" 的動作
其他 nWR、nRD、A0、D[7:0] 的存取才會有效,所以可以將晶片的 "打開" 動作 與
"關閉" 動作進行抽象化,實現兩條 "Open" 與 "Close" 的 HAL 函式

#define SL811Open()  SL811_CS = 0
#define SL811Close() SL811_CS = 1


有了上面這些低階的基礎定義後,根據讀寫時序圖,我們需要一條能夠用來選擇
位址的 HAL 函式,SL811SelectRegisterAddress 此 HAL 函式可以讓我們選擇位址

#define SL811SelectRegisterAddress(ADR) \
{                                       \
    SL811_PORT = (ADR);                 \
    SL811_A0   = DATABUS_IS_ADDRESS;    \
    SL811_WR   = 0;                     \
    SL811_WR   = 1;                     \
}


第二部分在看讀寫時序圖資料的部分,類似的技巧我們分別可以對讀寫一個位元組
產生兩條 HAL 函式 SL811ReadRegister 與 SL811WriteRegister

#define SL811ReadRegister(DAT)    \
{                                 \
    SL811_PORT = 0xff;            \
    SL811_A0   = DATABUS_IS_DATA; \
    SL811_RD   = 0;               \
    (DAT)      = SL811_PORT;      \
    SL811_RD   = 1;               \
}

#define SL811WriteRegister(DAT)   \
{                                 \
    SL811_PORT = (DAT);           \
    SL811_A0   = DATABUS_IS_DATA; \
    SL811_WR   = 0;               \
    SL811_WR   = 1;               \
}


SL811HS 有支援 Auto Address Increment Mode,這個功能簡單的講就是當選定一個位址 A 後
後,你讀寫一個值從 位址 A 後,硬體內部會自動作 A++,預先指向下一個位址等待使用者
讀取或寫入值,這種功能其實就是硬體版的自動推進指標 ( 所以前面才說類似 *p++ ),所以
能夠很輕易實現連續讀寫多的位元組的 HAL 函式

#define SL811ReadMultiRegisters(BUF,LEN) \
{                                        \
    while((LEN)--)                       \
    {                                    \
        /* Read a value from databus */  \
        SL811ReadRegister(*(BUF)++);     \
    }                                    \
}

#define SL811WriteMultiRegisters(BUF,LEN) \
{                                         \
    while((LEN)--)                        \
    {                                     \
        /* Write a value to databus */    \
        SL811WriteRegister(*(BUF)++);     \
    }                                     \
}


看到上面的 HAL 函式可以注意到,其實就是在用迴圈連續呼叫讀寫一個位元組的 HAL 函式
並不需要在選定寫入的位址,這就是 Auto Address Increment Mode 的特性。
因為我們已經把需要用到的低階介面完全 HAL化,整個記憶體存取介面就變得很簡單,就是
利用這些 HAL 函式就能夠輕鬆實現前面所說的四條讀取 SL811HS 暫存器內容的函式
分別是 SL811ReadSL811WriteSL811BufReadSL811BufWrite

/* 
    Read a byte from SL811
    reg_adr = register address
    return  = a data byte at register address in register
*/
BYTE SL811Read(BYTE reg_adr)
{
    BYTE u8_data;

    SL811Open();                         /* Chip selection is enabled */
    SL811SelectRegisterAddress(reg_adr); /* Write a address to databus */
    SL811ReadRegister(u8_data);          /* Read a value from databus */
    SL811Close();                        /* Chip selection is disabled */
    
    return u8_data;
}

/* 
    Write a byte to SL811
    reg_adr = register address
    u8_data = a data byte to be written to this register address
*/
void SL811Write(BYTE reg_adr,BYTE u8_data)
{  
    SL811Open();                         /* Chip selection is enabled */
    SL811SelectRegisterAddress(reg_adr); /* Write a address to databus */
    SL811WriteRegister(u8_data);         /* Write a value to databus */    
    SL811Close();                        /* Chip selection is disabled */
}

/*
    Read bytes from SL811 to a buffer
    reg_adr = register address
    buf     = buffer address
    len     = buffer length
*/
void SL811BufRead(BYTE reg_adr,BYTE *buf,BYTE len)
{
    /* 使用 Auto Address Increment
       SL811 具有硬體級指標後置推進 */
    SL811Open();
    SL811SelectRegisterAddress(reg_adr);
    SL811ReadMultiRegisters(buf, len);
    SL811Close();
}


可以看到有系統的建構 HAL 函式會逐漸將問題簡化,最後整個實現的這四條基礎存取
SL811HS 暫存器的函式,可讀性變得非常好,而不是一堆像魔術般的運算式直接操作暫存器
今天這個 Part01 就是到這裡,一旦我們有了這四條函式,就可以存取 DATASHEET 裡面所
記載 SL811HS 內各種 USB 介面有關的暫存器,也就可以開始討論 USB 協定基礎並且一樣
把整個 SL811HS 的 USB 介面存取一樣進行 HAL 化,當然這得要根據 DATASHEET 先對
SL811HS 進行建模的動作,總之,這些就下一個 Part 在討論了。

沒有留言:

張貼留言