ExpressLRS受信機をUSBドングルにする

## 500Hzの超高速リフレッシュレートで低遅延のデータがPCに送られます。
今までのUSBドングルは約150Hzぐらいなので500Hzは約３倍高速です。
2msごとにプロポからのデータが送られてきます。
受信機のデータをArduino互換機 Seeed XIAO RP2040を使ってPCと接続します。
### これでシミュレーターも高レスポンスでヌルヌル動く？
| <img width=200 alt="XIAO_RP2040.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/33cecbd9-311f-a362-e862-54190c7c3f92.jpeg"> | <img width="200" alt="ELRS_nano.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/62491db8-33e1-dd91-924b-40859288fe9e.jpeg"> | <img width="300" alt="送信機.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/81bcf88d-77c7-54ce-ef7d-42d8bd66baca.jpeg"> |
|:-:|:-:|:-:|
| Seeed XIAO RP2040  | ExpressLRS 2.4GHz受信機  | BETAFPV nano 2.4GHz送信機  |

#### 用意するもの
①　Arduino互換機　Seeed XIAO RP2040 (初期のXIAOでも良い）
②　ELRS 受信機 2.4GHz　(今回使用したのはHappymodel 2.4GHz ExpressLRS nano EP2 RXです)
③　電線　4本
④　熱収縮チューブ
⑤　USB Type-C ケーブル

#### 配線は下図のように４本繋ぎます。
5V ＜－－－＞ 5V
GND＜－－－＞ GND
RX ＜－－－＞ TX を間違えないように
TX ＜－－－＞ RX 
<img width="600" alt="配線図.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/f99a6a89-e889-dc4e-1b93-0e297a3429ed.jpeg">
ELRSはCrossfireプロトコルでの通信なので、SBUSのように信号反転回路が必要なく配線が楽です。信号線はどちらも3.3Vなのでそのまま接続するだけです。
プログラムもUARTのボーレートを420000にするだけで、簡単にELRSと通信できます。
またXIAOにAdafruitのArduino用TinyUSBライブラリをインストールするだけで、簡単にゲームパッドとして認識してくれるので、プログラムも簡単です。（出来てしまえば…仕組みを調べるのには苦労しました）
**※注意※**　初期のXIAOの場合はTinyUSBライブラリーのバージョンを**0.10.5**にする必要があります。最新のバージョンではコンパイルエラーが出ます。XIAO RP2040 なら最新バージョンでOKです。

<img width="200" alt="配線画像１.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/adf3e1d6-eb5d-b108-41b0-3bf4575dc755.jpeg"> <img width="200" alt="配線画像２.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/7add323d-71ed-1cd3-beb6-487a6cc9c69b.jpeg"><img width="400" alt="完成図１.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/40fc072a-e466-4ff9-f3ff-e33895fb1892.jpeg">
配線した後、熱収縮チューブで絶縁しました。

#### 完成形

<img width="200" alt="完成図２.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/bb6fc7c7-45c8-31fd-31a5-513c0ce249d4.jpeg">　<img width="200" alt="送信機.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/81bcf88d-77c7-54ce-ef7d-42d8bd66baca.jpeg">
シミュレータの＜VelociDrone＞や＜DCL the Game＞でちゃんと動作することを確認しています。

##### ＜ファームウェア―の書き換えにも対応しました＞
ExpressLRS Configurator の Flashing Method は [UART]ではなく [**BetaflightPassthough**] にすること。
ずっとUARTモードでやろうとしていて、どうしてもエラー出て、ボーレートを途中で変えたり試行錯誤した結果、もしやと思いBetaflightモードでやったらあっさり旨くいきました。

言い忘れていましたが、このELRS受信機はWifi出力機能を有しているんですが、技適が通っていないのでそのまま電源をいれて使っては違法になります。ですので、まず最初にファームウェアーを書き換えてWifiをOFFにする必要があります。
PCでExpressLRS Configuratorを起動しWifiをOFFにしファームウェアを書き替えてから使ってください。

##### ＜バインド方法＞
　① 今回作成したUSBドングルのUSBを3回抜き差しするとバインドモードに入ります。
　　（LED２回点滅の繰り返しになります）
　② プロポをONにしてELRS送信モジュールのバインドボタンを短く押すか、　
　　 OpenTX送信機ならExpressLRS Luaスクリプトの[BIND]ボタンを押します。
　③ 受信機のLEDが点灯に変わればバインド完了です。

##### ＜感想＞
私には他のUSBドングルとの速さの違いは感じることができませんでした。運動神経や動体視力のいい人には違いが判るのかな？
オシロスコープで信号を確認したところでは、明らかに早くなっているのですが・・・
そもそも私のPCモニターのリフレッシュレートが最大165Hzなので、それが限界なのか・・・

原因がなんとなく分かったので、[こちらに追加記事を書きました。](https://qiita.com/kobatan/items/253f1614a8653a1dcb1a)

#### Arduinoのプログラムは以下です。
（2022.10.8　以下を修正しました。）
① 最新ライブラリーではUSBDeviceが使えなくなっていたので、TinyUSBDeviceに変更。
② 出来るだけ処理速度を上げるため、5bitシフト処理をやめました。


```c++
#include "Adafruit_TinyUSB.h"
/* Adafruit_TinyUSB ライブラリは
 *  XIAO の場合 <Version 0.10.5> を使うこと。それ以上だとXIAOではコンパイルエラーが出ます。
 *  XIAO RP2040 は最新バージョンでOKです。
*/

//#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;

typedef struct crsf_header_s
{
    uint8_t device_addr; // from crsf_addr_e
    uint8_t frame_size;  // counts size after this byte, so it must be the payload size + 2 (type and crc)
    uint8_t type;        // from crsf_frame_type_e
    uint8_t data[0];
}crsf_header_t;

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

typedef struct gamepad_data{
  uint16_t ch[8];     // 16bit 8ch
  uint8_t sw;         // 1bit  8ch
}gp_t;

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

void setup()
{
  // USB HID デバイス設定
  usb_hid.setPollInterval(1);         // 1msポーリング（ELRS v3.0の1000Hzに対応）     
  usb_hid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report));
  usb_hid.begin();
  while( !TinyUSBDevice.mounted() ) delay(1);	// wait until device mounted

  datardyf = 0;
  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()
{ 
  // Remote wakeup
  if ( TinyUSBDevice.suspended() )
  {
    // Wake up host if we are in suspend mode
    // and REMOTE_WAKEUP feature is enabled by host
    TinyUSBDevice.remoteWakeup();
  }
  crsf();	// CRSF受信処理
  uart();	// UART通信処理(Firmware書き換え用)
  if(datardyf){     // データが揃ったらUSB送信
    if ( usb_hid.ready() ){
      usb_hid.sendReport(0, &gp, 17);  // 17 = sizeof(gp) コンパイルでsizeof()のサイズが変なので直接数値で指定
#ifdef DEBUG
      debug_out();  // デバッグ用 (シリアルモニターで数値を確認)
#endif
  	}
 	datardyf = 0;   // データ揃ったよフラグをクリア
  }
}

// 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目はフレームサイズ
	    }
		rxbuf[rxPos++] = data;  // 受信データをバッファに格納
		if (rxPos>1 && rxPos >= frameSize+2){
			crsfdecode();       // １フレーム受信し終わったらデーコードする
			rxPos = 0;
		}
	}
	else{
		if(rxPos>0 && micros()-gaptime>800){ // 800us以上データが来なかったら区切りと判定
			rxPos = 0;
		}
	}
}

// CRSFから受信した11bitシリアルデータを16bitデータにデコード
void crsfdecode () {
  if(rxbuf[0] == CRSF_ADDRESS_FLIGHT_CONTROLLER){       // ヘッダチェック
    if(rxbuf[2] == CRSF_FRAMETYPE_RC_CHANNELS_PACKED){  // CHデータならデコード
      gp.sw = 0; 
      gp.ch[0] =(rxbuf[3] | rxbuf[4]<<8) & 0x07ff;
      gp.ch[1] =(rxbuf[4]>>3 | rxbuf[5]<<5) & 0x07ff;
      gp.ch[2] =(rxbuf[5]>>6 | rxbuf[6]<<2 | rxbuf[7] <<10) & 0x07ff;
      gp.ch[3] =(rxbuf[7]>>1 | rxbuf[8]<<7) & 0x07ff;
      if( ((rxbuf[8]>>4 | rxbuf[9]<<4) & 0x07ff) > 0x3ff )  gp.sw |= 0x01;  // AUX1は2値データ
      gp.ch[4] =(rxbuf[9]>>7 | rxbuf[10]<<1 | rxbuf[11]<<9) & 0x07ff;
      gp.ch[5] =(rxbuf[11]>>2 | rxbuf[12]<<6) & 0x07ff;
      gp.ch[6] =(rxbuf[12]>>5 | rxbuf[13]<<3) & 0x07ff;
      gp.ch[7] =(rxbuf[14] | rxbuf[15]<<8) & 0x7ff;
      if( ((rxbuf[15]>>3 | rxbuf[16]<<5) & 0x07ff) > 0x3ff )                 gp.sw |= 0x02;
      if( ((rxbuf[16]>>6 | rxbuf[17]<<2 | rxbuf[18]<<10) & 0x07ff) > 0x3ff ) gp.sw |= 0x04;
      if( ((rxbuf[18]>>1 | rxbuf[19]<<7) & 0x07ff) > 0x3ff )                 gp.sw |= 0x08;
      if( ((rxbuf[19]>>4 | rxbuf[20]<<4) & 0x07ff) > 0x3ff )                 gp.sw |= 0x10;
      if( ((rxbuf[20]>>7 | rxbuf[21]<<1 | rxbuf[22]<<9) & 0x07ff) > 0x3ff )  gp.sw |= 0x20;
      if( ((rxbuf[22]>>2 | rxbuf[23]<<6) & 0x07ff) > 0x3ff )                 gp.sw |= 0x40;
      if( ((rxbuf[23]>>5 | rxbuf[24]<<3) & 0x07ff) > 0x3ff )                 gp.sw |= 0x80; 

      datardyf = 1; // データ揃ったよフラグ
    }
  }
}

// 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();
}
```
#### Arduino IDE の設定
すでにArduino IDE はインストール済みの前提で進めます。

Arduino IDE を起動します。
ボード Seeeduino XIAO が使えるように準備します。
[ファイル]➡[環境設定]を選択し、追加のボードマネージャーのURL：と言う項目に、
<img width="400"　alt="Arduino設定１.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/7e92a060-2741-6a9f-c1f2-b3484f066608.jpeg">
https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json
というURLを1行追加しす。
これで、Seeeduinoシリーズのボードが次のボード検索で出て来るようになります。

[ツール]➡[ボード]➡[ボードマネージャ]を選択し
検索欄に "XIAO" と入力すると,候補が出で来るので該当のボードをインストールする
<img width="400"　alt="Arduino設定２.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/8a3d2c12-7709-9cf4-a297-c8b4483346e3.jpeg">
ただのXIAOなら Seeed SAMD Boards の方、
XIAO RP2040 なら　Seeed XIAO RP2040 の方をインストールします。
<font color="Red">__（2022.10.8 追記）※注意　最新バージョンの2.7.2は、XIAO RP2040ではうまく動かないので、1.12.0バージョンを指定してインストールして下さい。__</font>

次にライブラリーをインストールします。
[ツール]➡[ライブラリを管理]を選択しライブラリマネージャウィンドウを出します。
[スケッチ]➡[ライブラリをインストール]➡[ライブラリを管理]からも行けます。
<img width="400"　alt="Arduino設定７.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/838fab81-ded4-d621-2e5d-c83f07003cee.jpeg">
検索欄に"TinyUSB"と入力して、出てきたAdafruit TinyUSB Library をインストールします。
<img width="400"　alt="Arduino設定８.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/e3a85502-0cbf-793b-c06e-7c595068d5f6.jpeg">
この時、ただのXIAOの場合はバージョンを選択し0.10.5を必ず選択してください。最新バージョンではコンパイルエラーになります。
XIAO RP2040の場合はボードをインストールした段階でTinyUSBライブリーが自動でインストールされる見たいなので、あえてインストールする必要はないかもしれません。

次に書き込みボード設定をします。
[ツール]➡[ボード]
XIAO RP2040の場合は [Seeed RP2040 Board]➡[Seeed XIAO RP2040] を選択
ただのXIAOの場合は [Seeed SAMD]➡[Seeeduino XIAO]を選択
<img width="600"　alt="Arduino設定３.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/8cb075b8-efc7-5a96-f3c7-98a324fa2b2a.jpeg">
<img width="600"　alt="Arduino設定４.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/80985083-96af-0101-6b13-8b038a18dd82.jpeg">
USBスタック設定
[ツール]➡[USB Stack]➡[Adafruit TinyUSB]を選択
<img width="400"　alt="Arduino設定５.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/1066c688-e849-818a-54c0-c06877df7150.jpeg">
シリアルポート設定
[ツール]➡[シリアルポート]
USBドングルをPCに接続して認識されるとポート名にSeeed XIAOという名前が出て来ると思うのでそれを指定します。
<img width="400"　alt="Arduino設定６.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/4cf0fc22-b3e1-42eb-7cff-ad6fa7182c89.jpeg">
以上でArduinoの設定は終わりです。
あとはプログラムを書き込んで、[マイコンボードに書き込む]➡印ボタンを押して書き込めば完成です。

#### 最後にキャリブレーションをしましょう
Windowsの場合で説明します。
[スタート]➡[設定]➡[デバイス]➡[デバイスとプリンター]をクリックすると、
<img width="400" alt="キャリブレーション１.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/bf4ceef7-08e0-198f-a6d5-3c6f176d8857.jpeg">
ゲームパッド型のアイコンがあると思うので、それを右クリックして
[ゲームコントローラーの設定]➡[プロパティ]➡[設定]➡[調整]➡[次へ]
を押し、プロポのスティックを全て中央にしたら、後は指示通りにすればキャリブレーション完了です。お疲れ様でした。
<img width="300" alt="キャリブレーション.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/6298e748-b266-520b-6639-338fedc5c6c8.jpeg">