mbed USB クラスを使用してコントロールリクエスト経由でデータを送信

USB のコントロールリクエストはベンダー独自のコマンドを送信できます。
さらに、セットアップステージに続いてデータを送信または受信できます。データのパケットサイズは通信速度ごとに規定されており、low-speed 8 bytes、full-speed 64 bytes です。セットアップステージは次のような固定データフィールドで構成されています。

Field Size 説明
bmRequestType 1 データの方向、種類、受け取り相手などを指定
bRequest 1 リクエストコマンド
wValue 2
wIndex 2 インデックス
wLength 2 データバイト長

ベンダーリクエストでは wValue と wIndex を自由に利用できます。送信したいデータが 4 バイト以下であれば wValue と wIndex に入れてしまったほうが楽です。それよりも長いデータを受信したいときには、サブコマンドの指定などに利用すると便利かもしれません。

ホスト側クライアント

ホストからベンダーリクエストを送信するプログラムは libusb - Python で作成しました。

4 バイトのキーコードをデータとして送信、または受信します。リクエストコマンドは以下の二つです。

  • Get_Keycodes: 0x10、4 バイトデータをホストが受信
  • Set_Keycodes: 0x11、4 バイトデータをホストが送信
import usb1
import libusb1
import platform
import struct


class USBControl:
    """ USB user driver
    
    """
    
    MANUFACTURER_NAME = ""
    PRODUCT_NAME_PREFIX = ""
    
    VENDOR_ID = 0x1235
    PRODUCT_ID = 0x0050
    
    INTERFACE = 0
    
    HOST_TO_DEVICE = 0 << 7
    DEVICE_TO_HOST = 1 << 7
    
    TYPE_VENDOR = libusb1.LIBUSB_TYPE_VENDOR
    REC_INTERFACE = libusb1.LIBUSB_RECIPIENT_INTERFACE
    
    USB_VENDOR_GET_KEYCODES = 0x10
    USB_VENDOR_SET_KEYCODES = 0x11
    
    USB_GET_KEYCODES_REQUEST_TYPE = DEVICE_TO_HOST | TYPE_VENDOR | REC_INTERFACE
    USB_SET_KEYCODES_REQUEST_TYPE = HOST_TO_DEVICE | TYPE_VENDOR | REC_INTERFACE
    
    def __init__(self):
        self.handle = None
        self.claimed = False
    
    def __enter__(self):
        self.open()
        return self
    
    def __exit__(self, a, b, c):
        self.close()
    
    def open(self):
        self.ctx = usb1.USBContext()
        dev = self.ctx.getByVendorIDAndProductID(self.VENDOR_ID, self.PRODUCT_ID)
        if dev is None:
            raise Exception("No device found")
        
        self.handle = dev.open()
        
        if platform.system() == "Linux" and self.handle.kernelDriverActive(self.INTERFACE):
            self.handle.detachKernelDriver(self.INTERFACE)
        
        # check manufacturer and product name
        """
        if dev.getManufacturer() != self.MANUFACTURER_NAME or \
            not dev.getProduct().startswith(self.PRODUCT_NAME_PREFIX):
            # illegal device, close device
            self.close()
            raise Exception("Manufacturer or product not match")
        """
        self.handle.claimInterface(self.INTERFACE)
        self.claimed = True
    
    def close(self):
        if self.handle:
            if self.claimed:
                self.handle.releaseInterface(self.INTERFACE)
            if platform.system() == "Linux" and not self.handle.kernelDriverActive(self.INTERFACE):
                self.handle.attachKernelDriver(self.INTERFACE)
            self.handle.close()
            self.handle = None
            self.claimed = False
        self.ctx.exit()
    
    def get_keycodes(self):
        if self.handle is None:
            raise Exception("Device not opened")
        data = self.handle.controlRead(
                self.USB_GET_KEYCODES_REQUEST_TYPE,
                self.USB_VENDOR_GET_KEYCODES,
                0,
                0,
                self.KEYCODES_LENGTH,
                timeout=2000)
        return struct.unpack("<BBBB", data)
    
    def set_keycodes(self, keycodes):
        if self.handle is None:
            raise Exception("Device not opened")
        
        return self.handle.controlWrite(
                self.USB_SET_KEYCODES_REQUEST_TYPE, # bmRequestType
                self.USB_VENDOR_SET_KEYCODES, # bRequest
                0, # wValue
                0, # wIndex
                struct.pack("<BBBB", keycodes), # data
                timeout=2000)


def main():
    keycodes = [1, 2, 3, 4]
    try:
        with USBControl() as b:
            # set keycode passed as command line arguments
            try:
                b.set_keycodes(keycodes)
            except Exception as e:
                print(e)
            
            # read current keycodes
            data = b.get_keycodes()
            print(data)
    except Exception as e:
        print(e)

if __name__ == "__main__":
    main()

プログラムは (1, 2, 3, 4) の 4 バイトデータとして送信します。受信したデバイスは変数に保存しておき、次の送信リクエストにそのデータを返送します。これで正しく送受信できたかどうか確認できます。

mbed 側

mbed のライブラリにある USBDevice クラスを継承するクラスでは、USBCallback_request メソッドがクラスリクエストとベンダーリクエストの処理を行う起点となります。データを受信したい場合、USBCallback_requestCompleted メソッドも必要です。

#define USB_VENDOR_GET_KEYCODES      0x10
#define USB_VENDOR_SET_KEYCODES      0x11

static uint8_t codes[] = {0x10, 0x20, 0x30, 0x40};

bool USBBootKeyboard::USBCallback_request() {
    bool success = false;
    CONTROL_TRANSFER *transfer = getTransferPtr();
    
    switch (transfer->setup.bRequest) {
        case USB_VENDOR_GET_KEYCODES:
            transfer->remaining = 4;
            transfer->ptr = (uint8_t *)&codes;
            transfer->direction = DEVICE_TO_HOST;
            success = true;
            break;
        case USB_VENDOR_SET_KEYCODES:
            if (transfer->setup.wLength == 4) {
                // check length
                //transfer->ptr = (uint8_t *)&codes; // not usable?
                transfer->remaining = transfer->setup.wLength;
                transfer->direction = HOST_TO_DEVICE;
                transfer->notify = true;
                success = true;
            }
            break;
        default:
            break;
    }
    return success;
}

Get_Keycodes コマンドでは transfer ポインタの ptr にデータを保持する変数を指定、データバイト数を remainig に設定します。direction を DEVICE_TO_HOST にし、メソッドから true を返します。これでデータがホストに送信できます。

Set_Keycodes コマンドでは wLength で送信データバイト数を確認、transfer ポインタの remaining メンバに受信するデータバイト数を指定します。direction を HOST_TO_DEVICE に、notify を true に変更します。そして USBCallback_request メソッドから true を返します。このようにすると USBCallback_requestCompleted メソッドが後で呼び出されます。

void USBBootKeyboard::USBCallback_requestCompleted(uint8_t *buf, uint32_t length) {
    CONTROL_TRANSFER *transfer = getTransferPtr();
    
    switch (transfer->setup.bmRequestType.Type) {
        case VENDOR_TYPE:
            if (transfer->setup.bRequest == USB_VENDOR_SET_KEYCODES) {
                if (length == 4) {
                    // copy data
                    memcpy(&codes, buf, length);
                }
            }
            break;
        default:
            break;
    }
}

USBCallback_requestCompleted メソッドでもまだ transfer ポインタへアクセスできるので、リクエストの種類に応じて分岐できます。
受信データをここで処理します。

利用について

コントロールパイプを利用したリクエストは帯域が狭く速度が上がりません。しかし、専用のインターフェースを消費する必要もないため、頻度が低くデータ量も少ない簡単なコマンドを送信するには便利です。