ESP32でSPI通信を実装して、
「低速なら動くのに、クロックを上げると通信エラーになる」
「机の上では動いていたのに、基板にしたら不安定になった」
「同じプログラムなのに、ときどき受信データが化ける」
といった現象に遭遇することがあります。
SPI通信が一応動いているため、プログラムの問題なのか、配線なのか、クロックなのか判断しにくいトラブルです。
特にSPIは通信速度を上げるほど、単純な「HIGHとLOW」だけでは説明できない信号品質の影響が大きくなります。
今回はESP32-C5とESP-IDFを例に、
SPI通信が不安定になるとき、どこから確認すればよいのか
を実際の切り分け方法と合わせて解説します。
SPIクロックを下げて確認する
SPI通信が不安定な場合、最初に試したいのがクロック周波数を下げることです。
例えば現在20MHzで通信しているのであれば、
20MHz
↓
10MHz
↓
5MHz
↓
1MHz
のように下げて確認します。
ここで1MHzなら安定して通信できるのであれば、
- SPI Modeが完全に間違っている
- MOSI/MISOを逆接続している
- CSがまったく動いていない
といった根本的な問題である可能性は低くなります。
一方、1MHzなら安定するが20MHzではエラーになる
のであれば、クロック速度によって顕在化する問題を疑えます。
このように、条件を一つずつ変えて原因候補を減らしていくことが重要です。
今回使用するサンプルコード
今回はSPI Deviceへ8byteを送信し、同時に8byteを受信する簡単なテストコードを使用します。
実際のセンサーなどに依存しないよう、SPI通信部分そのものを確認する構成です。
※実際に受信値を検証する場合は、接続するSPI Deviceの仕様に合わせて送信コマンドを変更してください。
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.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 GPIO_NUM_7
#define SPI_CLOCK_HZ (1 * 1000 * 1000)
static const char *TAG = "SPI_TEST";
static spi_device_handle_t spi_handle;
static void spi_test_transfer(void)
{
uint8_t tx_data[8] = {
0x10, 0x20, 0x30, 0x40,
0x50, 0x60, 0x70, 0x80
};
uint8_t rx_data[8] = {0};
spi_transaction_t transaction = {
.length = sizeof(tx_data) * 8,
.tx_buffer = tx_data,
.rx_buffer = rx_data,
};
esp_err_t ret = spi_device_transmit(
spi_handle,
&transaction);
if (ret != ESP_OK)
{
ESP_LOGE(
TAG,
"SPI transmit failed: %s",
esp_err_to_name(ret));
return;
}
ESP_LOGI(
TAG,
"RX: %02X %02X %02X %02X %02X %02X %02X %02X",
rx_data[0],
rx_data[1],
rx_data[2],
rx_data[3],
rx_data[4],
rx_data[5],
rx_data[6],
rx_data[7]);
}
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_config = {
.clock_speed_hz = SPI_CLOCK_HZ,
.mode = 0,
.spics_io_num = PIN_NUM_CS,
.queue_size = 1,
};
ESP_ERROR_CHECK(
spi_bus_add_device(
SPI_HOST,
&device_config,
&spi_handle));
ESP_LOGI(
TAG,
"SPI initialized: %d Hz",
SPI_CLOCK_HZ);
while (1)
{
spi_test_transfer();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
このコードではSPIクロックを、
#define SPI_CLOCK_HZ (1 * 1000 * 1000)
としているため、1MHzで通信します。
まずこの状態で安定して通信できることを確認します。
クロック周波数を変更する
先ほどのサンプルコードでは、SPIクロックをマクロで定義しています。
#define SPI_CLOCK_HZ (1 * 1000 * 1000)
そしてDevice登録時に、その値を使用しています。
抜粋コード
spi_device_interface_config_t device_config = {
.clock_speed_hz = SPI_CLOCK_HZ,
.mode = 0,
.spics_io_num = PIN_NUM_CS,
.queue_size = 1,
};
例えば5MHzで確認する場合は、
#define SPI_CLOCK_HZ (5 * 1000 * 1000)
へ変更します。
ただし、この5MHzのコードは上記サンプルコードそのものではなく変更例です。
サンプルコードとして掲載している完成版では、最初の動作確認を目的として1MHzにしています。
「最大クロック」と「安定して使えるクロック」は別
SPI Deviceのデータシートを見ると、
Maximum SCLK Frequency:20MHz
などと記載されていることがあります。
ここで注意したいのは、
20MHzまで対応していることと、現在の基板・配線条件で20MHzが安定することは同じではない
ということです。
通信品質には、
- ESP32側の信号
- Device側の入力特性
- PCBパターン
- ジャンパ線
- コネクタ
- 配線長
- 負荷容量
- GND
- 電源品質
なども関係します。
そのため開発初期では、まず低速で通信を成立させ、その後クロックを上げていく方が原因を切り分けやすくなります。
SPI Modeを確認する
クロックを十分下げても通信内容がおかしい場合は、SPI Modeも確認します。
今回のサンプルコードでは、
spi_device_interface_config_t device_config = {
.clock_speed_hz = SPI_CLOCK_HZ,
.mode = 0,
.spics_io_num = PIN_NUM_CS,
.queue_size = 1,
};
としているため、SPI Mode 0です。
接続するDeviceがMode 3を要求しているのであれば、この設定では正常に通信できません。
SPI Modeは、
- CPOL
- CPHA
によって決まり、Deviceのデータシートに指定されています。
通信速度を下げても改善しない場合には、クロック周波数だけを疑い続けず、Mode設定も再確認します。
エラーと「データ化け」は分けて考える
SPI通信のトラブルでは、
ESP-IDFがエラーを返す場合
と、
APIとしては成功しているのに受信値がおかしい場合
を分けて考える必要があります。
今回のサンプルコードでは、
esp_err_t ret = spi_device_transmit(
spi_handle,
&transaction);
if (ret != ESP_OK)
{
ESP_LOGE(
TAG,
"SPI transmit failed: %s",
esp_err_to_name(ret));
return;
}
として、spi_device_transmit()の戻り値を確認しています。
ここでESP_OKが返ったからといって、
接続先Deviceから正しいデータが返ってきたことまで保証されるわけではありません。
SPI Master Driverから見ればトランザクション処理が正常に完了していても、
- Deviceがコマンドを正しく認識していない
- SPI Modeが違う
- 信号品質が悪い
- 読み出すレジスタを間違えている
などによって受信値がおかしくなることがあります。
受信値をログで確認する
そこで今回のサンプルでは、受信した8byteをログへ出しています。
ESP_LOGI(
TAG,
"RX: %02X %02X %02X %02X %02X %02X %02X %02X",
rx_data[0],
rx_data[1],
rx_data[2],
rx_data[3],
rx_data[4],
rx_data[5],
rx_data[6],
rx_data[7]);
例えば、
1MHz → 毎回同じ正常値
5MHz → 毎回同じ正常値
10MHz → ときどき異常値
20MHz → 頻繁に異常値
という結果になった場合、
通信速度と異常発生に相関があることが分かります。
この情報が得られれば、ソフトウェアのロジックだけでなく、信号品質やタイミングを確認する段階へ進めます。
ジャンパ線では動作条件が変わる
ブレッドボードやジャンパ線でSPI通信を確認する場合は、特に高速通信へ注意が必要です。
SPIクロックを高速化すると、ジャンパ線も単なる「導線」としてだけでは扱えなくなってきます。
例えば、
ESP32
│
│ 長いジャンパ線
│
├──────── SCLK
├──────── MOSI
├──────── MISO
└──────── CS
│
SPI Device
のような構成では、配線長やGNDの取り方によって波形が悪化することがあります。
低速では問題が見えなくても、高速化したときに初めて問題が現れるケースがあります。
確認したい切り分け順序
SPI通信が不安定なとき、私は次のように原因候補を減らしていくのが良いと考えています。
① SPIクロックを下げる
↓
② SPI Modeを確認
↓
③ CS動作を確認
↓
④ MOSI / MISO / SCLKを確認
↓
⑤ GND・電源を確認
↓
⑥ オシロスコープで波形確認
↓
⑦ 信号品質対策を検討
重要なのは、最初からすべてを変更しないことです。
クロック、Mode、配線、抵抗値を同時に変更してしまうと、
何を変更したことで改善したのか分からなくなります。
一つ変更して、一つ結果を見る。
これはSPIに限らず、組み込み開発のデバッグ全般で有効な考え方です。
SCLK波形を確認する
SPI通信が低速では安定しているのに、高速化すると不安定になる場合は、まずSCLK波形を確認したいところです。
デジタル信号は、論理的にはHIGHとLOWの2値ですが、実際の配線上では立ち上がり時間、立ち下がり時間、オーバーシュート、リンギングなどの影響を受けます。
特にSCLKは一定周期で繰り返し変化するため、信号品質の問題が顕在化しやすい信号です。
立ち上がり・立ち下がりが遅い場合
SCLKのエッジが鈍っていると、Device側でしきい値を跨ぐタイミングが曖昧になります。
理想波形
┌─────┐ ┌─────┐
│ │ │ │
│ │ │ │
┘ └───┘ └─────
立ち上がりが遅い波形
/─────
/
┘
のような違いです。
クロック周波数が低いと多少エッジが遅くても余裕がありますが、高速化するとセットアップ時間やホールド時間に対する余裕が減ります。
その結果、同じ回路でも1MHzでは正常、20MHzでは不安定という現象が起こります。
オーバーシュートとリンギング
高速なデジタル信号では、配線長やドライバの立ち上がり速度によって、波形が一度大きく振れてから振動することがあります。
これがオーバーシュートやリンギングです。
理想
──────┐
│
└──────
リンギングあり
─────╮╲╱╲╱────────
この振動が入力しきい値を何度も跨ぐと、Device側が余分なクロックエッジとして認識する可能性があります。
特にSCLKで発生すると、1bit余計に読み込む、ビット位置がずれる、といった問題につながることがあります。
シリーズ抵抗を検討する
リンギングが確認された場合、SCLKやMOSIへシリーズ抵抗を追加すると改善することがあります。
例えば、
ESP32 GPIO ──[22Ω〜47Ω]── SCLK ── SPI Device
のように、信号源の近くへ抵抗を配置します。
シリーズ抵抗は信号の立ち上がりを少し緩やかにし、反射やリンギングを抑える目的で使用します。
ただし、
とりあえず抵抗を入れればよい
というものではありません。
抵抗値が大きすぎると立ち上がり・立ち下がりが遅くなり、高速通信では逆にタイミング余裕を減らす可能性があります。
そのため実際には、オシロスコープで波形を確認しながら調整するのが理想です。
SCLKだけでなくMOSIも確認する
クロックだけ正常でも、MOSI側のデータ波形が崩れていれば通信は不安定になります。
確認したいのは、
- SCLK
- MOSI
- MISO
- CS
の4信号です。
特に、SCLKとMOSIを同時に観測すると、
データがクロックエッジに対して十分早く確定しているか
を確認しやすくなります。
CSのタイミングを確認する
通信開始直前や終了直後のCSタイミングも重要です。
今回のサンプルコードでは、Device登録時にCSをESP-IDFへ任せています。
spi_device_interface_config_t device_config = {
.clock_speed_hz = SPI_CLOCK_HZ,
.mode = 0,
.spics_io_num = PIN_NUM_CS,
.queue_size = 1,
};
この場合、spi_device_transmit()に合わせてSPI Master DriverがCSを制御します。
esp_err_t ret = spi_device_transmit(
spi_handle,
&transaction);
それでも接続するDeviceによっては、
- CSを下げてから最初のクロックまでの待ち時間
- 最後のクロックからCSを上げるまでの待ち時間
- トランザクション間の待ち時間
などに条件がある場合があります。
そのため、DeviceのデータシートにあるCS timingも確認します。
GNDリターンパスを確認する
SPI信号はGPIOからDeviceへ流れて終わりではありません。
電流はGND側を通って戻ります。
つまり、信号線だけでなくリターンパスも重要です。
例えばPCB上でSCLKの直下に連続したGNDプレーンがあれば、比較的きれいなリターンパスを確保できます。
一方、
- GNDプレーンが途中で分断されている
- スリットを跨いでいる
- コネクタ経由で遠回りしている
といった状態では、信号品質が悪化する可能性があります。
高速SPIでは、
信号線だけを見るのではなく、戻り電流がどこを流れるか
まで考える必要があります。
配線の分岐を短くする
複数DeviceへSPIを分岐する場合、前回の記事で紹介したようにSCLK・MOSI・MISOを共有できます。
ただし、ハードウェア的には分岐が増えるほど信号品質は厳しくなります。
例えば、
ESP32
│
├──────────── Device A
│
└──────────── Device B
よりも、
ESP32 ──────────┬── Device A
└── Device B
のように、長いスタブができる構成では反射の影響が出やすくなります。
特にSCLKは、不要に長い分岐を避けた方が安全です。
ブレッドボードと製品基板は同じではない
開発初期にブレッドボードやジャンパ線で動作確認できたとしても、製品基板で必ず同じ結果になるとは限りません。
逆に、ブレッドボードでは10MHzが不安定でも、PCBでは問題なく20MHzで動作することもあります。
これは、
- 配線長
- 配線インピーダンス
- GND
- コネクタ
- Device配置
などの条件が異なるためです。
そのため、
試作環境で確認できた最大クロック = 製品での最終クロック
とは考えない方がよいです。
ロジックアナライザとオシロスコープの使い分け
SPIデバッグでは、ロジックアナライザとオシロスコープの両方が役立ちます。
ロジックアナライザ
向いている確認項目は、
- MOSIデータ
- MISOデータ
- CS
- SPI Mode
- コマンド内容
- 通信順序
です。
プロトコルデコーダを使えば、送受信データをHEX表示できるため非常に便利です。
オシロスコープ
向いているのは、
- 立ち上がり時間
- 立ち下がり時間
- オーバーシュート
- リンギング
- ノイズ
- 電圧レベル
- クロックとデータのタイミング関係
です。
つまり、
通信内容を見る → ロジックアナライザ
波形品質を見る → オシロスコープ
という使い分けが基本です。
「ロジアナで正常」は信号品質が正常とは限らない
ここは注意したいポイントです。
ロジックアナライザは入力信号をHIGH / LOWとして判定します。
そのため、多少波形が崩れていても、しきい値を正しく跨いでいれば正常に見えることがあります。
一方、実際のSPI Deviceでは入力条件が異なり、同じ波形でも誤動作する可能性があります。
つまり、
ロジックアナライザで正しくデコードできた
だけで、信号品質まで保証されたわけではありません。
不安定現象がある場合は、オシロスコープでも確認する価値があります。
電源品質も確認する
SPI通信が高速化すると、Device側の内部回路も高速に動作します。
そのため、電源が不安定な場合は通信エラーとして現れることがあります。
確認したいのは、
- 3.3V電源
- デカップリングコンデンサ
- GND
- Device直近の電源波形
です。
特にDeviceの近くにデカップリングコンデンサがない場合や、長い配線で電源を供給している場合は注意が必要です。
不安定な通信を切り分ける実践フロー
今回の記事の内容を実際の切り分け順にまとめると、次のようになります。
SPI通信が不安定
↓
クロックを下げる
↓
安定する?
┌──────┴──────┐
YES NO
↓ ↓
信号品質を疑う Mode・配線・CS確認
↓
SCLK/MOSI観測
↓
リンギング確認
↓
配線・GND確認
↓
必要ならシリーズ抵抗
↓
徐々にクロックUP
この順番なら、無闇に条件を変更せずに原因を絞り込めます。
実務で大切なのは「最大速度」ではない
SPI通信では高速化に目が向きがちですが、製品設計では必ずしも最大クロックで使う必要はありません。
例えば20MHzまで対応しているDeviceでも、5MHzで必要な処理が十分間に合うなら、あえて5MHzで使う判断もあります。
通信速度を上げれば、
- 信号品質
- EMI
- クロストーク
- タイミング余裕
は厳しくなります。
必要な性能を満たしているなら、
余裕を持った通信速度を選ぶ
ことも立派な設計判断です。
まとめ
ESP32のSPI通信が不安定な場合、プログラムだけを疑うのではなく、クロック・配線・GND・信号品質まで含めて確認する必要があります。
特に、
- 低速では安定する
- 高速化するとエラーが増える
という現象は、重要な手掛かりです。
まずクロックを下げ、
次にSPI ModeやCSを確認し、
それでも高速化時だけ問題が出るのであれば、オシロスコープで信号品質を確認します。
必要に応じて、
- 配線の短縮
- GNDリターンパス改善
- シリーズ抵抗
- クロック周波数の見直し
などを検討します。
SPI通信では、
「最大何MHzまで出せるか」より、「必要な性能を安定して満たせるか」
の方が重要です。
安定した条件を一つずつ確認しながら、必要な性能へ近づけていく。
この進め方が、結果的に最短のデバッグになることがあります。
技術は、経験から価値になる。
DLROW Design|現場エンジニア