SoftwareUSB(Boot Loader)を作ってみた。

Micronucleus(v-usb)というDigispark(ATtiny85)などで使われているソフトウェアでUSB通信を実現する有名なプログラムがある。今まで何度も利用してきたがどう実現しているのかについての興味や未だにUSBはよくわからんということもあったりするので理解を深めるために勉強がてら自作に挑戦してみた。

Micronucleus(v-usb)のようにクロック別にソースを分けずクロックの動的補正により15MHz以上の任意クロックでの動作や12MHzでも動作するという目標で考えてみた。

12MHzで動作するにはUSB-1bitあたり8サイクル以下で処理しなければならず同時にUSBの符号化処理(NRZI/BIT-STUFFING)も行う必要がある。当初はUSBの符号化処理を別にすれば楽勝じゃないかとも思ったが実際やってみると別にした符号化処理のオーバーヘッドによりUSBプロトコルのリトライに間に合わなくなり完全にボツ。理想と現実のギャップを感じてしまう結果となってしまった。

初めてMicronucleus(v-usb)のソースを見たとき複雑なコードだなって感じしかなかったけど自分でやって見て改めてMicronucleus(v-usb)のソースを見直してみるとMicronucleus(v-usb)の開発者達はとても素晴らしい仕事をしたんだなと感心するばかりだ。

まずはクロックについてであるがUSBのクロック誤差は+-0.25%以下となっているがAVRのRCクロックは+-10%も誤差がある。USB1.1のクロック(1.5MHz)に換算すると12MHzでは+-2.0%(0.25×8)以下に抑える必要があるがRCオシレータの校正ステップは1.5%程度と結構荒くて絶妙な調整が必要だ。

UARTであれば10bit程度なのでかなりの誤差を許容できるがUSB1.1の最大ビット数は96bit(12×8)であり最終誤差が96倍にもなってしまう。なのでクロック誤差を許容値内に収められるかどうかが極めて重要となってくる。

次はRCオシレータの校正処理であるが1ms毎に出力されるUSBのkeep alive signalにより校正している。余計に校正しすぎてから最終調整するところがミソかも。

ちなみにDigisparkが16.5Mhzという中途半端?なクロックになっているのはCPUクロックをUSBクロックの整数倍にすることでクロック誤差がでないようにするための処置である。

今回は全てC++で記述している。なーんて言いつつinline-asmを多用してたりするが、そのinline-asmの記述の仕方が少しイレギュラーだ。でもこのほうがすっきり書けて見通しが良くなる。但し、試してみたら意図した結果になったというだけで公式な記述方法ではないことには注意したほうがいいかもしれない。

クロックの動的補正処理は次のように行う。CPUクロック/USBクロックの整数部分をループとNOPの組み合わせにより処理し、小数部分は1バイトに収まるよう1/256(0.39%/F_CPU)の精度で処理している。レジスタに誤差を蓄積しオーバーフロー毎に1サイクル追加するという動作を繰り返すだけだ。誤差の初期値をバイトの中間値の128とすることで+-0.5サイクル以内、数十ナノ秒程度の誤差に収めることができる。

誤差補正処理はC/C++のコンパイル時評価+最適化機能により必要なinline-asmだけを組み合わせるコンパイル時コード生成(勝手に命名)という手法を使っている。コンパイラのデッドコード削除を積極的に利用することで必要なコードだけを残す方法である。下記の#define内のコードが全て展開されるわけではなく引数評価により到達可能なinline-asmのみが展開され実行する意味のないif文や変数は除去される。但し、定数評価できる引数を指定する必要がある。

送信処理の基本ロジックはbit-stuffingありなしでクロックが乱れないよう工夫している。基本ロジックは6サイクルで処理が完了するが送信データをロードするだけでも2サイクル追加となるため余裕はない。他に必要な処理は各ビット処理の残りの2サイクルに分散配置している。

ちなみにcall/retではなくbrsh(2)/rjmp(2)を使っているのはcall(3)/ret(4)だとサイクル数が大きすぎて使えないから。

※ATtiny1614などのAVR1シリーズではシングルサイクルの出力反転機能が削除されてしまった(改悪だ!)ため1サイクル追加となり16.5MHz以上でないとクロック動的補正することができない。20MHzで実行できるから特に問題ではないんだけど...モヤッと感は残る...

受信処理の基本ロジックはMicronucleus(v-usb)とほぼ同じコードになっている。送信処理と同じく基本ロジックは6サイクルで他に必要な処理は各ビット処理の残りの2サイクルに分散配置している。

CRC計算はMicronucleus(v-usb)で使用されているものを有難く使わせてもらったがデータコピーとCRC計算を同時にできるように改良してある。この改良により送信パケット生成処理に必要なクロック数を半減させることができる。

コーディングもほぼ完了したので試してみたら全く動かない。USBプロトコル処理のタイミング規定というものがあるのかどうかはわからないが送信タイミングが想定外でかなりハマってしまった。ホスト側のリトライ送信間隔が最短で20usと非常に短くてCRCを含む送信パケットを組み立ててる途中でホスト側がリトライ送信をかけるためデバイス側の送信とカチあったりしてしまうのだ。このへんは割込み処理で受信すれば必然的に避けられそうなのだが割込み対応すると何かと面倒くさいことになるためポーリングによるバス調停により回避してみた。ちなみにMicronucleus(v-usb)もポーリングによるバス調停をしている。

動き始めたときは2Kバイト程度に収まっていたのだが、その後にちょっとした対策などを入れてたら2.5Kくらいになってしまった。2Kバイトを切っているMicronucleus(v-usb)は凄いなと改めて思ったり...でも、クロック動的補正などを入れたからMicronucleus(v-usb)より大きくなるのは当然か...

【概要仕様】

Digispark(Micronucleus)ブートローダーと互換仕様。Digisparkを追加したArduino環境からDigisparkとして書き込みできる。

ブートローダー自体は、AVRシリーズ共通かつ下記クロックの範囲内で動作するようにしたつもり。

AVR 12.0MHz/13.5MHz and 15.0MHz-20.0MHz
AVR1 13.5MHz/15.0MHz and 16.5MHz-20.0MHz

※ATtiny85(16MHz)でのみ検証している。その他環境は未試験であることに注意すべし!
※USBがPCに接続されていない(給電のみ)場合は5秒待たずにアプリを即座に起動するように改良している。

【ブートローダー書き換え】

Digisparkを追加したArduinoから書き込むとブートローダーを書き換えてくれる。

bootcode.zip (Arduino Project)

【ブートローダー書き換えソースコード生成ツール】

ブートローダー書き換え(Arduino)のソースコードを生成するためのツール。ブートローダーのHEXファイル(swusb.hex)を読み込んでbootcode.c/bootcode.hを生成する。

bootcode-eclipse.zip (Eclipse-CDT Project)

【ブートローダー開発環境】

開発はAtme Studioで行った。ブートローダーのコードをフラッシュの後方アドレスに移動し正しいリセットベクターを設定するためにリンカーのMemory SettingsのFlash segmentの指定が必要。アドレスは64(128)バイト境界アドレスをwordアドレスに変換した値を設定すること。

なぜかはわからないがAtme Studio起動直後のコンパイルが失敗するが2回目以降は問題なくコンパイルできる。なんかおかしなことをしているのかな?

Atme Studio起動直後のコンパイルが失敗するのは、Memory SettingsのFlash segmentの設定順の問題だった。既存のセグメントを移動する前にリセットベクターアドレスを指定してしまうと領域が重なるためにエラーになるみたい...

swusb.zip (Atmel Studio Project)

【ブートローダーライブラリ】