ESP32-S3のUSBをESP-IDFとarduinoモードでCDCを構成した覚書。V6になって少し変更があったのでダブりもありますが「ESP32-S3-1-N16R8を試してみる-USBIFでCDCデバイス」の続編です。
両方ともバックグラウンド受信してくれますが、ESP-IDFは受信イベントが使える代わりに、V6でtinyUSBが外付けになってコーディング量も多く、それに対しarduinoモードは受信イベントは無いけどシンプル記述できるので、両方同時に使えればよいな~、と思いましたが、現時点のesp32-s3-devkitc-1ではplatformio.iniにesp-idfとarduino同時記述はできず、また当然ですがesp-idfコードに<arduino.h>を追加してもエラーになるので、別々に確認した記事です。
ESP32-S3には、デバッグにも使える汎用USBがある
ESP32-S3は、GPIO43,44が書き込みやモニタで使うUART、GPIO19,20が汎用USBポートです。仮想シリアルにはパリティ/フレーミングエラーなどのデータリンク層レベルのエラーは存在せず、USBスタックですべて吸収するので、UARTよりも簡潔です。
PlatformIOのESP-IDFモードではUARTと同じように構造体を設定
PlatformIO版ESP-IDF V6以降(本家ESP-IDFではV5:これがややこしい)ではtinyUSBが非標準になったので、手動で付け加えます。ESP-IDFマニュアルサイトのサンプルを引っ張ってきてライブラリをフォルダにコピーするのがてっとり早いです。私がやった時には「components」フォルダを丸ごとコピーしました。
イニシャルはCDC-ACM構造体に値を設定し書き込み関数を呼び出す。下記は初期化と受信イベントのコード例です。受信は一文字とは限らないので受信割り込みではなくイベントと呼んでいます。
その他、仮想ハンドシェイク信号も扱えますが、私は使わないのでコメントアウトしてます。
#include <stdio.h>
#include <ctype.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "tinyusb.h"
#include "tusb_cdc_acm.h"
#include "sdkconfig.h"
#include "USB_CDC.h"
// USBCDC受信イベント
void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
size_t rx_size = 0;
static uint8_t buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1];
esp_err_t ret = tinyusb_cdcacm_read(itf, buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
// bufにrx_sizeバイト格納されている→個別の受信処理を記載
}
int dtr,rts,itfcpy;
// USBCDC ハンドシェイク変化割り込み
void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) {
dtr = event->line_state_changed_data.dtr;
rts = event->line_state_changed_data.rts;
itfcpy = itf;
ESP_LOGI(TAG, "Line state changed on channel %d: DTR:%d, RTS:%d", itf, dtr, rts);
}
void USB_init() {
const tinyusb_config_t tusb_cfg = {
.device_descriptor = NULL,
.string_descriptor = NULL,
.external_phy = false,
.configuration_descriptor = NULL,
};
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
tinyusb_config_cdcacm_t acm_cfg = {
.usb_dev = TINYUSB_USBDEV_0,
.cdc_port = TINYUSB_CDC_ACM_0,
.rx_unread_buf_sz = 64,
.callback_rx = &tinyusb_cdc_rx_callback, // the first way to register a callback
.callback_rx_wanted_char = NULL,
.callback_line_state_changed = NULL,
.callback_line_coding_changed = NULL
};
ESP_ERROR_CHECK(tusb_cdc_acm_init(&acm_cfg));
// ESP_ERROR_CHECK(tinyusb_cdcacm_register_callback( // RTS,DTR監視する場合
// TINYUSB_CDC_ACM_0,
// CDC_EVENT_LINE_STATE_CHANGED,
// &tinyusb_cdc_line_state_changed_callback));
}
static uint8_t trbuf[255]; // 送信バッファ
// 文字列送信
void USB_PutString(char *mess) {
size_t len = strlen(mess);
if((dtr==1) && (rts==1)) {
memcpy(trbuf,mess,len);
tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0,trbuf,len);
tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, 0);
}
}
上記は、初期化とイベントだけで、コマンド処理やタイムアウト処理は含まれてないので、自力で受信文字列から追加します。サンプル見本で冗長なコードはご容赦。
arduinoモードでは、もっとシンプル
たしか、arduinoIDEでMSDを組んだ時は、tinyUSBライブラリをプロジェクトに組み込んだ記憶がありますが、PlatformIOのarduinoモードでのCDCは標準ライブラリだけで使えます。
arduinoは基本的にはC++です。PlatformIOで.inoファイルをコンパイルする際、一時的に同名の.cppファイルが作られます。declareやexternなくてもフォルダ内を探索してくれるので楽といえば楽ですが、VScode+platformIOではintellicenceが赤いウニョウニョを引きまくるので、.inoではなく.cppにして、C/C++に沿ってヘッダファイルは作った方が良いと思っています。
シリアル関連については、まず、
Serial.begin(xxxx);
とやれば通常はUART0のモニタに接続しますが、
Serial.begin();
Serial1.begin(115200, SERIAL_8N1, 44, 43);
と、ポートNoなどを指定すれば、クラスSerialがUSB、クラスSerial1がUARTになります。
下記は、\n、\rで終わるコマンド受信をタイムアウト処理まで入れてみたコードです。バックグラウンド受信した文字列をポーリングで処理していますが、loop()内を受信専用タスクに移動してqueue等でメインタスクへ送れば、受信イベントと同等です。
#include <Arduino.h>
#define USB_CDC Serial
#define UART_SERIAL Serial1
#define TIMEOUT_MS 10000
#define CHAR_TIMEOUT_MS 500
// コマンド処理
void commandAnalize(const String& command) {
USB_CDC.printf("len:%d str:%s\r\n",command.length(),command.c_str()); // とりあえずエコー
}
void setup() {
USB_CDC.begin();
UART_SERIAL.begin(115200, SERIAL_8N1, 44, 43); // UART prog/monitor port
}
void loop() {
static String receivedCommand;
static unsigned long startTime = 0;
static unsigned long charReceiveTime = 0;
char c;
static int cnt=0;
if (USB_CDC.available() > 0) { // USB (CDCデバイス)からの受信
c = USB_CDC.read();
if ((c == '\n') || (c == '\r')) { // CR/LFを受信、コマンド処理
if(receivedCommand.length()>0) commandAnalize(receivedCommand); // 文字列長1以上
receivedCommand = "";
startTime = 0;
} else {
receivedCommand += c; // 受信文字を追加
charReceiveTime = millis();
}
}
// タイムアウト処理
if (receivedCommand.length() > 0) {
if (startTime == 0) {
startTime = millis();
} else if (millis() - startTime > TIMEOUT_MS) {
USB_CDC.println("TimeOut");
receivedCommand = ""; // バックグラウンドで受信した文字を破棄
startTime = 0;
}
}
// UART_SERIAL.printf("%d ",cnt++);
// 文字間の受信のタイムアウト処理
// if (receivedCommand.length() > 0 && millis() - charReceiveTime > CHAR_TIMEOUT_MS) {
// commandAnalize(receivedCommand);
// receivedCommand = "";
// startTime = 0;
// }
delay(10); // 10msループ
}
連続受信や、キーボード入力受信などの不連続文字受信どちらも対応できます。コメントアウトした部分で文字間のタイムアウトも入れることができます。
PlatformIOは変更ファイル以外は再コンパイルされず、ライブラリも改変しなければ再構築はしないので、arduinoIDEに比べて構築時間が短縮されている気がします(最新のarduinoIDEがそうなっていたらゴメンナサイ)。
PlatformIOのarduinoモード恐るべき!