ESP32 ソフトウェア 回路設計

ESP32 UART完全ガイド|UART0・UART1・UART2の違いと使い方を現役エンジニアが解説

更新日:

ESP32は、Wi-FiやBluetoothだけでなく、UART(Universal Asynchronous Receiver/Transmitter)を使ったシリアル通信にも優れたマイコンです。

センサーやGPSモジュール、RS-485変換器、USB-シリアル変換ICなど、多くの周辺機器と接続できるため、IoT機器や組み込みシステムでは欠かせない通信インターフェースとなっています。

しかし、ESP32を使い始めた方からは、次のような疑問をよく聞きます。

  • UART0・UART1・UART2の違いは?
  • デバッグ用のUARTとユーザーアプリで使うUARTは分けるべき?
  • GPIOは自由に変更できるの?
  • USBシリアル書き込みとUART通信は競合しない?

この記事では、ESP32のUARTの基本から、開発時・製品化時に知っておきたいポイントまで、実務目線で分かりやすく解説します。

UARTとは?

UARTは、マイコンと外部機器がシリアル通信を行うための代表的なインターフェースです。

SPIやI²Cとは異なり、基本的には

  • TX(送信)
  • RX(受信)

の2本の信号線で通信できるため、構成がシンプルで扱いやすいという特長があります。

ESP32では、デバッグログの出力だけでなく、

  • GNSS(GPS)モジュール
  • セルラーモデム
  • Bluetoothモジュール
  • RS-232/RS-485変換器
  • 他のマイコン

など、さまざまな機器との接続にUARTが利用されています。

ESP32には複数のUARTが搭載されている

ESP32には複数のUARTコントローラが搭載されています。

従来のESP32には、UART0、UART1、UART2の3つのUARTコントローラが搭載されています。

ただし、ESP32シリーズによってUARTコントローラの数や構成は異なります。

例えばESP32-C5には、UART0、UART1、LP UARTの3つが搭載されており、従来のESP32と同じUART2はありません。

そのため、使用するUARTポートを決める際は、対象となるESP32シリーズのデータシートを確認してください。。

このうちUART0は、多くの開発ボードでUSB-シリアル変換ICに接続されており、

  • プログラムの書き込み
  • シリアルモニターへのログ出力

に使用されることが一般的です。

そのため、UART0をユーザーアプリケーションで使用する場合は、書き込みやデバッグへの影響を考慮する必要があります。

一方、UART1やUART2は、外部機器との通信に利用されることが多く、センサーや通信モジュールとの接続に適しています。

GPIOは自由に割り当てられる

ESP32の大きな特長の一つが、GPIOマトリクス機能です。

UARTのTX・RX信号は、多くのGPIOへ割り当てることができるため、基板設計の自由度が高くなります。

例えば、回路レイアウトや他の周辺機能との兼ね合いに応じて、UARTの使用ピンを変更できるケースも少なくありません。

ただし、すべてのGPIOが同じように使えるわけではありません。

ブートストラップピンや入力専用ピンなど、用途に注意が必要なGPIOも存在するため、ピン配置はデータシートや技術資料を確認したうえで決定することが重要です。

UART0・UART1・UART2の使い分け

ESP32には複数のUARTが搭載されていますが、それぞれ役割を意識して使い分けることで、開発や保守がしやすくなります。

UART0

UART0は、多くのESP32開発ボードでUSB-シリアル変換ICと接続されています。

そのため、

  • プログラムの書き込み
  • ブートログの表示
  • デバッグメッセージの出力

に使用されることが一般的です。

製品開発では、UART0をそのままデバッグ専用として残しておくと、不具合解析やメンテナンスが容易になります。

UART1・UART2(従来のESP32)

UART1とUART2は、外部機器との通信に使用されることが多いUARTです。

例えば、

  • GNSS(GPS)モジュール
  • LTE/5G通信モジュール
  • RS-232・RS-485変換器
  • 他のマイコン
  • PLCや産業機器

などとの接続に適しています。

ESP32ではGPIOマトリクス機能により、多くのGPIOへTX・RXを割り当てられるため、基板設計の自由度が高いことも特徴です。

ESP32-C5では、外部機器との一般的なUART通信にはUART1を使用できます。

LP UARTは低消費電力用途を考慮したUARTであり、通常のUARTとは機能や使用方法に違いがあります。

従来のESP32向けプログラムでUART2を使用している場合、ESP32-C5へそのまま移植することはできないため、UART1などへの変更が必要です。

今回使用する開発環境

この記事では、次の環境でUART通信を確認します。

  • ESP-IDF:v5.5系
  • ターゲット:ESP32-C5
  • 開発環境:Visual Studio Code
  • 使用するUART:UART1
  • ボーレート:115200bps
  • データビット:8bit
  • パリティ:なし
  • ストップビット:1bit
  • フロー制御:なし

今回は、ESP32-C5のUART1から送信したデータを同じUART1で受信するループバック試験を行います。

TXとRXをジャンパ線で接続するだけなので、外付けのUARTモジュールやUSB−シリアル変換ICは必要ありません。

UARTループバック試験の接続

今回のサンプルでは、次のGPIOを使用します。

  • UART1 TX:GPIO6
  • UART1 RX:GPIO7

GPIO6とGPIO7をジャンパ線で接続します。

GPIO6(UART1 TX)──────── GPIO7(UART1 RX)

UARTのTXから送信されたデータが、そのままRXへ入力される構成です。

プログラムから文字列を送信し、同じ文字列を正常に受信できれば、UARTの初期化、GPIO設定、送信処理、受信処理が正しく動作していることを確認できます。

接続時の注意点

GPIO6とGPIO7は、ESP32-C5内部で接続されるわけではありません。必ず外部のジャンパ線で接続してください。

実際の外部機器と接続する場合は、次のようにクロス接続します。

ESP32-C5 TX ───────── 外部機器 RX 
ESP32-C5 RX ───────── 外部機器 TX 
ESP32-C5 GND ──────── 外部機器 GND

UART通信では、送信側のTXを受信側のRXへ接続します。また、両方の機器でGNDを共通にする必要があります。

ESP-IDFでUARTを使用する流れ

ESP-IDFでは、UARTは次のような手順で利用します。

  1. UARTの通信条件を設定する
  2. UARTドライバをインストールする
  3. TX・RXピンを設定する
  4. データの送受信を行う

このように、ESP-IDFではUARTドライバが用意されているため、レジスタを直接操作することなく実装できます。

通信速度やパリティ、ストップビットなどもAPIから設定できるため、さまざまな機器との接続に対応できます。

CMakeLists.txtの設定

UARTドライバを使用するため、mainフォルダ内のCMakeLists.txtを次のように記述します。

idf_component_register( SRCS "main.c" INCLUDE_DIRS "." REQUIRES esp_driver_uart )

ESP-IDF v5系では、UARTドライバはesp_driver_uartコンポーネントとして提供されています。

driver/uart.hを使用するコンポーネントでは、依存関係としてesp_driver_uartを指定できます。

UART1で送受信するサンプルコード

以下は、ESP32-C5のUART1から文字列を送信し、ループバック接続されたRXで同じ文字列を受信するサンプルです。

1秒ごとに文字列を送信し、受信したデータをシリアルモニターへ表示します。



#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "driver/uart.h"
#include "esp_err.h"
#include "esp_log.h"

#define APP_UART_PORT             UART_NUM_1

#define APP_UART_TX_GPIO          6
#define APP_UART_RX_GPIO          7

#define APP_UART_BAUD_RATE        115200
#define APP_UART_RX_BUFFER_SIZE   1024
#define APP_UART_READ_BUFFER_SIZE 128

#define APP_UART_READ_TIMEOUT_MS  500
#define APP_UART_TX_TIMEOUT_MS    1000
#define APP_UART_INTERVAL_MS      1000

static const char *TAG = "UART_SAMPLE";

/**
 * UART1を初期化します。
 */
static void uart_initialize(void)
{
    /*
     * UARTの通信条件を設定します。
     *
     * 115200bps
     * 8データビット
     * パリティなし
     * 1ストップビット
     * フロー制御なし
     */
    const uart_config_t uart_config = {
        .baud_rate = APP_UART_BAUD_RATE,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .rx_flow_ctrl_thresh = 0,
        .source_clk = UART_SCLK_DEFAULT,
        .flags = {
            .allow_pd = 0,
            .backup_before_sleep = 0,
        },
    };

    ESP_ERROR_CHECK(
        uart_param_config(
            APP_UART_PORT,
            &uart_config));

    /*
     * UART1のTXとRXをGPIOへ割り当てます。
     *
     * RTSとCTSは使用しないため、
     * UART_PIN_NO_CHANGEを指定します。
     */
    ESP_ERROR_CHECK(
        uart_set_pin(
            APP_UART_PORT,
            APP_UART_TX_GPIO,
            APP_UART_RX_GPIO,
            UART_PIN_NO_CHANGE,
            UART_PIN_NO_CHANGE));

    /*
     * UARTドライバをインストールします。
     *
     * RXバッファのみ確保します。
     * TXはブロッキング送信を使用するため、
     * TXバッファサイズを0にしています。
     *
     * イベントキューも使用しないため、
     * キューサイズを0にします。
     */
    ESP_ERROR_CHECK(
        uart_driver_install(
            APP_UART_PORT,
            APP_UART_RX_BUFFER_SIZE,
            0,
            0,
            NULL,
            0));
}

void app_main(void)
{
    uart_initialize();

    ESP_LOGI(
        TAG,
        "UART1 loopback test started");

    ESP_LOGI(
        TAG,
        "Connect GPIO%d TX to GPIO%d RX",
        APP_UART_TX_GPIO,
        APP_UART_RX_GPIO);

    uint32_t message_number = 0;
    uint8_t receive_buffer[APP_UART_READ_BUFFER_SIZE];

    while (1) {
        char transmit_buffer[64];

        int transmit_length = snprintf(
            transmit_buffer,
            sizeof(transmit_buffer),
            "Hello UART! Count=%lu\r\n",
            (unsigned long)message_number);

        if ((transmit_length < 0) ||
            (transmit_length >= (int)sizeof(transmit_buffer))) {

            ESP_LOGE(
                TAG,
                "Failed to create transmit message");

            vTaskDelay(
                pdMS_TO_TICKS(APP_UART_INTERVAL_MS));

            continue;
        }

        /*
         * 前回の通信で受信バッファにデータが残っていた場合に備え、
         * 送信前にRXバッファをクリアします。
         */
        ESP_ERROR_CHECK(
            uart_flush_input(APP_UART_PORT));

        /*
         * UART1から文字列を送信します。
         */
        int written_length = uart_write_bytes(
            APP_UART_PORT,
            transmit_buffer,
            transmit_length);

        if (written_length != transmit_length) {
            ESP_LOGE(
                TAG,
                "UART transmit failed: expected=%d, actual=%d",
                transmit_length,
                written_length);

            vTaskDelay(
                pdMS_TO_TICKS(APP_UART_INTERVAL_MS));

            continue;
        }

        /*
         * 送信FIFO内のデータがすべて送信されるまで待ちます。
         */
        esp_err_t wait_result = uart_wait_tx_done(
            APP_UART_PORT,
            pdMS_TO_TICKS(APP_UART_TX_TIMEOUT_MS));

        if (wait_result != ESP_OK) {
            ESP_LOGE(
                TAG,
                "UART transmit timeout: %s",
                esp_err_to_name(wait_result));

            vTaskDelay(
                pdMS_TO_TICKS(APP_UART_INTERVAL_MS));

            continue;
        }

        /*
         * ループバックされたデータをUART1のRXから読み取ります。
         *
         * 文字列終端用の1バイトを残すため、
         * バッファサイズから1を引いています。
         */
        int received_length = uart_read_bytes(
            APP_UART_PORT,
            receive_buffer,
            sizeof(receive_buffer) - 1,
            pdMS_TO_TICKS(APP_UART_READ_TIMEOUT_MS));

        if (received_length > 0) {
            receive_buffer[received_length] = '\0';

            ESP_LOGI(
                TAG,
                "Received %d bytes: %s",
                received_length,
                (char *)receive_buffer);

            /*
             * 送信内容と受信内容が同じか確認します。
             */
            if ((received_length == transmit_length) &&
                (memcmp(
                    receive_buffer,
                    transmit_buffer,
                    transmit_length) == 0)) {

                ESP_LOGI(
                    TAG,
                    "Loopback test passed");
            } else {
                ESP_LOGW(
                    TAG,
                    "Loopback data mismatch");
            }
        } else {
            ESP_LOGW(
                TAG,
                "No UART data received. Check the jumper wire");
        }

        message_number++;

        vTaskDelay(
            pdMS_TO_TICKS(APP_UART_INTERVAL_MS));
    }
}

このプログラムでは、uart_write_bytes()でUART1から文字列を送信し、uart_read_bytes()でループバックされたデータを受信します。

さらに、memcmp()を使って送信内容と受信内容を比較し、完全に一致した場合はループバック試験成功と判定します。

ESP-IDFのUARTドライバでは、通信条件の設定、GPIOの割り当て、ドライバのインストール、データの送受信という流れでUARTを使用します。

サンプルコードのポイント

UARTの通信条件を設定する

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_DEFAULT, 
};

この部分では、UARTの通信条件を設定しています。

今回の設定は一般的に「115200bps、8N1」と呼ばれるものです。

8N1は、次の設定を表します。

  • データビット:8bit
  • パリティ:なし(None)
  • ストップビット:1bit

UARTで接続する両方の機器は、基本的に同じ通信条件へ設定する必要があります。

TXとRXをGPIOへ割り当てる

uart_set_pin( UART_NUM_1, GPIO_NUM_6, GPIO_NUM_7, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

このサンプルでは、UART1の信号を次のGPIOへ割り当てています。

  • TX:GPIO6
  • RX:GPIO7

RTSとCTSによるハードウェアフロー制御は使用しないため、UART_PIN_NO_CHANGEを指定しています。

使用するGPIOは、基板回路や他の周辺機能との競合に合わせて変更できます。

UARTドライバをインストールする

uart_driver_install( UART_NUM_1, 1024, 0, 0, NULL, 0);

UARTドライバをインストールし、受信データを保持するためのリングバッファを確保します。

今回のサンプルでは、受信用に1024バイトのバッファを確保しています。

TX側はドライバ内の送信バッファを使用せず、送信FIFOへのブロッキング書き込みを行うため、TXバッファサイズを0にしています。

実行結果

GPIO6とGPIO7をジャンパ線で接続してプログラムを実行すると、シリアルモニターへ次のようなログが表示されます。

I (310) UART_SAMPLE: UART1 loopback test started 
I (310) UART_SAMPLE: Connect GPIO6 TX to GPIO7 RX 
I (320) UART_SAMPLE: Received 21 bytes: Hello UART! Count=0 
I (320) UART_SAMPLE: Loopback test passed 
I (1320) UART_SAMPLE: Received 21 bytes: Hello UART! Count=1 
I (1320) UART_SAMPLE: Loopback test passed 
I (2320) UART_SAMPLE: Received 21 bytes: Hello UART! Count=2 
I (2320) UART_SAMPLE: Loopback test passed

Loopback test passedと表示されれば、送信した文字列と受信した文字列が一致しています。

ジャンパ線を外すと、次のような警告が表示されます。

W (2820) UART_SAMPLE: No UART data received. Check the jumper wire

掲載しているログは表示例です。記事を公開する際は、可能であれば実機で取得したログへ差し替えてください。

外部UART機器と接続する場合の注意点

今回のループバック試験ではESP32-C5のTXとRXを直接接続していますが、実際に外部機器と接続する場合は信号電圧に注意が必要です。

ESP32-C5のGPIOは3.3V系のデジタル信号です。

そのため、3.3V UARTに対応した機器とは接続できますが、5V信号をそのままESP32-C5のRXへ入力してはいけません。

また、RS-232やRS-485はUARTと同じデータ形式を利用することがありますが、電気的な信号レベルは異なります。

  • 3.3V UART:基本的にGPIOへ直接接続できる
  • 5V UART:レベル変換回路が必要
  • RS-232:RS-232トランシーバが必要
  • RS-485:RS-485トランシーバが必要

UARTという名称だけを見て直接接続せず、接続する機器の信号電圧とインターフェース規格を確認してください。

UART利用時によくあるトラブル

UART通信で相談を受ける内容として、次のようなものがあります。

  • TXとRXを逆に接続している
  • ボーレートが一致していない
  • GPIOの割り当てミス
  • USBシリアルと外部UARTを混在させている
  • フロー制御の設定が一致していない

特にUART0は、書き込みやデバッグにも使用されるため、用途を十分に理解したうえで利用することが大切です。

通信ができない場合は、配線だけでなく、ソフトウェア側の設定も合わせて確認しましょう。

UARTから文字列を送信する

int written_length = uart_write_bytes( UART_NUM_1, transmit_buffer, transmit_length);

uart_write_bytes()を使用して、指定したバイト数のデータをUARTへ送信します。

戻り値には、UARTドライバへ書き込まれたデータのバイト数が返されます。

送信完了を待つ

uart_wait_tx_done( UART_NUM_1, pdMS_TO_TICKS(1000));

uart_write_bytes()の処理が完了しても、UARTの送信FIFOやシフトレジスタ内に送信途中のデータが残っている場合があります。

uart_wait_tx_done()を使うことで、UARTからすべてのデータが出力されるまで待機できます。

RS-485の送受信方向をGPIOで切り替える場合など、物理的な送信完了を待つ必要がある場面でも重要です。

UARTからデータを受信する

int received_length = uart_read_bytes( UART_NUM_1, receive_buffer, sizeof(receive_buffer) - 1, pdMS_TO_TICKS(500));

uart_read_bytes()は、UARTの受信リングバッファからデータを読み出します。

最後の引数には、受信を待つ最大時間をFreeRTOSのTick単位で指定します。

今回は500ms以内にデータを受信できなかった場合、タイムアウトとして処理します。

送信内容と受信内容を比較する

if ((received_length == transmit_length) && (memcmp( receive_buffer, transmit_buffer, transmit_length) == 0)) 
{ 
    ESP_LOGI( TAG, "Loopback test passed"); 
}

受信バイト数と送信バイト数が一致し、さらにデータ内容が完全に同じ場合に試験成功と判定します。

単にデータを受信できたかだけでなく、途中で文字化けや欠落が発生していないことも確認できます。

設計時のポイント

従来のESP32を使用する場合は、次のように役割を分ける方法があります。

  • UART0:書き込み・デバッグ
  • UART1:ユーザー機器との通信
  • UART2:予備、または追加機器との通信

ESP32-C5を使用する場合は、次の構成が考えられます。

  • UART0:書き込み・デバッグ
  • UART1:ユーザー機器との通信
  • LP UART:低消費電力機能が必要な場合に使用

ただし、ESP32-C5はUSBによる直接書き込みにも対応しているため、基板構成によってはUART0を別の用途へ割り当てることもできます。

開発時のログ出力方法、ファームウェアの書き込み方法、製品出荷後の保守方法を事前に整理したうえで、各UARTの役割を決めることが重要です。。

まとめ

ESP32には複数のUARTが搭載されており、GPIOマトリクス機能によって柔軟にピンを割り当てられます。

一方で、UART0は書き込みやデバッグにも使用されるため、用途を理解したうえで設計することが重要です。

UART1・UART2を外部機器との通信に活用し、それぞれの役割を明確に分けることで、開発効率や保守性を高めることができます。

ESP32は、Wi-FiやBluetoothだけでなく、有線シリアル通信にも優れたマイコンです。

UARTの特徴を理解し、用途に応じて使い分けることで、より安定したシステム設計につながるでしょう。

※本記事はESP32(オリジナル)を中心に説明しています。ESP32-C3、C5、S3ではUART数や利用可能なGPIOが異なるため、詳細は各データシートをご確認ください。

技術は、経験から価値になる。

DLROW Design

スポンサーリンク

スポンサーリンク

-ESP32, ソフトウェア, 回路設計

Copyright© 江藤良樹の仕事は物づくり , 2026 All Rights Reserved.