SNSへはこちら

ラズピコでTinyUSBやってみよう(1) - 取り敢えず作ってみる

ラズピコこと Raspberry Pi Pico ですが、この SDK に TinyUSB という物がついています。これで結構楽ちんに USB プログラムを書けるっぽいので色々やっていこうというわけです。自由気ままに、悪く言えば適当にやっていきます。
この TinyUSB、他にも STM32 とか、色々なマイコンに対応していて、USB クラスのいくつかをサポートしているようです。これは面白そう。

なお本記事群ではラズピコでの使用法を模索していきますが、サブモジュール云々等以外のプログラム記法については他のマイコンでも同様ですので、参考にしていただければと思います。

本記事はシリーズ化してやっていきますが、今回はどの USB デバイスクラスも用いることなく、取り敢えずビルドが通ってホスト(PC)に認識されればいいや程度の内容です。いわば入門ですね。

ドキュメント

かなり貧弱。以下しか無いですね。あとは先人のリポジトリとか?
ちなみにこの貧弱な現状が、この記事を書く大きなきっかけになりました。こういうのって探検するみたいで燃えるんですよね。

サブモジュールの導入

これ一撃で Pico-SDK に TinyUSB をインストールします。

$ git submodule update --init

プロジェクトの準備

ひとまず、こちらの pico SDK リポジトリに存在する指示に従ってプロジェクトを作成してください。

...出来ましたでしょうか。しかし、そのままでは TinyUSB は使えません。使えるように設定しましょう。

CMakeの設定

予め、プロジェクト内部 CMakeLists.txttarget_link_librariestinyusb_ で始まる2つのライブラリを記述しておきます。そしてプロジェクトルートをインクルードパスに追加しておきましょう。というかこれだけで導入完了です。

target_link_libraries(${proj_name} pico_stdlib tinyusb_device tinyusb_board)

include_directories(${proj_name} PRIVATE ${CMAKE_CURRENT_LIST_DIR})

また、同ファイル内で pico_enable_stdio_usb が記述されていない or 0に設定されていることを確認してください。

tusb_config.hの設定

また、tusb_config.h というファイルも必要になるようなので、SDK (というより TinyUSB のサンプルファイル)からパクってきます。

# 現在プロジェクトルートにいると仮定
$ cp ../../pico-sdk/lib/tinyusb/examples/device/hid_generic_inout/src/tusb_config.h .

また、ここではマクロが再定義されてしまうのでコメントアウトします。

// 67行目付近

// This example doesn't use an RTOS
// #define CFG_TUSB_OS               OPT_OS_NONE

そして、現状どの USB デバイスクラスも使う気がありませんので、以下のように全て 0 に設定します。
本来ですとこのマクロによって使用する関数や、必要なコールバック関数定義が決定されるようです(正確には TinyUSB のソースコード内にある #if#endif ブロックによって必要な関数等がビルド回避される)。

// 96行目付近

//------------- CLASS -------------//
#define CFG_TUD_CDC               0
#define CFG_TUD_MSC               0
#define CFG_TUD_HID               0
#define CFG_TUD_MIDI              0
#define CFG_TUD_VENDOR            0

main.cの中身

ソースファイルの雛形として、以下を記述すればいいでしょう。下で述べますように、まだこれじゃあコンパイルは通りません。

#include <stdio.h>
#include "pico/stdlib.h"
#include "tusb.h"
#include "bsp/board.h"

int main() {
    board_init();
    tusb_init();

    while(1) {
        tud_task(); // device task
    }
    return 0;
}

で、この関数が何をしているかですが、概ね以下のような理解で良いようです。

  • board_init で対応ボード(今回はRaspberry Pi Pico)のペリフェラル初期化
    • とうやら基板上の LED とかが初期化されるらしい。
    • だから Pin25 の初期化は不要で使える。
  • tusb_init は TinyUSB の初期化

  • while ループ中の tud_task で各種処理を行っているよう。
    • コールバック関数を内部で呼んでいるため、ループ内で wait 等のブロッキング処理は厳禁
    • 通常割り込みで処理するべきだが、どうやらこのライブラリはイベントをポーリングしているっぽい?
    • そもそも割り込みハンドラを用いて内部処理しているようだが...ちょっとこの辺は不明。

ここで一旦ビルドするとエラーが出る。コールバック関数が足りないよう。

$ make
[  1%] Performing build step for 'ELF2UF2Build'
[100%] Built target elf2uf2
[  2%] No install step for 'ELF2UF2Build'
[  3%] Completed 'ELF2UF2Build'
[ 10%] Built target ELF2UF2Build
[ 12%] Built target bs2_default
[ 15%] Built target bs2_default_padded_checksummed_asm
[ 16%] Linking CXX executable tinyUSB_test.elf
/usr/local/Cellar/arm-gcc-bin/10-2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld: CMakeFiles/tinyUSB_test.dir/Users/yuki/Micom_Projs/RPPico_projs/pico-sdk/lib/tinyusb/src/device/usbd.c.obj: in function `tud_task':
usbd.c:(.text.tud_task+0x5f4): undefined reference to `tud_descriptor_configuration_cb'
/usr/local/Cellar/arm-gcc-bin/10-2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld: usbd.c:(.text.tud_task+0x7b8): undefined reference to `tud_descriptor_string_cb'
/usr/local/Cellar/arm-gcc-bin/10-2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld: usbd.c:(.text.tud_task+0x7c2): undefined reference to `tud_descriptor_configuration_cb'
/usr/local/Cellar/arm-gcc-bin/10-2020-q4-major/bin/../lib/gcc/arm-none-eabi/10.2.1/../../../../arm-none-eabi/bin/ld: usbd.c:(.text.tud_task+0x7cc): undefined reference to `tud_descriptor_device_cb'
collect2: error: ld returned 1 exit status
make[2]: *** [tinyUSB_test.elf] Error 1
make[1]: *** [CMakeFiles/tinyUSB_test.dir/all] Error 2
make: *** [all] Error 2

ディスクリプタの種類によって関数が分かれてるんだね。これは好感が持てます。

USBディスクリプタ用のコールバック関数を用意

しましょう。関数の型が不明なのでライブラリのソースコードを見て解決。pico-sdk/lib にあるようで。

すると、tinyusb/src/device/usbd.h に記述がありました。一部抜粋します。

// Invoked when received GET DEVICE DESCRIPTOR request
// Application return pointer to descriptor
uint8_t const * tud_descriptor_device_cb(void);

// Invoked when received GET CONFIGURATION DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
uint8_t const * tud_descriptor_configuration_cb(uint8_t index);

// Invoked when received GET STRING DESCRIPTOR request
// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid);

この const いいね。とか思いながら実装〜

// **絶対**動きませんが...
uint8_t const *tud_descriptor_device_cb(void) {
    return NULL;
}
uint8_t const *tud_descriptor_configuration_cb(uint8_t index) {
    return NULL;
}
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
    return NULL;
}

コメントにもある通り、絶対動きませんが、こう記述することで取り敢えずビルドは通るようになります。では、各種ディスクリプタを作っていきましょう。

USBディスクリプタを用意

取り敢えず書き書きします。Vendor-specific デバイスで、特に何も出来ないものとして作成します。

const uint8_t device_desc[] = {
    18, // bLength
    1, // bDescriptorType
    0x10,
    0x01, // bcdUSB
    0x00, // bDeviceClass
    0x00, // bDeviceSubClass
    0x00, // bDeviceProtocol
    CFG_TUD_ENDPOINT0_SIZE, // bMaxPacketSize0
    0x34,
    0x12, // idVendor
    0xcd,
    0xab, // idProduct,
    0x00,
    0x00, // bcdDevice
    0x00, // iManufacturer
    0x01, // iProduct
    0x00, // iSerialNumber
    0x01, // bNumConfigurations
};

const uint8_t conf_desc[] = {
    9, // bLength
    2, // bDescriptorType
    9 + 9,
    0, // wTotalLength
    1, // bNumInterface
    1, // bConfigurationValue
    0, // iConfiguration
    0x20, // bmAttributes
    0x0F, // bMaxPower

    // --- Interface ---
    9, // bLength
    4, // bDescriptorType
    1, // bInterfaceNumber
    0, // bAlternateSetting
    0, // bNumEndpoints
    0xFF, // bInterfaceClass
    0xFF, // bInterfaceSubClass
    0xFF, // bInterfaceProtocol
    0, // iInterface
};

const uint16_t string_desc_lang[] = { // Index: 0
    4 | (3 << 8), // bLength & bDescriptorType
    0x411 // てきとーにja-JP
};
const uint16_t string_desc_product[] = { // Index: 1
    16 | (3 << 8),
    'R', 'a', 's', 'p', 'i', 'c', 'o'
};

コンフィグレーションディスクリプタには、エンドポイント情報を含んでいません。マジで何も出来ないデバイスです。

あとはコールバック関数を書くだけ。簡単ですね。僕もこれだけでサクッと動いちゃうとは思いませんでしたよ。

uint8_t const *tud_descriptor_device_cb(void) {
    return device_desc;
}
uint8_t const *tud_descriptor_configuration_cb(uint8_t index) {
    return conf_desc;
}
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
    uint16_t const *ret = NULL;
    switch(index) {
        case 0:
            ret = string_desc_lang;
            break;
        case 1:
            ret = string_desc_product;
            break;
        default:
            break;
    }
    return ret;
}

実行&認識

あとは出来たバイナリを放り投げるだけで出来ます。ラズピコの場合はブートローダが USB MSC として認識されますので、cp コマンド撃つだけですね。

$ cp tinyUSB_test.uf2 /Volume/RPI-RP2

認識されたデバイス情報を USB Prober で見るとこんな感じです。

今回の main.c におけるコード全体はこちら。次回は HID ですかねぇ。