ESP-IDFを使ってみる

UART通信

こ ちらにUARTのサンプルが幾つか公開されています。
なかでも興味深いのはこ ちらのサンプルです。
一般的にUART通信を行うタスクは、受信処理をノンブロッキング受信に設定して、送信と受信をタスク内部で交互に繰り返しますが、
ESP-IDFでは受信処理をブロッキング受信にしたままで、送信と受信のタ スクを分けることができ ます。
これができると、タスクはものすごくシンプルになります。

ESP32はUART0/UART1/UART2の3組のUARTを持っています。
ESP32のデータシートによると、それぞれのUARTは以下のGPIOにアサインされています。
但し、GPIO9、GPIO10はFlashに接続されていて、デフォルトでは使うことができません。

RX TX
UART0 GPIO03 GPIO01
UART1 GPIO9 GPIO10
UART2 GPIO16 GPIO17

こ ちらのサンプルでは、UART1のGPIOを以下の様に定義しています。
#define TXD_PIN (GPIO_NUM_4)
#define RXD_PIN (GPIO_NUM_5)

GPIO4とGPIO5をワイヤーケーブルで直結してビルドすると以下のようにUARTを使った送受信を行います。
I (337) app_start: Starting scheduler on CPU0
I (342) app_start: Starting scheduler on CPU1
I (342) main_task: Started on CPU0
I (352) main_task: Calling app_main()
I (352) TX_TASK: Wrote 11 bytes
I (352) main_task: Returned from app_main()
I (1352) RX_TASK: Read 11 bytes: 'Hello world'
I (1352) RX_TASK: 0x3ffb6f48   48 65 6c 6c 6f 20 77 6f  72 6c 64                 |Hello world|
I (2352) TX_TASK: Wrote 11 bytes
I (3352) RX_TASK: Read 11 bytes: 'Hello world'
I (3352) RX_TASK: 0x3ffb6f48   48 65 6c 6c 6f 20 77 6f  72 6c 64                 |Hello world|
I (4352) TX_TASK: Wrote 11 bytes
I (5352) RX_TASK: Read 11 bytes: 'Hello world'
I (5352) RX_TASK: 0x3ffb6f48   48 65 6c 6c 6f 20 77 6f  72 6c 64                 |Hello world|
I (6352) TX_TASK: Wrote 11 bytes
I (7352) RX_TASK: Read 11 bytes: 'Hello world'
I (7352) RX_TASK: 0x3ffb6f48   48 65 6c 6c 6f 20 77 6f  72 6c 64                 |Hello world|
I (8352) TX_TASK: Wrote 11 bytes



ESP-IDFのUARTドライバーは4線式のUART通信をサポートしています。
4線式のUART通信とは、TX/RX以外にRTS/CTSの信号を使ってハードウェアフロー制御を行う方式です。
ハードウェアフロー制御を行うことで、受信側のバッファーオーバーフローによる受信データの取りこぼしを防ぐことができます。
こ ちらのサンプルを一部変更して、ハードウェアフロー制御の動作を確認することができます。
uart_write_bytes()でカーネルのUART_INTR_TXFIFOに送信データを書き込み、 uart_wait_tx_done()でこのFIFOがEMPTYになるのを待ちます。
時間内にEMPTYにならないときはTIMEOUTとなる仕組みです。
#define TXD_PIN (GPIO_NUM_4)
#define RXD_PIN (GPIO_NUM_5)
#define RTS_PIN (GPIO_NUM_23)
#define CTS_PIN (GPIO_NUM_22)

void init(void) {
    const uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_CTS_RTS,
        .rx_flow_ctrl_thresh = 122,
        .source_clk = UART_SCLK_DEFAULT,
    };
    // We won't use a buffer for sending data.
    uart_driver_install(UART_NUM_1, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
    uart_param_config(UART_NUM_1, &uart_config);
    uart_set_pin(UART_NUM_1, TXD_PIN, RXD_PIN, RTS_PIN, CTS_PIN);
}

int sendData(const char* logName, const char* data)
{
    const int len = strlen(data);
    const int txBytes = uart_write_bytes(UART_NUM_1, data, len);
    ESP_LOGI(logName, "Wrote %d bytes", txBytes);
    esp_err_t err = uart_wait_tx_done(UART_NUM_1, 100);
    ESP_LOGI(logName, "uart_wait_tx_done=%d", err);
    if (err == ESP_ERR_TIMEOUT) {
        ESP_LOGE(logName, "TX TIMEOUT");
    }
    return txBytes;
}

GPIO4とGPIO5に加え、GPIO22とGPIO23をワイヤーケーブルで直結してビルドすると正常に送受信を行います。
GPIO22とGPIO23のワイヤーケーブルを外すと送信タイムアウトとなります。
GPIOのON/OFFで送受信をコントロールできるので、ソフトウェアフロー制御よりも簡単です。
I (4362) TX_TASK: Wrote 11 bytes
I (4362) TX_TASK: uart_wait_tx_done=0
I (5362) RX_TASK: Read 11 bytes: 'Hello world'
I (5362) RX_TASK: 0x3ffb6f48   48 65 6c 6c 6f 20 77 6f  72 6c 64                 |Hello world|
I (6362) TX_TASK: Wrote 11 bytes
I (6362) TX_TASK: uart_wait_tx_done=0
I (7362) RX_TASK: Read 11 bytes: 'Hello world'
I (7362) RX_TASK: 0x3ffb6f48   48 65 6c 6c 6f 20 77 6f  72 6c 64                 |Hello world|
I (8362) TX_TASK: Wrote 11 bytes
I (8362) TX_TASK: uart_wait_tx_done=0
I (9362) RX_TASK: Read 11 bytes: 'Hello world'
I (9362) RX_TASK: 0x3ffb6f48   48 65 6c 6c 6f 20 77 6f  72 6c 64                 |Hello world|
I (10362) TX_TASK: Wrote 11 bytes
I (11362) TX_TASK: uart_wait_tx_done=263
E (11362) TX_TASK: TX TIMEOUT
I (13362) TX_TASK: Wrote 11 bytes
I (14362) TX_TASK: uart_wait_tx_done=263
E (14362) TX_TASK: TX TIMEOUT



UART1とUART2を同時に使用するサンプルを作ってみました。
タスク起動時のパラメータでUARTの番号を渡しているので、送信タスクと受信タスクは1組だけで済みます。
GPIO4とGPIO16、GPIO5とGPIO17をワイヤーケーブルで直結すると、2つのタスク間で送受信を行います。
/* UART asynchronous example, that uses separate RX and TX tasks

         This example code is in the Public Domain (or CC0 licensed, at your option.)

         Unless required by applicable law or agreed to in writing, this
         software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
         CONDITIONS OF ANY KIND, either express or implied.
*/
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "driver/uart.h"
#include "string.h"
#include "driver/gpio.h"

static const int RX_BUF_SIZE = 1024;

#define UART1_TXD_PIN (GPIO_NUM_4)
#define UART1_RXD_PIN (GPIO_NUM_5)

#define UART2_TXD_PIN (GPIO_NUM_17)
#define UART2_RXD_PIN (GPIO_NUM_16)

#define TAG "MAIN"

void init_uart1(void) {
        const uart_config_t uart_config = {
                .baud_rate = 115200,
                .data_bits = UART_DATA_8_BITS,
                .parity = UART_PARITY_DISABLE,
                .stop_bits = UART_STOP_BITS_1,
                .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
                .source_clk = UART_SCLK_APB,
        };
        // We won't use a buffer for sending data.
        uart_driver_install(UART_NUM_1, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
        uart_param_config(UART_NUM_1, &uart_config);
        // U1RXD:GPIO9 U1TXD->SD_DATA2:GPIO10->SD_DATA3
        uart_set_pin(UART_NUM_1, UART1_TXD_PIN, UART1_RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}

void init_uart2(void) {
        const uart_config_t uart_config = {
                .baud_rate = 115200,
                .data_bits = UART_DATA_8_BITS,
                .parity = UART_PARITY_DISABLE,
                .stop_bits = UART_STOP_BITS_1,
                .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
                .source_clk = UART_SCLK_APB,
        };
        // We won't use a buffer for sending data.
        uart_driver_install(UART_NUM_2, RX_BUF_SIZE * 2, 0, 0, NULL, 0);
        uart_param_config(UART_NUM_2, &uart_config);
        // U2RXD:GPIO16 U2TXD:GPIO17
        uart_set_pin(UART_NUM_2, UART2_TXD_PIN, UART2_RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
        // Don't work
        //uart_set_pin(UART_NUM_2, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}

int sendData(uart_port_t port, char* data)
{
        const int len = strlen(data);
        const int txBytes = uart_write_bytes(port, data, len);
        ESP_LOGI(pcTaskGetName(NULL), "Wrote %d bytes", txBytes);
        return txBytes;
}

static void tx_uart(void *pvParameters)
{
        uart_port_t uart_num = (uart_port_t)pvParameters;
        ESP_LOGI(pcTaskGetName(NULL), "Start uart_num=%d", uart_num);
        char data[64];
        strcpy(data, "Hello world");
        while (1) {
                sendData(uart_num, "Hello world");
                vTaskDelay(2000 / portTICK_PERIOD_MS);
        }
}

static void rx_uart(void *pvParameters)
{
        uart_port_t uart_num = (uart_port_t)pvParameters;
        ESP_LOGI(pcTaskGetName(NULL), "Start uart_num=%d", uart_num);
        static const char *RX_TASK_TAG = "RX_TASK";
        esp_log_level_set(RX_TASK_TAG, ESP_LOG_INFO);
        uint8_t* data = (uint8_t*) malloc(RX_BUF_SIZE+1);
        while (1) {
                const int rxBytes = uart_read_bytes(uart_num, data, RX_BUF_SIZE, 1000 / portTICK_RATE_MS);
                if (rxBytes > 0) {
                        data[rxBytes] = 0;
                        ESP_LOGI(pcTaskGetName(NULL), "Read %d bytes: '%s'", rxBytes, data);
                        ESP_LOG_BUFFER_HEXDUMP(pcTaskGetName(NULL), data, rxBytes, ESP_LOG_INFO);
                }
        }
        free(data);
}

void app_main(void)
{
        ESP_LOGI(TAG, "UART_NUM_1=%d", UART_NUM_1);
        init_uart1();
        init_uart2();

        // UART1 受信処理
        xTaskCreate(rx_uart, "UART1_RX", 1024*2, (void *)UART_NUM_1, 4, NULL);
        // UART1 送信処理
        xTaskCreate(tx_uart, "UART1_TX", 1024*2, (void *)UART_NUM_1, 4, NULL);

        // UART2 受信処理
        xTaskCreate(rx_uart, "UART2_RX", 1024*2, (void *)UART_NUM_2, 4, NULL);
        // UART2 送信処理
        xTaskCreate(tx_uart, "UART2_TX", 1024*2, (void *)UART_NUM_2, 4, NULL);

        while(1) {
                vTaskDelay(1);
        }
}

UART2はデフォルトのGPIOのまま、UART_PIN_NO_CHANGEで使えると思っていましたが、動きませんでした。
uart_set_pin()で必ずTXD/RXDのGPIOを指定する必要が有ります。



UARTの数はSoCにより変わり、UART_NUM_MAX変数で知る事ができます。
ESP32、ESP32S3はUART_NUM_MAX=3なので、UART_NUM_0/UART_NUM_1/UART_NUM_2を使うこ とができます。
これ以外のSoCはUART_NUM_MAX=2なので、UART_NUM_0/UART_NUM_1を使うことができます。
いずれのSoCもUART_NUM_0は標準出力にアサインされているので、これをUART通信で使うとロギングが表示されなくなります。



こ ちらのサンプルではUARTを仮想ファイルシステム(/dev/uart/0)としてオープンしています。
Raspberry Piの様にUARTをファイルシステムとして扱うことができるようになります。
UARTをファイルシステムとして扱うことで、select関数によるノンブロッキングの読み取りが使えるようになります。
シリアル通信を行うLinuxのコードがそのまま動きます。



こ ちらのサンプルではUARTから受信したデータをQueue経由で受け取っています。
uart_driver_install()の5番目のパラメータにQueueのハンドルを渡すと、ドライバーがここにイベントを書き込みます。
イベントの種類はこ ちらのenum uart_event_type_tに定義されています。
受信だけを行うアプリでは、これが一番簡単な方法です。



ESP32/ESP32S3にはUARTが3組ありますが、UARTを4組以上使いたい場合は、SPI/I2C→UARTの変換チップが必要にな ります。、
こちらで SC16IS75Xのドライバーを公開しています。
SC16IS752を使うと、UARTを2ポート増設することができます。

このチップはUARTだけでなく、RS485をサポートしています。
RS485を使う場合、送信/受信に応じてトランシーバーのDE/RE信号を切り替える必要が有ります。
このチップにはRS485のDE/RE信号を自動的に切り替える機能が有ります。
この機能を有効にすると、チップが自動的にDE/RE信号を切り替えてくれるので、
アプリケーションはトランシーバーのDE/REを切り替える必要が無くなります。



ESP-IDF V5.1から、Dedicated GPIOと言う機能が追加されました。
こ ちらに、この機能を使ったSoftwareUARTのサンプルが公開されています。
Dedicated GPIOは、ESP32SシリーズとESP32Cシリーズで使用することができますが、なぜかESP32では使えません。
ESP32SシリーズとESP32Cシリーズでは、ハードウェアを追加することなく、UARTを1つ追加することができますが、
115200/230400/460800/921600 bpsしかサポートしていません。



esp-idfのLoggingライブラリには、「esp_log_set_vprintf」関数が含まれています。
デフォルトでは、ロギング出力はUART0に送られます。
この関数を使用すると、ロギング出力をファイルやネットワークなどの他の宛先にリダイレクトすることができます。
この機能を利用して、ロギングをネットワークにリダイレクトするツールを作りました。
標準では、ロギングを有効にしたまま、UART0を周辺機器との通信に使うと、UART0にロギングが出てしまうので、使い物になりません。
UART0への出力とロギングの出力を分離することが出来るので、ロギングを有効にしたまま、UART0を周辺機器との通信に使うことができま す。
ソースはこちらで 公開しています。
ロギングの出力には、udp/tcp/mqtt/httpのプロトコルを利用することができます。

続く...