2019年10月25日 星期五

Hinet 網頁系列 --- USB DIY 系列(十)---USB DIY 講座 (八)Bulk Transfer 基礎

(補充說明:說真的,在我自己本身的USB 系統開發經驗裡,真的遇到 Bulk Transfer 的真的

不多,這一部分真的需要考驗著USB 控制IC 及系統功力的時刻,我想除非真的是有大案子或是

大公司的特殊應用,否則我認為一般系統應用者,用的機會真的不多。話雖這麼說,

但當時我這一篇文章還真的花了我一點功夫與心血才完成的,我想也不一定會有人會認真的

拜讀,除非真的很不幸的是:您真的遇到這樣子的應用場合了。那有可能你真的被逼到要把

USB 的傳輸效能給"搾出來",你哪就多少加減研究一下吧。其實在這篇文章中也是許多

USB 韌體的基本架構與觀念,真的也是值得有空研究一下吧。最後。祝各位好運吧。)



>>>>++++++++++++++++++++++++++++++++++++
--------------------------------------------------------------------------------------------------------------------
        在之前我介紹過有關 USB I/O 的東西,那些基本上的 I/O 可以用 Control pipe 作掉就可以了。

也可用來傳輸一些少許的資料,但若是要以 Control Pipe 來傳一些比較多的資料時,

就有點不太方便了。為什麼?!因為,Control pipe 是走 Endpoint 0 的路徑。

雖然,也可以準確的傳輸資料但(當然速度上沒有像 Bulk 那麼快)。但他最不好用的部分

應該是:他在Firmware 方面,很容易跟我們USB 的Housekeeping 的程式混在一起。

在 Firmware 維護真的很不好用,當然,您用哪一家的解決方案,我是不清楚,但我至少

寫過三家的方案,基本上都是一樣的。我是不知道大家有沒有K過一本 Intel 的USB firmware 

的書,薄薄的一本,書名我忘了,只有一百多頁,應該是一位新加坡的老中寫的

(因為封面的英文名字是姓:Tan~不要誤會,就是閩南語的陳!)。

我想那本書應該是寫USB  Firmware 的經典。(對不起,我當初也是跟別人借來看的,我想,

目前所有以8051為主的 USB Controller 的起源應該就是這本了!)所以,在此我才會介紹用 

Bulk Pipe 來傳一些比較大的資料,(什麼是比較大資料?我是覺得只要是KBytes級 都算是

比較大的了!)。您會發現,只要Control pipe 搭配 Bulk Transfer pipe 在韌體與軟體就可以

發揮不錯的傳輸性能了。也可以拿來作一些很不錯的東西了。至少,我認為在這兩方面

(軟體與韌體)的維護上就比較輕鬆一點。因為這兩者在PC端的軟體有點類似,甚至,

有些在DRIVER上是可以共用一個 Function Call 的。

        另外,因為一般 USB Controller 的data buffer 都不可能太大的,所以,勢必在Firmware 上

必須作到一些記憶體的管理,以期達到USB 傳輸的Performance 表現。不過,若大家對USB 

的感覺還不到的話,我還是建議先求穩吧。接下來,我就用一些範例來說明基本的 

Bulk Transfer。然後,我再利用另外一個章節來作一些好玩又實際應用的東西。
--------------------------------------------------------------------------------------------------------------------------

        在一些實際運用,我講的實際運用指的是DIY一些用來傳一些資料,像傳統的UART (Serial) 

或 Parallel來說,就屬 Control 及Bulk 最常用。所以我才專門講這兩部分。首先先看一下這

兩者之間的差異:以下是一個 Control Pipe 的 傳輸:


這張圖是從Cypress 的一份資料抓下來的。再來考考或複習一下大家對USB的基本觀念,

其實,這張圖舉的例子不好,不知大家有沒有注意到圖中有一個很奇怪或較不合理的地方?

首先,您要會解釋上圖的意義。一般 Control pipe 包含了兩個方向(針對同一Endpoint,

而Bulk 就只能定義一個方向而已!),看您是否能夠跟著我解讀上圖的所隱含的意義?

上圖是一個 Control In Protocol ,就是Host 透過Control Pipe 跟DEVICE 要了一筆資料。

很簡單,大家都會。而比較奇怪的地方是:當 Host 要完資料之後,會再發出一個 Out Token

 ,但 Device 竟然是回 NAK !!以  USB Procotol 來說,沒有錯!但以系統應用來說:

真的沒意義。因為這個OUT Token 就只是一個 NULL OUT (不帶資料的),

所以Device 的韌體根本不用理他,直接回ACK就可以了。好了,您以為您跟我的解釋

已經解釋完了?!錯!這個故事只講一半,因為我說的,您很少會真的去看USB 真正的

運作,尤其是 USB Housekeeping 的韌體。上圖若反過來看就對了,而且合情合理了。

什麼?反過來看?!就是,上圖若是一 Control Out 一筆資料給Device,再結束一個

 NULL In 就合理了 !是嗎?您可以解釋這個現象嗎?為什麼 ?因為一般USB Control In 

一筆資料之後,Device 其實不用再處理其他有關跟 USB 的東西了。就算 Host 還要繼續

要資料,USB Device 的 USB Housekeeping 韌體也不用急得回NAK,因為可以直接擋在

這個  In Token 動作上。聽不懂?!要好好的體會喔!

    反過來,若是Host Out 了一筆資料給 Device 的話,當您USB Device 的USB Housekeeping 

韌體回 Out Token 之ACK之後,可能還要處理這筆資料,他可能是一組命令或需要及時

處理的資料,此時,您就得先搬資料,再回 USB Protocol 

這個 USB Protocol 指的就是後面那個  NULL In 的ACK~就是說您要先用NAK把最後

那個 NULL In 擋下來,讓HOST先等在這一組Setup Token 中),您不要以為您USB Controller 

性能有多好,跟您說:PC 南橋的USB 比您還強,一旦您錯放,下一個Setup 再緊接

來,您USB Housekeeping 韌體就手忙腳亂。尤其當您碰到OHCI的南橋,

包您USB 出狀況。我想這種Bug 您要抓也不是一天或一個月功力的人就抓得到的!!

真的不騙您,書本或教科書才不會教您這個呢!!您學USB 學這麼久,您有真的去解析

這樣的問題嗎?!為什麼人家 USB Controller 要設計成那個樣子?為什麼要用韌體來維護 

 USB Protocol 的東西?您真的有體會到嗎?說到這裡您就知道要用 Control pipe 去傳一些

資料是蠻討厭的,我說的,這部分會跟您 USB Device 的 Housekeeping 韌體混在一起,

又要  Parsing 所傳的資料內容,所以,一不小心韌體程式就容易長大。

所以,我們才需要額外的 Bulk Transfer Pipe 來作簡單的資料傳輸。

以下就是BULK Transfer Protocol :



上圖就是分別是一個 Host 要資料(Bulk In),一個傳資料(Bulk Out),您應該知道哪一個是

哪一個吧?從上圖我們可以體會得到,一般Bulk In 會比Bulk Out 有效率多了,因為不會有 

Payload 出來暫住USB 的傳輸頻寬。所以,相對來說:若您 USB Device 的 Data Buffer 

若不夠大的話,中間那個 NAK 會一直出現來搶您USB BUS的頻寬。

又相對於Setup 的 Control Pipe 來說,至少 Bulk Transfer 沒有那些NULL IN 或NULL OUT 的

東西來干擾,速度當然快多了。(更何況一對 SOF 之間可以塞進好幾個BULK In/Out!)

但相對來說,因為在Bulk In/Out 過程中,基本上是沒有其他對應的Token 出現,所以,

要傳資料的多寡,就要事先定義清楚的(您不要以為在所謂 MSDC--Mass Storage Device

 Class中的 BOT -- Bulk Only Transfer 就不用事先定義長度,他也是有的!幸好我也做過 !)

這樣又會牽涉到屬於您自己寫韌體時,所必須自己定義的東西了,說真的,

您平常其他沒有寫過一些有關資料傳輸的韌體的話,對您來說,可能會有一些挑戰性。

所以,您若沒有這方面的經驗的話,我建議您可以先利用一般的 8051 的 UART 做一些

簡單的練習會比較有手感。瞭解?

        接下來,我就用一些實際的例子來說明:

        首先,我們先瞭解一下我的資料流的大綱,在USB DIY 講座( 七) 中知道我的USB Controller

 中有 16 KBytes 的Data Buffer,那到底要切多少出來作應用,就看每個人的需求而定。

當然,您可以把他一切為二,各為 8 KBytes + 8 KBytes 的 AB buffer 方式,這就是有名的

 Ping-pong Buffer的作法。但在此我不這樣做,因為,許多真正DIY的應用來說,

使用彈性遠比性能更重要,當然有越大的Data  Buffer  他所能 Gain 的好處就越多,

不只是性能表現而已,而是更多的使用彈性,譬如,可切成不同的BANK 來作不同的

應用與儲存空間。在此範例我就用 2KBytes 來作範例,我說的以KBytes 的大小來看,

是比較有效率一點,因為 2KBytes約佔兩個  SOF ,所以,萬一有些資料要放棄的話,

剛好也可以塞Setup Token來通知Device ,理想上,在Bulk 傳輸上,就得盡量作到像我

USB DIY 講座( 二) 中所顯示的內容,幾乎是每64 Bytes 緊接著64Bytes 的一筆一比進來

會比較好一點。但我說過,Buffer畢竟有限,但不求完美,也求盡善。若是像 Cypress 的

架構那樣,每64 Bytes 就休息一下(因為有其他中斷程式要處理啊!當然他Endpoint 的 Buffer

也定義這麼大了!),做起來還蠻沒效率的,重點還要韌體介入(關於這部分的原因,

您可參考我以下的範例),相對來說會增加穩定性的風險。

        在作之前再強調一個USB 傳輸很重要的觀念:不管您要傳的資料大小,那怕是只有 

64 Bytes (一個 Packet),USB在寫傳輸韌體的觀念:是用擋的,而非用寫的

我想大部分的 USB Controller都是一樣的。解釋一下:什麼是用擋的?而非用寫的

這點是跟 Serial 或 Parallel 或是在PC上寫程式迴然不同的觀念。先講為什麼?

因為USB Bus上,是隨時都有Protocol 訊號在跑,您用寫的根本來不及硬體Protocol 訊號

還是強調一下:USB 不比Serial or Parallel,serial & Parallel 是有傳資料時,BUS 才有訊號的。

什麼是用寫的?就像在PC上或在一般 8051 韌體上,像我們在8051 要傳一個Bytes 就是:

            mov    SBUF,  a ;

但 USB 的傳輸觀念是用擋的:先定義要傳的內容位置與傳的資料長度,然後像水閘門一樣

放開(填USB Control Register ),硬體就把您的資料批哩啪啦放出去或抓進來。

然後您才後知後覺的知道傳完了(中斷通知您)!我想,一般人開始作USB時都沒體會到,

因為書本都只教您要填這個Register或清那個 Control bit 。渾然不知原因。這就上面那兩張 

Bulk In/Out 圖所要彰顯的意思,看一下範例(在韌體方面程式):

InitialM2BufferToBulKIn:
        lcall ClrDMA3Addr
        EP1TxNak         ;; PC 請您等一下,我要準備資料!
        clr A
        mov iBulkLSB, A ;; USB Bulk In Tx Loop,
        mov iBulkMSB, A ;; mov iROMBank, A
        mov iPageCNTLSB, A ;; 1 page = 256 bytes, i.e. ignor the LSB of Address
        mov iPageCNTMSB, A ;; 1 page = 256 bytes, i.e. ignor the LSB of Address

        ;; REG_WR MBLHR, #FFh ;; Set Buffer Ready Size 64 Bytes
        REG_WR MBLHR, #00h ;; Set Image-Ready always ready
        REG_WR MBLLR, #00h

        ;;---USBLoopPageSize:
        REG_WR USBTxBc1, #20h ;; Set "Page size" Counter
        REG_WR USBTxBc2, #00h ;; DMA3 Byte Counter for USB

        REG_WR USBTxPk1A, #40h ;; for bulk transfer fix 64 bytes
        REG_WR USBTxPk2A, #00h ;; Set "Page Size" for 64 Bytes

        lcall ISP_BulkRead2KBytesUSingPAGE    ;; 別吵!我還在搬資料!


        clr A
        ACC2IO D3ARCR

        USBReadM2En
        ;; LED_G_ON
       EP1Enable         ;;PC 好了!您可以把資料取走了!

        ret

這是一個 Bulk In 的韌體程式,看到沒當我在 USB Controller 中準備資料時,我是先用NAK 

把PC 端的要求先檔掉,再寫一個Call function 慢慢搬~(呵~不好意思讓您看到她是一個

Atmel ISP的應用程式)。然後,最後放開~讓USB 抓資料。您能體會嗎?!若反過來呢?

是PC要將資料傳下來呢?!就是 Bulk Out :

InitialM2BufferFromBulKOut:        lcall ClrDMA3Addr

        clr A
        mov iBulkLSB, A
        mov iBulkMSB, A
        mov iBulkOutCnt, A
        mov iROMBank, A
        mov iPageCNTLSB, A ;; 1 page = 256 bytes, i.e. ignor the LSB of Address
        mov iPageCNTMSB, A ;; 1 page = 256 bytes, i.e. ignor the LSB of Address

        ACC2IO USBRxLoByCnt
        ACC2IO USBRxHiByCnt

        REG_WR MBLHR, #00h ;; Set Image-Ready always ready
        REG_WR MBLLR, #40h

        REG_WR USBTxPk1A, #40h ;; for bulk transfer fix 64 bytes
        REG_WR USBTxPk2A, #00h ;; Set "Page Size" for 64 Bytes

        clr A
        REG_WR USBRxLoByCntIrq, #00h ;; for bulk transfer fix 64 bytes
        REG_WR USBRxHiByCntIrq, #40h ;; Set "Page Size" for 64 Bytes


        mov DPTR,#D3CR
        mov A,#11101000b ;;b4=0:M2 b3=1:Write b2=0:USB, inhibit the DMA3 BCount load!
        movx @DPTR,A

       EP2TxTimeOut        ;; PC 您可以把資料放進來了!

        ret
    有沒有?我先定義要收資料是要擺在哪?之後,才放開讓資料能收進來

(放心好了,當PC的應用程式要傳資料給您時,在USB BUS 一定都塞滿 了PayLoad 外加

您USB Controller 所回的 NAK ,就像上圖中所示!),哈~哈~ 您又被我給擺了一道了,

看上面那段Bulk Out initailize 程式是沒有意義的!!Bulk Out 的學問不在Initial 時,

而是『後知後覺』的收到資料後的中斷程式:

EP2_Rx:        EP2NAK        ;;請PC您等一下!        ;;--- 2K Full--
        ;; cpl P1.1
        ;;==== LED Control Routine===    ;; 閃一下燈代表還再傳!
        cpl LEDR_                        ;; 既然都叫PC等了~利用一下前一章節I/O 控制 !
        jnb LEDR_, LED_R_Blanking        ;; Firmware 自己控制I/O !
            LED_G_ON
        sjmp LED_R_Blanking_END
LED_R_Blanking:
            LED_G_OFF
LED_R_Blanking_END:
;;---
;;----- Move the internal Buffer to target buffer
;;---
        lcall ISP_BulkWrite2KBytesUSingByte    ;;慢慢搬資料!
        lcall ClrDMA3Addr

        inc iBulkLSB
        mov A, iBulkLSB
        jnz BulkOutCNT01
        ;;--
        inc iBulkMSB
BulkOutCNT01:
        mov A, iBulkLSB
        cjne A,BulkLength_LSB,ContinueousBulkOut
        mov A, iBulkMSB
        cjne A,BulkLength_MSB,ContinueousBulkOut
        ;;--- End of Host request --
        LED_G_OFF

        ret        ;; 搬完了!ContinueousBulkOut:

        EP2TxTimeOut    ;; 沒搬完,放開閘門讓資料流進來,繼續搬!!
        ret

寫完了,簡單吧!

            還沒完~ USB 又不是只作 USB Device 端,還得寫PC 端程式耶!先看Bulk In 的程式:

       hRead = open_file(inPipe);
       if(hRead==INVALID_HANDLE_VALUE)
       {
            ///----
            MessageBox("Open in pipe Error",NULL,MB_OK);
            return ;
        }
        BOOL success;
        UCHAR *fwbufBK;
        fwbufBK = fwbuf; // backup the pointer
        for (int j = 0; j < ROMMax; j++)
        {
            success = ReadFile(hRead,
                                fwbuf,
                                64*32,
                                &nBytesRead,
                                NULL);

            fwbuf += 64*32; // move buffer point to next Bank !!
        }
        CloseHandle(hRead);

            所以,我可以以 2 KBytes 為單位,愛傳多少就多少,對上層AP應用程式也只能管到

這裡而已,至於如何丟封包,那是底層Driver 跟微軟的USB 硬體Driver 的事了。 

相對來說: Bulk Out 的程式:

        // Open Bulk Out pipe
        hWrite = open_file(myoutpipe);
        if(hWrite==INVALID_HANDLE_VALUE)
        {
            MessageBox("Open Out Pipe Error",NULL,MB_OK);
            return ;
        }
        //---
        //--- Loop of Download routine ---
        //==============================================================
        for(k=0; k<ROMMax;k++)
        {
            //--- Bulk Out 2KBytes
            success = WriteFile(hWrite,
                                fwbuf,
                                64*32,
                                &nBytesRead,
                                NULL);

            fwbuf += 64*32; // move buffer point to next Bank !!
        }
        //--------------------------------
        //--- End of the Download Loop                                                 
        CloseHandle(hWrite);

真的寫完了~哇~PC程式更簡單!誰說USB 很難?!

            我這個人就是喜歡務實,口說無憑,就讓一些圖像結果來說明上述的實驗:


我是故意讓韌體產生一個固定的資料型態,讓我們容易除錯,畫面上只秀出前面的 

256 Bytes而已。而且是要求Device 傳了 4KBytes 上來!也就是 2* 2KBytes 

(就上述PC軟體程式中 ROMMax = 2),這代表韌體還 得準備第二組 2KBytes 資料來

測試韌體的反應:


 從以上的兩張圖,代表我沒有騙您,我真的是USB Device 抓資料上來的!而且當我韌體

知道PC端是要跟我Device 端要資料時(就是利用一般 Setup Control pipe來下命令的~

而MSDC的BOT 就是一直以 Bulk 傳命令的),我就開始用NAK 擋掉PC的需求,

就是圖上 Packet#5072 一直到 Packet #6313,都是硬體幫我擋掉的。一旦我準備好資料。

我韌體一放開水閘門~資料就以飛快不間斷的傳出!直到下張圖所示:


就是因為PC端要了 4 KBytes 資料,所以,PC 端的Driver 會『死咬』我的Bulk-in 要資料~

這時您就要一定回資料,否則您PC端的USB底層的Driver 會一直Lock 住,不是當機

(此時,您的應用程式也沒輒了,我想連您的USB Driver 也是一樣的,這時是歸微軟

USB作業系統的驅動程式在管的),而是您韌體要加油了~這是做 Bulk 跟 Control不一樣

的地方。也是很容易玩到PC當機的地方(剛開始寫PC軟體,尤其是寫Driver 的人,

您會常看他們常重新開機PC就是這樣子來的!這樣的Protocol 不是很危險嗎?

您又沒好好K USB規格了,規格跟您講,此時您可回Bulk Stall 來解套的哩啦~

不過您也不要太高興,當您回個 Stall 之後,作業系統會馬上來問您:為什麼?!

結果您的韌體還是得回他為什麼的哩啦!!哈~哈~您都作不完的啦!)。

        所以,上圖中 Packet #6410 到 Packet #19224 之間,又是硬體幫我擋掉,韌體能夠慢慢

的準備資料(對不起,在這個韌體程式內我是沒準備,我是故意delay讓PC等,然後再

傳不同的資料以示不同的資料區塊!)~ 講完Bulk Transfer的基本操作流程了。

清楚了嗎?!

        再來利用上面的例子來討論 USB Performance 的問題:從 Packet #6313 到 Packet #6410 為 

6313-6410 = # 97,而每三個的Protocol就傳一筆 payload,所以是約 97/3 = 32 個

Payload = 32*64 = 2048 bytes!果然是一點耽誤都沒有,果然是性能優越

(不是我愛吹牛,這就是不只要懂USB,還得悟到USB的精髓的)。

但是從Packet #6410 到 Packet #19224 之間 就耽誤了很久~不是沒傳,而真的被耽誤了~

所以也驗證了我所說的~一來Device 內部的  Buffer 不夠大,不能一氣呵成的傳資料

會造成傳輸瓶頸;二來就是Device 的微控器(Such as 8051)速度不夠快,讓USB 的

頻寬在浪費!當然,IC內部的Buffer 總不能無限的擴充,所以瓶頸一定會存在,

但就看您如何去調配~就像我這顆USB Controller 我可以全部 16 KBytes 都用來支援 Bulk In ,

那速度至少也要拉個四倍~當然您還可用我上面所說的 Ping-pong Buffer 的作法~

但是Ping-pong 的作法也是需要微控器去設定控制的暫存器(Registers),此時又會牽扯到

微控器的性能問題。因為一般IC設計的暫存器都會擺在對8051 說,

就是外部記憶體的位置,而8051 對外部記憶體存取方式就是最長的 12T (Movx + DPTR) 

所以,要怎麼作,我想我已經點出了所有做USB常碰到的問題了。說真的,這些問題都是

作USB Controller IC 的人要去想的。而對我們USB DIY 一族來說,能夠適度調配我們記憶體

的配置,發揮有效傳輸功能就夠了。您同意嗎?

        當然,對一般使用USB DIY來說,您大概也不會留意到這些細節,但您有沒有體會過:

當您解工程問題時,您只要沒把握,心中存著那一點疑問時,那不久的將來肯定

會出問題。(莫非定律)

USB 真的不像傳統的Serial & Parallel那般的簡單,但的確從傳輸的定義與設計觀念來說:

USB真的優異多了,也就是人家可以很容易取代傳統Serial & Parallel的原因。

        其實,講到這裡,我想一般USB 的應用就已經可以作到很好了,甚至相對 USB 1.1 與 2.0 

來說,兩者的精髓是一樣的,差別也只是在傳輸過程中,傳送資料載波的Clock 速度而已,

但是否過來說:您真的抓到我上述討論的重點了嗎?還是您真的用USB 2.0 傳輸Clock ,

反而一直在浪費USB的頻寬呢?

        我想:經由這一系列的講解USB的基本觀念與實驗操作,我相信利用 USB Control + Bulk  

就可以做出許許多多 DIY的東西了。未來我會利用這些觀念,來作一些好玩又有趣的

東西。敬請期待,也歡迎您們提出您們的看法,一起研究。謝謝!
 




1 則留言:

  1. 文章中提到的那一種關於USB 書是這一本:
    "Developing USB PC Peripherals" 作者: Wooi Ming Tan
    https://www.amazon.com/Developing-Peripherals-Using-8X930AX-Microcontroller/dp/0929392388

    我記得網路裡好像有 PDF 檔,這本書很薄的...有興趣的可以自行搜尋
    找一下,但因為已經是20 幾年前的東西。
    有沒有參考價值?就看各位的怎麼看?
    我是當然沒那個必要了。我USB 功力應該是可以了啦。

    回覆刪除