SNSへはこちら

CH559マイコンをいじってみよう(8) - USBデバイス

このマイコンの目玉である USB です!特にホスト機能に人気が高いと思いますが、慣れないので今回は USB デバイスの方をやっていきたいと思います。

進め方

自分がソフトウェア実装をどう進めていったかを示します。参考にどうぞ。

まず、割り込みが入るかとかリセット割り込みがかかるかとかの簡単な確認までは LED 点灯でデバッグしていました。割り込み関数内の if 文内に核といったやり方。
SETUP パケットが送られるような段階になると、UART を使って print します。ただ、僕は UART0 という低速なペリフェラルを使いました(高速な UART1 と USBホスト は両立不可、とのことだが僕は勘違いしてデバイスでもできないと思っていた)。皆さんは UART1 を使いましょう。
当然低速なので多くの情報を print していくと通信に支障を来たします。最初こそは色々と出してもいいでしょうが、ある程度地に足が付いてからは最小限にしましょうね。

途中よくわからなくなったときは相変わらずの print デバッグを行ったり、サンプルプログラムをこっそりみて参考にしていました。

また、やっぱり USB は複雑で紛らわしいので、要所要所で見やすいようにマクロを作ったりと自分の中での可読性に気を使いました。他にも if のネストを極力減らすとか。
ロジックは完璧なはずなのに動かないというときは、不思議なことにコードのリファクタリングなど最適化を行うとうまくいくことがあります。これは僕の中で金言(?)です。

初期化

汚いですが初期化コードをどうぞ。D+, D- 端子のプルダウンがどうのこうのって結構大事なので、知らない人は調べてみましょう。
データシートにはレジスタが散在しているので、設定する必要のあるレジスタの存在に気が付きにくくてスタックしていました。

void usb_device_init(void) {
    USB_CTRL = 0; // release all resets
    UEP0_CTRL = 0xF; // send STALL to all packets

    USB_INT_EN = (1 << 0) | (1 << 1) | (1 << 4);
    // IRQs: BUS RESET, TRANSFER, FIFO OVERFLOW
    EA = 1;
    IE_EX |= 1 << 2;

    UDEV_CTRL = (1 << 4) | (1 << 5); // disable all pull-downs
    USB_CTRL = (2 << 4) | (1 << 5) | (1 << 3) | (1 << 0); // enable pull-ups, DMA en
    UDEV_CTRL |= 1 << 0; // enable physical port

    if( (unsigned int)ep0_buffer & 1 ) {
        ep0_buffer++;
    }
}

if 文のところはこのあと述べます。

動作コードたち

ご覧のようにコード行数が長いので Gist で失礼します。久々に cat を本来の意図で使った。

$ cat src/usb.c inc/usb.h | wc -l
238

usb.c
usb.h

久々に USB プログラムを書きましたが、案外忘れているもんですねえ。いくつかのハマりどころを除いて、このマイコンは USB デバイス入門には結構いいと思う。
usb.h の共用体sdcc と他のコンパイラでエンディアンが異なるらしいので、必要に応じて書き換えてください。今回は sdcc 使用。

ハマりどころ

出ました。この記事の一番の存在目的。色んな意味で時間を食った部分を書き出します。

DMAのためにはxdata領域を用いる必要がある

ここが一番ハマりました。通常グローバル領域に変数を宣言すると、内部 RAM (internal RAM) 領域に確保されます。
ダイレクトアドレッシングができて都合がいい、というわけには行かず、「xdata 領域にヨロ」とコンパイラに教える必要があります。そのためのキーワードが __xdata です。

__xdata unsigned char buffer[64];

このように指定すると OK です。いくら割り込みが入っても __xdata を付けなかった部分の RAM が更新されていなかったので詰まっていました。

DMAのためのアラインメント

USB を通してデータを転送するためには DMA を当然ながら用いるのですが、その転送に使うメモリ領域についてです。

その領域(前述のように xdata 内部に含まれる必要あり)のアドレスを指定する必要があるのですが、このレジスタでは最下位ビットが0に固定されています。つまり、16bit アラインメントに揃えよとのことです。
gcc であれば __attribute__((aligned(8)) と書けばいいだけですが、sdcc の場合はアラインメントを指定する属性やプラグマが存在しません。そこでパクった熟考の末に実装したのがこちらの大変参考になる Qiita 記事に書かれていた方法です。
アドレスを見てズラす、と言う感じ。

// グローバル変数
__xdata unsigned char ep_buffer_[1][65];

// ↓初期化関数内...
    if( (unsigned int)ep0_buffer & 1 ) {
        ep0_buffer++;
    }

...あれ、全部 DMA 関係じゃん

以上です。割と死ぬほどハマるという箇所は少なかったという所感です。

ここで、一旦 CH559L の開発を止めようと思います。目玉の USB ホストができていませんが、締めます。
というのも、心理的にホストの学習に対して障壁が大きいことが一番です。別のことをしながら徐々に学ぼうかなあと。現に USB デバイスを初めてやった STM32 では、問題が USB の規格にあっていないのか、ペリフェラルがうまく動いていないのか、全く分からんと言う状況になってスタックしたこともあり、効率的に行きたいなあと思っています。

ところで会社では日常的に、ハードもソフトもマイコンに触れることができて非常に楽しいです。
部外秘なので詳しくは言えませんが、社内で使っている IDE において、既存の環境ではなくリンカスクリプトも含めて自作したり、という息抜きもしています。業務に関係ないがね。今ところ趣味を仕事にしてよかったと思います。
楽しくていじっていて気づいたら残業なんてこともありました。もちろん翌日は定時退社を決めました。今後はどうなるのか...