ch55xduinoを使ってch552で独自のUSB機器
ch552を使って,vendor specificな独自のUSB機器を作ろうと思ったが,情報がなかなか見つからなかった. メーカーのサイトのCH554EVT.zipに含まれている EVT/EXAM/USB/Device/VendorDefinedDev.Cがその公式な例であるが,コメントが中国語だし,汎用性に乏しく,Keil C51という特殊なコンパイラ用のコードで書かれているため,流用が難しい. そこで,いろいろと調べたり試したりした結果,ch55xduinoでch552を使ったvendor specific機器の作り方が分かったので,それを記録に残しておこうと思う.
ch55xduinoに含まれているusb関係の定義だけでは,基本的な処理をするコードが足りないので,sdcc用に書かれたコードを利用することにした. ch554_sdcc_usb_blinkyのprojects/includeからusb_desc.hとusb_intr.hを取って来て使う. このとき,usb_intr.hの中の次の一行目の行を,二行目のように変更すると,ch55xduinoで使えるようになる.
void DeviceUSBInterrupt(void) __interrupt(INT_NO_USB) void USBInterrupt(void)
これは,USBの割り込みがch55xduinoの中ですでに定義されているためである.
これらのファイルと同じフォルダにinoファイルを作って,arduinoを使ってプログラムをするのだが,その段階では,いくつか注意するべき点がある. 例としてend point 0とend point 1を使う場合について説明しよう. Arduino.hを読み込む時に,ch5xx_usb.hが読み込まれるので,default値から変更する場合には,その前にEP0_BUFF_SIZEとMAX_PACKET_SIZEの値を定義しておく必要がある. その後,上記のusb_desc.hを読み込んで, 独自のUSBに関する関数の定義をしてからusb_intr.hを読み込む. そして,end point 1のためのバッファーを確保する. 独自の関数としては,end point 1の初期化と,control transferのIN/OUTの処理,end point 1のIN/OUTの処理を書く. setupではUSBの初期化ルーチンを呼び出し,loopでは送受信のデータの処理などを行う.
end point 0は主に制御に使われて,USB機器を認識するための情報などがやり取りされるが,一部はvendorも使うことができる. ホストからvendor用のデータを転送する場合には,bRequestTypeを0x41とすると,USB_CUST_CONTROL_DATA_HANDLERで定義された関数が呼ばれる. ホストからのvendor用のデータを要求する場合には,bRequestTypeを0xc1とすると,USB_CUST_CONTROL_TRANSFER_HANDLERで指定された関数が呼ばれて,その返り値のバイト数のデータがホストに送られる. これらの関数中では,UsbIntrSetupReqにはbRequestの値が入っているので,その値によって適切な動作をするように定義する. さらに,UsbSetupBuf->wValueL,wValueHで値を受け取れるので,様々な動作をさせることができる. しかし,やり取りできるデータのサイズは最大でも64バイトであり,小さいデータしか扱うことが出来ない.
大きなデータのやりとりは,end point 0以外を使う. end point 1は,バッファーの大きさが64バイトだが,データを分割することによって,大きなデータを扱うことができる. bulk transferの場合について,この処理を説明する. マイコンへの送信(OUT)の場合には,64バイトのデータを受け取った段階で,NAKにすると,ホストが送信を待ってくれるので,次のデータを受け取れるようになったときに,ACKとすると,次のデータが送られてくる. 送られてきたデータが64バイトのときには,次のデータがあり,それよりも小さい場合には,データの終了であると判断できる. ここで,注意しなければならないのは,データのサイズが64バイトの倍数の場合には,最後のデータとして0バイトのデータを送らないと,データの終わりを判断できないということである. この処理はlibusbが自動でやってくれると思っていたが,最後の0バイトは,自分で送らないといけないということに気付くまでに,試行錯誤をしてしまった. マイコンからの送信(IN)の場合にも,同様の処理を行う. 送りたいデータの64バイトが準備できたら,ACKとすると,ホストが読み取りに来るが,読み取り終わったらNAKにして,次のデータを準備する,という感じで繰り返す. 最後のデータは0から63バイトのデータとして送る.
簡単な動作をさせるプログラムを例として示す.
#define EP0_BUFF_SIZE 16
#define MAX_PACKET_SIZE 64
#if (EP0_BUFF_SIZE+2*MAX_PACKET_SIZE) > USER_USB_RAM
#error "This example needs more USB ram. Increase this setting in menu."
#endif
#include <Arduino.h>
#include "usb_desc.h" // from https://github.com/ole00/ch554_sdcc_usb_blinky
// custom USB definitions, must be set before the "usb_intr.h" is included
#define USB_CUST_VENDOR_ID 0x4348
#define USB_CUST_VENDOR_NAME_LEN 7
#define USB_CUST_VENDOR_NAME {'w','c','h','.','c', 'n', 0}
#define USB_CUST_PRODUCT_ID 0x5537
#define USB_CUST_PRODUCT_NAME_LEN 7
#define USB_CUST_PRODUCT_NAME { 'v', 'e', 'n', 'd', 'o', 'r', 0 }
#define USB_CUST_CONF_POWER 120
#define USB_CUST_EP_COUNT 2
#define USB_CUST_EP_DEF USB_EP_DSC ep01o; USB_EP_DSC ep01i;
#define USB_CUST_EP_DESC {sizeof(USB_EP_DSC), USB_DESC_EP, USB_EP01_OUT, USB_TRNT_BULK, MAX_PACKET_SIZE , 0x00}, {sizeof(USB_EP_DSC), USB_DESC_EP, USB_EP01_IN, USB_TRNT_BULK, MAX_PACKET_SIZE , 0x00}
#define USB_CUST_EP_INIT initEndPoint()
#define USB_CUST_EP1_IN_HANDLER handlerVendorEndPoint1in()
#define USB_CUST_EP1_OUT_HANDLER handlerVendorEndPoint1out()
#define USB_CUST_CONTROL_TRANSFER_HANDLER handleVendorControlTransfer()
#define USB_CUST_CONTROL_DATA_HANDLER handleVendorDataTransfer()
void initEndPoint();
void handlerVendorEndPoint1in();
void handlerVendorEndPoint1out();
static uint16_t handleVendorControlTransfer();
void handleVendorDataTransfer();
#include "usb_intr.h" // from https://github.com/ole00/ch554_sdcc_usb_blinky and modify as below
// //void DeviceUSBInterrupt(void) __interrupt(INT_NO_USB)
// void USBInterrupt(void)
__xdata __at (EP0_BUFF_SIZE) uint8_t Ep1outBuffer[MAX_PACKET_SIZE]; // OUT
__xdata __at (EP0_BUFF_SIZE+MAX_PACKET_SIZE) uint8_t Ep1inBuffer[MAX_PACKET_SIZE]; // IN
volatile __xdata uint8_t writing = false;
volatile __xdata uint8_t reading = false;
volatile __xdata uint8_t ready = false;
volatile __xdata uint8_t len = 0;
volatile __xdata uint16_t sum=0;
void initEndPoint(){
UEP1_DMA = (uint16_t) Ep1outBuffer; // EP1 data transfer address
UEP1_CTRL = bUEP_AUTO_TOG // EP1 Auto flip sync flag
| UEP_T_RES_NAK // EP1 IN transaction returns NAK
| UEP_R_RES_ACK; // EP1 OUT transaction returns ACK
UEP4_1_MOD = bUEP1_RX_EN | bUEP1_TX_EN; // EP1 OUT/IN buffer
}
void handleVendorDataTransfer(){ // vendor OUT
len = USB_RX_LEN;
switch(UsbIntrSetupReq){
case 1 : *Ep0Buffer+=1; break;
case 2 : *Ep0Buffer+=2; break;
default: *Ep0Buffer+=0xff;
}
}
uint16_t handleVendorControlTransfer(){ //vendor IN
uint16_t value;
value=UsbSetupBuf->wValueH; value=(value<<8) | UsbSetupBuf->wValueL;
switch(UsbIntrSetupReq){ //UsbSetupBuf->bRequest
case 0 : reading=true; return 0; break;
case 1 : *Ep0Buffer=ready; return 1; break;
case 2 : Ep0Buffer[0]=sum & 0xff; Ep0Buffer[1]=sum>>8; return 2; break;
case 5 : *Ep0Buffer=1; return 1; break;
case 16: return len; break;
default: return 0xFF; // Command not supported
} // end of the switch
return 0; // no data to transfer back to the host
}
void handlerVendorEndPoint1out(){ // host to device
len=0;
if ( U_TOG_OK ) { // Out-of-sync packets will be discarded
// UEP1_CTRL ^= bUEP_R_TOG; // Automatically flipped
len = USB_RX_LEN;
UEP1_CTRL = (UEP1_CTRL & ~MASK_UEP_R_RES) | UEP_R_RES_NAK; // respond NAK. Let main code change response after handling.
writing=true;
}
ready=false;
}
void handlerVendorEndPoint1in(){ // device to host
UEP1_CTRL = UEP1_CTRL & ~ MASK_UEP_T_RES | UEP_T_RES_NAK; //NAK by default
ready = false;
}
void setup() {
delay(5);
USBDeviceCfg();
}
void loop() {
uint8_t pos=0;
if(writing && !ready){
for(pos=0;pos<len;pos++) Ep1inBuffer[pos]=Ep1outBuffer[pos];
sum+=len;
if(len<MAX_PACKET_SIZE) writing=false; // end of data
else ready=true;
UEP1_CTRL = (UEP1_CTRL & ~MASK_UEP_R_RES) | UEP_R_RES_ACK; // receive next data
}
if(reading && !ready){
for(pos=0;pos<MAX_PACKET_SIZE;pos++){
if(pos==sum){ reading=false; break; }
Ep1inBuffer[pos]+=1;
}
sum-=pos;
UEP1_T_LEN = len=pos;
UEP1_CTRL = UEP1_CTRL & ~MASK_UEP_T_RES | UEP_T_RES_ACK; // transmit
ready=true; // data ready for IN
}
}
arduinoのUSB settingsは"USER CODE w/ 148B USB ram"を選択する. また,書き込む権限のためには,ch55xduinoのudevの設定を適切に行っておく. CH552はP3.6を3.3Vにプルアップして,USBに接続することによりDFUモードにして,すぐさま書き込む. コンパイルに時間がかかる場合には,コンパイルが終わるタイミングを見計らって,USBに接続すると良い.
このデバイスにrubyからアクセスするには,libusbを使う. それをインストールするために,以下のコマンドを実行する.
sudo aptitude install ruby-dev sudo gem install libusb
権限を設定していない場合には,usbにアクセスするためには,sudoが必要である. プログラムでは,まずは以下のようにhandleやinterfaceを定義する.
require "libusb" usb = LIBUSB::Context.new device = usb.devices(idVendor: 0x4348, idProduct: 0x5537).first handle=device.open
end point 0にデータを送信するときには,例えば以下のようにする.
handle.control_transfer(bmRequestType: 0x41, bRequest: 1, wValue: 0x0000, wIndex: 0x0000, dataIn: 2)
end point 0に制御信号を送って,データを要求するときには,例えば以下のようにする.
handle.control_transfer(bmRequestType: 0xc1, bRequest: 1, wValue: 0x0000, wIndex: 0x0000, dataIn: 2)
なぜかわからないのだが,bRequestが5ではエラーが生じるので,そこは使わないようにしている. pythonでやっても同じエラーが出るのだが,原因は不明である. end point 1でデータを送受信するときには,それぞれ以下のようにする.
handle.bulk_transfer(endpoint: 1, dataOut:"123456"*12) handle.bulk_transfer(endpoint: 0x81, dataIn:123)
0x80の桁は,送受信の方向を表している. データの分割は内部で行われるので,バッファの大きさは気にしないで良いが,前に説明したようにバッファのサイズの倍数のデータを送信するときには,自分で0バイトのデータを加えるようにする.
以上で説明したことを使うと,様々な独自のUSB機器を作ることができるが,いくつかの改善点が思いつく. ここで用いたch554_sdcc_usb_blinkyの定義ファイルは,公式サイトのコードをsdcc用に書き換えて,汎用性を高めるために工夫をしたものだと思われる. そのusb_desc.hの内容の一部はch5xx_usb.hの内容と重複しており,名称の変更をすることによって,省略が可能である. また,usb_intr.hの中身を書き換えないとserial numberが指定できないという欠点もある. そのため,同じ機器を二台つないだときには,それらを区別できなくなってしまう. もっとも,descriptorのすべてのパラメータを変更可能にするのは大変ではあるが. しかし,こららのファイルがch55xduinoと共存できたのはラッキーだった. どちらもsdccを使っているという共通点があり,公式のコードを参考にしているだろうから,かなり似通ったものになっていたのだろう. この手法を使って,何か作ってみたいと思う.