USBデバイスをつくってみたい(2) HIDデバイスコントロール転送編



(続きその3→ http://qiita.com/takeru@github/items/23f41616db9367ea26b5)


ここまでのあらすじ
---------------
* http://qiita.com/takeru@github/items/c591323acf24e58ed6c0
* AVRにUSBケーブル直結でUSBデバイスが作れるV-USB
* AVR-CDCでシリアル読み書きデバイスはできたがイマイチ
* HIDに準じたものを作るのが良いっぽい
* とりあえず、LEDがついたりきえたりするHIDデバイスを作ってみる。


調査
----

* USBの仕様
 * http://www.picfun.com/usb00.html
 * http://vusb.wikidot.com/usb-device-classes
* HIDでシリアル通信
 * http://rayshobby.net/?p=7363
 * /dev/tty*ができるわけではない。独自のアプリが必要。
 * 速度が遅いのが問題だそうだ。
 * ArduinoIDEのUSBaspで使えるブートローダがある
* USBaspx
 * http://d.hatena.ne.jp/deb/20110103/1294060194
* Rubyでlibusb
 * https://github.com/larskanis/libusb
 * host側はこれでつくろう
* HIDデバイスを作りながら解説
 * http://jsdiy.web.fc2.com/vusb_hid/


キーワードがわかるとどんどん情報がみつかってくる。



ハードウエア
----------
AVR-CDCとおなじ。
http://qiita.com/takeru@github/items/c591323acf24e58ed6c0
http://www.recursion.jp/avrcdc/cdc-ioj.html

クリスタルだけ20MHzなので、Makefileの設定を変えています。


ソフトウエア
----------

* コントロール転送で8byte送信、8byte受信ができるようになった。
* usbHidReportDescriptorが難しいっぽいが、ググって見つけた秘伝のタレをなんとなく使っている。
* https://github.com/takeru/hidled
* https://github.com/larskanis/libusb をいれた 
 * brew install libusb
 * gem install libusb


```main.c
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/wdt.h>
#include <util/delay.h>
#include "usbdrv.h"

uchar recv_buf[8];
uchar counter = 0;

/*
typedef struct usbRequest{
    uchar       bmRequestType;
    uchar       bRequest;
    usbWord_t   wValue;
    usbWord_t   wIndex;
    usbWord_t   wLength;
}usbRequest_t;
*/

PROGMEM const char usbHidReportDescriptor[USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH] = {
    0x06, 0x00, 0xff,              // USAGE_PAGE (Generic Desktop)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)

    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //   REPORT_SIZE (8)

    0x95, 0x08,                    //   REPORT_COUNT (8)
    0x09, 0x00,                    //   USAGE (Undefined)
    0xb2, 0x02, 0x01,              //   FEATURE (Data,Var,Abs,Buf)

    0xc0                           // END_COLLECTION
};


/* You need to implement this function ONLY if you provide USB descriptors at
 * runtime (which is an expert feature). It is very similar to
 * usbFunctionSetup() above, but it is called only to request USB descriptor
 * data. See the documentation of usbFunctionSetup() above for more info.
 */
/*
usbMsgLen_t usbFunctionDescriptor(struct usbRequest_t *rq)
{
}
*/


/* This function is called when the driver receives a SETUP transaction from
 * the host which is not answered by the driver itself (in practice: class and
 * vendor requests). All control transfers start with a SETUP transaction where
 * the host communicates the parameters of the following (optional) data
 * transfer. The SETUP data is available in the 'data' parameter which can
 * (and should) be casted to 'usbRequest_t *' for a more user-friendly access
 * to parameters.
 *
 * If the SETUP indicates a control-in transfer, you should provide the
 * requested data to the driver. There are two ways to transfer this data:
 * (1) Set the global pointer 'usbMsgPtr' to the base of the static RAM data
 * block and return the length of the data in 'usbFunctionSetup()'. The driver
 * will handle the rest. Or (2) return USB_NO_MSG in 'usbFunctionSetup()'. The
 * driver will then call 'usbFunctionRead()' when data is needed. See the
 * documentation for usbFunctionRead() for details.
 *
 * If the SETUP indicates a control-out transfer, the only way to receive the
 * data from the host is through the 'usbFunctionWrite()' call. If you
 * implement this function, you must return USB_NO_MSG in 'usbFunctionSetup()'
 * to indicate that 'usbFunctionWrite()' should be used. See the documentation
 * of this function for more information. If you just want to ignore the data
 * sent by the host, return 0 in 'usbFunctionSetup()'.
 *
 * Note that calls to the functions usbFunctionRead() and usbFunctionWrite()
 * are only done if enabled by the configuration in usbconfig.h.
 */
usbMsgLen_t usbFunctionSetup(uchar data[8])
{
    usbRequest_t *rq = (usbRequest_t*)data;

    if((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS){
        if(rq->bRequest == USBRQ_HID_SET_REPORT){
            // host to device
            return USB_NO_MSG; // => usbFunctionWrite
        }
        if(rq->bRequest == USBRQ_HID_GET_REPORT){
            // device to host
            return USB_NO_MSG; // => usbFunctionRead
        }
    }

    return 0;
}

/* This function is called by the driver to provide a control transfer's
 * payload data (control-out). It is called in chunks of up to 8 bytes. The
 * total count provided in the current control transfer can be obtained from
 * the 'length' property in the setup data. If an error occurred during
 * processing, return 0xff (== -1). The driver will answer the entire transfer
 * with a STALL token in this case. If you have received the entire payload
 * successfully, return 1. If you expect more data, return 0. If you don't
 * know whether the host will send more data (you should know, the total is
 * provided in the usbFunctionSetup() call!), return 1.
 * NOTE: If you return 0xff for STALL, 'usbFunctionWrite()' may still be called
 * for the remaining data. You must continue to return 0xff for STALL in these
 * calls.
 * In order to get usbFunctionWrite() called, define USB_CFG_IMPLEMENT_FN_WRITE
 * to 1 in usbconfig.h and return 0xff in usbFunctionSetup()..
 */
uchar usbFunctionWrite(uchar *data, uchar len)
{
    // host to device
    if(len==8){
        memcpy(recv_buf, data, len);
        return 1;
    }else{
        return -1; // STALL
    }
}

/* This function is called by the driver to ask the application for a control
 * transfer's payload data (control-in). It is called in chunks of up to 8
 * bytes each. You should copy the data to the location given by 'data' and
 * return the actual number of bytes copied. If you return less than requested,
 * the control-in transfer is terminated. If you return 0xff, the driver aborts
 * the transfer with a STALL token.
 * In order to get usbFunctionRead() called, define USB_CFG_IMPLEMENT_FN_READ
 * to 1 in usbconfig.h and return 0xff in usbFunctionSetup()..
 */
uchar usbFunctionRead(uchar *data, uchar len)
{
    // device to host
    if(len==8){
        //memset(data, 0, len);
        memcpy(data, recv_buf, len);
        data[7] = counter++;
        return len;
    }else{
        return -1; // STALL
    }
}

/* This function is called by the driver when data is received on an interrupt-
 * or bulk-out endpoint. The endpoint number can be found in the global
 * variable usbRxToken. You must define USB_CFG_IMPLEMENT_FN_WRITEOUT to 1 in
 * usbconfig.h to get this function called.
 */
 /*
void usbFunctionWriteOut(uchar *data, uchar len)
{
    usbDisableAllRequests();
    // ...
    usbEnableAllRequests();
}
*/

static void hardwareInit(void)
{
    uchar i;
    USB_CFG_IOPORT = (uchar)~((1<<USB_CFG_DMINUS_BIT)|(1<<USB_CFG_DPLUS_BIT));

    usbDeviceDisconnect();
    for(i=0; i<10; i++){
        wdt_reset();
        _delay_ms(10);
    }
    usbDeviceConnect();
}

int main(void)
{
    wdt_enable(WDTO_1S);
    hardwareInit();
    usbInit();

    sei();
    for(;;){
        wdt_reset();
        usbPoll();
    }
    return 0;
}

```

```
hidled% AVR_PATH=/Applications/Arduino.app/Contents/Resources/Java/hardware/tools/avr/bin; PATH=$AVR_PATH:$PATH

hidled% make
avr-gcc -I"." -I"./v-usb/usbdrv"  -mmcu=atmega88p -Wall -gdwarf-2 -DF_CPU=20000000UL -Os -fsigned-char -MD -MP -MT main.o -MF dep/main.o.d  -c  main.c
avr-gcc -I"." -I"./v-usb/usbdrv"  -mmcu=atmega88p -mmcu=atmega88p -Wall -gdwarf-2 -DF_CPU=20000000UL -Os -fsigned-char -MD -MP -MT usbdrvasm.o -MF dep/usbdrvasm.o.d  -x assembler-with-cpp -Wa,-gdwarf2 -c  v-usb/usbdrv/usbdrvasm.S
avr-gcc -I"." -I"./v-usb/usbdrv"  -mmcu=atmega88p -Wall -gdwarf-2 -DF_CPU=20000000UL -Os -fsigned-char -MD -MP -MT oddebug.o -MF dep/oddebug.o.d  -c  v-usb/usbdrv/oddebug.c
avr-gcc -I"." -I"./v-usb/usbdrv"  -mmcu=atmega88p -Wall -gdwarf-2 -DF_CPU=20000000UL -Os -fsigned-char -MD -MP -MT usbdrv.o -MF dep/usbdrv.o.d  -c  v-usb/usbdrv/usbdrv.c
avr-gcc -mmcu=atmega88p  main.o usbdrvasm.o oddebug.o usbdrv.o     -o hidled.elf
avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock -R .signature  hidled.elf hidled.hex
avr-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" --change-section-lma .eeprom=0 --no-change-warnings -O ihex hidled.elf hidled.eep || exit 0
avr-objdump -h -S hidled.elf > hidled.lss
```

```
hidled% avrdude -C /Applications/Arduino.app/Contents/Resources/Java/hardware/tools/avr/etc/avrdude.conf -P /dev/tty.usbserial-A9005bvI -c avrisp -p m88p -b 19200 -U flash:w:hidled.hex

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.06s

avrdude: Device signature = 0x1e930f
avrdude: NOTE: FLASH memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "hidled.hex"
avrdude: input file hidled.hex auto detected as Intel Hex
avrdude: writing flash (1582 bytes):

Writing | ################################################## | 100% 2.78s

avrdude: 1582 bytes of flash written
avrdude: verifying flash memory against hidled.hex:
avrdude: load data flash data from input file hidled.hex:
avrdude: input file hidled.hex auto detected as Intel Hex
avrdude: input file hidled.hex contains 1582 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 1.82s

avrdude: verifying ...
avrdude: 1582 bytes of flash verified

avrdude: safemode: Fuses OK

avrdude done.  Thank you.
```



```hidled_host.rb
require "libusb"

include LIBUSB

USBRQ_HID_GET_REPORT    = 0x01
USBRQ_HID_GET_IDLE      = 0x02
USBRQ_HID_GET_PROTOCOL  = 0x03
USBRQ_HID_SET_REPORT    = 0x09
USBRQ_HID_SET_IDLE      = 0x0a
USBRQ_HID_SET_PROTOCOL  = 0x0b

usb = LIBUSB::Context.new

devices = usb.devices(:idVendor => 0x16c0, :idProduct => 0x05dc)
puts "devices = #{devices.inspect}"
exit if devices.size==0

device = devices.first
device.open_interface(0) do |handle|
  16.times do |n|
    # send to device
    data = [n+0,n+1,n+2,n+3,n+4,n+5,n+6,n+7]

    sent_byte_size = handle.control_transfer(
      :bmRequestType => ENDPOINT_OUT|REQUEST_TYPE_CLASS|RECIPIENT_ENDPOINT,
      :bRequest      => USBRQ_HID_SET_REPORT,
      :wValue        => 0x0000,
      :wIndex        => 0x0000,
      :dataOut       => data.pack("C*")
    )
    puts "sent_byte_size = #{sent_byte_size}"

    # receive from device
    received_bytes = handle.control_transfer(
      :bmRequestType => ENDPOINT_IN|REQUEST_TYPE_CLASS|RECIPIENT_ENDPOINT,
      :bRequest      => USBRQ_HID_GET_REPORT,
      :wValue        => 0x0000,
      :wIndex        => 0x0000,
      :dataIn        => 8
    )
    puts "received_bytes = #{received_bytes.unpack('C*').inspect}"
  end
end

```



```
hidled% ruby hidled_host.rb
devices = [#<LIBUSB::Device 26/7 16c0:05dc ssktkr.com HID-LED ? (Vendor specific (00,00))>]
sent_byte_size = 8
received_bytes = [0, 1, 2, 3, 4, 5, 6, 0]
sent_byte_size = 8
received_bytes = [1, 2, 3, 4, 5, 6, 7, 1]
sent_byte_size = 8
received_bytes = [2, 3, 4, 5, 6, 7, 8, 2]
sent_byte_size = 8
received_bytes = [3, 4, 5, 6, 7, 8, 9, 3]
sent_byte_size = 8
received_bytes = [4, 5, 6, 7, 8, 9, 10, 4]
sent_byte_size = 8
received_bytes = [5, 6, 7, 8, 9, 10, 11, 5]
sent_byte_size = 8
received_bytes = [6, 7, 8, 9, 10, 11, 12, 6]
sent_byte_size = 8
received_bytes = [7, 8, 9, 10, 11, 12, 13, 7]
sent_byte_size = 8
received_bytes = [8, 9, 10, 11, 12, 13, 14, 8]
sent_byte_size = 8
received_bytes = [9, 10, 11, 12, 13, 14, 15, 9]
sent_byte_size = 8
received_bytes = [10, 11, 12, 13, 14, 15, 16, 10]
sent_byte_size = 8
received_bytes = [11, 12, 13, 14, 15, 16, 17, 11]
sent_byte_size = 8
received_bytes = [12, 13, 14, 15, 16, 17, 18, 12]
sent_byte_size = 8
received_bytes = [13, 14, 15, 16, 17, 18, 19, 13]
sent_byte_size = 8
received_bytes = [14, 15, 16, 17, 18, 19, 20, 14]
sent_byte_size = 8
received_bytes = [15, 16, 17, 18, 19, 20, 21, 15]
```



まとめ
-----
* HIDクラスのデバイスをつくってRubyで操作できるようになった。
* ハブを通しても直接接続でもちゃんと動く
* デスクリプタは後々詳しいことを調べるかもしれない。
* インタラプト転送はまだやっていない。
* TODO
 * 受信データによってLEDを点けたり消したりする
 * シリアルデバッグできるようにする
 * インタラプト転送


![スクリーンショット 2014-05-18 21.28.59.png](https://qiita-image-store.s3.amazonaws.com/0/16626/a99210f1-98d7-f0f7-5c98-6781eee2164e.png "スクリーンショット 2014-05-18 21.28.59.png")