Skip to content

USB_Peripheral

askn37 edited this page Oct 19, 2026 · 7 revisions

AVR DU 系列の USB 2.0 周辺機能実装

USB周辺機能の制御

AVR-DU系列のUSB0周辺機能は、XMEGA-AU 系列のそれとよく似ている。エンドポイント構成テーブルの構成はほぼ同等だ。ただし随分と機能が整理され、理解しやすくなっている。

  • USB2.0 Full-Speed (12Mbps) 対応固定。ハードウェア的な Low-Speed 対応はなく、関連制御ビットもない。装置記述子でソフトウェア的に USB 1.x を指定しても、物理層まではダウングレードされない。
  • Pull during Reset 機能はなく、関連制御ビットもない。
  • ATmega32U4 のように、D+/D-ポートを GPIO的に操作できる機能はない。
  • USB_CLK(48MHz)生成は 内蔵された PLL による完全自動制御である。キャリブレーションも自動で行われる。
  • VUSBポートに繋がる内蔵 LDOの制御も既定ではオンであるから忘れていても良い。外付けカップリングコンデンサ(0.1〜0.47uF)の付け忘れに注意するだけ。
  • 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.

どのみち Microchip AVR-DU 系列製品専用でしか実行できないコードを書くのだからこれで問題はない。Criosity Nano AVR64DU32 をリファレンスハードウェアとし、AVR-DU系列純正ベアメタルチップを採用する限り、これらの同一性は問題ではない。USBホスト側から見て区別する理由もない。

関連リンクにも掲示した Microchip PIC & AVR Examples を参照すると、AVR-DU系統用には以下の VID:PID ペアが汎用的に使いまわされていることが見て取れる。AVR DU系統を必ず採用し、商用に使用せず、目的を逸脱しないかぎり、これらを流用するのは比較的安全だと考えられる。

idVendor idProduct description
0x04D8 MCHP VID
0x0010-0x002F reserved for testing/non-public demos
0x0B0A Vendor Class
0x0B12 HID Class - keypad communication etc.
0x0B13 HID Class - mouse communication etc.
0x0B14 (Composite Class reserved?)
0x0B15 CDC Class - serial communication etc.
0x0B16 CDC Class - peripheral bridge etc.
0x0F30-0x0F33 Develop Driver Sample
0x03EB ATML VID
0x2175 PKOBN JTAGICE3 (Curiosity Nano Series)
  • 0x0010 から 0x002F の範囲は、非公開社内利用用途の仮符号と思われ、故に非公開のサンプルコードやデモンストレーション用途以外では決して使用してはならない。
  • 0x0F30 から 0x0F33 の範囲は、OS用ドライバー開発用の設定ファイルサンプル中に見られるが、デバイスサンプルコードで使用されている形跡はない。
  • PKOBN 用 VID:PID は、EDBG/JTAGICE3 互換動作で OCD デバッガー通信を利用できる完全なコンポジットクラス実装を施した場合以外での選択は薦められない。逆に OCD ドライバとの通信を可能にするにはこれを選択すべきとなる。

なお、AVR DU系統 10製品については、2026年現在でも専用の DFU(Device Firmware Update)用 PID の専用割り当てはされていない。他の既知の Atmel USB対応 MPU製品全種には、全てに専用の VID:PID (0x03EB+0x2Fxx)が割り当てられており、公知である。

  • つまり Microchip 買収以後の AVR 製品には、DFU 公式対応製品が存在していない。
  • 自ずから、Windows 8 未満用の Microchip/Atmel 公式 USBドライバには AVR DU系統専用項目が存在しないため、既知の VID:PID ペアを流用するか、Windows 8 以降の標準 USBドライバを想定しなければならない。
  • 参考:dfu-programmer/arguments.c

USB-CDC用 エンドポイントの設計

USB-CDCでは以下の3組で計5個のエンドポイントを使用する。ここでいう入出力とはホスト側基準なので、デバイス側から見ると逆の名称である。

  • EP0_OUT - 制御受信: SETUP割込
  • EP0_IN - 制御送信:
  • EP1_IN - 割込送信: TRNCOMPL割込
  • EP2_OUT - バルク受信:
  • EP2_IN - バルク送信:

各エンドポイントには 8byteの仮想レジスタが割り当てられ、USB制御器(それ自体が一種の独立したマイクロコントローラーだ)から非同期で参照される。従ってこれらは必ず SRAM上に確保されなければならない。

EP1_OUTは使用しない(EP0_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;
    uint16_t wValue;
    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_OUT_BUFF (*(Descriptor_Header*)&USB_BUFF.header)
#define EP0_IN_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周辺機能 制御開始 */

// FUSE.SYSCFG1 = FUSE_USBSINK_bm;    /* ...であると仮定して */
SYSCFG_VUSBCTRL = SYSCFG_USBVREG_bm;  /* 内蔵LDO による VUSB制御許可 */

USB0_ADDR = 0;                        /* USB Address 未定義状態を確実にする */
USB0_CTRLB = USB_ATTACH_bm;           /* D+線プルアップ許可:USBバス接続有効 */
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 間隔で毎秒千回発生する可能性がある。なので不要な時は発生しないようにしておくべきだ。

#define SEND_DELAY_MS 10
volatile uint8_t SOFCNT = 0;
void enable_interrupt_sof (void) { SOFCNT = SEND_DELAY_MS; bit_set(USB0_INTCTRLA, USB_SOF_bp); }
void disable_interrupt_sof (void) { bit_clear(USB0_INTCTRLA, USB_SOF_bp); }
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();  /* 主処理の interrupt_sof_pending() を解除 */
    }
    USB0_INTFLAGSA |= USB_SOF_bm;
  }
  /* ここにその他の割込フラグ処理 */
}

こうしておいて主処理の必要なところでenable_interrupt_sof()を叩くとおよそ 10msの遅延送出を実現できる。その間は満杯になるまで EP2_IN_BUFFに送信データを押し込める。主処理側で送信中かもしれないから、その成否に関わらずSOF割込は規定のカウントダウン後に停止する。

EP1_INのUNFOVFはBUSNAKの効果でホスト側がアンダーランになるとセットされる。

制御転送 Control transactions

制御転送のあらましは USB2.0概説 に過不足なく書かれている。この文書の記述範囲は USB物理通信でやりとりされる 4種類の USBパケットがどのようにエンコード/デコードされ、そのデータペイロードがワークメモリに対して読み書きされるかまでだ。

        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 */
    if (bit_is_clear(bmRequestType, USB_REQTYPE_DIRECTION_bp)) {  /* bp=7 */
      EP0_OUT_flush();  /* EP0_OUTにDATAパケットを読む */
      /* DATAが格納されるのは EP0_OUT_BUFFの位置:EP0_SETUPの 後ろから */
    }
    /* 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();
    }
    else if (bmRequestType == USB_REQTYPE_VENDOR_gc) {    /* gc=0x40 */
      /* 必要ならここにベンダー要求処理 */
      control_request_vender();
    }
  }
  /* ここに後述の 割込バルク転送 処理 */
}

標準制御の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確保してから、本番の構成ディスクリプタを取得する二段階の手順を踏む。

USB-DMA は(ハーバードアーキテクチャにおける)データバス空間にしかアクセスできないので、プログラムメモリ空間内で定数定義された構成記述子(ディスクリプタ)を直接読むことができない。故に通常は SRAMバッファに対して memcpy を行う。これは EEPROM、BOOTROW および USERROW 拡張メモリ空間をディスクリプタ定義領域とすれば、単なる参照ポインタ変更に置き換えることができる。

標準制御の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;  /* Get 7bit USB Device Address */
}

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 */
}

USB 制御エニュメレーション

ここまでの標準制御が実装できると、USB 制御エニュメレーションに進むことができる。このネゴシエートが完了するまでは他の制御通信が始まることはない。

本項については別項。⇨ USB 2.0 制御エニュメレーション

割込バルク転送 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)パケットを投げる。これは RS-232C の、TxD、RxD 以外のピン論理が変更されたことを ホストへ通知するために使われる。

バルク送信 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 に指定できるのは データバス範囲中のアドレスだけで、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を超えるデータ塊を送るなら各自でアドレス起点と長さを逐一計算して分割送信するよう考えなければならない。

安易にリングバッファを使うよりもこの方法でダブルバッファを用意する方が、効率の良い転送処理を書ける。

USBエニュメレーション中の ディスクリプタ構造体送信は、この方法を使うと BOOTROW や USERROW 拡張メモリ領域に置いたまま memcpy なしで直接渡すことができる。

バルク受信 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がセットされていない限り受信バッファに触れることを許されないので(同期すべく)ブロッキングが必要だ。似たような条件式が繰り返し続くが、それぞれ役割が違う。

  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(アンダーラン)を返すため、ホスト側も再送に対応する。

/* _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幅の正数でのオフセットと解釈する。これを符号拡張して(単に上位バイトに0xFFを補完して)計算して得る結果はエンドポイント構造体を示すメモリアドレスであり、エンドポイント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ケーブル抜線時のスタンドアロン動作に苦労する。

残念ながら Criosity Nano AVR64DU32 の PC3(AINP4)に繋がっている VBUS検出回路は デバッグ側 USBポートからの給電も拾ってしまうため、セルフパワー動作向けの挙動を示さない。

なお、AVR DU系統の PC3 デジタル入出力は、内蔵 LDO から電力供給を受けているため USB周辺機能活動中は VDD によらず強制的に 3.3V 固定となる。アナログ入力については 5V トレラントであるが デジタル入力はそうではないため、GPIO 用途が限られることに注意したい。

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コマンド(これは Java で実装されている)などの USB情報表示ツールでは日本語文字列その他をそのまま表示させることができる。

逆に言えばこういう OSの仕様なので、本来の意味のシリアル番号文字列を HEXフォーマットで記述するような場合は、その有効文字数も考えないと意図しなかった挙動を示すかもしれない。

まとめ/要点

ここまでの考察と検証で、AVR-DU系列での USB-CDCの実装が可能になった。実際には構成ディスクリプタの設計やそのエニュメレーション(承認確認)、USB-CDC仕様に備わる付帯機能(LineEncodingやSerialState)への対応など多岐にわたるコードを記述しなければならないが、それでも USBプロトコルスタックの中核は 3KiB程度を目安に実装することができた。

  • EPPTRやDATAPTRには データバス範囲中のアドレスしか指定できない。かつ必ず偶数のアドレス(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割込が届いたら、7ms 以内に低消費電力状態(0.5mA〜2mA以下)へ移行しなければ電力喪失の恐れがある。
  • バスパワー機器で(BUS)RESUME割込が届いたら、通常電力状態(最大500mA、構成ディスクリプタ内で必要電力量を指定)に戻って良い。

関連リンク

参考文献

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

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

UPDI4AVR-USB / github.com

  • AVR DU系統にインストールできる UPDI/TPI/PDI/ISP プログラムライターファームウェア。USB-HID/CDC クラスコンポジット複合デバイスの実装例。

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

LINKS

multix.jp/てくにかるむ(休眠中)
Multix Zinnia Product SDK [*AVR]
AVR.JP(日本語訳)
AVR-LIBC(日本語訳)

Clone this wiki locally