SPI通信を使っていると、
「1台のESP32に複数のSPIデバイスを接続したい」
という場面が出てきます。
例えば、
- SPI接続のセンサー
- ADC
- Flash ROM
- エンコーダ
- ディスプレイ
などを同じESP32から使用するケースです。
SPIではSCLK・MOSI・MISOを複数のデバイスで共有できるため、GPIOを節約しながら複数のデバイスを接続できます。
ただし、単純にすべての信号を並列接続すればよいわけではありません。
今回はESP-IDFを使って、複数のSPIデバイスを1つのSPIバスへ接続する方法を実装してみます。
複数のSPIデバイスはどう接続する?
SPIで複数のデバイスを接続する場合、
- SCLK
- MOSI
- MISO
は基本的に共有できます。
一方、CS(Chip Select)はデバイスごとに個別に用意します。
例えば、2台のSPIデバイスを接続する場合は次のようになります。
┌──────────── Device A
│ CS_A
│
ESP32 ── SCLK ──┼──────────── SCLK
── MOSI ──┼──────────── MOSI
── MISO ──┼──────────── MISO
│
└──────────── Device B
CS_B
ESP32 ── CS_A ─────────────── Device A
ESP32 ── CS_B ─────────────── Device B
通信したいデバイスのCSだけをアクティブにすることで、同じSPIバス上の通信相手を選択します。
つまり、
SPIバスは共有、CSは個別
という構成になります。
ESP-IDFでは「Bus」と「Device」を分けて考える
複数デバイスを扱う場合、ESP-IDFのSPI Master Driverの構造を理解すると実装しやすくなります。
ESP-IDFでは、
SPI Bus
│
├── Device A
│
├── Device B
│
└── Device C
という考え方で管理します。
まずspi_bus_initialize()でSPIバスを1回だけ初期化します。
その後、接続するデバイスごとにspi_bus_add_device()を実行します。
ここが、単一デバイスだけを使う場合との大きな違いです。
SPIバスは1回だけ初期化する
まず、複数デバイスで共有するSPIバスを作ります。
今回の例では、
- MOSI:GPIO4
- MISO:GPIO5
- SCLK:GPIO6
とします。
CSは後からデバイスごとに設定します。
2台のSPIデバイスを登録するサンプルコード
ここまでの内容を、実際に使用できるコードとしてまとめます。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_err.h"
#include "esp_log.h"
#define SPI_HOST SPI2_HOST
#define PIN_NUM_MOSI GPIO_NUM_4
#define PIN_NUM_MISO GPIO_NUM_5
#define PIN_NUM_CLK GPIO_NUM_6
#define PIN_NUM_CS_A GPIO_NUM_7
#define PIN_NUM_CS_B GPIO_NUM_8
static const char *TAG = "SPI_MULTI";
static spi_device_handle_t device_a_handle;
static spi_device_handle_t device_b_handle;
void app_main(void)
{
spi_bus_config_t bus_config = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = PIN_NUM_MISO,
.sclk_io_num = PIN_NUM_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
ESP_ERROR_CHECK(
spi_bus_initialize(
SPI_HOST,
&bus_config,
SPI_DMA_CH_AUTO));
spi_device_interface_config_t device_a_config = {
.clock_speed_hz = 1 * 1000 * 1000,
.mode = 0,
.spics_io_num = PIN_NUM_CS_A,
.queue_size = 1,
};
ESP_ERROR_CHECK(
spi_bus_add_device(
SPI_HOST,
&device_a_config,
&device_a_handle));
spi_device_interface_config_t device_b_config = {
.clock_speed_hz = 5 * 1000 * 1000,
.mode = 3,
.spics_io_num = PIN_NUM_CS_B,
.queue_size = 1,
};
ESP_ERROR_CHECK(
spi_bus_add_device(
SPI_HOST,
&device_b_config,
&device_b_handle));
ESP_LOGI(TAG, "SPI bus initialized");
ESP_LOGI(TAG, "Device A registered");
ESP_LOGI(TAG, "Device B registered");
}
このコードではまだ実際のデータ送受信は行っていません。
1つのSPIバスへ、異なる通信条件を持つ2台のデバイスを登録するところまでを実装しています。
SPIバスは1回だけ初期化する
まず、複数デバイスで共有するSPIバスを作ります。
今回の例では、
- MOSI:GPIO4
- MISO:GPIO5
- SCLK:GPIO6
とします。
CSは後からデバイスごとに設定します。
spi_bus_config_t bus_config = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = PIN_NUM_MISO,
.sclk_io_num = PIN_NUM_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
ESP_ERROR_CHECK(
spi_bus_initialize(
SPI_HOST,
&bus_config,
SPI_DMA_CH_AUTO));
ここで重要なのは、CSをspi_bus_config_tでは設定していないことです。
CSはバスそのものではなく、各デバイスの設定として登録します。
Device Aを登録する
まず1台目のデバイスを登録します。
Device AではCSとしてGPIO7を使用します。
spi_device_interface_config_t device_a_config = {
.clock_speed_hz = 1 * 1000 * 1000,
.mode = 0,
.spics_io_num = PIN_NUM_CS_A,
.queue_size = 1,
};
ESP_ERROR_CHECK(
spi_bus_add_device(
SPI_HOST,
&device_a_config,
&device_a_handle));
spi_bus_add_device()を実行すると、Device A専用のハンドルが取得できます。
以降は、このdevice_a_handleを指定して通信します。
Device Bを同じSPIバスへ追加する
2台目も同じ方法で登録します。
今回はDevice BのCSをGPIO8とします。
spi_device_interface_config_t device_b_config = {
.clock_speed_hz = 5 * 1000 * 1000,
.mode = 3,
.spics_io_num = PIN_NUM_CS_B,
.queue_size = 1,
};
ESP_ERROR_CHECK(
spi_bus_add_device(
SPI_HOST,
&device_b_config,
&device_b_handle));
これで、
SPI2_HOST
│
├── Device A → CS GPIO7
│
└── Device B → CS GPIO8
という構成になります。
デバイスごとに通信条件を変更できる
ここがSPIを複数デバイスで使用するときの重要なポイントです。
spi_device_interface_config_tはデバイスごとに設定できます。
例えばDevice Aを、
.clock_speed_hz = 1 * 1000 * 1000,
.mode = 0,
としても、
Device Bは、
.clock_speed_hz = 5 * 1000 * 1000,
.mode = 3,
と設定できます。
つまり同じSPIバスを共有していても、
| 項目 | Device A | Device B |
|---|---|---|
| SPI Clock | 1MHz | 5MHz |
| SPI Mode | Mode 0 | Mode 3 |
| CS | GPIO7 | GPIO8 |
という構成が可能です。
これは実際の製品設計で非常に便利です。
接続するICによって、
- 最大SPIクロック
- CPOL
- CPHA
などの仕様が異なるためです。
CSはESP-IDFが自動制御する
ここも実装するときに疑問になりやすいところです。
spics_io_numへGPIOを設定した場合、通常はアプリケーション側で、
gpio_set_level(CS_A, 0);
spi_device_transmit(...);
gpio_set_level(CS_A, 1);
のようにCSを手動操作する必要はありません。
spi_device_transmit()で対象のデバイスハンドルを指定すると、SPI Master Driverが対応するCSを制御します。
例えば、
spi_device_transmit(
device_a_handle,
&transaction);
ならDevice AのCS、
spi_device_transmit(
device_b_handle,
&transaction);
ならDevice BのCSが使用されます。
この仕組みを利用すると、複数のSPIデバイスを比較的シンプルに管理できます。
完成サンプルコード
こまでの処理を1つのmain.cにまとめます。
前半で説明したSPIバス初期化とDevice登録に、Device A / Bへの送信処理を追加したコードです。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_err.h"
#include "esp_log.h"
#define SPI_HOST SPI2_HOST
#define PIN_NUM_MOSI GPIO_NUM_4
#define PIN_NUM_MISO GPIO_NUM_5
#define PIN_NUM_CLK GPIO_NUM_6
#define PIN_NUM_CS_A GPIO_NUM_7
#define PIN_NUM_CS_B GPIO_NUM_8
static const char *TAG = "SPI_MULTI";
static spi_device_handle_t device_a_handle;
static spi_device_handle_t device_b_handle;
static void transmit_device_a(void)
{
uint8_t tx_data[] = {
0x10,
0x20,
0x30,
0x40
};
spi_transaction_t transaction = {
.length = sizeof(tx_data) * 8,
.tx_buffer = tx_data,
};
ESP_ERROR_CHECK(
spi_device_transmit(
device_a_handle,
&transaction));
ESP_LOGI(TAG, "Device A transmitted");
}
static void transmit_device_b(void)
{
uint8_t tx_data[] = {
0xAA,
0xBB,
0xCC,
0xDD
};
spi_transaction_t transaction = {
.length = sizeof(tx_data) * 8,
.tx_buffer = tx_data,
};
ESP_ERROR_CHECK(
spi_device_transmit(
device_b_handle,
&transaction));
ESP_LOGI(TAG, "Device B transmitted");
}
void app_main(void)
{
spi_bus_config_t bus_config = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = PIN_NUM_MISO,
.sclk_io_num = PIN_NUM_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
ESP_ERROR_CHECK(
spi_bus_initialize(
SPI_HOST,
&bus_config,
SPI_DMA_CH_AUTO));
spi_device_interface_config_t device_a_config = {
.clock_speed_hz = 1 * 1000 * 1000,
.mode = 0,
.spics_io_num = PIN_NUM_CS_A,
.queue_size = 1,
};
ESP_ERROR_CHECK(
spi_bus_add_device(
SPI_HOST,
&device_a_config,
&device_a_handle));
spi_device_interface_config_t device_b_config = {
.clock_speed_hz = 5 * 1000 * 1000,
.mode = 3,
.spics_io_num = PIN_NUM_CS_B,
.queue_size = 1,
};
ESP_ERROR_CHECK(
spi_bus_add_device(
SPI_HOST,
&device_b_config,
&device_b_handle));
ESP_LOGI(TAG, "SPI bus initialized");
ESP_LOGI(TAG, "Device A registered");
ESP_LOGI(TAG, "Device B registered");
while (1)
{
transmit_device_a();
vTaskDelay(pdMS_TO_TICKS(500));
transmit_device_b();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
このサンプルでは実際のICを指定せず、複数SPIデバイスを扱う基本構造に絞っています。
そのため、実際のセンサーやFlash ROMなどを接続する場合は、それぞれのICが要求するコマンド形式にtx_dataを変更してください。
Device Aへデータを送信する
まず、Device Aへ4byteのデータを送信します。
ESP-IDFではspi_transaction_tに送信内容を設定し、spi_device_transmit()で通信を実行します。
static void transmit_device_a(void)
{
uint8_t tx_data[] = {
0x10,
0x20,
0x30,
0x40
};
spi_transaction_t transaction = {
.length = sizeof(tx_data) * 8,
.tx_buffer = tx_data,
};
ESP_ERROR_CHECK(
spi_device_transmit(
device_a_handle,
&transaction));
}
.lengthには送信するデータ長をbit単位で指定します。
そのため、4byteのデータを送信する場合は、
sizeof(tx_data) * 8
となります。
.tx_bufferには実際に送信するデータへのポインタを設定します。
そしてspi_device_transmit()へdevice_a_handleを渡すことで、Device Aに設定したCSが使用されます。
Device Bへデータを送信する
Device Bについても基本的な処理は同じです。
違うのは、spi_device_transmit()へ渡すDeviceハンドルです。
static void transmit_device_b(void)
{
uint8_t tx_data[] = {
0xAA,
0xBB,
0xCC,
0xDD
};
spi_transaction_t transaction = {
.length = sizeof(tx_data) * 8,
.tx_buffer = tx_data,
};
ESP_ERROR_CHECK(
spi_device_transmit(
device_b_handle,
&transaction));
}
Device AとDevice Bは同じSCLK・MOSI・MISOを共有しています。
しかし、
spi_device_transmit(
device_b_handle,
&transaction)
とすることで、Device Bとして登録した通信条件で通信できます。
前半ではDevice Aを1MHz / Mode 0、Device Bを5MHz / Mode 3として登録しているため、同じSPIバスでもそれぞれ異なる条件で通信できます。
2つのDeviceを順番に通信する
それでは、Device AとDevice Bを交互に通信してみます。
while (1)
{
transmit_device_a();
vTaskDelay(pdMS_TO_TICKS(500));
transmit_device_b();
vTaskDelay(pdMS_TO_TICKS(500));
}
処理としては非常にシンプルです。
Device A
↓
500ms
↓
Device B
↓
500ms
↓
Device A
↓
・・・
という通信を繰り返します。
transmit_device_a()ではdevice_a_handleを使用し、transmit_device_b()ではdevice_b_handleを使用します。
アプリケーション側では「どのCSをLOWにするか」ではなく、**「どのDeviceハンドルへ通信するか」**を指定すればよいわけです。
複数デバイス接続で注意したいMISO
複数のSPIデバイスを同じバスへ接続するとき、ハードウェア側で特に注意したいのがMISOです。
SCLKとMOSIはControllerであるESP32から各Deviceへ出力されるため、複数のDeviceへ分岐できます。
一方、MISOは複数のDeviceからESP32へ接続されます。
そのため、選択されていないDeviceはMISOをHi-Z(High Impedance)状態にする必要があります。
Device A MISO ──┐
├──── ESP32 MISO
Device B MISO ──┘
Device Aを選択しているときにDevice BまでMISOを駆動してしまうと、出力同士が競合する可能性があります。
通常のSPI DeviceはCSが非アクティブになるとMISOをHi-Zにしますが、実際に使用するICのデータシートで確認しておくことが重要です。
CSの起動時状態にも注意する
ESP32の起動直後は、アプリケーションによるGPIO設定がまだ完了していません。
そのため、外部Device側から見るとCSの状態が不定になる可能性があります。
例えばCSがActive LowのDeviceで、起動時にCSがLOWになってしまうと、意図しない選択状態になることがあります。
製品として基板を設計する場合は、
3.3V
│
[Pull-up]
│
├──── CS
│
ESP32 GPIO
のように、必要に応じて外付けプルアップ抵抗を設けます。
特に電源投入直後の状態が重要なICでは、ソフトウェアだけでなくハードウェアでCSの初期状態を保証することも検討します。
SPIクロックは最初から上限まで上げない
Deviceのデータシートに、
Maximum SPI Clock = 20MHz
と記載されていても、最初から20MHzで動作確認する必要はありません。
開発初期では
.clock_speed_hz = 1 * 1000 * 1000,
など、比較的低いクロックから通信を確認する方がトラブルを切り分けやすくなります。
通信が安定してからクロックを上げることで、
- 配線
- SPI Mode
- コマンド
- タイミング
などの問題を切り分けやすくなります。
配線が長くなる場合
SPIは基板内や比較的短い距離での通信に向いています。
クロックを高速化すると、
- 配線容量
- 反射
- クロストーク
- GND品質
- 信号の立ち上がり
などの影響を受けやすくなります。
特に複数DeviceへSCLKを分岐すると、単一Deviceの場合より配線条件が複雑になります。
基板設計では、
- SCLKを必要以上に引き回さない
- GNDリターンパスを確保する
- 分岐配線を必要以上に長くしない
- 必要に応じてシリーズ抵抗を検討する
といった対策も重要になります。
FreeRTOSで複数タスクからアクセスする場合
実際のESP32アプリケーションでは、
Task A → SPI Device A
Task B → SPI Device B
のように、異なるFreeRTOSタスクからSPIデバイスへアクセスしたくなることがあります。
ここでは、同じSPIバスを共有しているという点を忘れてはいけません。
単純なサンプルコードでは1つのタスクから順番に通信していますが、複数タスク・複数トランザクションを扱う実際のアプリケーションでは、ESP-IDFのSPI Master Driverが提供するバス取得・トランザクション管理の仕組みを理解したうえで設計する必要があります。
特に、
Device Aとの一連の通信中に、別Deviceのトランザクションを割り込ませたくない
というケースでは、バスを占有して一連の通信を完了させる設計が必要になることがあります。
これは単純なCS切り替えとは別の問題です。
よくあるトラブル
複数のSPIデバイスを接続して通信できない場合は、次の項目を確認してみてください。
- CSがDeviceごとに分かれているか
- SPI Modeが各Deviceの仕様と一致しているか
- SPIクロックが仕様範囲内か
- 非選択DeviceのMISOがHi-Zになるか
- CSの起動時状態に問題がないか
- SCLK / MOSI / MISOの配線が正しいか
- GNDが共通になっているか
- SPI信号の配線が長すぎないか
特に、
「Device Aだけなら動く」「Device Bだけなら動く」「2台接続すると動かない」
という場合は、MISOの競合やCS制御を確認する価値があります。
まとめ
今回はESP32-C5とESP-IDFを使って、1つのSPIバスへ複数のDeviceを接続する方法を紹介しました。
基本となる考え方は、
SCLK ──共有
MOSI ──共有
MISO ──共有
CS ──Deviceごとに個別
です。
ESP-IDFではSPIバスを1回初期化し、spi_bus_add_device()を使ってDeviceを追加します。
そして、
device_a_handledevice_b_handle
のようにDeviceハンドルを分けることで、それぞれ異なるCS・クロック・SPI Modeを使用できます。
単一DeviceでSPI通信ができるようになった次のステップとして、覚えておくと実際の製品開発で役立つ構成です。
特に複数Deviceでは、ソフトウェアだけでなく、MISOのHi-Z、CSの初期状態、信号品質まで含めて設計することが重要です。