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は次のような手順で利用します。
- UARTの通信条件を設定する
- UARTドライバをインストールする
- TX・RXピンを設定する
- データの送受信を行う
このように、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