現在、某ウィルスで自宅ニート状態にあります。
そこでこのまま時間を YouTube やゲームに溶かすのは勿体ないということで、マイコンの USB ペリフェラルを手でコードを書くことで実装していきたいと思います。この記事シリーズはハマったところとか、新しく理解したところの日記ですので、雑な記述が多いかも知れません。今後見つけ次第改善していきますが、公開したての時は粗が多いかも。とりあえず日誌や頭の中の情報整理の目的で書いていますので、予めご了承下さい。
また、この試みは過去に何回か挑戦していて、毎回決まって挫折しています。今回はうまくいくか...?
HAL 等のライブラリは一切使わず(ここ重要)、デバイスとして PC に認識されるように実装できたらなあと思います。ってか何で皆さん HAL に頼りっきりなんでしょうか。Qiita の記事等を見ても HAL HAL HAL。何が良いんだか全くわかりません(メリットしか無い)。ともかく、低レイヤerを自称する私は STM32HAL になんか頼りたくありません。
目標はこんな感じ。予定は変わったり、ひょっとしたら一旦諦めるかもですが。
- STM32F042 マイコンの USB ベリフェラルを使って、USB デバイスを作る
- このマイコンは外部プルアップ抵抗や外付けクリスタルが不要でとても手軽
- プログラムの書き込み自体も USB DFU で行えるので動作確認が楽々
- 目標はとりあえず小さく。ひとまず PC に認識されるようにする。
- Homebrew でインストールした
lsusb
で見られるようになったら第1段階のゴール。
- Homebrew でインストールした
- 最終的には USB CDC を実装して、USB シリアルができればいいなと思う
USBのお勉強
そもそも USB って何だ?とか信号線は何があるんだ?とかあんまり良く分かってないので、とりあえず勉強をすることにしました。勉強に使ったサイトはこちら。
- USBの通信プロトコル - picfun
- USBの基本アーキテクチャ - picfun
- Bare metal STM32: Writing a USB driver | Projects & Libraries
- 実際に僕と同じようなことをした人の記録。STM32 USB ペリフェラルの面倒なことを中心に述べている。レベル高。
- STM32F0x2 のリファレンスマニュアル
- かなり説明が雑(愚痴)。長ったらしく文章で書いてあったり、肝心なところの説明が一切なかったり色々と抜けがある。上の先人の例等も参考にしながら頑張って解釈していきます。
この他に、HAL を参考にしました。
ピン配線
使うピンは D-, D+。それぞれ差動で動いているみたい。確かに差動ペアはノイズに強いよね。んで下手に UART 知ってる人だと、PC(ホスト)側の D+ と、マイコン(デバイス)側の D- を結びたくなっちゃうんだけど、そうすると電流がとめどなく流れるのでやらないように。くれぐれも D+ は D+ に、D- は D- につなぎます。
本来はこれの他にプルアップ抵抗をどうするとかありますが、とりあえずここらへんの話は知らなくてもできるので放置。
このマイコンに限れば、通信線はそのままマイコンに繋ぐ。VBUS は 5V なのでこれを電源にしたらマイコンが死にます。よってレギュレータ等で VBUS を 3.3V に落としてからマイコンに入れること。
フレーム・パケット・トランザクション
通信の単位です。picfun をよく見ればいいですが、左から順に単位が小さくなってくる。ここらへんは覚えにくいけどしっかりと頭に入れておかないと、マイコンのマニュアルや USB の規格書を読んでいる時に混乱したりイライラしたりするのできちっと基本は抑えておきましょう。
エンドポイントと転送方式
もうここらへんで専門用語のオンパレードです。ギブ...
パイプ
USB はデータ線1ペアのみの通信方式ではあるが、擬似的に複数の独立した通信線があるかのように考えるのが基本です。この擬似的な通信線(データライン)をパイプといいます。パイプごとにデータの通信方式が決まっており、IN(デバイス -> ホスト)、OUT(ホスト -> デバイス)というように分ける(後述するコントロール転送は例外)。ですのであるデータの送受信を実装したい場合は、デバイス側に送信と受信専用のパイプをそれぞれ1つずつ、合計2つ用意する必要があります。
エンドポイント
パイプの行き着く先がエンドポイント(EP)で、これは送受信したデータを格納するバッファを示しています。バッファのことをエンドポイントと呼ぶらしいです。が、実際のところパイプと混同されることが多いようです。STM32 のリファレンスマニュアルでは事実そうなっているきらいがあります。
なおデフォルトのパイプというのがあって、それはエンドポイント0です。これは後述するコントロール転送を最初から有効化しておく必要があり、まずホストから認識されるようになる際に通信が行われるエンドポイントです。
コントロール転送
ざっくりと書きます。ひとまず EP0 を何とかすることが目標なので、とりあえずその他の転送方式は省略しますね。
コントロール転送とは、EP0 が担う、双方向のややこしい転送方式。通信はセットアップステージ、データステージ、ステータスステージに分かれています。
セットアップステージはセットアップトランザクションで出来ています。ここに、次に来るデータの転送方向や個数(0個のこともある。この時はデータステージが存在しない)が書かれています。
データステージは送受信するデータそのものです。もちろんいくつかのパケットで出来ているわけですが...
ステータスステージは直前のトランザクションと逆方向の通信をします。データ数は0。
...と、コントロール転送は以下を注目する必要があるわけです。
- SETUPトランザクションに含まれるデータ数と次の通信方向
- データ情報(ポインタ、どこまで送受信したか)
- 現在どのトランザクションをしているのか、次はどのトランザクションになるのか
ちょっと実装が面倒くさそうですね。ちょっと気が引けます、ががんばります。
STM32のUSBペリフェラルのお勉強
直感的にわかりにくいものを載せます。
動作のトリガー
割り込みで動かします。データをホストから受信したタイミング等できっちり割り込みが入るので、ハンドラ内部でガチャガチャやっていきます。なので割り込みレベルを上げておくことをお忘れなく。
また、エラー無き場合の割り込みはトランザクション単位で入るようです。以下のように。
- SETUP の場合は SETUP パケットを受信したら
- IN/OUT の場合は その処理が終了したら
IN/OUT が通信完了後に割り込みとなっているのがポイントかつ嫌なところです。何が嫌かって?実際にこのペリフェラルを動かしてみれば分かりますよ。例えば、以下のことを常に気にかけねばなりません。
- 更にデータを送受信しなければならないのか(送受信し残しているのか)
- ただレジスタにぶち込めばいいだけではないことを意味している。すなわちデータのポインタ、データ長をマイコン側で記憶させておかねばならない。
- 今はただの IN/OUT なのか、それとも STATUS_IN/STATUS_OUT なのか
- これによって次の割り込み時の挙動が異なる。STATUS だったら次は SETUP を準備しなければならないし、そうじゃなかったらまた別の話になる。
あれ、これって、有限状態機械(FSM) を使う出番では...
有限状態機械とは、状態遷移をいい感じにコンパクトに表したものです。現在のステートと何らかの値を入力として、次の状態と値を出力する関数って考えるといいでしょう。
PMA
USB ペリフェラルが用いる専用の SRAM 領域があります。サイズは 1kB で、CAN と共用らしいです。今回は無視しておきましょう。
そしてここはマイコンによって異なりますが、どうやら 16bit アクセス専用のようです。なので様々な値を読み書きするためには、一度別途 RAM 上で設えて、転送等を行う必要がありそうです。
開始アドレスは USB_PMAADDR
というマクロで定義されています。
BTABLE
バッファテーブルです。各エンドポイントは以下のようなバッファを持ちます。そのバッファを PMA 上に配置する必要があり、そのバッファ配列と USB_PMAADDR
とのオフセットを格納するのが BTABLE
レジスタです。
typedef struct
__attribute__((packed))
tagPacketBuffer {
uint16_t addr_tx;
uint16_t count_tx;
uint16_t addr_rx;
uint16_t count_rx;
} PacketBuffer;
USB接続時の挙動
概ね、以下の様らしいです。
- ホストがリセットをリクエストするので、デバイスはすぐに応じる
- ホストは GET_CONFIGURATION を送る
- ホストはアドレス 0 のデバイスに SET_ADDRESS を送るので応じる
- デバイスは与えられたアドレスを実装する。以後ホストはそのアドレスで指示を出す
- ホストは GET_DESCRIPTOR を送る。一旦 8Bytes のみの送出をリクエストするので応じる
- 再度ホストは GET_DESCRIPTOR を送る。今度はフルサイズの 18Bytes なので応じる
後は知りません。多分デバイスの種類に依存するんでしょう。ディスクリプタとか用意しなければなりませんな。おおん
あ、ディスクリプタ(Descriptor) というのはデバイス情報を配列にした一連のデータ群を指します。これによってどのようなデバイスなのか、通信速度は如何ほどか、製造者は誰なのか等々が分かります。
〆
次回は何らかの通信を PC からできればなと思います。それでは。
コメント
初めまして。jujurouと申します。
記事の内容で気になる点が有りましたのでコメント記載します。
パイプの説明欄のIN, OUTの内容が反対だと思います。
ホストが中心でホストに対してINなのかOUTなのかという定義だと理解しています。
> IN(ホスト -> デバイス)、OUT(デバイス -> ホスト)
↓
IN(デバイス -> ホスト)、OUT(ホスト -> デバイス)
ご指摘ありがとうございます...
私もその理解でしたが、どうも慣れずに逆に書いてしまいます。他の記事でも類似のご指摘がありました。
ややこしくしてすみません。修正しておきます。