[modernAVR] AVR-DU専用USB-CDCプロトコルスタックをフルスクラッチした話

このレポートは [[modernAVR] AVR-DU の USB周辺機能考察](https://qiita.com/askn37/items/b6c9268022eceea61a99)の続編だ。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)](https://www.microchip.com/en-us/development-tool/EV59F82A) - 前提ハードウェア：もちろん素のベアメタルチップも可
- [GCC Compilers for AVR® and Arm®-Based MCUs and MPUs](https://www.microchip.com/en-us/tools-resources/develop/microchip-studio/gcc-compilers/) - AVR 8-Bit Toolchain + avr-libc
- [Microchip AVR-Dx Series Device Support](https://packs.download.microchip.com) - 上記のサポートデバイス拡張セット
- [askn37/multix-zinnia-sdk-modernAVR](https://github.com/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`割込をトリガーするが、その他のエンドポイント入出力はシステム割込を必要としないので、主たる設定は次のようになる。

```cpp
#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構造体で重ねてしまえる。

```plain
0       8                     64                   152
+-------+---------------------+--------------------+
| EP0_OUT (64)                |                    |
+-------+---------------------+--------------------+
|       | EP0_IN (144)                             |
+-------+-------------+--------------+-------------+
|       | EP1_IN (16) | EP2_OUT (64) | EP2_IN (64) |
+-------+-------------+--------------+-------------+
0       8             24             88            152
```

```cpp
#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`属性がセットの場合にだけ安全に書き換えができる。

```cpp
/* 割込バッファが更新可能なら真：ノンブロッキング */
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回の頻度だ。なので不要な時は発生しないようにしておくべきだ。

```cpp
#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); }
```

```cpp
/* バス状態変化割込ベクター */
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概説](https://avr.jp/user/EC/PDF/USBspcs.pdf) に過不足なく書かれている。

```plain
        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`を返さなければならない。

```cpp
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`機能を使うので、パケット分割の必要はない。

```cpp
/* 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`が変更できる。

```cpp
/* 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`構造体を取り出す。これはクラスリクエストであるからバルク転送とは並行して発生しえるため、使用可能なバッファ領域は他の同時使用バッファと重ならない部分だけだ。

```cpp
/* 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 の任意の間隔で発生させることができる。これを利用するとデバイス側で他のリソースを使うことなく、ミリ秒精度のタイマーを簡易的に実装可能だ。

```cpp
#define USB_INTR_INTERVAL_MS 1  /* The value declared in the descriptor */
volatile uint32_t _milliscount = 0;
```

```cpp
/* 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`属性変化によるハードウェアフロー制御だけを使用する。

```cpp
/* 送信バッファを空にする：ブロッキングあり */
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`を再設定すると、効率の良いデータ送出ができる。

```cpp
/* _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`（オーバーラン）を意識的に使い分けるハードウェアフロー制御が適する。

```cpp
/* 受信バッファ内の未読文字数を返す：ノンブロッキング */
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`がセットされていない限り受信バッファに触れることを許されないので（同期すべく）ブロッキングが必要だ。似たような条件式が繰り返し続くが、それぞれ役割が違う。

1. まず EP2_OUT_BUFFが空の場合は、`BUSNAK`が既にセット済ならクリアし、改めてセットされるまでブロッキングして待つ。
2. この時点で`BUSNAK`は必ずセット済であるから、EP2_OUT_BUFFが空でなければ受信キャラクタを取り出す。これはバッファを空にするかもしれない。
3. 改めて 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`（アンダーラン）を返すため、ホスト側も再送に対応する。

```cpp
/* _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メモリ構造になる。

```plain
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で書き換えた例を示そう。

```cpp
/* FIFO機能の有効化 */
USB0_FIFOWP = 0;  /* FIFORP も連動して初期化される */
USB0_CTRLA |= USB_FIFOEN_bm;
```

```cpp
/* 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以上の通番

```cpp
#define USB_SERIALNUMBER L"_AVR64DU32abcd"

USB_String_Descriptor PROGMEM serialnumber_string = {
  .bLength         = sizeof(USB_SERIALNUMBER)
, .bDescriptorType = USB_DTYPE_String
, USB_SERIALNUMBER
};
```

```sh
$ 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概説](https://avr.jp/user/EC/PDF/USBspcs.pdf) - 日本語訳PDF / avr.jp
- [XMEGA AU手引書](https://avr.jp/user/DS/PDF/xmegaAU.pdf) - 日本語訳PDF / avr.jp ：AVR-DUと同一ではないが USB周辺機能が似ている
- [AVR® DU Product Family](https://www.microchip.com/en-us/products/microcontrollers-and-microprocessors/8-bit-mcus/avr-mcus/avr-du) - 製品情報 / microchip.com
- [Class definitions for Communication Devices 1.2](https://www.usb.org/document-library/class-definitions-communication-devices-12) - USB-IF仕様書 / usb.org

成果物：以下は [MultiX Zinnia Product SDK [*AVR] for Arduino IDE](https://github.com/askn37/askn37.github.io/wiki/InformationSDK) ベアメタル開発環境の一部 / github.com

- [SerialUSB_Echo.ino](https://github.com/askn37/MacroMicroAPI_lib/tree/main/examples/USB%20for%20AVR-DU%20series/SerialUSB_Echo) - 実演サンプル
- [SerialUSB_Class.h](https://github.com/askn37/MacroMicroAPI_lib/blob/main/src/SerialUSB.h) - Arduino風インタフェース実装
- [USB/CDC](https://github.com/askn37/MacroMicroAPI_lib/tree/main/src/USB) - 下位実装

> 実際の USBデバイス設計ではかなり深い部分までカスタマイズする必要が多いため、汎用ライブラリとして使いまわせるようには記述していない。いくらかは weak宣言された コールバック関数をオーバーライドすることで対応できるが、それを超える範囲は必要に応じてソースファイルを自プロジェクト内にコピーし、記述内容と namespace を変更して実用に供する方針である。ゆえにこれらは includeパスに依存しないファイル構成を持ち、MITライセンスに基づくオープンソースとして公開している。

__Microchip MPLAB X (XC8)__ コードジェネレーターサンプル / github.com

- [USB Keypad with AVR64DU32](https://github.com/microchip-pic-avr-examples/avr64du32-keypad-mplab-mcc) 2024/4/14
- [USB to SPI and I2C Converter With AVR64DU32](https://github.com/microchip-pic-avr-examples/avr64du32-serial-bridge-mplab-mcc) 2024/5/12
- [USB CDC to USART Bridge using AVR® DU Microcontroller](https://github.com/microchip-pic-avr-examples/avr64du32-cnano-usb-cdc-to-usart-bridge-mplab-mcc) 2024/6/2
- [USB CDC Virtual Serial Port using AVR® DU Microcontroller](https://github.com/microchip-pic-avr-examples/avr64du32-cnano-usb-cdc-virtual-serial-port-mplab-mcc) 2024/6/2
- [USB Human Interface Device (HID) Communication Demo Using On-Board Button](https://github.com/microchip-pic-avr-examples/avr64du32-cnano-usb-hid-keyboard-mplab-mcc) 2024/6/12

## Copyright and Contact

Twitter(X): [@askn37](https://twitter.com/askn37) \
BlueSky Social: [@multix.jp](https://bsky.app/profile/multix.jp) \
GitHub: [https://github.com/askn37/](https://github.com/askn37/) \
Product: [https://askn37.github.io/](https://askn37.github.io/)

Copyright (c) askn (K.Sato) multix.jp \
Released under the MIT license \
[https://opensource.org/licenses/mit-license.php](https://opensource.org/licenses/mit-license.php) \
[https://www.oshwa.org/](https://www.oshwa.org/)