救火奇兵之 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;
}
後記
很久沒當救火隊長了,偶爾當當救火奇兵,也算是練練腦袋,還好腦袋還算靈活。
作者已經移除這則留言。
回覆刪除您好,非常感謝你的文章。
回覆刪除我們目前在手上的案子也遇到類似的問題,不知道是方便給予技術上的協助?
如果有需求,可以直接 email 跟我聯絡。:-)
刪除