V-USBの使い方まとめ

HIDaspxのコピーが手に入ったので年末年始の宿題として2台拝借。
これを使ってV-USBやUSB1.1 LowSpeedについて勉強しようとする魂胆。

概要・HIDaspxについて

http://www.binzume.net/library/avr_hidasp.html
 上記のサイトの人が作ったHIDaspをブラッシュアップして高速化、使いやすくした物。またはそのカスタム品。
ブラッシュアップ、カスタムについては下記のサイトを参照してください。
http://hp.vector.co.jp/authors/VA000177/html/A3C8A3C9A3C4A3E1A3F3A3F0A3F8.html
http://www-ice.yamagata-cit.ac.jp/ken/senshu/sitedev/index.php?AVR%2FHIDaspx00

概要・USB

USB1.1 LowSpeedについては以下の通り。
通信線のD-ピンをプルアップすることによってホスト側にLowSpeedと区別させる。
NRZIによる符号化、ヘッダやサムチェック(CRC)、ACKを含めた上で1.5Mbpsの通信速度。もちろん実際に送信できるデータ量と通信速度は遅くなる。
ホスト側のコントローラによって通信速度が違い、Intel, VIAのUHCIでは速度が最悪1/4まで低下する、Microsoft策定のNECら率いるOHCIではそうでもない。
上記の問題でUSB2.0EHCIではUSB1.1のハブをかませる事によって速度低下を防ぐことができる。
論理的にはPCとデバイスをつなぐ線をパイプとよびFIFOとする。パイプ末端とエンドポイントと呼ぶ。
エンドポイント0はコントロール転送にも用いられFeatureと定義されてWindowsではHidD_GetFeature(), HidD_SetFeature()関数を用いて通信する。
その他のエンドポイントはインタラプト通信に使われInputもしくはOutputと定義され、WindowsではWriteFile()もしくはReadFile()関数を用いて通信を行う。

  • スペック
    • (実質)半二重通信
    • データ量 1〜64byte
    • パケット長 8byte
    • ポーリング間隔
      • コントロール転送では通信間隔を1ms以内としインタラプト転送では最低10msとする。HID-CDCでは例外的にエンドポイント3を用いてインタラプト転送でも通信間隔を最低3ms間隔とする場合がある。

概要・V-USB

http://www.obdev.at/products/vusb/index.html
元AVR-USB、本家ATmelの商標とかぶるためにV-USBとした。
USBの通信線をAVRの外部割り込みポートにつなぐ事によりUSB1.1 LowSpeedを扱えるようにしたUSBスタックである。
ライセンスはGPL、またGPLを使うに限り用意されたベンダーIDを用いることができるがベンダーネームをobdev.atから書き換えてはならないとしている。

実装・デバイス

実装

大体、実装できるのは以下である

  • Standard HID class device
    • マウスやキーボードなどの仕様によって実装。ドライバいらず。
  • CDC class devices
    • USBシリアルなどの実装。OS側でデバイスドライバを補完しなくてはならない。(COMポートのバインドなど)
  • その他のHIDデバイス
    • HIDデバイスではあるが、通信内容は自分で定義。ドライバいらずだが、操作するアプリは自前で作る。

何を選ぶかによって設定方法は変わるがここではその他のHIDデバイスを作成する。

ハードウエア

AVRライターのHIDaspxのハードウエア構成は以下である。
HIDaspxのファームウエアのソースコードから引用した。

         ATtiny2313
         ___    ___
RESET    [1  |__| 20] Vcc
PD0(NC)  [2       19] PB7(SCK)
PD1(NC)  [3       18] PB6(MISO)
XTAL2    [4       17] PB5(MOSI)
XTAL1    [5       16] PB4(RST)
PD2(12M) [6       15] PB3(BUSY LED)
PD3      [7       14] PB2(READY LED)
PD4      [8       13] PB1(NC)
PD5(PUP) [9       12] PB0(NC)
GND      [10      11] PD6(NC)
         ~~~~~~~~~~~

   ---------------------------------------
   SPI:     PB7-4 ===> [Target AVR Device](MISOとMOSIは交差)
   USB:     PD4   ===> USB D-
            PD3   ===> USB D+
            PD5   ===> USB D- PULL UP
   XTAL:    XTAL1,2 => Crystal 12MHz
   PD2:     Clock Output(12MHz)
   ---------------------------------------

気をつけなければならないところはPD5によってD-がプルアップされた上でPD4に直接つながれていて、D+はPD2のINT0でなくPD3のINT1で外部入力割り込みされていることである。
また12MHzで動かす上では必ずしもクリスタルやセラロックは必要ではない。
内発8MHz出力の校正用レジスタを変更することによって最大12.8MHzまでクロックアップすることができる。
16MHzのクリスタルを用いることが一番安定していてコード量も小さくなるがHIDaspxでは内発でないクリスタル12MHzを使用している。
CPUはATmelのATtiny2313を用いていてPORTBの8ピンはUSB動作においては使用していない。

V-USB・設定(usbconfig.h)

usbconfig-prototype.hをusbconfig.hにリネームして使用する。

#define USB_CFG_DPLUS_BIT       3

上記のとおりD+ピンはPORTDのPD3に接続されているのでここは3にする。

#define USB_CFG_PULLUP_IOPORTNAME      D

何がDなのかというとPORTDのDである。コメントを外しておく。

#define USB_CFG_PULLUP_BIT          5

上記のとおりD-をプルアップしているのはPD5なのでここは5としてコメントを外しておく。

#define USB_CFG_HAVE_INTRIN_ENDPOINT          1

エンドポイントを用いるので1に。

#define USB_CFG_IMPLEMENT_FN_WRITE          1

コントロール転送でデータを受信できるようにするために1とする。ファームウエア内でusbFunctionWrite()関数がコールバックされる。

#define USB_CFG_IMPLEMENT_FN_READ          1

コントロール転送でデータを受信できるようにするために1とする。ファームウエア内でusbFunctionRead()関数がコールバックされる。

#define USB_CFG_IMPLEMENT_FN_WRITEOUT   0

インタラプト転送でデータを受信したときにファームウエア内のusbFunctionWriteOut()関数をコールバックするための設定。1で有効となる。

#define USB_USE_FAST_CRC                0

CRC計算を通常の計算にくらべてほぼ2分の1にする設定だが、コード量が32byte増える諸刃の剣。ポーリング、割り込み時間を低減したい場合に使用する?

#define USB_CFG_DEVICE_CLASS        0

HIDデバイスとして認識させるために0とする。

#define USB_CFG_INTERFACE_CLASS     3
#define USB_CFG_INTERFACE_SUBCLASS  0
#define USB_CFG_INTERFACE_PROTOCOL  0

HIDクラスとして認識させるためにUSB_CFG_INTERFACE_CLASSは3、残りを0とする。

#define USB_INTR_CFG_SET        ((1 << ISC10) | (1 << ISC11))
#define USB_INTR_ENABLE_BIT     INT1
#define USB_INTR_PENDING_BIT    INTF1
#define USB_INTR_VECTOR         INT1_vect

V-USBは外部割り込みポートを用いて動作するが、基本はINT0の割り込みを用いる。HIDaspxはINT1を使うために上記のように設定する。

#define USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH    22

最後に説明するが、設定項目は最後にはない。HIDデバイスは自らの仕様をホストとなるコンピュータに提示するためにHIDレポートディスクリプタという構造体を用いる。その構造体のサイズをバイトで記述する。ここでは22とした。

V-USB・設定(メイン)

インクルードしなければならない最低限のヘッダファイルは以下である。

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>

上記でも記したがHIDデバイスは自らの仕様、つまり通信フォーマットをホストコンピュータに提示するためにレポートディスクリプタが必要である。
もし、レポートディスクリプタを設定しない場合はHIDデバイスであったとしてもWindowsであれば自前のデバイスドライバを作らなければならない。

PROGMEM char usbHidReportDescriptor[22] = {    /* USB report descriptor */
    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) <--- データサイズ(byte)
    0x09, 0x00,                    //   USAGE (Undefined)
    0xb2, 0x02, 0x01,              //   FEATURE (Data,Var,Abs,Buf)
    0xc0                           // END_COLLECTION
};

コントロール転送において8byteのデータをやりとりするためのレポートディスクリプタの例が以上である。
パケット長が8byteなのはV-USB上では仕様で変更できない。
なのでデータサイズを8byte以上に設定すると8byteに区切って処理しなければならないので8の倍数で設定すると実装が楽である。
またコントロール転送では一回の通信時間を1ms間隔とするので送るデータサイズが大きいほど見かけの通信速度が早くなる。

inline uchar usbFunctionRead(uchar *data, uchar len)
{
	// 送信すべきデータのアドレスをdataにセット。
	return 1; // 処理が終了すれば1を返す。
}

inline uchar usbFunctionWrite(uchar *data, uchar len)
{
	// 受信したデータをdataから処理する。
	return 1; // 処理が終了すれば1を返す。
}

inline usbMsgLen_t usbFunctionSetup(uchar data[8])
{
	usbRequest_t *rq = (void *)data;

	if((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS){
		if(rq->bRequest == USBRQ_HID_GET_REPORT){ // usbFunctionRead()でデータ送信命令が来たフラグ。
			return USB_NO_MSG; // USB_NO_MSGと返すことによってこの関数終了後にusbFunctionRead()が呼ばれる
		} else if(rq->bRequest == USBRQ_HID_SET_REPORT){ // usbFunctionWrite()でデータの受信が来たフラグ。
			return USB_NO_MSG; // USB_NO_MSGと返すことによってこの関数終了後にusbFunctionWrite()が呼ばれる
		}
	}
	
	return 0;
}

コントロール転送に限らずエンドポイント0で処理されなければならないデータは皆usbFunctionSetup()関数で適切に処理されなければならない。
USBRQ_HID_GET_REPORTやUSBRQ_HID_SET_REPORTではコントロール転送に必要な処理が渡されUSB_NO_MSGを返すことによってusbFunctionRead()やusbFunctionWrite()に処理が移る。
実装によっては他にも必要なフラグがあるが、例えばUSBRQ_HID_GET_IDLEやUSBRQ_HID_SET_IDLE等などではインタラプト転送におけるポーリング間隔を指定もしくは設定するものがある。
以下、V-USBのマウス実装の例をあげる。

usbMsgLen_t usbFunctionSetup(uchar data[8])
{
	usbRequest_t *rq = (void *)data;

	/* The following requests are never used. But since they are required by
	* the specification, we implement them in this example.
	*/
	if((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS){    /* class request type */
		DBG1(0x50, &rq->bRequest, 1);   /* debug output: print our request */
		if(rq->bRequest == USBRQ_HID_GET_REPORT){  /* wValue: ReportType (highbyte), ReportID (lowbyte) */
			/* we only have one report type, so don't look at wValue */
			usbMsgPtr = (void *)&reportBuffer;
			return sizeof(reportBuffer);
		} else if(rq->bRequest == USBRQ_HID_GET_IDLE){
			usbMsgPtr = &idleRate;
			return 1;
		} else if(rq->bRequest == USBRQ_HID_SET_IDLE){
			idleRate = rq->wValue.bytes[1];
		}
	} else {
		/* no vendor specific requests implemented */
	}
	return 0;   /* default for not implemented requests: return no data back to host */
}

usbFunctionSetup()で0が帰った場合はホストに対して返答を行わないが1以上を設定するとusbMsgPtrのアドレスのデータを設定した値だけバイト数で送信される。
USBRQ_HID_SET_IDLEでは設定されるべき値はusbRequest_tのwValue.bytes[1]に含まれるがカスタムなUSBリクエストでは任意のバリューを設定できるはず。
USBRQ_HID_GET_REPORTやUSBRQ_HID_SET_REPORTでは何が入っているかは未確認。レポートID?

void main(void)
{
	PORTD = ((1<<PD6)|(1<<PD5)|(1<<PD2)|(1<<PD1)|(1<<PD0));
	DDRD = ~(USBMASK|(1<<PD6)|(1<<PD2)|(1<<PD1)|(1<<PD0));
	
	usbInit();
	usbDeviceDisconnect();
	{
		uchar i = 0;
		while(--i){
			_delay_ms(1);
		}
	}
	usbDeviceConnect();
	sei();
	for(;;){
		usbPoll();
		/*
		char interruptSendData[] = "12345678"; // インタラプト転送データサイズに合わせる
		if(usbInterruptIsReady()){
			usbSetInterrupt((void *)&interruptSendData, sizeof(interruptSendData));
		}
		*/
	}
}

最低限のmain関数は以上。
インタラプト転送における送信は簡単だが、使用するにはもちろんレポートディスクリプタにインタラプト転送における送信設定を記述しなければならない。

PROGMEM char usbHidReportDescriptor[] = {
    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)  // コントロール転送用設定ここまで
    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)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs) // インタラプト送信用設定ここまで
    0xc0                           // END_COLLECTION
};

もちろんUSB_CFG_HID_REPORT_DESCRIPTOR_LENGTHの値を適当な値に変えておくこと。
インタラプト転送の送信をホスト側で受信するにはWindowsであればReadFile()を用いる。
ReadFile()はデータを受信するまで処理をブロックするが、ReadFile()が用意されていない状態でデバイス側がデータを送信してもデータは破棄されてしまうので注意。

AVR-GCCについて

環境はAVR StudioとAVR Toolchain Installerである。
現在AVR Toolchain Installerで提供されるAVR-GCCのバージョンは4.4.3である。
コード量が増えるといってVer4のGCCを避ける人がいるかも知れないがVer4でもコードが小さくなる設定があるので此処に書く。

コンパイルオプション
-fdata-sections 
-ffunction-sections
余分な関数や変数がロードされなくてすっきり!
-ffreestanding
宣言にint __attribute__((noreturn)) main(void);
追加でmain関数のreturnによるレジスタ処理を抑制
-fno-schedule-insns2
プリフェッチのある深いパイプライン向けのロード/ストア命令最適化なのでAVRでは逆にUSIの実装などでは逆に邪魔。
-mcall-prologues
AVR向け関数呼び出しの最適化
-fno-tree-scev-cprop
-fno-split-wide-types
最適化レベルOsでは含まれないコード展開を抑制

リンカオプション
-Wl,--relax,--gc-sections
AVR-GCC用にも--relaxが用意されているらしい。

ホスト側・実装

Windowsにおいて必要なDLLではhid.dll, setupapi.dll, kernel32.dllの三つ。
libusbやwinusbなどのライブラリもあるが、バグいろいろやり方いろいろなのでMinGWとwin32api(DDK含む)で開発。
MFCだのなんだの開発するにはC#だのpythonでのバインドとかはやっぱり邪道で男は黙ってVCかMinGWとwin32apiが楽だと思った。
http://www.crimson-systems.com/tips/t085a.htm
以上のサイトを参考に以下にソースを提示する。

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <setupapi.h>
#include <ddk/hidsdi.h>

#pragma comment(lib, "setupapi.lib")
#pragma comment(lib, "hid.lib")

HANDLE __stdcall HIDOpen(int iVendorID, int iProductID)
{
	GUID hidGuid;
	HANDLE hDevHandle = NULL;
	HDEVINFO hDevInfo;
	HIDD_ATTRIBUTES Attributes;
	PSP_DEVICE_INTERFACE_DETAIL_DATA pspDidd;
	SP_DEVICE_INTERFACE_DATA spDid;

	HidD_GetHidGuid(&hidGuid);
	hDevInfo = SetupDiGetClassDevs(&hidGuid, NULL, NULL, DIGCF_PRESENT|DIGCF_DEVICEINTERFACE|DIGCF_ALLCLASSES);
	{
		USHORT i;
		for(i = 0; i < 128;i++){
			memset(&spDid, 0, sizeof(SP_DEVICE_INTERFACE_DATA)); // spDid Clear;
			spDid.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
			if(!SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &hidGuid, i, &spDid)) continue;
			DWORD dwRequiredLength = 0;
			SetupDiGetDeviceInterfaceDetail(hDevInfo, &spDid, NULL, 0, &dwRequiredLength, NULL);
			pspDidd = (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(dwRequiredLength);
			pspDidd->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
			if(SetupDiGetDeviceInterfaceDetail(hDevInfo, &spDid, pspDidd, dwRequiredLength, &dwRequiredLength, NULL)){
				hDevHandle = CreateFile(pspDidd->DevicePath, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
				if(hDevHandle != INVALID_HANDLE_VALUE){
					Attributes.Size = sizeof(Attributes);
					if(HidD_GetAttributes(hDevHandle, &Attributes)){
						if(iVendorID == Attributes.VendorID && iProductID == Attributes.ProductID){
							free(pspDidd);
							break;
						}
					}
					CloseHandle(hDevHandle);
				}
			}
			free(pspDidd);
		}
	}
	return hDevHandle;
}

ベンダーIDとプロダクトIDを用いて指定のHIDデバイスのファイルハンドラを取得する。
もちろん使用後のファイルハンドルはCloseHandle()で処理すべきである。
for文を127回を上限としているのは接続できるUSBデバイスが127個以下と規定しているからであるが、USB外のHIDデバイス(Bluetoothなど)ではこの限りではない。

HIDP_CAPS __stdcall HIDGetCaps(HANDLE hDevHandle)
{
	PHIDP_PREPARSED_DATA lpData;
	HIDP_CAPS caps;

	if(HidD_GetPreparsedData(hDevHandle, &lpData)){
		HidP_GetCaps(lpData, &caps);
	}
	HidD_FreePreparsedData(lpData);

	return caps;
}

レポートディスクリプタからHIDデバイスのスペックを取得する関数である。
HIDP_CAPSにおける、InputReportByteLength, OutputReportByteLength, FeatureReportByteLengthから通信に使用するバッファ長を求めるために用いる。

BOOL __stdcall HIDSetFeature(HANDLE hDevHandle, PVOID data, ULONG length, UCHAR nReportID)
{
	BOOL bResult;

	if(length <= 1){
		bResult = HidD_SetFeature(hDevHandle, &nReportID, length);
	} else {
		memmove(data+1, data, length - sizeof(UCHAR));
		((UCHAR *)data)[0] = nReportID;
		bResult = HidD_SetFeature(hDevHandle, data, length);
	}

	return bResult;
}

コントロール転送においてHIDデバイス対してデータを送信する関数である。
レポートディスクリプタで設定したデータサイズよりも1byte大きくHIDP_CAPSのFeatureReportByteLengthから報告されるが、最初にセットする1byteはレポートIDに使用する。

MinGW(DLL)

DLL化したのは他のアプリ、C#pythonからでも操作できるようにした意思からであるが、MinGWにおけるDLL作成のメモを期する。
ずばり言えばリンカオプションに

-Wl,--dll,--add-stdcall-alias,--kill-at,--enable-stdcall-fixup

あとは関数にstdcallを宣言すればDLLが出来上がる。
DllMain()関数はなくても勝手に処理してくれるみたいだ。
本来はエクスポートすべき関数を指定するdefファイルを真面目に記述する必要があるが、ここでは手抜き。

Pythonから使う

#!
#
from ctypes import *
from ctypes.wintypes import c_char, ULONG, BOOLEAN, BYTE, WORD, DWORD

class HIDD_ATTRIBUTES(Structure):
	_fields_ = [
		("cb_size", DWORD),
		("vendor_id", c_ushort),
		("product_id", c_ushort),
		("version_number", c_ushort)
	]

class HIDP_CAPS(Structure):
	_fields_ = [
		("usage", c_ushort),
		("usage_page", c_ushort),
		("input_report_byte_length", c_ushort),
		("output_report_byte_length", c_ushort),
		("feature_report_byte_length", c_ushort),
		("reserved", c_ushort * 17),
		("number_link_collection_nodes", c_ushort),
		("number_input_button_caps", c_ushort),
		("number_input_value_caps", c_ushort),
		("number_input_data_indices", c_ushort),
		("number_output_button_caps", c_ushort),
		("number_output_value_caps", c_ushort),
		("number_output_data_indices", c_ushort),
		("number_feature_button_caps", c_ushort),
		("number_feature_value_caps", c_ushort),
		("number_feature_data_indices", c_ushort)
	]

hid = windll.LoadLibrary(r"HIDDevice.dll")
dev = hid.HIDOpen(5824,1503)
if dev != -1:
	hid.HIDGetAttrbutes.restype = HIDD_ATTRIBUTES
	print "Product:", hid.HIDGetAttrbutes(dev).product_id
	hid.HIDGetCaps.restype = HIDP_CAPS
	res = hid.HIDGetCaps(dev)
	print "F:", res.feature_report_byte_length, res.number_link_collection_nodes, res.number_feature_value_caps, res.number_feature_data_indices
	hid.HIDClose(dev)

ここまでCでラッピングしておけばpythonからの操作も楽にできる。

最後に

今回はHIDaspxを2台つかい片方をライタ、もう片方を実験台として使用しました。
HIDaspxではHIDmonというPCからAVR内部のレジスタを操作する機能があるが、I/OやPWMなど単体で動くペリフェラルの他にUARTやUSIなど内部で完結していないと実装が難しい処理が多くある。
これらを自らファームで実装出来ればそれらのペリフェラルを使い倒せるのではないか。
そもそもFT232RLなどの表面実装パッケージで値段が高いUSBtoシリアルドライバや、たとえDIPパッケージでもマイコンにADM3202とかのインターフェイスドライバにシリアルポートをハンダ付けとかにくらべれば多少頭をひねるだけでPCとマイコンの通信が出来るのならいろいろ楽しく遊べるのではないかなとかおもうお正月休みでした。