Arduino上でTinyUSBを使ったCH32VのUSBデバイス


TinyUSBがCH32に対応した事を知ったので,それを使ってUSBデバイスを作ってみた. TinyUBBを使ってみた感想だが,USB serialなどの頻繁に使われるデバイスを作るのは簡単だが,癖が強くてファイルの構造が複雑なので,独自のデバイスの開発は難しいように感じた. 今回は,CH32シリーズの中で,USB機能が内蔵されている安いものとして,CH32V203K8T6を使ったが,そのときに分かったことを,簡単に説明する.

ArduinoにWCHの公式サポートを入れた後に,スケッチ-ライブラリをインクルード-ライブラリを管理から,TinyUSBを検索してインストールする. 他のライブラリもセットでインストールするかを聞かれるが,すべてインストールして良い. それらのファイルは,~/Arduino/libraries/Adafruit_TinyUSB_Library/に保存されるので,必要に応じて参照することができる. 簡単な例はファイルのスケッチ例から見ることができるので,プログラムの参考になるだろう.

基本的な使い方としては,はじめに#include <Adafruit_TinyUSB.h>としてライブラリを組み込んでから,setup()でTinyUSBDevice.begin(0);を,loop()ではTinyUSBDevice.task();を実行する. 作りたいデバイスの種類に応じて,以下のようにオプションを指定することが出来,その後でライブラリをincludeするようにする.

#define CFG_TUD_CDC 1
#define CFG_TUD_MSC 0
#define CFG_TUD_HID 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 1
#define CFG_TUD_VIDEO 0

注意しなければならないのは,必ずUSB Serialが組み込まれてしまうという点である. いじっていて,何をやってもttyACM0として認識されるので,困っていたが,そういう仕様らしい. これを除くには,beginした後で,TinyUSBDevice.clearConfiguration()を実行すれば良い. その後で,自分が作りたいデバイスの定義をして行く. このとき,CDCを0にすると,うまく認識されなくなる. おそらくこれもUSB Serialが一旦組み込まれるからかも知れない.

以下ではvendor specificの場合について説明する. TinyUSBDevice.addInterfaceでAdafruit_USBD_Interfaceのサブクラスを指定すると,そのgetInterfaceDescriptorメソッドが呼び出されるので,その中でdescriptorを作るようにする. descriptorはTinyUSBDevice.allocInterfaceとTinyUSBDevice.allocEndpointとTUD_VENDOR_DESCRIPTORを使って簡単に作ることができる.

control transferについては,device/usbd.cで記述されているtud_vendor_control_xfer_cbに処理を書いておくと,それが実行される. request->bRequestなどの値で分岐を行い,tud_control_xferでデータを返すことができる.

end pointを介したデータのやりとりは,class/vendor/vendor_device.cに記述されている. データの読み書きは,tud_vendor_n_readやtud_vendor_n_writeを使って,指定したバッファに対して入出力する. この際,tud_vendor_n_availableやtud_vendor_n_write_flushなども併用すると良い. TinyUSBDevice.task()が呼ばれたときに,様々な処理が行われるが,その中で, マイコンがデータを受信したときにtud_vendor_rx_cbが, 送信し終えたときにはtud_vendor_tx_cbが呼び出される. これらの関数にデータの送受信の後に行う処理を記述することによって,デバイスの動作を制御することができる.

これまでに書いたUSBデバイスのプログラムでは,PC側からの通信で割り込みがかかったときに,どのような処理をするかを書くことによって,データの送受信を行っていた. end pointの大きさは64バイトなので,データは64バイト毎に区切られるが,割り込みを使うことによって,送受信の要求があったときに次のデータの準備をするようにすると,バッファの大きさを気にせずにデータのやり取りをすることが出来た. しかし,TinyUSBでは割り込みの処理の部分はユーザーからは使わないような設計思想のようである. TinyUSBの現在の仕様では,送信用には1023バイトのバッファが用意されているようだが,バッファに64バイト以上のデータが溜まるか,flushされたときにのみ,実際にUSBへの転送が行われる. 64バイト未満のデータしか無い場合には,flashしないと,読み取ろうとしてもtimeoutしてしまうので,注意が必要である. また,64バイトより大きなデータの場合には,タイミングによっては64バイト溜まったときに一旦データが送られて,データの終了として0バイトのデータも送られることもあり,そこでデータが切れてしまうので,そこにも注意が必要である.

最小限の変更でデータが64byteで分割されないようにするために,tud_vendor_tx_cbがデータの終了の有無を返すようにして,終了のときにだけ必要に応じて0バイトのデータを送るようにしたら,64byteより大きなデータも途切れずに送信できるようになった. 具体的な変更するのは,vendor_device.hとvendor_device.cの中のtud_vendor_tx_cbの型をvoidからboolにして,defaultではtrueを返すようにして,vendor_device.cの中のtud_vendor_tx_cbを呼び出しているところで,その値を以下のように変数に入れて,

// tud_vendor_tx_cb(itf, (uint16_t) xferred_bytes);
bool fin=tud_vendor_tx_cb(itf, (uint16_t) xferred_bytes);

その変数が真のときにのみ,終端信号を送るように,

// tu_edpt_stream_write_zlp_if_needed(rhport, &p_vendor->tx.stream, xferred_bytes);
if(fin) tu_edpt_stream_write_zlp_if_needed(rhport, &p_vendor->tx.stream, xferred_bytes);

とするのである. tud_vendor_tx_cbをユーザーが定義しないときには,trueとなって,これまでと同じ動作をするので,この変更が悪い影響を及ぼすことは無いだろう.

この変更をするために,TinyUSBのソースがどのような構造になっているのかを調べるのに,かなりの時間がかかってしまった. ファイルの構造が複雑だし,関数が多段に呼び出されているので,どこでどの処理が行われているのかを見つけ出すのが大変だった. すべての構造を理解した訳ではなく,ソースの一部をみて判断しているので,上の説明は間違っている部分があるかも知れないが,期待したように動くようになったので,大筋では合っていると思う.

ArduinoでTinyUSBを使えばUSBのプログラムが簡単に書けるだろうと思ってやってみたが,調べても必要な情報が見つからないので,ソースを読む必要があった. CH32V203のUSBDをもっと単純な形で使いたいのだが,ch32funやArduinoのWCH公式ではまだ対応していないっぽいし,Platform IOはまだ使い方が分からないので,ArduinoでやるならTinyUSBが唯一の方法かも知れない. 今回書いたプログラムは,無駄な処理が多いように思うので,もう少しすっきりしたプログラムが書けたら,公開しようかな.