以前にArduino IDEで、ELRS USBドングルを作成する記事を書きましたが、
どうやらボードやライブラリーのバージョンアップのせいで、うまくコンパイルできなくなっている様なので、新たに作り直しました。
今回の開発環境は、[ VSCode + PlatformIO ] で行います。
PlatformIOならボードやライブラリのバージョンをプロジェクトごとに指定できるので、Arduino IDEの時のようにアップデートしたら動かなくトラブルを回避できると思ったからです。
用意するもの
① マイコンボード Seeed XIAO RP2040 ( スイッチサイエンス 979円)
② ELRS受信機 2.4GHz(何でもいいです)
③ 熱収縮チューブ
④ USB Type-C アダプタ(又はケーブル)
配線
プログラム
VSCodeとPlatformIO のインストールは既に済んでいる前提で行きます。
新規プロジェクトを作成します。下図のように設定して下さい。
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に下記をコピペして下さい。
右上にコピーアイコンがあるのでそれをクリックすれば、全コピーできます。
#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) // データを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(); // 1フレーム受信し終わったらデーコードする
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に焼いて下さい。
ポートを指定して、下の[→]アイコンをクリックすれば焼けます。
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でした。)
バインド方法
ExpressLRS ConfiguratorでBINDING_PHRASEを指定した場合は、自動でバインドするので、以下の作業は必要ありません。
① USBドングルのUSBを3回抜き差しするとバインドモードに入ります。(LED2回点滅の繰り返しになります)
② プロポをONにしてELRS送信モジュールのバインドボタンを短く押すか、
EdgeTXのExpressLRS Luaスクリプトの[BIND]を押します。
③ 受信機のLEDが点灯に変わればバインド完了です。
キャリブレーションをします
Windows11で説明します。
画面下の[スタート]を右クリック
[設定]→左側の[Bluethoothとデバイス]→[デバイス]→ずっと下にスクロールして[その他のデバイスとプリンタの設定]→[XIAO RP2040]を右クリック
[ゲームコントローラの設定]→[プロパティ]→[設定]→[調整]
プロポのスティックを中央にセットしてから、あとは指示に従いグルグルすれば完成です。
デバッグ方法
シリアルモニターで受信データの確認をするには、
プログラムの最初の方にある #define DEBUG の //コメントアウトを外して書き込んでください。
#define DEBUG // <---受信データをシリアルモニターに表示させたい時は、コメントアウトを外してください。
PlatformIO画面の下のコンセントアイコンを押せば、シリアルモニターが起動します。
CRSFフレームの受信間隔時間も確認できます。
どんどんスクロールして確認しづらいときは、プロポの電源をオフれば表示が止まるので、それまでのデータを確認できます。
あなたのプロポがELRSの速度について行ってない時は、同じデータが続けて送られてきます。
こちらに以前書きましたので参考までに。
以上です。