このレポートは [modernAVR] AVR-DU の USB周辺機能考察の続編だ。2023/12の記事公開から5ヶ月を経て、AVR-DU シリーズ最初の製品版 AVR64DU28/32 のローンチが成された。2024年末にかけて AVR16DU14/20/28/32、AVR32DU14/20/28/32 も発売され、予定された製品ラインナップが完成する見込みである。
当時の記事からアップデートされた情報だが;
- AVR16DU20、AVR32DU20 の 20pin外囲器に、3mm角の VQFN-20系列が加わった。これは実用上重要だ。ちいさいは正義。
- 公称最大速度は AVR-Dx系列共通の 24MHz に落ち着いた。32MHz/28MHz対応は(消費電力や実用精度が考慮されない)非公式オーバークロック動作となる。
- AVR64DU28/32の初期ロットシリコン(Rev.A3)は、20MHzまでしか動作保証されないエラッタがある。一応 USARTや USB周辺機能への影響は確認できないがおそらくシリコン製造の問題で、24MHz以上での USB周辺動作が可能かどうかは個体差による。
- 公式開発環境として Microchip MPLAB X (XC8) のコードジェネレーターが AVR-DU シリーズに対応した。
困ったのは件の Microchip MPLAB X (XC8) の出力するコードが非常に大きいことだった。これは全ての HAL(Hardware Abstraction Layer; ハードウェア抽象化層)がコールバック関数で動的に再定義されることによる。Microchip社のあらゆる MCUに適応しなければならない反動であるが、かなりのコードスペースがコールバック処理で埋め尽くされ、どんなに簡単な実装でも 8KiB 程度の Flash容量と、少なくないワークメモリを奪われてしまう。
筆者が求める実装目的はより低階層の ブートローダーやプログラム書込器のファームウェアなので、これは甚だ都合が悪い。許容できるリソース消費量はその半分だ。つまるところ USBプロトコルスタックのフルスクラッチ(完全自作)に挑むほかなかった。
さてそれであるなら「こういうのができたから見て見て」とソースコードを晒すだけで終わるのだが、実際どれがどう言う理由で、アルゴリズムで、ハードウェア特性に基づいたコードをしてるのか何も解説しないのは不親切だろう。特に AVR-DU シリーズは最新製品なので集合知も非常に少ない。流行の AIにお伺いを立てても全く的を得ないのは、肝心の公知がまだ世に存在しないのだから当然だろう。出されるのは過去に自分が発言した件ばかりなのは如何なものか。
そういうことでここからは実際の実働チップでどういう(HALに当たる)低階層コードを書いたら USB-CDCプロトコルが動いたかを述べてみよう。物理面や構成ディスクリプタの構築についてはいくらでも関連資料があるので、ここでは触れない。
記事の前提となる必要要件は以下とする;
- AVR64DU32 Curiosity Nano (EV59F82A) - 前提ハードウェア:もちろん素のベアメタルチップも可
- GCC Compilers for AVR® and Arm®-Based MCUs and MPUs - AVR 8-Bit Toolchain + avr-libc
- Microchip AVR-Dx Series Device Support - 上記のサポートデバイス拡張セット
- askn37/multix-zinnia-sdk-modernAVR - これらを内包する AVR-Dx/Exシリーズ専用 Arduino IDE 対応のベアメタル開発環境
結局、参照できるまともな資料が Microchip社と USB.org の公開データシート数枚に限られ、実質的にクリーンルーム開発となった。まあ MITライセンスで公開するぶんには縛りがなくて都合が良い。
なお現状では ATmega32U4 等と違い、公式純正 DFU bootloaderファームウェアの開発と配布はなさそうな塩梅だ。現在までに入手した製品にはいずれも DFU bootloader は焼かれておらず、中身は空か、サンプル動作コードが入っているだけである。チップの初期化は AVR-DU対応のプログラム書込器と書込ユーティリティが必須だ。その点で難易度はやや高め。
USB周辺機能の制御
AVR-DU系列のUSB0
周辺機能は、XMEGA-AU 系列のそれとよく似ている。エンドポイント構成テーブルの構成はほぼ同等だ。ただし随分と機能が整理されている。
- USB2.0 Full-Speed (12Mbps) 対応固定。ハードウェア的な Low-Speed 対応はなく、関連制御ビットもない。
- Pull during Reset 機能はなく、関連制御ビットもない。デバイスリセット中の D+/D-ポートプルアップ維持に関する可否は記載がない。
- ATmega32U4 のように、D+/D-ポートを GPIO的に操作できる機能はない。
- USB_CLK(48MHz)生成は内部自動制御である。PLL関連設定はなく、キャリブレーション調整も存在しない。
- VUSBポートに繋がる内蔵 LDOの制御も既定ではオンであるから忘れていても良い。外付けカップリングコンデンサの付け忘れに注意するだけ。
- PingPong 自動応答動作は削除され、存在しない。
- BUSNAK0/1 の個別制御は削除され、単一の BUSNAK制御ビットに整理されている。
- レジスタビット位置の変更や、同一機能でもレジスタ名称変更が加えられているので、互換性はない。
AUXDATA
からMCNT
など。これについては適宜読み替えねばならない。
使用する USBベンダー/プロダクトID
USB-CDC互換実装とし、Windows/macosとも OS標準デバイスドライバによる USBシリアル通信の実現を目標とする。それに関連してVID/PIDペア、および供給者識別符号は、Microchip社公式 MCHP製品向け公開サンプルコードに準じて次のようにした。
- USB-CDC VID:
0x04D8
+ PID:0x0B15
- 供給者識別符号:
Microchip Technology Inc.
(あるいはMCTI
と略す)
どのみち Microchip AVR-DU 系列製品専用でしか実行できないコードを書くのだからこれで問題はない。Criosity Nano AVR64DU32 をリファレンスハードウェアとし、AVR-DU系列純正ベアメタルチップを採用する限り、これらの同一性は問題ではない。USBホスト側から見て区別する理由もない。
エンドポイントの設計
USB-CDCでは以下の3組で計5個のエンドポイントを使用する。ここでいう入出力とはホスト側基準なので、デバイス側から見ると逆の名称である。
- EP0_OUT - 制御受信:
SETUP
割込 - EP0_IN - 制御送信:
- EP1_IN - 割込送信:
TRNCOMPL
割込 - EP2_OUT - バルク受信:
- EP2_IN - バルク送信:
各エンドポイントには 8byteの仮想レジスタが割り当てられ、USB制御器(それ自体が一種の独立したマイクロコントローラーだ)から非同期で参照される。従ってこれらは必ず SRAM上に確保されなければならない。
EP1_OUTは使用しないため、該当エンドポイントに割り当てられた領域(CTRL
を除く7byte)は未使用となる。そこは別の何かに流用しても構わないのだが、ここでは見通しが煩雑になるので触れない。
これらのエンドポイントは個々の処理完了後にUSB0_TRNCOMPL_vect
割込ベクターをトリガーできる。AVRの MCUは割込処理のコストが良くないので、できるだけ割込発生頻度は下げるようにしたい。必要のない割込はなるべく発生しないようにすべきだ。そして MULTIPKT
転送+AZLP
自動処理は便利なので使える限りは積極的に活用したい。
ここでは EP0_OUTは専用のSETUP
割込を、EP1_INはTRNCOMPL
割込をトリガーするが、その他のエンドポイント入出力はシステム割込を必要としないので、主たる設定は次のようになる。
#include <avr/io.h>
/* 使用するエンドポイント組数 */
#define USB_MAXEP 3
/* エンドポイントテーブル構造体定義 */
typedef struct {
register8_t FIFO[USB_MAXEP * 2]; /* FIFO Index Table */
USB_EP_PAIR_t EP[USB_MAXEP]; /* USB Device Controller EP */
_WORDREGISTER(FRAMENUM); /* FRAMENUM count */
} __attribute__((packed)) USB_EP_TABLE_t;
/* 未初期化SRAM確保 */
alignas(2) __attribute__((section(".noinit"))) USB_EP_TABLE_t USB_EP_WORK;
/* 各エンドポイントの省略表現(ショートカット) */
#define EP0_OUT USB_EP_WORK.EP[0].OUT
#define EP0_IN USB_EP_WORK.EP[0].IN
#define EP1_OUT USB_EP_WORK.EP[1].OUT
#define EP1_IN USB_EP_WORK.EP[1].IN
#define EP2_OUT USB_EP_WORK.EP[2].OUT
#define EP2_IN USB_EP_WORK.EP[2].IN
/* エンドポイントテーブル先頭アドレスを設定 */
USB0_EPPTR = (register16_t)&USB_EP_WORK.EP;
/* 個々のエンドポイントの性質 */
EP0_OUT.CTRL = USB_TYPE_CONTROL_gc | USB_TCDSBL_bm | USB_BUFSIZE_DEFAULT_BUF64_gc;
EP0_IN.CTRL = USB_TYPE_CONTROL_gc | USB_MULTIPKT_bm | USB_AZLP_bm | USB_TCDSBL_bm | USB_BUFSIZE_DEFAULT_BUF64_gc;
EP1_OUT.CTRL = USB_TYPE_DISABLE_gc;
EP1_IN.CTRL = USB_TYPE_BULKINT_gc | USB_MULTIPKT_bm | USB_AZLP_bm | USB_BUFSIZE_DEFAULT_BUF16_gc;
EP2_OUT.CTRL = USB_TYPE_BULKINT_gc | USB_TCDSBL_bm | USB_BUFSIZE_DEFAULT_BUF64_gc;
EP2_IN.CTRL = USB_TYPE_BULKINT_gc | USB_MULTIPKT_bm | USB_AZLP_bm | USB_TCDSBL_bm | USB_BUFSIZE_DEFAULT_BUF64_gc;
/* 初期状態で有効にする割込種別 */
USB0_INTCTRLA = /* USB_SOF_bm | */ 0; /* don't set USB_SOF_bm */
USB0_INTCTRLB = USB_TRNCOMPL_bm | USB_SETUP_bm; /* don't use USB_UNFOVF_bm */
<avr/io.h>
はregister8_t
、_WORDREGISTER
等に加え、USB_EP_PAIR_t
、USB_EP_t
構造体も宣言しているため必須
<avr/io.h>
で定義されるUSB_MAX_ENDPOINTS
宣言とUSB_EP_TABLE_t
構造体は、エンドポイント16組フルサイズ用であるため、ここでは使用できない。
FIFO
配列は、使用しないなら存在しなくて良い。しかし必要な場合はUSB0_EPPTR
に設定する SRAMアドレスに対して負のインデックス位置に実体が存在しなければならない。C/C++でこれを確実にするには該当 SRAM領域全体をまとめた構造体で宣言する必要がある。
EP0_OUTとEP0_INは必ずTYPE_CONTROL
属性を持たなければならない。そして他のエンドポイントはそうであってはならない。さもないと以後のメカニズムが正しく動かない。
TCDSBL
属性は該当エンドポイントに対するTRNCOMPL
割込発生を禁止する。しかしTYPE_CONTROL
属性はそれと無関係にSETUP
割込を発火するので、EP0_OUT/INにはTCDSBL
属性を付加する。
EP2_OUT/INのバルク入出力ストリームに関しては、主要動作を主処理側からのポーリングとするためTRNCOMPL
割込不要である。したがってTCDSBL
属性を与えないのは EP1_INの一つだけだ。
EP2_OUTにはMULTIPKT
属性は使用しない。これを有効化すると受信バッファ上限を超える 文字落ち を救済できなくなってしまう。USB2.0 First-Speed の実態は半二重通信なので、受信バッファ溢れ(OVF
)に対してはNAK
で応答するハードウェアフロー制御を意識するのが正しい。送信方向のMULTIPKT+AZLP
属性については、デバイス側がNAK
を返せばそこでホスト側が送信を保留/リトライするため、問題ない。
USB0_EPPTR
や各エンドポイントのDATAPTR
に設定する RAM内アドレスは、必ず偶数アドレス値でなければならない。ゆえに起点となる領域確保にはalignas(2)
属性を付加すべきだ。
送受信バッファレイアウト
USB-CDC(PSTN)の規約に則ると、制御転送とバルク転送が同時に発生する状況は限られる。そのため両者のバッファは大部分を union構造体で重ねてしまえる。
0 8 64 152
+-------+---------------------+--------------------+
| EP0_OUT (64) | |
+-------+---------------------+--------------------+
| | EP0_IN (144) |
+-------+-------------+--------------+-------------+
| | EP1_IN (16) | EP2_OUT (64) | EP2_IN (64) |
+-------+-------------+--------------+-------------+
0 8 24 88 152
#define USB_SETUP_PK_SIZE 64 /* This length cannot be changed. */
#define USB_DATA_PK_SIZE 64 /* 64 is the maximum allowed by USB 2.0 */
#define USB_INTR_PK_SIZE 16 /* 16 is enough. */
#define USB_BULK_INTR_MAX USB_INTR_PK_SIZE
#define USB_BULK_RECV_MAX USB_DATA_PK_SIZE /* Cannot be changed. */
#define USB_BULK_SEND_MAX 64 /* It can be expanded up to 1023. */
#define USB_DATA_BUFFER_SIZE (USB_BULK_SEND_MAX + USB_BULK_RECV_MAX + USB_BULK_INTR_MAX)
/* SETUPパケット 8byteを表現する構造体 */
typedef struct {
uint8_t bmRequestType;
uint8_t bRequest;
uint16_t wValue;
uint16_t wIndex;
uint16_t wLength;
} __attribute__((packed)) Setup_Packet;
/* ディスクリプタ先頭 2byteだけを扱う構造体 */
typedef struct {
uint8_t bLength;
uint8_t bDescriptorType;
} __attribute__((packed)) Descriptor_Header;
/* バッファメモリを表す union構造体 */
union USB_WM_TABLE_t {
Setup_Packet setup;
union {
Descriptor_Header header;
uint8_t data[USB_DATA_BUFFER_SIZE];
struct {
uint8_t intr[USB_BULK_INTR_MAX];
uint8_t recv[USB_BULK_RECV_MAX];
uint8_t send[USB_BULK_SEND_MAX];
};
};
};
/* 未初期化SRAM確保 */
alignas(2) __attribute__((section(".noinit"))) USB_WM_TABLE_t USB_BUFF;
volatile uint16_t RECVCNT = 0;
volatile uint16_t SENDCNT = 0;
/* 便利なショートカット */
#define EP0_SETUP (*(Setup_Packet*)&USB_BUFF.setup)
#define EP0_IN_BUFF (*(Descriptor_Header*)&USB_BUFF.header)
#define EP0_OUT_BUFF (USB_BUFF.data)
#define EP1_IN_BUFF (USB_BUFF.intr)
#define EP2_OUT_BUFF (USB_BUFF.recv)
#define EP2_IN_BUFF (USB_BUFF.send)
/* その他の初期値 */
EP0_OUT.STATUS = 0;
EP2_OUT.CNT = 0;
EP0_OUT.DATAPTR = &EP0_SETUP;
EP0_IN.STATUS = 0;
EP0_IN.CNT = 0;
EP0_IN.DATAPTR = &EP0_IN_BUFF;
EP0_IN.MCNT = 0;
/* このセンテンスは本来 USB_REQ_SetInterface 制御内で行う */
EP1_IN.STATUS = USB_BUSNAK_bm;
EP1_IN.CNT = 0;
EP1_IN.DATAPTR = &EP1_IN_BUFF;
EP1_IN.MCNT = 0;
EP2_OUT.STATUS = USB_BUSNAK_bm;
EP2_OUT.CNT = 0;
EP2_OUT.DATAPTR = &EP2_OUT_BUFF;
EP2_IN.STATUS = USB_BUSNAK_bm;
EP2_IN.CNT = 0;
EP2_IN.DATAPTR = &EP2_IN_BUFF;
EP2_IN.MCNT = 0;
/* USB0周辺機能 制御開始 */
USB0_ADDR = 0;
USB0_CTRLA = /* USB_FIFOEN_bm | USB_STFRNUM_bm | */ USB_ENABLE_bm | (USB_MAXEP - 1);
EP0_OUTの先頭 8byteは制御転送(SETUP
パケット)で参照する領域なので、EP0_IN_BUFF
の先頭はそれを避けた位置とする。またEP0_IN_BUFF
は 64byte長を超える構成ディスクリプタ情報をMULTIPKT
一回で送信できるよう、十分な長さとする。
USB_BULK_RECV_MAX
定数はMULTIPKT
を使わないため、64以外に変更できない。一方でUSB_BULK_SEND_MAX
定数はMULTIPKT
で扱える最大値 1023まで拡大できる。
エンドポイント関係共通関数
以下はよく使われる仮想レジスタ操作を抽象化した共通関数群だ。RMWBUSY
属性の確認は、エンドポイント中のSTATUS
仮想レジスタを書き換える場合の、RMW(Read-Modify-Write)排他制御である。これを怠るとSTATUS
書き換えが無視される不具合に遭遇したりする。STATUS
中のビットのセット/クリアには、原則として各エンドポイント専用の RWM対応 IOレジスタを使用する。
エンドポイントテーブルは
USB0
周辺機能内蔵の DMA(Direct Memory Access)制御器の支配下にあるので、MCU操作とは常にアトミックな排他制御だ。
STATUS
変更専用レジスタを使用するとRMWBUSY
属性がセットされ、DMA制御器によってクリアされる。
STATUS
以外の仮想レジスタは、初期化時を除けばBUSNAK
属性がセットの場合にだけ安全に書き換えができる。
/* 割込バッファが更新可能なら真:ノンブロッキング */
bool is_EP1_IN_ready (void) { return bit_is_set(EP1_IN.STATUS, USB_BUSNAK_bp); }
/* 受信バッファが更新可能なら真:ノンブロッキング */
bool is_EP2_OUT_ready (void) { return bit_is_set(EP2_OUT.STATUS, USB_BUSNAK_bp); }
/* 送信バッファが更新可能なら真:ノンブロッキング */
bool is_EP2_IN_ready (void) { return bit_is_set(EP2_IN.STATUS, USB_BUSNAK_bp); }
/* ホストが受信バッファの空きを待っているなら真:ノンブロッキング */
bool is_EP2_OUT_overrun (void) { return bit_is_set(EP2_OUT.STATUS, USB_UNFOVF_bp); }
/* ホストが送信バッファの到着を待っているなら真:ノンブロッキング */
bool is_EP2_IN_underrun (void) { return bit_is_set(EP2_IN.STATUS, USB_UNFOVF_bp); }
/* EP0_OUT_BUFFに書けるのを待つ:ブロッキングループ */
void EP0_OUT_pending (void) { loop_until_bit_is_set(EP0_OUT.STATUS, USB_BUSNAK_bp); }
/* EP0_IN_BUFFに書けるのを待つ:ブロッキングループ */
void EP0_IN_pending (void) { loop_until_bit_is_set(EP0_IN.STATUS, USB_BUSNAK_bp); }
/* EP2_OUT_BUFFに書けるのを待つ:ブロッキングループ */
void EP2_OUT_pending (void) { loop_until_bit_is_set(EP2_OUT.STATUS, USB_BUSNAK_bp); }
/* EP2_IN_BUFFに書けるのを待つ:ブロッキングループ */
void EP2_IN_pending (void) { loop_until_bit_is_set(EP2_IN.STATUS, USB_BUSNAK_bp); }
/* ホストから新たなEP0_OUTを受け取る:これ単体はノンブロッキング */
void EP0_OUT_listen (void) {
loop_until_bit_is_clear(USB0_INTFLAGSB, USB_RMWBUSY_bp);
USB0_STATUS0_OUTCLR = ~USB_TOGGLE_bm;
}
/* EP0_INをホストへ送る:これ単体はノンブロッキング */
void EP0_IN_listen (void) {
loop_until_bit_is_clear(USB0_INTFLAGSB, USB_RMWBUSY_bp);
USB0_STATUS0_INCLR = ~USB_TOGGLE_bm;
}
/* EP1_INをホストへ送る:これ単体はノンブロッキング */
void EP1_IN_listen (void) {
loop_until_bit_is_clear(USB0_INTFLAGSB, USB_RMWBUSY_bp);
USB0_STATUS1_INCLR = ~USB_TOGGLE_bm;
}
/* ホストから新たなEP2_OUTを受け取る:これ単体はノンブロッキング */
void EP2_OUT_listen (void) {
loop_until_bit_is_clear(USB0_INTFLAGSB, USB_RMWBUSY_bp);
USB0_STATUS2_OUTCLR = ~USB_TOGGLE_bm;
}
/* EP2_INをUSBホストに送る:これ単体はノンブロッキング */
void EP2_IN_listen (void) {
loop_until_bit_is_clear(USB0_INTFLAGSB, USB_RMWBUSY_bp);
USB0_STATUS2_INCLR = ~USB_TOGGLE_bm;
}
/* EP0_OUT_BUFFを空にして受信:`UNFOVF`または`BUSNAK`セット状態でなければ使用不可 */
void EP0_OUT_flush (void) {
EP0_OUT.CNT = 0; /* new packet receive */
EP0_OUT_listen();
}
/* EP0_IN_BUFFを送信する:`UNFOVF`または`BUSNAK`セット状態でなければ使用不可 */
void EP0_IN_flush (void) {
/* EP0_IN.CNT には事前に送信量をセットしておく */
EP0_IN.MCNT = 0; /* new packet sender */
EP0_IN_listen();
}
/* EP1_INで空の割込応答(ZLP)を送信する:`UNFOVF`または`BUSNAK`セット状態でなければ使用不可 */
void EP1_IN_flush (void) {
EP1_IN.CNT = 0; /* nodata */
EP1_IN.MCNT = 0; /* nodata */
EP1_IN_listen();
}
/* EP2_OUT_BUFFを空にして受信:`UNFOVF`または`BUSNAK`セット状態でなければ使用不可 */
void EP2_OUT_flush (void) {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
EP2_OUT.CNT = 0;
RECVCNT = 0;
}
EP2_OUT_listen();
}
/* EP2_IN_BUFFを送信し、空にする:`UNFOVF`または`BUSNAK`セット状態でなければ使用不可 */
void EP2_IN_flush (void) {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
EP2_IN.CNT = SENDCNT;
EP2_IN.MCNT = 0;
SENDCNT = 0;
}
EP2_IN_listen();
}
SOFトークン受信割込 Start Of Frame interrupt
ここでUSB0_BUSEVENT_vect
ベクターに属するSOF
割込の使用例を挙げよう。
データ不定長のキャラクタデバイス型通信を行う場合、1文字ずつ送信しているのでは甚だ効率が悪い。できるだけ大きなデータ塊にまとめて一括送信しないと、限られた USB帯域を有効活用することもできず速度も上がらない。なので送信バッファの遅延送信機構が必要になる。
SOF
割込は毎秒数千回発生する可能性がある。最低でも1ms間隔で毎秒1000回の頻度だ。なので不要な時は発生しないようにしておくべきだ。
#define SEND_DELAY_MS 10
volatile uint8_t SOFCNT = 0;
void enable_interrupt_sof (void) { SOFCNT = SEND_DELAY_MS; USB0_INTCTRLA |= USB_SOF_bm; }
void disable_interrupt_sof (void) { USB0_INTCTRLA &= ~USB_SOF_bm; }
bool interrupt_sof_pending (void) { loop_until_bit_is_clear(USB0_INTCTRLA, USB_SOF_bp); }
/* バス状態変化割込ベクター */
ISR(USB0_BUSEVENT_vect) {
if (bit_is_set(USB0_INTFLAGSA, USB_SOF_bp)) {
if (0 == (--SOFCNT)) {
if (SENDCNT > 0 && is_EP2_IN_underrun()) EP2_IN_flush();
disable_interrupt_sof();
}
USB0_INTFLAGSA |= USB_SOF_bm;
}
/* ここにその他の割込フラグ処理 */
}
こうしておいて主処理の必要なところでenable_interrupt_sof()
を叩くとおよそ 10msの遅延送出を実現できる。その間は満杯になるまで EP2_IN_BUFFに送信データを押し込める。主処理側で送信中かもしれないから、その成否に関わらずSOF
割込は規定のカウントダウン後に停止する。
EP1_INの
UNFOVF
はBUSNAK
の効果でホスト側がアンダーランになるとセットされる。
制御転送 Control transactions
制御転送のあらましは USB2.0概説 に過不足なく書かれている。
CONTROL IN CONTROL OUT
BUFFER BUFFER
+---------+ +---------+
SETUP | EP0_OUT | | EP0_OUT | SETUP段階は常に ホスト->デバイス
+---------+ +---------+
V V
+--------+ +---------+
DATA | EP0_IN | | EP0_OUT | DATA段階は、空であればいずれも ZLP でありえる
+--------+ +---------+
V V
+-------------+ +------------+
STATUS | EP0_OUT ZLP | | EP1_IN ZLP | 必ず DATA段階と逆のエンドポイントで ZLP を発信/受信する
+-------------+ +------------+
SETUP
段階の受信はBUSNAK
属性を無視して必ず到着する特殊なものでSETUP
割込を発火する。しかもTRNCOMPL
割込フラグはセットしない。DATA
段階以降はSETUP
割込ではなくTRNCOMPL
割込の対象だ。実際のトランザクション処理は(同期処理として)直列に記述した方がシンプルなので、TCDSBL
属性を付加して不要な割込発生を抑制しておく。
ここを割込処理中から追い出して非同期処理にしても利点が少ない。USBエニュメレーション待機中まで MCUに他の仕事をさせたい状況はそう多くないだろう。
DATA
段階がどちらの方向で使われるかは、EP0_SETUPバッファに格納される先頭バイトbmRequestType
項のBit[7]
(MSB)で見分けることができる。そして制御OUTの場合SETUP
に続けてDATA
をEP0_OUTで受信する。この時DATA
が格納されるのは、EP0_SETUPバッファの 8byte目以降、つまりSETUP
の続きからだ。EP0_OUT.CNTに残る値は、SETUP
の 8byteを減じた数になる。
最後のSTATUS
段階では、DATA
段階で使ったのとは逆のエンドポイントで応答する。つまり EP0_OUTでDATA
を受けたら EP0_INでZLP
をデバイスが発信しなければならず、EP0_INでDATA
を送信したら、EP0_OUTでホストが送るZLP
をデバイスが着信しACK
を返さなければならない。
ISR(USB0_TRNCOMPL_vect) {
USB0_INTFLAGSB |= USB_SETUP_bm;
if (bit_is_set(EP0_OUT.STATUS, USB_EPSETUP_bp)) {
/* EP0_OUT先頭バイトを読む */
uint8_t bmRequestType = EP0_SETUP.bmRequestType;
/* DIRECTION属性が偽なら制御OUTなので wLength を確認 */
if (bit_is_clear(bmRequestType, USB_REQTYPE_DIRECTION_bp)) { /* bp=7 */
EP0_OUT_flush(); /* EP0_OUTにDATAパケットを読む */
/* DATAが格納されるのは EP0_OUT_BUFFの位置:EP0_SETUPの 8byte目から */
}
/* REQTYPE属性をマスク抽出 */
bmRequestType &= USB_REQTYPE_TYPE_gm; /* gm=0x60 */
if (bmRequestType == USB_REQTYPE_STANDARD_gc) { /* gc=0x00 */
/* スタンダードリクエストを処置:制御エニュメレーション本体 */
control_request_standard();
}
else if (bmRequestType == USB_REQTYPE_CLASS_gc) { /* gc=0x20 */
/* クラスリクエストを処理:例えばUSB-CDCに固有 */
control_request_class();
}
/* 必要ならここにベンダーリクエスト処理 */
}
/* ここに後述の 割込バルク転送 処理 */
}
標準制御のGetDescriptor
指令の場合、要求された構成ディスクリプタ構造体はwValue
項に応じて選び、EP0_IN_BUFF に格納しDATA
部分としてホストに送る。この時wLength
項で指定されたサイズ以上を送り返してはならない。この送信はMULTIPKT+AZLP
機能を使うので、パケット分割の必要はない。
/* control_request_standard() の中で */
if (EP0_SETUP.bRequest == USB_REQ_GetDescriptor) { /* 0x06 */
size_t _length = (size_t)memcpy_descriptor(&EP0_IN_BUFF, EP0_SETUP.wValue);
EP0_IN.CNT = EP0_SETUP.wLength < _length ? EP0_SETUP.wLength : _length;
EP0_IN_flush(); /* DATA sending */
EP0_OUT_flush(); /* STATUS ZLP receive */
}
ホスト側の実装によっては、まず構成ディスクリプタの先頭 2byteを要求し、そこに書かれた
bLength
項で示されるメモリ量を malloc確保してから、本番の構成ディスクリプタを取得する二段階の手順を踏む。
標準制御のSetAddress
指令の場合、デバイスに割り当てられた USBアドレスはSETUP
パケット中のwValue
項に届く。EP0_INでZLP
を "完全に" 送り終えたら(BUSNAK
がセットされたら)、デバイスに与えられた 7bit幅のUSB0_ADDR
が変更できる。
/* control_request_standard() の中で */
if (EP0_SETUP.bRequest == USB_REQ_SetAddress) { /* 0x05 */
EP0_IN.CNT = 0; /* ZLP */
EP0_IN_flush(); /* STATUS (ZLP) sending */
EP0_IN_pending(); /* wait */
USB0_ADDR = EP0_SETUP.wValue & 0x7F;
}
USB-CDC(PSTN)制御のSetLineEncoding
指令の場合、前述したDATA
到着完了を待つ。それはEP0_SETUP
ではなくEP0_OUT_BUFF
に格納されるので、ここからLineEncoding_t
構造体を取り出す。これはクラスリクエストであるからバルク転送とは並行して発生しえるため、使用可能なバッファ領域は他の同時使用バッファと重ならない部分だけだ。
/* control_request_class() の中で */
if (EP0_SETUP.bRequest == CDC_REQ_SetLineEncoding) {
EP0_OUT_pending(); /* DATA receive pending */
memcpy(&sLineEncoding, &EP0_OUT_BUFF, sizeof(LineEncoding_t));
EP0_IN.CNT = 0; /* ZLP */
EP0_IN_flush(); /* STATUS (ZLP) sending */
}
割込バルク転送 Bulk-IN interrupt transactions
USB-CDCプロトコルでは、割込バルク転送用のエンドポイント EP1_INを定義するが、実際のその使用はオプションだ。使用しないならEP1_IN.STATUS
のBUSNAK
属性を事前に立てておき、ホストへは常にNAK
(アンダーラン)を返すようにしておけば、あとは全く忘れてしまって良い。
一方で構成ディスクリプタでの設定に依存するが、ホストからの EP1_IN要求を 1ms〜255ms の任意の間隔で発生させることができる。これを利用するとデバイス側で他のリソースを使うことなく、ミリ秒精度のタイマーを簡易的に実装可能だ。
#define USB_INTR_INTERVAL_MS 1 /* The value declared in the descriptor */
volatile uint32_t _milliscount = 0;
/* ISR(USB0_TRNCOMPL_vect) の最後で */
if (bit_is_set(USB0_INTFLAGSB, USB_TRNCOMPL_bp)) {
USB0_INTFLAGSB |= USB_TRNCOMPL_bm;
if (is_EP1_IN_ready()) {
_milliscount += USB_INTR_INTERVAL_MS;
EP1_IN_flush(); /* clear BUSNAK */
}
}
USB-CDCの割込送信では
SerialState
を含む 10byteの定型フォーマットされたCDC_REQ_SerialState
(bRequest=0x20
)パケットを返す。
バルク送信 Bulk-In sending transactions
SOF
割込の遅延送出を踏まえてwrite()
関数を実装すると、次のように書ける。なおここではよくあるリングバッファは使わない。単一の送信バッファとBUSNAK
およびUNFOVF
属性変化によるハードウェアフロー制御だけを使用する。
/* 送信バッファを空にする:ブロッキングあり */
void flush (void) {
if (SENDCNT > 0 && is_EP2_IN_ready()) EP2_IN_flush();
EP2_IN_pending();
}
/* 送信バッファにあと何文字押し込めるかを返す:ノンブロッキング */
size_t availableForWrite (void) {
return is_EP2_IN_ready() ? USB_BULK_SEND_MAX - SENDCNT : 0;
}
/* 送信バッファに1文字書く:ブロッキングあり */
size_t write (const uint8_t _c) {
EP2_IN_pending();
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
EP2_IN_BUFF[SENDCNT++] = _c;
}
if (SENDCNT >= USB_BULK_SEND_MAX) EP2_IN_flush();
else enable_interrupt_sof();
return 1; /* 送出した文字数:write関数では常に1 */
}
EP2_IN_BUFF
にキャラクタを追記できるのは、EP2_IN.STATUS
のBUSNAK
がセットされていてかつEP2_IN_BUFF
に空きがある時だけだ。しかし多くの場合write()
関数が書き込みエラー(0)を返すことは好まれない。printf
等の連続する送信中にエラーを拾うと意図しない 文字落ち を経験するだろう。ゆえにこのwrite()
関数は冒頭でEP2_IN_pending()
により送信可能になるまで処理進行をブロッキングする。
ブロッキングを抜けた後は必ず最低 1文字はバッファに書けるはずなので、バッファの満杯を確認していない。キャラクタ押し込み後に満杯であるかを確認し、EP2_IN_flush()
で直ちに送信するか、enable_interrupt_sof()
でSOF
割込に遅延送信を依頼する。いずれの結果も確認しないのでこれは非同期処理だ。
このwrite()
実装だけではブロッキングされることが事前に予測できるなら回避できる手段が別に欲しくなるためavailableForWrite()
関数を用意する。これは単にバッファ残量を返すだけだ。BUSNAK
がクリアなら送信実行中=バッファに書けないので0
を返す。
マルチパケットバルク送信 Multi packet Bulk-IN sending transactions
EP2_INをMULTIPKT
有効で運用する場合、送信バッファサイズの上限は USB2.0 Full-Speed の規約により 1023byteだ。普段からその領域を確保しておく必要はなく、必要なときに一時的にEP2_IN.DATAPTR
を再設定すると、効率の良いデータ送出ができる。
/* _buffer に指定できるのは SRAM内アドレスだけで、PROGMENなどであってはならない */
/* _length に指定可能なのは 0〜1023 の範囲 */
size_t write_blocks (const void* _buffer, size_t _length) {
interrupt_sof_pending(); /* 遅延送信が進行中なら待つ */
flush(); /* 既存送信バッファを空にする */
EP2_IN.DATAPTR = (register16_t)_buffer;
EP2_IN.CNT = _length;
EP2_IN.MCNT = 0;
EP2_IN_listen(); /* 送出 */
EP2_IN_pending(); /* 完了待ち */
EP2_IN.DATAPTR = &EP2_IN_BUFF; /* 復帰 */
return _size; /* この返却値はダミー */
}
まず通常の送信バッファが空である事を確実にする。そしてDATAPTR
を送信データ実体のあるアドレスそのものに変更し、1回の操作で送出する。EP2_IN_pending()
で送出完了を確認できたらDATAPTR
は元に戻す。この実装では送信バッファにキャラクタ単位で複写する必要がないため、とても高速だ。ただし 1024byteを超えるデータ塊を送るなら各自でアドレス起点と長さを逐一計算して分割送信するよう考えると良い。
安易にリングバッファを使うよりもこの方法でダブルバッファを用意する方が、効率の良い転送処理を書ける。
バルク受信 Bulk-OUT receiving transactions
バルク受信ではMULTIPKT
を使うか否かで戦略が異なる。
1文字単位でデータを扱うキャラクタデバイス型の通信ではMULTIPKT
は無効とし、ACK
とNAK
(オーバーラン)を意識的に使い分けるハードウェアフロー制御が適する。
/* 受信バッファ内の未読文字数を返す:ノンブロッキング */
size_t available (void) {
size_t _s = 0;
if (is_EP2_OUT_ready()) {
_s = EP2_OUT.CNT - RECVCNT;
if (_s == 0) EP2_OUT_flush();
}
return _s;
}
/* 受信バッファから1文字読む:ブロッキングあり */
int read (void) {
int _c = -1; /* _ 必ずBUSNAK確認が先でないとCNTは正しく読めない */
if (is_EP2_OUT_ready() && EP2_OUT.CNT == RECVCNT) EP2_OUT_flush();
EP2_OUT_pending();
if (EP2_OUT.CNT != RECVCNT) {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
_c = EP2_OUT_BUFF[RECVCNT++];
}
}
if (EP2_OUT.CNT == RECVCNT) EP2_OUT_flush();
return _c;
}
/* 受信バッファ最後の1文字を読めるなら返す:ノンブロッキング */
int peek (void) {
return is_EP2_OUT_ready() && EP2_OUT.CNT == RECVCNT ? -1 : EP2_OUT_BUFF[RECVCNT];
} /* ^ 必ずBUSNAK確認が先でないとCNTは正しく読めない */
available()
は常にノンブロッキングで受信バッファからあと何文字読めるかを返す。バッファが空なら次回の呼び出しに備えて新たな読み込みを試みるがBUSNAK
完了は待たない。
一方でread()
はBUSNAK
がセットされていない限り受信バッファに触れることを許されないので(同期すべく)ブロッキングが必要だ。似たような条件式が繰り返し続くが、それぞれ役割が違う。
- まず EP2_OUT_BUFFが空の場合は、
BUSNAK
が既にセット済ならクリアし、改めてセットされるまでブロッキングして待つ。 - この時点で
BUSNAK
は必ずセット済であるから、EP2_OUT_BUFFが空でなければ受信キャラクタを取り出す。これはバッファを空にするかもしれない。 - 改めて EP2_OUT_BUFFが空なら、次回に備えて
BUSNAK
をクリアする。BUSNAK
セットは待たない。
USBホストから文字を読めないなら「どのみち
read()
が負を返すからavailable()
は不要である」という論もあるが、それは主処理がブロッキングされる可能性を失念している。この実装では両者の挙動は同じではない。
マルチパケットバルク受信 Multi packet Bulk-OUT receiving transactions
MULTIPKT+AZLP
有効での受信はシンプルで、MCNT
に 1回で受信可能な最大量を指定して受信を待つだけだ。ただし指定量を超えてバッファオーバーランに至るとUNFOVF
フラグがセットされCNT
にはMCNT
より大きい数値が残る。溢れたデータは保存メモリに残らず 破棄 されるのが特徴だ。これに対してホストへはACK
を返してしまうため、破棄されたデータを取り返すことはできない。
USB活線直後の溜まっているゴミデータ破棄には便利かも。
ひとたび
BUSNAK
がセットされた後はNAK
(アンダーラン)を返すため、ホスト側も再送に対応する。
/* _buffer に指定できるのは SRAM内アドレスだけで、PROGMENなどであってはならない */
/* _length に指定可能なのは 0〜1023 の範囲 */
size_t read_blocks (void* _buffer, size_t _length) {
EP2_OUT_pending(); /* ブロッキング */
RECVCNT = 0; /* 既存受信バッファ内容は破棄 */
EP2_OUT.CTRL |= USB_MULTIPKT_bm | USB_AZLP_bm; /* MULTIPKT 有効 */
EP2_OUT.DATAPTR = (register16_t)_buffer;
EP2_OUT.CNT = 0;
EP2_OUT.MCNT = _length; /* 最大受信量 */
EP2_OUT_listen(); /* 受信開始 */
EP2_OUT_pending(); /* 完了待ち */
EP2_OUT.DATAPTR = &EP2_OUT_BUFF; /* 設定復帰 */
EP2_OUT.CTRL &= ~(USB_MULTIPKT_bm | USB_AZLP_bm);
if (EP2_OUT.CNT < _length) _length = EP2_OUT.CNT;
return _length; /* 実際に読み込まれたバイト数 */
}
MULTIPKT
受信でのこの性質は、送信データ塊が定型長のブロックデバイス型実装では問題にならないが、キャラクタデバイス型で不定長のデータを扱う対話型の実装では 文字落ち を飲まなければならないので都合が悪い。よって文字単位での通信を目的とするならMULTIPKT
機能無効のほうが適する。
その他の TIPS
エンドポイントFIFOの読み方
制御エンドポイント組以外に1組のバルク転送しか実装しないのなら必須ではないが、複合 USBデバイスを設計して複数のエンドポイント組を同時に使い分ける場合、FIFO機能が役立つ。これを有効にするとそのFIFO用バッファはUSB0_EPPTR
設定アドレスの直前にUSB_MAXEP*2
byteぶん存在するようになるので、SRAM構成はやや複雑だ。FIFOとエンドポイントテーブルは連続したメモリ上に割り当てなければならないから、C/C++の場合は全体がひとまとめの構造体宣言で括られているべきだ。
前述の設計でのUSB_EP_WORK
テーブルは、USB_MAXEP==3
なので次のような SRAMメモリ構造になる。
USB_EP_TABLE_t USB_EP_WORK;
- USB_MAXEP *2 +---------+ 0xFA
| FIFO-6 |
-5 +---------+ 0xFB
| FIFO-5 |
-4 +---------+ 0xFC
| FIFO-4 |
-3 +---------+ 0xFD <--+
| FIFO-3 | --> 0x18 |
-2 +---------+ 0xFE | |
| FIFO-2 | | |
-1 +---------+ 0xFF | |
+------------+ | FIFO-1 | | | +-------------+
| USB0_EPPTR |--> +---------+ 0x00 | 0xFD <--| USB0_FIFORO |
+------------+ | EP0_OUT | | +-------------+
+8 +---------+ 0x08 |
| EP0_IN | |
+16 +---------+ 0x10 |
| EP1_OUT | |
+24 +---------+ 0x18 <--+
| EP1_IN |
+32 +---------+ 0x20
| EP2_OUT |
+40 +---------+ 0x28
| EP2_IN |
+ USB_MAXEP *16 +---------+ 0x30
| FRAME |
+---------+
FIFO最上位の値はUSB0_FIFORP
から読める。これを読んで仮に0xFD
が得られたとしよう。これはUSB0_EPPTR
レジスタ格納値に対する符号付き8bit幅の負数でのオフセットを表している。そこから計算して得られるのはFIFO-3
を示すメモリアドレスだ。そして仮にFIFO-3
を読んで0x18
を得られたならば、今度はUSB0_EPPTR
レジスタ格納値に対する符号なし8bit幅の正数でのオフセットと解釈する。これを計算して得る結果はエンドポイント構造体を示すメモリアドレスであり、エンドポイントEP1_IN
を指していることが明らかになる。
FIFO-n
から読める値はメモリオフセットであると同時に、上位ニブルがエンドポイント番号、下位ニブルが IN/OUT方向の区別である。これは構成ディスクリプタ/エンドポイント記述子でのエンドポイントアドレス指定値とは、上下ニブルが逆だ。
先にあげた割込バルク転送の例を、FIFOで書き換えた例を示そう。
/* FIFO機能の有効化 */
USB0_FIFOWP = 0; /* FIFORP も連動して初期化される */
USB0_CTRLA |= USB_FIFOEN_bm;
/* ISR(USB0_TRNCOMPL_vect) の最後で */
while (bit_is_set(USB0_INTFLAGSB, USB_TRNCOMPL_bp)) {
uint8_t EP_ID = *((register8_t*)(&USB_EP_WORK.EP - 256U + USB0_FIFORP));
if (EP_ID == 0x18) {
/* ここを通る時、EP1_IN.STATUS の BUSNAK は必ずセットされている */
_milliscount += USB_INTR_INTERVAL_MS;
EP1_IN_flush();
}
/* ここに他のエンドポイント処理 */
}
USB0_FIFORP
を読んだ結果、FIFOが空になると自動的にUSB0_INTFLAGSB
のTRNCOMPL
フラグはクリアされる。なので FIFO読み出し処理全体は単一のループで記述可能だ。
さらにFIFO-n
を読んで得たEP_ID
の比較一致だけで条件分岐を済ませて後段のアドレス計算を省いているが、多分それが一番記述しやすいだろう。
ここで考慮すべきは制御転送と、その他転送との割込区別だ。SETUP
パケットで始まる一連のトランザクション(つまりDATA
とSTATUS
パケット)が、FIFOループに混じってしまうと切り分ける面倒が増える。なのでここではエンドポイント設計で述べたように両者を区別して扱っている。
VBUS 検出
AVR-DU系列には、専用の VBUS検出ポートがない。必要なら通常の GPIOで実現するポリシーである。一般にこれはアナログコンパレータ(AC
周辺機能)を必要とするが、AVR-DU系列でこれが可能なのはPIN_PC3
とPIN_PD6
ポートだけなので、これらを専用に割り当てることになるだろう。もっともPIN_PD6
は貴重なUSART
周辺機能ピンでもあるから、PIN_PC3
以外に選択の余地はない。これを回避するには SOT-23タイプの外付けリセットIC(電圧低下検出チップ:閾値4.0Vディレイ250msが目安)を用いるのがスマートだ。
バスパワー動作では VBUS検出はなくても特に問題はないのだが、バッテリーや、デバッグポートから給電する場合は セルフパワー動作となるので、VBUS検出ができないと USBケーブル抜線時のスタンドアロン動作に苦労する。
残念ながら Curiosity Nano AVR64DU32 の VBUS検出回路は デバッグ側 USBポートからの給電も拾ってしまうため、セルフパワー動作向けの挙動を示さない。
CTS信号の扱い
USB-CDCのSerialState_t
構造体を見るとbTxCarrier=DSR
/bRxCarrier=DCD
/RI
入力属性はあるのにCTS
入力相当の属性定義がないことに気づく。だが実際の USBシリアル変換器ではCTS
信号入力端子を備えたものが多い。どういうことだろう?
これはCTS
信号入力が USBホスト=DTE装置に転送すべき性質のものではなく、USBシリアル "ブリッジ" 装置内部で解決すべき性質のものだからだ。対抗するRTS
信号出力は送信要求なのでCTS
がオフ(物理的には負論理なので、GPIOとしてはHIGH
入力)であったら、相手側=DCE装置へのデータ送信を抑止しなければならない。USB-CDCデバイスとしては USBホストからのread()
をしないということだ。それは結果として USBホストへの EP2_OUTエンドポイントからのNAK
(オーバーラン)応答とUNFOVF
属性セット(およびOVF
割込)に帰結する。ゆえに結論として USB-CDCホストはCTS
信号入力を実装する必要がないのだ。
Arduino Mini などで
CTS
端子がGND
に短絡しているのはこのためだ。USBシリアル変換器の設定でCTS
信号機能を無効にできない場合に備え、通信不能になるのを避けねばならない。
DCD
入力は対抗側にCD
出力がない場合、対抗側レセプタクルでGND
(FG
ではない)に短絡していればケーブル活線検出=属性値1
となる。またクロスケーブル配線では内部で自身のDSR
入力と対抗側のDTR
出力に短絡するのが一般的だろう。RI
入力はモデム着信通知なので、対応しないならオープン(プルアップHIGH
)になっていれば良く USB-CDCホストへも属性値0
を返せば良い。
CRCエラー
各エンドポイントのSTATUS
にはCRC
ビットがあるが、Isochronousタイプ以外では使用されない。その他の Controlタイプや BulkタイプでパケットCRCエラーが生じても、該当パケットは黙して捨てられる。
パケットが捨てられるとそれに関連する割込やフラグ更新もされないので、具体的にどのデータパートが失われたのかをアプリケーションレベルで把握することは困難だ。特にMULTIPKT
進行途中のパケットでエラーが生じた場合、データ全体のそのパートだけがごっそり抜け落ちた結果だけを見ることになるので困惑してしまう。そうした理由からアプリケーションレベルで再送やリカバリー対策を行うなら、送受信データ内にパケット通番やパケット長、自前のCRCを埋め込んで独自にエラー検出可能とすべきだ。
macosでの製品通番文字列記述(SN番号)の扱い
ストリングディスクリプタで設定する文字列は C/C++の場合L
プレフィクスを用いた UTF16フォーマットでソースファイル中に埋め込む。それ自体は USB規約による要請なので UTF-8などであってはならない。この時 macos については製品通番文字列記述(SN番号)において[A-Za-z0-9_]{1,14}
の条件に合致する場合、特別な挙動を示す。デバイスファイル名/dev/cu.usbmodem***N
の***
部分に、任意の文字列を自由に埋め込むことができるのだ。
既定の
***
位置は 120などの USBツリー階層に由来する番号、N
位置には同名被りの場合に増加される 1以上の通番
#define USB_SERIALNUMBER L"_AVR64DU32abcd"
USB_String_Descriptor PROGMEM serialnumber_string = {
.bLength = sizeof(USB_SERIALNUMBER)
, .bDescriptorType = USB_DTYPE_String
, USB_SERIALNUMBER
};
$ ls -1 /dev/cu.*
/dev/cu.Bluetooth-Incoming-Port
/dev/cu.usbmodem2302
/dev/cu.usbmodem_AVR64DU32abcd1 # <-- HERE!
/dev/cu.wlan-debug
埋め込めるのは数字に限らず英文字(大小区別可)も許されるので、ここに任意の製品名などを半角 14文字までの可読文字列として表示させることができる。ヌル文字列や15文字以上の場合はこの条件から外れて埋め込まれない。また英数字以外の 日本語漢字 などは_
に置換されて埋め込まれるが、usbdiagnose
コマンドなどの USB情報表示ツールでは日本語文字列その他をそのまま表示させることができる。
逆に言えばこういう OSの仕様なので、本来の意味のシリアル番号文字列を HEXフォーマットで記述するような場合は、その有効文字数も考えないと意図しなかった挙動を示すかもしれない。
まとめ/要点
ここまでの考察と検証で、AVR-DU系列での USB-CDCの実装が可能になった。実際には構成ディスクリプタの設計やそのエニュメレーション(承認確認)、USB-CDC仕様に備わる付帯機能(LineEncoding
やSerialState
)への対応など多岐にわたるコードを記述しなければならないが、それでも USBプロトコルスタックの中核は 3KiB程度を目安に実装することができた。
-
EPPTR
やDATAPTR
には SRAM領域内のアドレスしか指定できない。かつ必ず偶数のアドレス(LSB=0)でなければならない。 - エンドポイント毎の
STATUS
仮想レジスタは RMW 保護下にある。個別に専用のINCLR
/INSET
/OUTCLR
/OUTSET
レジスタを介して変更しなければならない。 - エンドポイント設定下の他の仮想レジスタやバッファメモリは
BUSNAK
がクリアされている限り変更してはならない。 -
BUSNAK
は USBホストが応答しなければセットされないので、USBホストがすぐに応答できない時はブロッキングされうることに注意。 -
UNFOVF
がセットされたなら、直近で再送による USBホストの応答が期待できる。 -
SOF
割込は 1ms につき最低 1回以上発生しうる。 - IN方向では常に
MULTIPKT
機能を有効にした方が便利。OUT方向は目的によって選択する。
以下は記事中では端折ったが、実装のコツとして挙げておく。
- AVR-DUシリーズの USB周辺機能は、USB 2.0規格の Full-Speed (12Mbps) 対応専用。
- よって制御転送の最大パケットサイズは 64byte。
- 構成ディスクリプタで USB 1.x互換動作を指定すると制御転送の最大パケットサイズが 8byteに制限される。よほどリソースが厳しくなければ、あえて選ぶ必要はない。
- VBUS検出対応は内蔵しておらず、USBケーブル抜線をソフトウェアだけでは正しく検出できない。セルフパワー機器とするなら要注意。
- USB 3.x対応ホストは制御転送で
Get_DeviceQualifier_Descriptor
を要求する。これに正しく応答しないとそこでSTALLED
してしまう。 -
STALLED
割込は正しく通信が維持できているなら、発生する理由がない。 -
UNF
/OVF
割込だけからは発生源のエンドポイントを特定できない。UNFOVF
フラグを個別に調べる必要がある。それでいてこの割込に対応する価値はほとんど見出せない。あえて使うならSerialState
の更新か? - (
BUS
)RESET
割込が届いたら、USB0_ADDR
を 0にリセットし、新たなエニュメレーション開始に備えなければならない。 - バスパワー機器で(
BUS
)SUSPEND
割込が届いたら、低消費電力状態(2mA以下)に移行しなければ電力喪失の恐れがある。 - バスパワー機器で(
BUS
)RESUME
割込が届いたら、通常電力状態(最大500mA、構成ディスクリプタ内で必要電力量を指定)に戻って良い。
関連リンク
参考文献
- USB2.0概説 - 日本語訳PDF / avr.jp
- XMEGA AU手引書 - 日本語訳PDF / avr.jp :AVR-DUと同一ではないが USB周辺機能が似ている
- AVR® DU Product Family - 製品情報 / microchip.com
- Class definitions for Communication Devices 1.2 - USB-IF仕様書 / usb.org
成果物:以下は MultiX Zinnia Product SDK [*AVR] for Arduino IDE ベアメタル開発環境の一部 / github.com
- SerialUSB_Echo.ino - 実演サンプル
- SerialUSB_Class.h - Arduino風インタフェース実装
- USB/CDC - 下位実装
実際の USBデバイス設計ではかなり深い部分までカスタマイズする必要が多いため、汎用ライブラリとして使いまわせるようには記述していない。いくらかは weak宣言された コールバック関数をオーバーライドすることで対応できるが、それを超える範囲は必要に応じてソースファイルを自プロジェクト内にコピーし、記述内容と namespace を変更して実用に供する方針である。ゆえにこれらは includeパスに依存しないファイル構成を持ち、MITライセンスに基づくオープンソースとして公開している。
Microchip MPLAB X (XC8) コードジェネレーターサンプル / github.com
- USB Keypad with AVR64DU32 2024/4/14
- USB to SPI and I2C Converter With AVR64DU32 2024/5/12
- USB CDC to USART Bridge using AVR® DU Microcontroller 2024/6/2
- USB CDC Virtual Serial Port using AVR® DU Microcontroller 2024/6/2
- USB Human Interface Device (HID) Communication Demo Using On-Board Button 2024/6/12
Copyright and Contact
Twitter(X): @askn37
BlueSky Social: @multix.jp
GitHub: https://github.com/askn37/
Product: https://askn37.github.io/
Copyright (c) askn (K.Sato) multix.jp
Released under the MIT license
https://opensource.org/licenses/mit-license.php
https://www.oshwa.org/