ELRS受信機をUSBドングルにする（VSCode + PlatformIO版）

以前に**Arduino ID**Eで、ELRS USBドングルを作成する記事を書きましたが、

https://qiita.com/kobatan/items/40728fbb625057d9f42b

どうやらボードやライブラリーのバージョンアップのせいで、うまくコンパイルできなくなっている様なので、新たに作り直しました。

今回の開発環境は、**[ VSCode + PlatformIO ]**　で行います。
PlatformIOならボードやライブラリのバージョンをプロジェクトごとに指定できるので、Arduino IDEの時のようにアップデートしたら動かなくトラブルを回避できると思ったからです。

### 用意するもの
①　マイコンボード　[Seeed XIAO RP2040 ( スイッチサイエンス 979円)](https://www.switch-science.com/products/7634)
②　ELRS受信機 2.4GHz（何でもいいです）
③　熱収縮チューブ
④　[USB Type-C アダプタ](https://www.amazon.co.jp/YFFSFDC-USB%E5%A4%89%E6%8F%9B%E3%82%A2%E3%83%80%E3%83%97%E3%82%BF%E3%83%BC%E3%80%903%E5%80%8B%E3%82%BB%E3%83%83%E3%83%88%E3%80%91USB-C-%E6%90%BA%E5%B8%AF%E9%9B%BB%E8%A9%B1%E5%85%85%E9%9B%BB%E3%81%8A%E3%82%88%E3%81%B3Windows%E3%80%81Mac%E3%80%81AndroidPC-%E3%83%A9%E3%83%83%E3%83%97%E3%83%88%E3%83%83%E3%83%97%E7%94%A8%E3%81%AE%E9%AB%98%E9%80%9F%E3%83%87%E3%83%BC%E3%82%BF%E8%BB%A2%E9%80%81%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-%E6%90%BA%E5%B8%AF%E9%9B%BB%E8%A9%B1OTG/dp/B0BL14PHNM/ref=sr_1_7?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=1BUHMFIYBGNRS&keywords=usb+typec+%E3%82%A2%E3%83%80%E3%83%97%E3%82%BF+%E3%82%AA%E3%82%B9%E3%82%AA%E3%82%B9&qid=1683059541&sprefix=usb+type+c+%E3%82%A2%E3%83%80%E3%83%97%E3%82%BF+%E3%82%AA%E3%82%B9+%E3%82%AA%E3%82%B9%2Caps%2C152&sr=8-7)（又はケーブル）
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/083c8652-bb1d-d278-4b5e-9b204d684166.jpeg"><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/cafa8f47-65c9-fde3-a14d-e25ef6d5c3f3.jpeg"><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/48c71aca-df57-af93-fd54-1aebf799d142.jpeg">

## 配線
下図のように配線します。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/138bbd88-02be-41a9-0a5c-d222d9d8639e.jpeg">
熱収縮チューブで絶縁します。
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/e993e511-03c3-e235-4802-8ae4597f14bf.jpeg"><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/7c9caf96-87c4-6724-b5c2-818d4f5e35b7.jpeg">
## プログラム
**VSCodeとPlatformIO** のインストールは既に済んでいる前提で行きます。

新規プロジェクトを作成します。下図のように設定して下さい。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/cff9ff49-15d3-3e3b-f4d9-d9fbeddc8cf8.jpeg">
platformio.iniの中身を下記のように書き換えます。
```platformio.ini
[env:seeed_xiao_rp2040]
platform = raspberrypi
board = seeed_xiao_rp2040
framework = arduino
monitor_speed = 420000
platform_packages =
    framework-arduinopico@https://github.com/earlephilhower/arduino-pico.git
lib_deps = adafruit/Adafruit TinyUSB Library@^2.1.0
build_flags = -DUSE_TINYUSB
```
main.cppに下記をコピペして下さい。
右上にコピーアイコンがあるのでそれをクリックすれば、全コピーできます。
```main.cpp
#include <Arduino.h>
#include "Adafruit_TinyUSB.h"

//#define DEBUG			// <---受信データをシリアルモニターに表示させたい時は、コメントアウトを外してください。

// USB HID report descriptor
// ゲームパッドデータの構造を指定します  （プロポ受信機用 (16bitデータ x 8ch) + (1bitデータ x 8ch）)
#define TUD_HID_REPORT_DESC_GAMEPAD_9(...) \
  HID_USAGE_PAGE ( HID_USAGE_PAGE_DESKTOP     ) ,\
  HID_USAGE      ( HID_USAGE_DESKTOP_GAMEPAD  ) ,\
  HID_COLLECTION ( HID_COLLECTION_APPLICATION ) ,\
  /* Report ID if any */\
  __VA_ARGS__ \
  HID_USAGE_PAGE   ( HID_USAGE_PAGE_DESKTOP   ) ,\
  HID_USAGE        ( HID_USAGE_DESKTOP_X      ) ,\
  HID_USAGE        ( HID_USAGE_DESKTOP_Y      ) ,\
  HID_USAGE        ( HID_USAGE_DESKTOP_Z      ) ,\
  HID_USAGE        ( HID_USAGE_DESKTOP_RX     ) ,\
  HID_USAGE        ( HID_USAGE_DESKTOP_RY     ) ,\
  HID_USAGE        ( HID_USAGE_DESKTOP_RZ     ) ,\
  HID_USAGE        ( HID_USAGE_DESKTOP_SLIDER ) ,\
  HID_USAGE        ( HID_USAGE_DESKTOP_DIAL   ) ,\
  HID_LOGICAL_MIN  ( 0                        ) ,\
  HID_LOGICAL_MAX_N ( 0x07ff,2                ) ,\
  HID_REPORT_COUNT ( 8                        ) ,\
  HID_REPORT_SIZE  ( 16                       ) ,\
  HID_INPUT        ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) ,\
  /* 8 bit Button Map */ \
  HID_USAGE_PAGE   ( HID_USAGE_PAGE_BUTTON    ) ,\
  HID_USAGE_MIN    ( 1                        ) ,\
  HID_USAGE_MAX    ( 8                        ) ,\
  HID_LOGICAL_MIN  ( 0                        ) ,\
  HID_LOGICAL_MAX  ( 1                        ) ,\
  HID_REPORT_COUNT ( 8                        ) ,\
  HID_REPORT_SIZE  ( 1                        ) ,\
  HID_INPUT        ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) ,\
  HID_COLLECTION_END\

  // CrossFire用
#define CRSF_BAUDRATE  420000
#define CRSF_MAX_PACKET_LEN 64
#define CRSF_NUM_CHANNELS 16

typedef enum
{
	CRSF_ADDRESS_BROADCAST          = 0x00,
	CRSF_ADDRESS_USB                = 0x10,
	CRSF_ADDRESS_TBS_CORE_PNP_PRO   = 0x80,
	CRSF_ADDRESS_RESERVED1          = 0x8A,
	CRSF_ADDRESS_CURRENT_SENSOR     = 0xC0,
	CRSF_ADDRESS_GPS                = 0xC2,
	CRSF_ADDRESS_TBS_BLACKBOX       = 0xC4,
	CRSF_ADDRESS_FLIGHT_CONTROLLER  = 0xC8,   // 受信データはこれで来る
	CRSF_ADDRESS_RESERVED2          = 0xCA,
	CRSF_ADDRESS_RACE_TAG           = 0xCC,
	CRSF_ADDRESS_RADIO_TRANSMITTER  = 0xEA,
	CRSF_ADDRESS_CRSF_RECEIVER      = 0xEC,
	CRSF_ADDRESS_CRSF_TRANSMITTER   = 0xEE,
} crsf_addr_e;

typedef enum
{
	CRSF_FRAMETYPE_GPS                = 0x02,
	CRSF_FRAMETYPE_BATTERY_SENSOR     = 0x08,
	CRSF_FRAMETYPE_LINK_STATISTICS    = 0x14,
	CRSF_FRAMETYPE_OPENTX_SYNC        = 0x10,
	CRSF_FRAMETYPE_RADIO_ID           = 0x3A,
	CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x16,   // チャンネルパックフレーム
	CRSF_FRAMETYPE_ATTITUDE           = 0x1E,
	CRSF_FRAMETYPE_FLIGHT_MODE        = 0x21,
	// Extended Header Frames, range: 0x28 to 0x96
	CRSF_FRAMETYPE_DEVICE_PING        = 0x28,
	CRSF_FRAMETYPE_DEVICE_INFO        = 0x29,
	CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY = 0x2B,
	CRSF_FRAMETYPE_PARAMETER_READ     = 0x2C,
	CRSF_FRAMETYPE_PARAMETER_WRITE    = 0x2D,
	CRSF_FRAMETYPE_COMMAND            = 0x32,
	// MSP commands
	CRSF_FRAMETYPE_MSP_REQ            = 0x7A,   // response request using msp sequence as command
	CRSF_FRAMETYPE_MSP_RESP           = 0x7B,   // reply with 58 byte chunked binary
	CRSF_FRAMETYPE_MSP_WRITE          = 0x7C,   // write with 8 byte chunked binary (OpenTX outbound telemetry buffer limit)
} crsf_frame_type_e;

#pragma pack(push,1)	// データを１バイト単位に詰めて配置
//----------- #pragma ここから ------------------------
typedef struct{
    unsigned ch0 : 11;	// 11bit チャンネルデータ
    unsigned ch1 : 11;
    unsigned ch2 : 11;
    unsigned ch3 : 11;
    unsigned ch4 : 11;
    unsigned ch5 : 11;
    unsigned ch6 : 11;
    unsigned ch7 : 11;
    unsigned ch8 : 11;
  unsigned ch9 : 11;
    unsigned ch10 : 11;
    unsigned ch11 : 11;
    unsigned ch12 : 11;
    unsigned ch13 : 11;
    unsigned ch14 : 11;
    unsigned ch15 : 11;
} crsf_channels;

typedef union{				// チャンネルデータ共用体
	uint8_t byte[22];
	crsf_channels b11;
} crsf_data;

typedef struct{				// CRSFフレーム
	uint8_t device_addr;	// アドレス
	uint8_t frame_size;		// この後からのバイト数
	uint8_t type;			// タイプ
	crsf_data data;			// チャンネルデータ
	uint8_t crc;			// CRC
} crsf_frame;

typedef struct {			// ゲームパッドデータ
	uint16_t ch[8];			// 16bit 8ch
	uint8_t sw;				// 1bit  8ch
} gamepad_data;

//------------#pragma ここまで-------------------------
#pragma pack(pop)


uint8_t const desc_hid_report[] =
{
	TUD_HID_REPORT_DESC_GAMEPAD_9()				// USB GamePad のデータ構造を指定
};

Adafruit_USBD_HID usb_hid;						// USB HID object

uint8_t rxbuf[CRSF_MAX_PACKET_LEN + 3];	// 受信した生データ
uint8_t rxPos = 0;
uint8_t frameSize = 0;
bool datardyf = false;							// USBに送るデータが揃った。
uint32_t gaptime;								// フレーム区切り測定用
uint32_t time_m;								// インターバル時間(debug用)
static gamepad_data gp;							// CH毎に並び替えたデータ

// CRSFから受信した11bitシリアルデータを16bitデータにデコード
void crsfdecode () {
	crsf_frame *crsf =(crsf_frame *)rxbuf;

	if (crsf->device_addr == CRSF_ADDRESS_FLIGHT_CONTROLLER) {	// ヘッダチェック
		if (crsf->type == CRSF_FRAMETYPE_RC_CHANNELS_PACKED) {	// CHデータならデコード
			gp.sw = 0;
			gp.ch[0] = (uint16_t)crsf->data.b11.ch0;
			gp.ch[1] = (uint16_t)crsf->data.b11.ch1;
			gp.ch[2] = (uint16_t)crsf->data.b11.ch2;
			gp.ch[3] = (uint16_t)crsf->data.b11.ch3;
			if ( (uint16_t)crsf->data.b11.ch4 > 0x3ff )  gp.sw |= 0x01; // AUX1は2値データ
			gp.ch[4] = (uint16_t)crsf->data.b11.ch5;
			gp.ch[5] = (uint16_t)crsf->data.b11.ch6;
			gp.ch[6] = (uint16_t)crsf->data.b11.ch7;
			gp.ch[7] = (uint16_t)crsf->data.b11.ch8;
			if ( (uint16_t)crsf->data.b11.ch9 > 0x3ff )		gp.sw |= 0x02;
			if ( (uint16_t)crsf->data.b11.ch10 > 0x3ff )	gp.sw |= 0x04;
			if ( (uint16_t)crsf->data.b11.ch11 > 0x3ff )	gp.sw |= 0x08;
			if ( (uint16_t)crsf->data.b11.ch12 > 0x3ff )	gp.sw |= 0x10;
			if ( (uint16_t)crsf->data.b11.ch13 > 0x3ff )	gp.sw |= 0x20;
			if ( (uint16_t)crsf->data.b11.ch14 > 0x3ff )	gp.sw |= 0x40;
			if ( (uint16_t)crsf->data.b11.ch15 > 0x3ff )	gp.sw |= 0x80;
			datardyf = true; // データ揃ったよフラグ
		}
	}
}	

// CRSF受信処理
void crsf(void) {
	uint8_t data;

	// CRSFから1バイト受信
	if (Serial1.available()) {	    // Serial1に受信データがあるなら
		data  = Serial1.read();		// 8ビットデータ読込
		gaptime = micros();
		if (rxPos == 1) {
			frameSize = data;		// 2byte目はフレームサイズ
		}
		if(rxPos < CRSF_MAX_PACKET_LEN){
		    rxbuf[rxPos++] = data;		// 受信データをバッファに格納
        }
		if (rxPos > 1 && rxPos == frameSize + 2) {
			crsfdecode();			// １フレーム受信し終わったらデーコードする
			rxPos = 0;
		}
	}
	else {
		if (rxPos > 0 && micros() - gaptime > 300) { // 300us以上データが来なかったら区切りと判定
			rxPos = 0;
		}
	}
}

// UART通信処理( Firmware書き換え用 )
// ExpressLRS Configurator の Flashing Method は [UART]ではなく [BetaflightPassthough] にすること。
void uart(void) {
	uint32_t t;

	if (Serial.available()) { // PCからデータが来たら、強制的に書き換えモードだと判断
		t = millis();
		do {
			while (Serial.available()) {      // PCからデータが来たら
				Serial1.write(Serial.read()); // PCからのデータを受信機に送る
				t = millis();
			}
			while (Serial1.available()) {     // 受信機からデータが来たら
				Serial.write(Serial1.read()); // 受信機のデータをPCに送る
				t = millis();
			}
		} while (millis() - t < 2000);        // データが来なくなったら終了
	}
}

// シリアルモニターに受信データを表示する（デバック用）
void debug_out() {
	int i;
	Serial.print(rxbuf[0], HEX);  		// device addr
	Serial.print(" ");
	Serial.print(rxbuf[1]);			 	// data size +1
	Serial.print(" ");
	Serial.print(rxbuf[2], HEX);  		// type
	Serial.print(" ");
	for (i = 0; i < 8; i++) {
		Serial.print(gp.ch[i]);
		Serial.print(" ");
	}
	Serial.print(gp.sw, BIN);
	Serial.print(" ");
	Serial.print(micros() - time_m); 	// インターバル時間(us)を表示
	Serial.println("us");
	time_m = micros();
}

void setup(){
	// USB HID デバイス設定
	usb_hid.setPollInterval(1);						// 1ms ポーリング
	usb_hid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report));	// リポートの記述形式を指定
	usb_hid.begin();
	while ( !TinyUSBDevice.mounted() ) delay(1);	// wait until device mounted

	datardyf = false;
	gaptime = 0;
	rxPos = 0;
	Serial.begin(CRSF_BAUDRATE);              // PCシリアル通信用 (受信機の速度に合わせる)
	Serial1.begin(CRSF_BAUDRATE, SERIAL_8N1); // CRSF通信用 (420kbps, 8bitdata, nonParity, 1stopbit)
	time_m = micros();						  // インターバル測定用
}

void loop(){
//	if ( TinyUSBDevice.suspended() ){
//		TinyUSBDevice.remoteWakeup();
//	}

	crsf();					// CRSF受信処理
	uart();					// UART通信処理(Firmware書き換え用)

	if (datardyf) {         // データが揃ったらUSB送信
		if ( usb_hid.ready() ) {
			usb_hid.sendReport(0, &gp, sizeof(gp));
#ifdef DEBUG
			debug_out();    // デバッグ用 (シリアルモニターで数値を確認)
#endif
		}
		datardyf = false;   // データ揃ったよフラグをクリア
	}
}
```
これをXIAO RP2040に焼いて下さい。
ポートを指定して、下の[→]アイコンをクリックすれば焼けます。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/89d626d0-39ea-f8e4-da6e-baddfaee8387.jpeg">
## ExpressLRS Configuratorでファームウェア書き換え
ExpressLRS Configuratorを起動して、受信機の種類とバージョンを設定し、
Flashing Method は [UART]ではなく **[BetaflightPassthough]** にします。
**WIFIをオフ**にして、**BINDING_PHRASE**を送信機と同じにします。
ポートを確認し、[BUILD＆FLASH]を押せば、書き換えられるはずです。
もしエラーが出る場合は、受信機を書き換えモードにする必要があるかもしれません。
（ちなみに私のHappyModel EP 2400 RXは、何もせずに書き込みできました。
ExpressLRS Configurator v1.5.9、firmware v3.2.1でした。）
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/04af418b-f398-cccc-c868-6546c106d30b.jpeg">
## バインド方法
ExpressLRS Configuratorで**BINDING_PHRASE**を指定した場合は、**自動でバインド**するので、以下の作業は必要ありません。

① USBドングルのUSBを3回抜き差しするとバインドモードに入ります。（LED２回点滅の繰り返しになります）
② プロポをONにしてELRS送信モジュールのバインドボタンを短く押すか、　
　　EdgeTXのExpressLRS Luaスクリプトの[BIND]を押します。
③ 受信機のLEDが点灯に変わればバインド完了です。
## キャリブレーションをします
Windows11で説明します。
画面下の[スタート]を右クリック
[設定]→左側の[Bluethoothとデバイス]→[デバイス]→ずっと下にスクロールして[その他のデバイスとプリンタの設定]→[XIAO RP2040]を右クリック
[ゲームコントローラの設定]→[プロパティ]→[設定]→[調整]
プロポのスティックを中央にセットしてから、あとは指示に従いグルグルすれば完成です。

## デバッグ方法
シリアルモニターで受信データの確認をするには、
プログラムの最初の方にある #define DEBUG の //コメントアウトを外して書き込んでください。
```main.cpp
#define DEBUG	// <---受信データをシリアルモニターに表示させたい時は、コメントアウトを外してください。

```
PlatformIO画面の下の**コンセントアイコン**を押せば、シリアルモニターが起動します。
CRSFフレームの受信間隔時間も確認できます。
　どんどんスクロールして確認しづらいときは、プロポの電源をオフれば表示が止まるので、それまでのデータを確認できます。
　あなたのプロポがELRSの速度について行ってない時は、同じデータが続けて送られてきます。
こちらに以前書きましたので参考までに。

https://qiita.com/kobatan/items/253f1614a8653a1dcb1a

以上です。