UIAPduinoをUSBデバイスとして動かす手順(Windows)

はじめに

前回の記事(UIAPduinoで名刺サイズのUSBテンキーを自作する)の補足記事です。

前回は、UIAPduinoを使って名刺サイズのUSBテンキーを自作するまでを一気に紹介しました。しかし、「まずはPCに繋いで、自作のUSBデバイスとして動かすところだけを体験したい」という方も多いと思います。

そこで今回は、UIAPduinoをUSBデバイスとして動かすところだけの手順を解説します。

UIAPduinoは、Arduino IDEを使ってLチカ(LEDの点滅)などを簡単に行うことができます。しかし、USBキーボードなどのデバイスを作ろうとしたとき、Arduinoの標準ライブラリである Keyboard.h は使えません。マイコン(CH32V003)自体にUSBのハードウェア周辺回路を持たないからです。

じゃあどうやってUSBデバイスにしているのかというと、ソフトウェア的に無理やりUSB通信を実装しているのです。そのため、専用の開発環境を使ってファームウェアをコンパイルする必要があります。

今回は、5秒おきにF9キーを自動送信するだけという一番シンプルな構成を作ってみます。 なぜF9なのか?というと、UIAPduinoにはUSBを切り離さずに再書き込みできるようなリセットボタンしかないので、今回は自動でキー入力するコードを書くことになります。ここでAキーやEnterキーが送られると、次のコマンドを打って書き込もうとするときの強烈な邪魔になってしまうからです。 キーボードにはPC操作を激しく妨害する厄介なキーがたくさん潜んでいますが、その中でF9キーは比較的パソコンの邪魔をしない無難なキーだからです。

開発環境について

私のメイン環境はWindowsです。なので、Windows環境でUIAPduinoのUSBファームウェアを開発する構成について説明します。

UIAPduinoでUSBデバイスを作るには、ch32funrv003usb というC言語のフレームワークを使います。これをWindows環境でコンパイルするには、RISC-V向けのGCCコンパイラなどが必要です。

Windowsに直接これらをインストールしてパスを通すような作業は、トラブルの元凶になります。そこで、Windowsの WSL(Windows Subsystem for Linux)上の Ubuntu を使います。 WSLを使えば、コマンド一発で環境が手に入ります。

1. WSL側の準備(コンパイル用)

WSLのUbuntuターミナルを開きます。(WSL自体のインストール手順は割愛します)

まずはC言語のコンパイラや、USB関連の開発ライブラリをインストールします。

sudo apt update
sudo apt install build-essential libnewlib-dev gcc-riscv64-unknown-elf libusb-1.0-0-dev libudev-dev

次に、USB通信の要となる rv003usb をダウンロードします。 rv003usb は、ハードウェアUSB機能を持たないCH32V003マイコンで、GPIOピンのON/OFF(ビットバンギング)によってUSBのLow-Speed通信を実現してしまう、凄まじいライブラリです。一緒に使う ch32fun もこの中にセットとして含まれています。

Windowsのエディタ(メモ帳やVSCodeなど)でコードを書きやすいように、Windowsのデスクトップなどによく使うファームウェア開発用の場所を作るのがオススメです。 WSLのUbuntuからWindows側のドライブ(Cドライブなど)にアクセスするには、/mnt/c/ を通ります。ここではデスクトップで作業するとして、WSLからデスクトップに移動してクローンします。(「自分の名前」はWindowsのユーザー名に読み替えてください)

cd /mnt/c/Users/自分の名前/Desktop

そして、今回の作業用ディレクトリ uiapduino_hid_minimal を作成して移動します。

mkdir uiapduino_hid_minimal
cd uiapduino_hid_minimal

ここに、USB通信の要となるライブラリ rv003usb をダウンロード(クローン)します。

git clone --recursive https://github.com/cnlohr/rv003usb.git

2. Windows側の準備(書き込み用)

コンパイルをWSLでやるなら、マイコンへの書き込みもWSLからやればいいのでは?と思うかもしれません。

しかし、WSLからWindowsに繋がっているUSBデバイスに直接アクセスするには、面倒な設定が必要です。そのまま書き込もうとしても Error: Could not initialize any supported programmers と怒られてしまいます。

そこで、ビルドはWSLでおこなって、書き込みはWindows側の minichlink.exe を使う といったように役割分担するのが一番簡単だとおもいます。

書き込みに使う minichlink.exe は、先ほどクローンした rv003usb/ch32fun/minichlink/minichlink.exe の中に、ビルド済みの本家ツールが入っています。

プロジェクトの作成

最小構成のファームウェアを作ってみます。 いまWSLで開いている uiapduino_hid_minimal ディレクトリの中に、必要な空ファイルを一括で作成してしまいましょう。

touch main.c usb_config.h funconfig.h Makefile

これで、ディレクトリの中身は以下のようになります。ファイルをWSL側で作ったので、あとはWindows側の好きなエディタでこれらのファイルを開いて、中身を書き込んでいくだけです。

Desktop/
├── uiapduino_hid_minimal/
│   ├── main.c            ← メインファームウェア
│   ├── usb_config.h      ← USBデバイスとしての設定
│   ├── funconfig.h       ← ch32funのビルド設定
│   └── Makefile          ← ビルドスクリプト
└── rv003usb/             ← さきほどWSLからクローンしたライブラリ

main.c

ファームウェアの心臓部です。

#include "ch32fun.h"
#include <string.h>
#include "rv003usb.h"

// 送信するキーコード (0x42 は F9キー)
#define KEY_F9 0x42

// USB に送信する 8バイトのデータバッファ
static volatile uint8_t hid_report[8] = {0};

void usb_handle_user_in_request(struct usb_endpoint *e, uint8_t *scratchpad,
                                 int endp, uint32_t sendtok,
                                 struct rv003usb_internal *ist)
{
    if (endp == 1) {
        // 現在設定されているレポートをPCへ送信
        usb_send_data((const void *)hid_report, 8, 0, sendtok);
        
        // 送信が終わったらバッファを空にする
        memset((void *)hid_report, 0, 8);
    } else {
        usb_send_empty(sendtok);
    }
}

void usb_handle_user_data(struct usb_endpoint *e, int current_endpoint,
                           uint8_t *data, int len,
                           struct rv003usb_internal *ist)
{
}

int main(void)
{
    SystemInit();
    Delay_Ms(100); 

    usb_setup();

    while (1) {
        // 5000ミリ秒(5秒)待つ
        Delay_Ms(5000);

        // バッファの3バイト目にキーコードをセットする
        hid_report[2] = KEY_F9;
    }
}

送信が終わったあとに memset でバッファを空にしているのがポイントです。空にしないと、キーがずっと押しっぱなしの判定になってしまいます。「1回押して離した」という挙動にするために必要です。

その他のファイル

残りの設定ファイルは以下の通りです。 usb_config.h では、PCに認識されるデバイス名(Product)を「UIAPduino HID Minimal」に設定しています。

usb_config.h
#ifndef _USB_CONFIG_H
#define _USB_CONFIG_H

#define ENDPOINTS 2
#define USB_PORT D
#define USB_PIN_DP 3
#define USB_PIN_DM 4
#define USB_PIN_DPU 5

#define RV003USB_OPTIMIZE_FLASH 1
#define RV003USB_EVENT_DEBUGGING 0
#define RV003USB_HANDLE_IN_REQUEST 1
#define RV003USB_OTHER_CONTROL 0
#define RV003USB_HANDLE_USER_DATA 1
#define RV003USB_HID_FEATURES 0

#ifndef __ASSEMBLER__
#include <tinyusb_hid.h>
#ifdef INSTANCE_DESCRIPTORS

static const uint8_t device_descriptor[] = {
    18, 1, 0x10, 0x01, 0x00, 0x00, 0x00, 0x08,
    0x09, 0x12, 0x03, 0xB8, 0x01, 0x00, 1, 2, 3, 1
};

static const uint8_t keyboard_hid_desc[] = {
    HID_USAGE_PAGE( HID_USAGE_PAGE_DESKTOP ),
    HID_USAGE( HID_USAGE_DESKTOP_KEYBOARD ),
    HID_COLLECTION( HID_COLLECTION_APPLICATION ),

    HID_REPORT_SIZE( 1 ),
    HID_REPORT_COUNT( 8 ),
    HID_USAGE_PAGE( HID_USAGE_PAGE_KEYBOARD ),
    HID_USAGE_MIN( 0xE0 ),
    HID_USAGE_MAX( 0xE7 ),
    HID_LOGICAL_MIN( 0 ),
    HID_LOGICAL_MAX( 1 ),
    HID_INPUT( 0x02 ),

    HID_REPORT_COUNT( 1 ),
    HID_REPORT_SIZE( 8 ),
    HID_INPUT( 0x03 ),

    HID_REPORT_COUNT( 5 ),
    HID_REPORT_SIZE( 1 ),
    HID_USAGE_PAGE( HID_USAGE_PAGE_LED ),
    HID_USAGE_MIN( 0x01 ),
    HID_USAGE_MAX( 0x05 ),
    HID_OUTPUT( 0x02 ),
    HID_REPORT_COUNT( 1 ),
    HID_REPORT_SIZE( 3 ),
    HID_OUTPUT( 0x03 ),

    HID_REPORT_COUNT( 6 ),
    HID_REPORT_SIZE( 8 ),
    HID_LOGICAL_MIN( 0 ),
    HID_LOGICAL_MAX( 101 ),
    HID_USAGE_PAGE( HID_USAGE_PAGE_KEYBOARD ),
    HID_USAGE_MIN( 0x00 ),
    HID_USAGE_MAX( 101 ),
    HID_INPUT( 0x00 ),

    HID_COLLECTION_END,
};

static const uint8_t config_descriptor[] = {
    9, 2, 0x22, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32,
    9, 4, 0, 0, 1, 0x03, 0x01, 0x01, 0,
    9, 0x21, 0x10, 0x01, 0x00, 0x01, 0x22, sizeof(keyboard_hid_desc), 0x00,
    7, 0x05, 0x81, 0x03, 0x08, 0x00, 10,
};

#define STR_MANUFACTURER u"UIAPduino"
#define STR_PRODUCT      u"UIAPduino HID Minimal"
#define STR_SERIAL       u"123"

struct usb_string_descriptor_struct {
    uint8_t bLength;
    uint8_t bDescriptorType;
    uint16_t wString[];
};

const static struct usb_string_descriptor_struct string0 __attribute__((section(".rodata"))) = { 4, 3, {0x0409} };
const static struct usb_string_descriptor_struct string1 __attribute__((section(".rodata"))) = { sizeof(STR_MANUFACTURER), 3, STR_MANUFACTURER };
const static struct usb_string_descriptor_struct string2 __attribute__((section(".rodata"))) = { sizeof(STR_PRODUCT), 3, STR_PRODUCT };
const static struct usb_string_descriptor_struct string3 __attribute__((section(".rodata"))) = { sizeof(STR_SERIAL), 3, STR_SERIAL };

const static struct descriptor_list_struct {
    uint32_t lIndexValue;
    const uint8_t *addr;
    uint8_t length;
} descriptor_list[] = {
    {0x00000100, device_descriptor, sizeof(device_descriptor)},
    {0x00000200, config_descriptor, sizeof(config_descriptor)},
    {0x00002200, keyboard_hid_desc, sizeof(keyboard_hid_desc)},
    {0x00000300, (const uint8_t *)&string0, 4},
    {0x04090301, (const uint8_t *)&string1, sizeof(STR_MANUFACTURER)},
    {0x04090302, (const uint8_t *)&string2, sizeof(STR_PRODUCT)},
    {0x04090303, (const uint8_t *)&string3, sizeof(STR_SERIAL)},
};

#define DESCRIPTOR_LIST_ENTRIES ((sizeof(descriptor_list))/(sizeof(struct descriptor_list_struct)))

#endif // INSTANCE_DESCRIPTORS
#endif // __ASSEMBLER__
#endif // _USB_CONFIG_H
funconfig.h
#ifndef _FUNCONFIG_H
#define _FUNCONFIG_H

#define CH32V003                1
#define FUNCONF_SYSTICK_USE_HCLK 1

#endif
Makefile
all : flash

TARGET:=main
CH32FUN:=../rv003usb/ch32fun/ch32fun
TARGET_MCU:=CH32V003

ADDITIONAL_C_FILES+=../rv003usb/rv003usb/rv003usb.S ../rv003usb/rv003usb/rv003usb.c
EXTRA_CFLAGS:=-I../rv003usb/lib -I../rv003usb/rv003usb

include $(CH32FUN)/ch32fun.mk

flash : cv_flash
clean : cv_clean

ビルド

WSL(Ubuntu)上で、さきほどWindows側に作成したプロジェクトのフォルダに移動し、make を実行します。

cd /mnt/c/Users/自分の名前/Desktop/uiapduino_hid_minimal
make main.bin

コンパイルは一瞬でおわるはずです。 main.bin という2〜3KB程度の非常に小さなバイナリファイルが生成されます。これでファームウェアは完成です。

ボードへの書き込み

生成した main.bin をUIAPduinoに書き込みます。 ここが一番のハマりポイントかもしれません。

1. リセットボタンを押しながらUSBを挿す

UIAPduinoは、「通電した瞬間(USBが挿入された瞬間)」にリセットボタンが押されていないと、書き込み待機モード(ブートローダー)に入らないことがあります。

USBを繋いだままリセットボタンを押せばいいのではと思うかもしれませんが、これだとうまくいかない場合があります。ソフトウェアUSBが動いている最中に中途半端にリセットをかけると、Windows側が「USBデバイスがエラーを起こした」と認識してしまい、正しく再接続してくれないからです。

なので、「USBを一旦抜いて、リセットボタンを押しっぱなしにしながら挿して、すぐに離す」という手順が必須になります。

2. コマンドを用意してから挿す

書き込み待機モードはタイムアウトが短く設定されています。放置すると数秒で通常の実行モードに戻ってしまいます。

そのため、あらかじめWindowsのPowerShellなどに以下のコマンドを入力しておき……

cd C:\Users\自分の名前\Desktop\uiapduino_hid_minimal
.\rv003usb\ch32fun\minichlink\minichlink.exe -c 0x1209b803 -w main.bin flash -b

USBを挿してボタンを離した直後に Enter キーを叩くのがコツです。

ちなみに、PowerShellを使っていると、書き込みが成功しているのに真っ赤な文字が表示されることがあります。 画面が真っ赤になっても、中に Image written. (書き込み完了)と書かれていれば成功しています。

ちなみに、コマンドの中にある -c 0x1209b803 は、書き込み対象のデバイスを指定する設定です。 0x1209 がオープンソース汎用のベンダーID(VID)で、0xb803 がUIAPduinoの「書き込み待機モード」時のプロダクトID(PID)です。PCにたくさん繋がっているUSB機器の中から、これを目印にして書き込み先を見つけているわけです。

動作確認

書き込みが成功すると、Image written. と表示されます。 その後自動的に再起動し、プログラムが動き出します。

今回は5秒おきにF9キーを入力するプログラムになっています。

これをどうやって確認するかですが、せっかくなので今あなたが読んでいるこの記事上でテストできるようにしました。 以下の点線の枠内を一度クリックし、そのまま5秒間待ってみてください。(※PC環境で見てくださいね)

ここをクリックして、そのまま待ってみてください。
F9キーに反応します。

無事に枠の色が変わって「F9キーの入力を検知しました」と表示されれば成功です。

おわりに

これだけのコードで、UIAPduinoをUSBデバイスとして動かすことができました。

あとは、main() のループの中で使いたいGPIOピンの入力を読み取る処理を追加するだけで、自作キーボードやマクロパッドとして拡張していくことができます。

この記事が参考になりましたら幸いです。