ESP32は、Wi-FiやBluetoothだけでなく、さまざまな周辺機器と高速に通信できるSPI(Serial Peripheral Interface)を搭載しています。
SPIは、液晶ディスプレイやタッチパネル、センサー、外部Flashメモリなど、多くのデバイスで採用されている通信方式です。
ESP32で開発を始めると、
- SPIとはどのような通信方式なの?
- I²CやUARTとは何が違うの?
- ESP32ではどのSPIを使えばよいの?
といった疑問を持つ方も多いでしょう。
この記事では、SPIの基本から、ESP32で使用する際のポイントや設計時の注意点まで、実務目線で分かりやすく解説します。
SPIとは?
SPI(Serial Peripheral Interface)は、マイコンと周辺機器が高速にデータをやり取りするためのシリアル通信方式です。
UARTのように送受信を1本ずつ行う方式とは異なり、SPIは送信と受信を同時に行える全二重通信に対応しています。
そのため、高速なデータ転送が求められる用途に適しています。
代表的な接続先には、
- TFT液晶ディスプレイ
- タッチパネルコントローラ
- SDカード
- 外部Flashメモリ
- 高速A/D・D/Aコンバータ
などがあります。
SPIの4本の信号線
SPIでは、基本的に次の4本の信号線を使用します。
- MOSI(Master Out Slave In):マスターからスレーブへデータを送信
- MISO(Master In Slave Out):スレーブからマスターへデータを送信
- SCLK(Serial Clock):通信タイミングを合わせるクロック信号
- CS(Chip Select):通信するスレーブを選択する信号
UARTやI²Cよりも配線本数は増えますが、その分、高速で安定した通信が可能になります。
また、複数のSPIデバイスを接続する場合は、MOSI・MISO・SCLKを共通化し、各デバイスごとにCS信号を用意する構成が一般的です。
UART・I²Cとの違い
ESP32では、UART・I²C・SPIのいずれもよく利用されますが、それぞれ得意分野があります。
- UART:マイコン同士や通信モジュールとのシリアル通信
- I²C:センサーやRTCなど複数デバイスを少ない配線で接続
- SPI:高速通信が必要なディスプレイやメモリとの接続
用途に応じて通信方式を使い分けることで、システム全体の性能や配線効率を高めることができます。
ESP32でSPIがよく使われる理由
ESP32はCPU性能が高く、Wi-FiやBluetoothと並行して多くの周辺機器を制御できます。
そのため、高速なデータ転送が必要なデバイスとの接続には、SPIが選ばれることが少なくありません。
例えば、
- TFT液晶へ画像を高速描画する
- 外部Flashメモリへ大量のデータを保存する
- SDカードへログデータを書き込む
といった用途では、SPIの高速性が大きなメリットになります。
ESP32の性能を十分に引き出すためにも、SPIの特徴を理解して適切に活用することが重要です。
ESP32のSPIコントローラ
ESP32には複数のSPIコントローラが搭載されています。
ただし、すべてを自由に使用できるわけではありません。
一般的なESP32では、内部Flashメモリとの通信に使用されるSPIコントローラが存在するため、ユーザーアプリケーションでは主に SPI2(HSPI) や SPI3(VSPI) を利用します。
ESP-IDFではSPIバスを初期化し、必要に応じて複数のSPIデバイスを同じバスへ接続できます。
また、GPIOマトリクス機能により、多くのGPIOへSPI信号を割り当てられるため、基板設計の自由度も高くなっています。
※ ESP32シリーズ(ESP32、ESP32-S2、ESP32-S3、ESP32-C3、ESP32-C5など)ではSPIコントローラの構成や名称が異なるため、使用するデバイスの技術資料を確認してください。
今回使用する開発環境
この記事では、次の環境でSPI通信を確認します。
- ESP-IDF:v5.5系
- ターゲット:ESP32-C5
- 開発環境:Visual Studio Code
- 接続デバイス:W25Q32JV SPI Flash
- SPIモード:Mode 0
- SPIクロック:1MHz
今回のサンプルでは、外付けSPI FlashへJEDEC ID読み出しコマンドを送信し、メーカーID・メモリタイプ・容量IDを取得します。
ESP32-C5では、汎用SPIコントローラをSPIマスターとして使用できます。SPIバスに複数のデバイスを接続する場合、MOSI・MISO・SCLKは共有し、デバイスごとにCS信号を用意します。
ESP32-C5のSPIマスタードライバでは、同じバス上の各デバイスがMOSI・MISO・SCLKを共有し、それぞれ個別のCS信号で選択されます。
W25Q32JVとの接続回路
今回のサンプルでは、次のGPIOを使用します。
- MOSI:GPIO2
- MISO:GPIO3
- SCLK:GPIO4
- CS:GPIO5
接続は次のとおりです。
ESP32-C5 W25Q32JV
3.3V ───────────────────────── VCC
GND ───────────────────────── GND
GPIO2(MOSI)───────────────────── DI / IO0
GPIO3(MISO)───────────────────── DO / IO1
GPIO4(SCLK)───────────────────── CLK
GPIO5(CS)────────────────────── /CS
3.3V ── 10kΩ ─────────────────── /WP / IO2
3.3V ── 10kΩ ─────────────────── /HOLD / IO3
W25Q32JVのVCCとGNDの近くには、0.1µFのセラミックコンデンサを配置してください。
/WPと/HOLDはLowになると通常のSPI通信へ影響するため、今回の1bit SPI通信では10kΩ程度の抵抗で3.3Vへプルアップします。
GPIO番号は一例です。使用するESP32-C5基板の回路や、他の周辺機能との競合に合わせて変更してください。
W25Q32JVは3V系のSPI NOR Flashであり、MOSIに相当するDI/IO0、MISOに相当するDO/IO1、CLK、CSを使って通常のSPI通信を行えます。製品仕様は使用する型番のデータシートで確認してください。
ESP-IDFでSPIを使用する流れ
ESP-IDFでは、SPI通信は次のような流れで設定します。
- SPIバスを初期化する
- 接続するSPIデバイスを登録する
- 通信設定(クロック、モードなど)を行う
- データを送受信する
- 通信終了後に必要に応じてデバイスを解放する
このように、バスとデバイスを分けて管理する構成になっているため、複数のSPI機器を接続する場合でも柔軟に設計できます。
CMakeLists.txtの設定
SPIマスタードライバを使用するため、mainフォルダ内のCMakeLists.txtを次のように記述します。
idf_component_register( SRCS "main.c" INCLUDE_DIRS "." REQUIRES esp_driver_spi )
ESP-IDF v5系では、SPIマスタードライバはesp_driver_spiコンポーネントとして提供されています。
driver/spi_master.hはesp_driver_spiコンポーネントに含まれているため、CMakeLists.txtにはREQUIRES esp_driver_spiを指定します。
W25Q32JVのJEDEC IDを読み取るサンプルコード
以下は、ESP32-C5からW25Q32JVへJEDEC ID読み出しコマンド0x9Fを送信するサンプルです。
SPI Flashから3バイトの識別情報を受信し、シリアルモニターへ表示します。
#include <stdint.h>
#include <string.h>
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#define SPI_HOST_USED SPI2_HOST
#define SPI_MOSI_GPIO 2
#define SPI_MISO_GPIO 3
#define SPI_SCLK_GPIO 4
#define SPI_CS_GPIO 5
#define SPI_CLOCK_HZ 1000000
#define W25Q_JEDEC_ID_COMMAND 0x9F
static const char *TAG = "SPI_FLASH_SAMPLE";
/**
* W25Q32JVからJEDEC IDを読み取ります。
*
* jedec_id[0]:メーカーID
* jedec_id[1]:メモリタイプ
* jedec_id[2]:容量ID
*/
static esp_err_t w25q_read_jedec_id(
spi_device_handle_t flash_handle,
uint8_t jedec_id[3])
{
if ((flash_handle == NULL) || (jedec_id == NULL)) {
return ESP_ERR_INVALID_ARG;
}
/*
* 1バイト目に0x9Fを送信し、
* 続く3バイトでJEDEC IDを受信します。
*/
uint8_t tx_data[4] = {
W25Q_JEDEC_ID_COMMAND,
0x00,
0x00,
0x00
};
uint8_t rx_data[4] = {0};
spi_transaction_t transaction;
memset(&transaction, 0, sizeof(transaction));
transaction.length = sizeof(tx_data) * 8;
transaction.tx_buffer = tx_data;
transaction.rx_buffer = rx_data;
esp_err_t result = spi_device_transmit(
flash_handle,
&transaction);
if (result != ESP_OK) {
return result;
}
/*
* 最初の受信バイトはコマンド送信中のデータなので、
* 2バイト目以降をJEDEC IDとして使用します。
*/
jedec_id[0] = rx_data[1];
jedec_id[1] = rx_data[2];
jedec_id[2] = rx_data[3];
return ESP_OK;
}
void app_main(void)
{
/*
* SPIバスを設定します。
*/
spi_bus_config_t bus_config = {
.mosi_io_num = SPI_MOSI_GPIO,
.miso_io_num = SPI_MISO_GPIO,
.sclk_io_num = SPI_SCLK_GPIO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4,
};
ESP_ERROR_CHECK(
spi_bus_initialize(
SPI_HOST_USED,
&bus_config,
SPI_DMA_CH_AUTO));
/*
* W25Q32JVをSPIバスへ登録します。
*/
spi_device_interface_config_t device_config = {
.clock_speed_hz = SPI_CLOCK_HZ,
.mode = 0,
.spics_io_num = SPI_CS_GPIO,
.queue_size = 1,
};
spi_device_handle_t flash_handle = NULL;
ESP_ERROR_CHECK(
spi_bus_add_device(
SPI_HOST_USED,
&device_config,
&flash_handle));
/*
* JEDEC IDを読み取ります。
*/
uint8_t jedec_id[3] = {0};
ESP_ERROR_CHECK(
w25q_read_jedec_id(
flash_handle,
jedec_id));
ESP_LOGI(
TAG,
"Manufacturer ID : 0x%02X",
jedec_id[0]);
ESP_LOGI(
TAG,
"Memory Type : 0x%02X",
jedec_id[1]);
ESP_LOGI(
TAG,
"Capacity ID : 0x%02X",
jedec_id[2]);
/*
* 通信結果がすべて0xFFまたは0x00の場合は、
* 配線やCS、SPIモードを確認します。
*/
if (((jedec_id[0] == 0xFF) &&
(jedec_id[1] == 0xFF) &&
(jedec_id[2] == 0xFF)) ||
((jedec_id[0] == 0x00) &&
(jedec_id[1] == 0x00) &&
(jedec_id[2] == 0x00))) {
ESP_LOGW(
TAG,
"Invalid JEDEC ID. Check SPI wiring.");
}
}
このプログラムでは、SPIバスを初期化した後、W25Q32JVをSPIデバイスとして登録します。
その後、spi_device_transmit()を使用して、コマンド送信とデータ受信を1回のSPIトランザクションとして実行します。
ESP-IDFでは、まずspi_bus_initialize()でSPIバスを初期化し、その後spi_bus_add_device()で接続デバイスを登録します。SPIトランザクションは、CSをアクティブにしてデータを転送し、終了時にCSを非アクティブにする一連の処理として扱われます。
サンプルコードのポイント
SPIバスを初期化する
spi_bus_config_t bus_config = {
.mosi_io_num = GPIO_NUM_2,
.miso_io_num = GPIO_NUM_3,
.sclk_io_num = GPIO_NUM_4,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4,
};
ここでは、MOSI・MISO・SCLKに使用するGPIOを設定します。
今回のサンプルは通常の1bit SPI通信を使用するため、Quad SPI用のquadwp_io_numとquadhd_io_numには-1を設定します。
その後、次の関数でSPIバスを初期化します。
spi_bus_initialize( SPI2_HOST, &bus_config, SPI_DMA_CH_AUTO);
SPI_DMA_CH_AUTOを指定すると、SPIドライバが使用可能なDMAチャネルを自動的に割り当てます。
SPIデバイスを登録する
spi_device_interface_config_t device_config = {
.clock_speed_hz = 1000000,
.mode = 0,
.spics_io_num = GPIO_NUM_5,
.queue_size = 1,
};
ここでは、次の項目を設定しています。
- SPIクロック:1MHz
- SPIモード:Mode 0
- CS:GPIO5
- トランザクションキュー数:1
最初から高いクロックを設定せず、まずは1MHz程度の低い周波数で通信を確認することで、配線や設定の問題を切り分けやすくなります。
JEDEC IDコマンドを送信する
uint8_t tx_data[4] = { 0x9F, 0x00, 0x00, 0x00 };
0x9FはJEDEC IDを読み出すためのコマンドです。
コマンド送信後もクロックを継続して出力する必要があるため、後半の3バイトにはダミーデータとして0x00を設定しています。
SPIトランザクションを実行する
spi_transaction_t transaction = {
.length = 32,
.tx_buffer = tx_data,
.rx_buffer = rx_data,
};
spi_device_transmit(flash_handle, &transaction);
lengthはバイト数ではなくビット数で指定します。
今回は4バイトを送受信するため、32bitを指定しています。
SPIは全二重通信であるため、MOSIからデータを送信している間、MISOからも同時にデータを受信します。
受信データを取り出す
jedec_id[0] = rx_data[1];
jedec_id[1] = rx_data[2];
jedec_id[2] = rx_data[3];
rx_data[0]はコマンド0x9Fを送信している間に受信した値です。
実際のJEDEC IDは、コマンド送信後に受信した2バイト目以降へ格納されます。
ESP32-C5のSPIマスタードライバは全二重通信に対応しており、書き込みと読み出しを同時に行えます。spi_transaction_t::lengthは送信データ長をビット単位で指定します。
実行結果
プログラムを書き込み、シリアルモニターを開くと、次のようなログが表示されます。
I (310) SPI_FLASH_SAMPLE: Manufacturer ID : 0xEF
I (310) SPI_FLASH_SAMPLE: Memory Type : 0x40
I (310) SPI_FLASH_SAMPLE: Capacity ID : 0x16
W25Q32JVでは、一般的に次の値が取得されます。
- メーカーID:
0xEF - メモリタイプ:
0x40 - 容量ID:
0x16
使用するFlashメモリの型番やシリーズによって、取得される値は異なります。
実際の記事へ掲載する際は、可能であれば実機で取得したログへ差し替えてください。
正しいIDが読み取れない場合
JEDEC IDがすべて0xFFまたは0x00になる場合は、SPI通信が正常に成立していない可能性があります。
次の項目を確認してください。
- MOSIとMISOを逆に接続していないか
- SCLKが正しく接続されているか
- CSが正しいGPIOへ接続されているか
- ESP32-C5とW25Q32JVのGNDが共通になっているか
- W25Q32JVへ3.3Vが供給されているか
/WPと/HOLDがHighになっているか- SPIモードがMode 0になっているか
- SPIクロックが高すぎないか
ロジックアナライザを使用できる場合は、次の順番で波形を確認すると原因を切り分けやすくなります。
- CSがLowになる
- SCLKが出力される
- MOSIから
0x9Fが送信される - MISOから3バイトのIDが返される
- CSがHighへ戻る
まずは1MHz程度の低いクロックで通信を確認し、正常動作を確認してから必要に応じて速度を上げることをおすすめします。
SPI通信でよくあるトラブル
SPIは高速通信が可能な反面、設定ミスによる通信エラーも少なくありません。
実務で特に多い事例を紹介します。
1.SPIモードの設定ミス
SPIにはMode0〜Mode3の4種類があります。
接続するデバイスとESP32でモードが一致していないと、正常に通信できません。
データシートで対応モードを必ず確認しましょう。
2.クロック周波数が高すぎる
SPIクロックを高く設定しすぎると、
- 通信エラー
- データ化け
- ノイズの影響
が発生することがあります。
まずは低いクロックで動作確認を行い、問題がないことを確認してから速度を上げることをおすすめします。
3.CS信号の制御ミス
複数のSPIデバイスを接続する場合は、
一度に有効にするCSは必ず1つだけです。
複数のCSを同時にアクティブにすると、通信が正常に行えなくなる可能性があります。
4.配線長が長い
SPIはクロックが高速になるほど信号品質の影響を受けやすくなります。
基板設計では、
- SPI配線をできるだけ短くする
- クロック線を長く引き回さない
- GNDを十分確保する
ことが重要です。
実務での設計ポイント
私が組み込み機器を設計する際には、SPIを使用する場合に次の点を意識しています。
- 通信速度は必要以上に上げない
- 基板レイアウトを優先してGPIOを割り当てる
- 将来の機能追加を考慮してCS信号に余裕を持たせる
- 信号品質を考慮したパターン設計を行う
SPIは設定だけでなく、ハードウェア設計も通信品質に大きく影響します。
ソフトウェアとハードウェアの両方を意識することで、トラブルの少ないシステムを構築できます。
まとめ
SPIはESP32で高速なデータ通信を実現するための重要なインターフェースです。
UARTやI²Cと比べて配線は増えますが、高速通信や全二重通信といった大きなメリットがあります。
ESP32ではGPIOマトリクスにより柔軟なピン配置が可能であり、ディスプレイ、SDカード、外部メモリ、各種センサーなど、多くのデバイスとの接続に活用されています。
通信モードやクロック設定、CS信号の制御など、基本を理解して設計すれば、安定したSPI通信を実現できます。
ESP32を使った組み込み開発では、SPIを正しく理解することがシステム全体の性能向上につながります。
関連記事
ESP32 UART完全ガイド|UART0・UART1・UART2の違いと使い方を現役エンジニアが解説
ESP32 BLEとWi-Fiは同時に使える?現役エンジニアが仕組みと注意点を解説
ESP32 Deep Sleepとは?消費電力を大幅に削減する省電力機能を現役エンジニアが解説
技術は、経験から価値になる。
DLROW Design|現場エンジニア