LoginSignup
0
0
この記事誰得? 私しか得しないニッチな技術で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

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

Last updated at Posted at 2024-06-24

このレポートは [modernAVR] AVR-DU の USB周辺機能考察の続編だ。2023/12の記事公開から5ヶ月を経て、AVR-DU シリーズ最初の製品版 AVR64DU28/32 のローンチが成された。2024年末にかけて AVR16DU14/20/28/32AVR32DU14/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プロトコルが動いたかを述べてみよう。物理面や構成ディスクリプタの構築についてはいくらでも関連資料があるので、ここでは触れない。

記事の前提となる必要要件は以下とする;

結局、参照できるまともな資料が 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_tUSB_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のUNFOVFBUSNAKの効果でホスト側がアンダーランになるとセットされる。

制御転送 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.STATUSBUSNAK属性を事前に立てておき、ホストへは常に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.STATUSBUSNAKがセットされていてかつ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は無効とし、ACKNAK(オーバーラン)を意識的に使い分けるハードウェアフロー制御が適する。

/* 受信バッファ内の未読文字数を返す:ノンブロッキング */
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(アンダーラン)を返すため、ホスト側も再送に対応する。

/* _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*2byteぶん存在するようになるので、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_INTFLAGSBTRNCOMPLフラグはクリアされる。なので FIFO読み出し処理全体は単一のループで記述可能だ。

さらにFIFO-nを読んで得たEP_IDの比較一致だけで条件分岐を済ませて後段のアドレス計算を省いているが、多分それが一番記述しやすいだろう。

ここで考慮すべきは制御転送と、その他転送との割込区別だ。SETUPパケットで始まる一連のトランザクション(つまりDATASTATUSパケット)が、FIFOループに混じってしまうと切り分ける面倒が増える。なのでここではエンドポイント設計で述べたように両者を区別して扱っている。

VBUS 検出

AVR-DU系列には、専用の VBUS検出ポートがない。必要なら通常の GPIOで実現するポリシーである。一般にこれはアナログコンパレータ(AC周辺機能)を必要とするが、AVR-DU系列でこれが可能なのはPIN_PC3PIN_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=DSRbRxCarrier=DCDRI入力属性はあるのに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出力がない場合、対抗側レセプタクルでGNDFGではない)に短絡していればケーブル活線検出=属性値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仕様に備わる付帯機能(LineEncodingSerialState)への対応など多岐にわたるコードを記述しなければならないが、それでも USBプロトコルスタックの中核は 3KiB程度を目安に実装することができた。

  • EPPTRDATAPTRには SRAM領域内のアドレスしか指定できない。かつ必ず偶数のアドレス(LSB=0)でなければならない。
  • エンドポイント毎のSTATUS仮想レジスタは RMW 保護下にある。個別に専用のINCLRINSETOUTCLROUTSETレジスタを介して変更しなければならない。
  • エンドポイント設定下の他の仮想レジスタやバッファメモリは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割込は正しく通信が維持できているなら、発生する理由がない。
  • UNFOVF割込だけからは発生源のエンドポイントを特定できない。UNFOVFフラグを個別に調べる必要がある。それでいてこの割込に対応する価値はほとんど見出せない。あえて使うならSerialStateの更新か?
  • (BUS)RESET割込が届いたら、USB0_ADDRを 0にリセットし、新たなエニュメレーション開始に備えなければならない。
  • バスパワー機器で(BUS)SUSPEND割込が届いたら、低消費電力状態(2mA以下)に移行しなければ電力喪失の恐れがある。
  • バスパワー機器で(BUS)RESUME割込が届いたら、通常電力状態(最大500mA、構成ディスクリプタ内で必要電力量を指定)に戻って良い。

関連リンク

参考文献

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

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

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

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/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0