救火奇兵之 Android USB Host API 反應遲緩

話說,Android 在某個版本後,開始提供了 USB Host API,這代表開發者可以不必再用 NDK 和硬梆梆的 C 語言去開發 USB 裝置的驅動程式,而可以完全用 Java 來開發。但是,現實往往沒有這麼美好。

日前,就協助了一個案子,解決了一個 USB 裝置驅動程式的問題,起因就是客戶用了 Android USB Host API 去控制 USB 裝置,但發現 USB 裝置的回應一直不如預期,有時像是掉資料,有時像是沒反應。而同樣的控制邏輯,用純 C 開發的驅動程式配上 libusb 就完全正常,所以我們相信肯定不是控制邏輯上的問題。

剛開始,大家都懷疑是 Java 本身的問題,懷疑是不是 JVM 執行驅動程式太慢,而造成接收 USB 裝置的資料時來不及。但我一直保持著懷疑,因為 USB 裝置回傳的資料並不多,如果 JVM 本身的效能連處理這幾 KB 的資料量都如此差,就實在是太可笑了,我無論如何不相信。

還好最後還是解決了,雖然過程曲折。

USB Request Block 的 16KB 限制

事實上,每次最多傳送 16KB 資料,是一個 bulk transfer 的 URB 限制,使用 Android USB Host API 就會直接遭遇到這個問題,所以不管用什麼方法,怎麼收資料,只要資料太大,你最多一次就只能收到 16KB。

多次收資料所發現的延遲問題

當然,既然一次最多只能收 16KB,我們可以分多次向 USB 裝置要求收資料,但就會發現會莫名掉資料。從 USB 的分析器上來看,該有的命令都有,但就是有掉,後續的資料不管怎麼取都是 0。

後續資料為 0,在這個案子的 USB 裝置設計上是可以理解的狀況,因為該 USB 裝置只會保留資料一小段時間,然後就會清空,所以若之後跟它要任何資訊,他都會回傳空的東西回來。這很明顯,就是我們要資料的過程時間,已經超過了該 USB 裝置正常的情況。

而從收到的資料來看,有收到的資料,經驗證過後發現是斷斷續續的,中間有漏資料。經過測試,發現是每個命令之間的間距時間太長,因為該 USB 裝置會不斷復寫一段緩衝區,如果我們太慢去要資料,那段緩衝區就會被新的資料蓋掉,理所當然的,我們就會漏掉一些資料。

經過各種測試紀錄,很明顯的,Android USB Host API 並沒有這麼聽我們的話,每當我們下命令或進行控制時,他並沒有馬上送到 USB 裝置,會有一些延遲,這才導致這樣的後果。

硬幹 usbfs 的系統程式

如果在這件事上, Android USB Host API 的遲緩導致沒辦法滿足我們的需要,我們只好繞過去自幹了。

但其實並不困難,不管怎麼說,Android 其實就是 Linux,底層肯定是透過 usbfs 去控制 USB 裝置,我們甚至可以不需要 libusb 和其他 framework,而直接去跟 usbfs 要資料。更何況我們只是要收資料而已,用 C 寫一小段程式去直接處理 URB 就可以解決,然後用 NDK 包裝成 JNI 即可。

於是有下面的實作,一個與 libusb 內部實作原理相同,但更為簡化的版本:

#include <stdio.h>
#include <stdlib.h>
#include <linux/usbdevice_fs.h>
#include <sys/ioctl.h>

// We have 32 URBs
#define NUM_URBS       32
#define BUFFER_SIZE    16384

char *getURBs(int fd, int ep)
{
    struct usbdevfs_urb urbs[NUM_URBS];
    struct usbdevfs_bulktransfer bt;
    int len = 307200;
    int sizeCount = len;
    unsigned int urb_num = 0;
    
    // Allocate buffer for image
    char *buf = (char *)malloc(len * sizeof(char));
    
    /* Send out initial URBs */
    memset(urbs, 0, sizeof urbs);
    for (unsigned int i = 0; i < NUM_URBS; i++) {
        urbs[i].type = USBDEVFS_URB_TYPE_BULK;
        urbs[i].endpoint = ep;
        urbs[i].buffer = buf + (i * BUFFER_SIZE);
        urbs[i].buffer_length = (sizeCount < BUFFER_SIZE) ? sizeCount : BUFFER_SIZE;
        urbs[i].actual_length = (sizeCount < BUFFER_SIZE) ? sizeCount : BUFFER_SIZE;
    
        if (sizeCount > BUFFER_SIZE)
            sizeCount -= BUFFER_SIZE;
    
        if (ioctl(fd, USBDEVFS_SUBMITURB, &urbs[i]) < 0) {
            free(buf);
            return NULL;
        }
    }
    
    /* Wait for completions */
    while(urb_num < NUM_URBS) {
    
        struct usbdevfs_urb *urb;
    
        if (ioctl(fd, USBDEVFS_REAPURB, &urb) < 0) {
            free(buf);
            return NULL;
        }
    
        // Completed early
        if (urb->actual_length < BUFFER_SIZE)
            break;
    
        urb_num++;
    }

    return buf;
}

後記

很久沒當救火隊長了,偶爾當當救火奇兵,也算是練練腦袋,還好腦袋還算靈活。

留言

  1. 作者已經移除這則留言。

    回覆刪除
  2. 您好,非常感謝你的文章。
    我們目前在手上的案子也遇到類似的問題,不知道是方便給予技術上的協助?

    回覆刪除
    回覆
    1. 如果有需求,可以直接 email 跟我聯絡。:-)

      刪除

張貼留言

這個網誌中的熱門文章

有趣的邏輯問題:是誰在說謊

Web 技術中的 Session 是什麼?

淺談 USB 通訊架構之定義(二)

淺談 USB 通訊架構之定義(一)

Reverse SSH Tunnel 反向打洞實錄