SNSへはこちら

LPC11U35はUSBがラク!(2) - CDCのコードを改造・解析

前回のコードで「いらないなあ」と思われる部分を消したり改変したり、その詳細をざっと解析していきたいと思います。
5月ですね。

サンプルコードの概要

このコードは、UART でゲットしたデータを USB CDC として PC に送信するというものらしいです。正しい CDC の使い方ですね。
ですが、今回はそんなことはせず、単純に USB CDC によってデータを PC に送信するということをしたいので、UART 関連のコードは削除していきます。

そうすることで、USB ペリフェラルの ROM API 記述部分のみにして、ファイル全体を見やすくしたいなあと思っています。そこ、「この操作要るのか?」とか言わない。

削除・修正箇所

マクロ

  • UART_BRIDGE と、/* UART defines */ と書かれた部分を全削除
  • その下の /* VCOM defines */ 部分も全削除
  • #ifdef UART_BRIDGE#endif で囲まれた部分全削除

pUsbApi

main 関数内に pUsbApi というポインタ変数に怪しげな値を代入していますが、ここは消します。結局定数値ポインタとして使うのなら、わざわざ変数を用意するまでもありませんからね。
一方でファイル上部にこのポインタ変数をグローバル宣言しているところがあります。そこをやはり削除して、以下のマクロに置き換えます。

#define pUsbApi ((USBD_API_T*)((*(ROM **)(0x1FFF1FF8))->pUSBD))

ね。変数じゃなくてマクロで十分ですよね。限りある資源を大切に。

VCOM_SendBreak関数

内部を空にして、return USB_OK にする。USART 使わないので、内部の記述はマズい。

ret = とか ret == LPC_OK を消す

鬱陶しいので。というか、関数内部を見る限り、LPC_OK 以外の値を返す実装ではないし、意味がないので。
しかし、関数定義の返り値型 ErrorCode_t は変えずにそのままにしてください。

memset?

main 関数内部の usb_paramcdc_param は謎に memset 初期化をしている。あまりいいものではないので、変えましょう。
というか、どちらも1度しか代入されない数なので、わざわざ memset を呼び出す意味はない。削除。

そのままだと初期値は不定なので、変数宣言時に空のブレースを代入するようにする。

  USBD_API_INIT_PARAM_T usb_param = {};
  USBD_CDC_INIT_PARAM_T cdc_param = {};

その他 memset を使っている変数についても同様です。

最適化

もうここまで来ると目的を見失っている気がしますが、デフォルトで -O0 な最適化レベルを -Os にします。すると、バイナリサイズが 17KBytes になりました〜〜
もちろん書き込むとちゃんと動きますよ。

main関数の構成

ちゃんとコード自体にコメントをしてくれているので、各ブロックが何をしているのか分かりやすいと思います。
以下のような構成ですね。

1. 変数の宣言、ペリフェラルの初期化

  • usb_param, cdc_param は初期化用の構造体。ここにコールバック関数を記述する。
  • desc はディスクリプタを管理する、ポインタの集合。
  • hUsb, hCdc は実は void * に型が等しい。スタック/モジュールのハンドラと言われているが、詳細は不明。取り敢えず各種処理で使うらしい。
    • やっぱりローカル変数を冒頭で宣言されると分かりづらいね。

2. USBペリフェラルの初期化情報を渡す

usb_param に渡す(変数名が分かりづらいな)。

  /* initilize call back structures */
  usb_param.usb_reg_base = LPC_USB_BASE;
  usb_param.mem_base = 0x10001000;
  usb_param.mem_size = 0x1000;
  usb_param.max_num_ep = 3;

usb_reg_basemax_num_ep は分かるが、mem_basemem_size が奇々怪々。

0x10001000、0x100 ってなんだ?

と思う人も多いはずです。まあ多分 RAM なんですが、これベタ書きされてもな...
こういうふうにアドレスをベタ書きすることは危険です。このプログラムはあくまでもただのサンプルなので、こういうふうにしてるんだと思います。
そうですね。ひとまず本当だったら uint8_t 型の配列をメモリ上に確保しておくとかが必要な気がします。

この構造体定義をしているヘッダファイルを覗いてみました。

typedef struct USBD_API_INIT_PARAM
{
...
  uint32_t mem_base;  /**< Base memory location from where the stack can allocate
                      data and buffers. \note The memory address set in this field
                      should be accessible by USB DMA controller. Also this value
                      should be aligned on 2048 byte boundary.
                      */
  uint32_t mem_size;  /**< The size of memory buffer which stack can use. 
                      \note The \em mem_size should be greater than the size 
                      returned by USBD_HW_API::GetMemSize() routine.*/
...

ふむ、どうやら USB DMA を使っているんですか。思ってたより高級な動作していますね。つまりこういうことです。

  • mem_base
    • USB DMA で使う領域が必要だから、そのベースアドレス書いてちょ
    • 2048byte のアラインメントでよろしく
  • mem_size
    • 使う領域のサイズを書いてちょ
    • USBD_HW_API::GetMemSize() で要求されるサイズ以上のものでよろしく

なるほど。2048bytes のアラインメントが必要なんて、大層なものを要求されている気がしますが、ROM API 様様という状態なので、そうですかと思っておきます。
mem_size はコメント通り、本来はそういうサイズが必要なので、コレも後に解決したいです。

3. ディスクリプタ情報等を入力し、ついに USB ペリフェラルの初期化

  /* user defined functions */
  cdc_param.SendBreak = VCOM_SendBreak;

  /* Initialize Descriptor pointers */
  desc.device_desc = (uint8_t *)&VCOM_DeviceDescriptor[0];
  desc.string_desc = (uint8_t *)&VCOM_StringDescriptor[0];
  desc.full_speed_desc = (uint8_t *)&VCOM_ConfigDescriptor[0];
  desc.high_speed_desc = (uint8_t *)&VCOM_ConfigDescriptor[0];

  /* USB Initialization */
  pUsbApi->hw->Init(&hUsb, &desc, &usb_param);

まず突然の cdc_param です。SendBreak をする際の関数を登録していますが、取り敢えずプログラムの流れ上はこの後でいいものなので、一旦無視します。
続いて desc のメンバにグローバルで宣言している各種ディスクリプタを渡して、最後にInit() によって情報を渡します。同時に USB ペリフェラルの初期化を行っているようです。
ちなみに渡しているディスクリプタは以下です。

  • デバイスディスクリプタ
  • ストリングディスクリプタ
  • コンフィギュレーションディスクリプタ
    • これ以下のものも渡しているみたい?

インターフェイスディスクリプタは以下でも渡します。

4. CDC の初期化

  // init CDC params
  cdc_param.mem_base = 0x10001500;
  cdc_param.mem_size = 0x300;
  cdc_param.cif_intf_desc = (uint8_t *)&VCOM_ConfigDescriptor[USB_CONFIGUARTION_DESC_SIZE];
  cdc_param.dif_intf_desc = (uint8_t *)&VCOM_ConfigDescriptor[USB_CONFIGUARTION_DESC_SIZE + \
                                                              USB_INTERFACE_DESC_SIZE + 0x0013 + USB_ENDPOINT_DESC_SIZE ];

  pUsbApi->cdc->init(hUsb, &cdc_param, &hCdc);

またベタ書きかといいたいのですが、これも先ほどと同じなのでスルーしておきます。
続いてディスクリプタを渡して、CDC の初期化です。
渡しているディスクリプタは以下。

  • Communicattions Class のインターフェイスディスクリプタ
  • Data クラス(CDC) のインターフェイスディスクリプタ

5. 散らばったデータを纏める・ハンドラの登録・接続

  /* store USB handle */
  g_vCOM.hUsb = hUsb;
  g_vCOM.hCdc = hCdc;
  g_vCOM.send_fn = VCOM_usb_send;

  /* allocate transfer buffers */
  g_vCOM.rxBuf = (uint8_t*)(cdc_param.mem_base + (0 * USB_HS_MAX_BULK_PACKET));
  g_vCOM.txBuf = (uint8_t*)(cdc_param.mem_base + (1 * USB_HS_MAX_BULK_PACKET));
  cdc_param.mem_size -= (4 * USB_HS_MAX_BULK_PACKET);

  /* register endpoint interrupt handler */
  ep_indx = (((USB_CDC_EP_BULK_IN & 0x0F) << 1) + 1);
  pUsbApi->core->RegisterEpHandler (hUsb, ep_indx, VCOM_bulk_in_hdlr, &g_vCOM);
  /* register endpoint interrupt handler */
  ep_indx = ((USB_CDC_EP_BULK_OUT & 0x0F) << 1);
  pUsbApi->core->RegisterEpHandler (hUsb, ep_indx, VCOM_bulk_out_hdlr, &g_vCOM);

  /* enable IRQ */
  NVIC_EnableIRQ(USB_IRQn); //  enable USB0 interrrupts
  /* USB Connect */
  pUsbApi->hw->Connect(hUsb, 1);

g_vCOM という構造体がありますが、一連のコードを見る限りポインタ等の散らばったデータをまとめる構造体みたいです。このコードブロック中程で API さんに渡していますが、結局 void * となり、更にユーザーがその処理を関数として書けるので、コードにある通りのメンバ、および順序である必要は無いっぽいですね。とにかく VCOM_DATA_T という型名を定義することが大事っぽい。

rxBuf txBuf は何というか、簡易的な malloc をしています。ってこれ、明らかに使用する領域サイズが上で述べた 0x1000 等に比べると圧倒的に小さいじゃないですか。ちょっとメモリ贅沢に使いすぎでは。

ep_indx については直下で述べるとして、それが終わったら USB をホストと接続するようです。

ep_indx問題

ep_indx多分エンドポイント番号を入れているのだと思いますが、ちょっと不明です。
pUsbApi->core->RegisterEpHandler のヘッダファイルを読む限り、よくわからん記述があります。

  • * \param[in] ep_index Class specific EP0 handler function.(原文ママ)
    • 訳:引数 ep_index はクラスに特有の EP0 のハンドラ関数です。
    • ん???関数??? uint32_t なのに?

他のプロジェクトを見てみたり、色々コードを弄ってみたところ、なんとなくですが、この値について分かってきたことがあります

その前に、そもそも RegisterEpHandler の機能について説明します。こちらはマニュアルで「EP0 が〜〜」と書いてありますが、忘れてください。多分間違いです。以下のように記述すると、指定したエンドポイント、方向でリクエストが入った時に ROM API が関数を呼んでくれます

  pUsbApi->core->RegisterEpHandler (hUsb, ep_indx, VCOM_bulk_out_hdlr, &g_vCOM);

ここで VCOM_bulk_out_hdlr戻り値 ErrorCode_t、引数が (USBD_HANDLE_T hUsb, void* data, uint32_t event) な関数です。
&g_vCOMこの関数の引数に指定される void * ポインタです。

さて、それでは ep_indx についてです。こちらはどのエンドポイントで、どの方向のトランザクションがあった時に関数を実行するか指定するもののようです。そしておそらくこの変数は単純なエンドポイント番号ではありません。おそらく以下の構成です。

bit位置 意味
0 通信方向(0: OUT, 1: IN)
1〜3 エンドポイント番号
4〜31 Reserved. 0にする必要あり

なのでソースコード中にある

 ep_indx = (((USB_CDC_EP_BULK_IN & 0x0F) << 1) + 1);

の謎コードはこれに対応するものだと思われます。

エンドポイント構成

そういえば、エンドポイントの設定は何もしていないですね。どうなっているんでしょう。

これも憶測ですが(まあほぼ確実だが)、必要なエンドポイントは ROM API によって設定されるのではないかと思います。ディスクリプタの記述によってフレキシブルに設定変えてくれたらうれしいな〜と思いましたが、当然そんなことはなく。
おそらく以下のような構成です。

EP 転送 役割
0 コントロール セットアップパケット等のやり取り
1 インタラプト(IN/OUT) Communications Class の何か
2 バルク(IN/OUT) Data Class のやりとり(実際のシリアルデータの通信路)

ということで

一通りソースコードざっと見が完了しましたね。ところで、今見てきた中でもやっぱりまだ不要不急なコードが散見されます。せっかくなんでもっと整理してみました。上げたいんですけど、Licenceが面倒くさそうなので見せられません。

make --no-print-directory post-build
Performing post-build steps
arm-none-eabi-size "USB_ROM_CDC.axf" ; arm-none-eabi-objcopy -O binary "USB_ROM_CDC.axf" "USB_ROM_CDC.bin" ; checksum -p LPC11U35_501 -d "USB_ROM_CDC.bin"
   text    data     bss     dec     hex filename
   1536       4      16    1556     614 USB_ROM_CDC.axf
Written checksum 0xefffe50b at offset 0x1c in file USB_ROM_CDC.bin
Previous value 0x00000000 at offset 0x1c in file USB_ROM_CDC.bin

1,556 KB になりました。流石にやりすぎましたかね。

すみません。メモリのベースアドレスについて色々改善しようと思ったんですが、飽きてしまいました。。。大したことはしないのですが、続きは次回とさせてください。