LoginSignup
14
8

More than 1 year has passed since last update.

SPI通信 (ESP32スレーブ←→Teensy4.0マスター)

Last updated at Posted at 2021-07-15

SS 192.png 時間がとけるほど苦労した(けど最後はライブラリに救われました)

電子工作の世界3大通信規格

電子工作界で初心者がよく目にする通信規格といえば、
UARTI2CSPI の3つが挙げられます。
Arduino等を使っている人はまずPCとの接続でUARTに出会い、そこでシリアル通信に親しみます。
その後センサーを接続しようというところでI2Cに触れることになります。
さらにSDカードやカメラ基盤との通信が必要になると、SPIを使う場面も出てくるようになります。

めんどくさがりな自分はできればシリアル(UART)一本槍で人生を済ませたいと思っていた派だったのですが、調べてみるとそれぞれの規格に魅力があり、現役であり続ける意義もわかってきました。
この記事はSPIについて調べてみたことのノートです。

3つの通信規格の特徴をまとめてみると、

規格名 通信方式 信号線の数 接続数 つかいどころ 速度 特徴
UART 非同期式シリアル 2本 1対1 機器間 普通 距離
I2C 同期式シリアル 2本 1対多 基板上 遅い 2本線
SPI 同期式シリアル 4本 1対多 基板上 早い 高速

となります。まさに三者三様、一長一短、適材適所で活躍の場があります。
各規格の説明はこちらのサイトがわかりやすかったです。
https://emb.macnica.co.jp/articles/8191/

SPI 通信の理解

本記事の主役であるSPI通信の基本については、こちらのサイトがわかりやすいです。
http://www.picfun.com/f1/f05.html

一通りSPI通信について調べたところで、いわゆるシリアル通信(UART)ばかりを使ってきた自分的な目線で、SPIの要点をまとめてみます。

UARTばかりだった自分的観点からまとめたSPI通信のポイント

  • 以下の4本の信号線で接続

  • 送信線 (SDO/MOSI[master]/MISO[Slave])

  • 受信線 (SDI/MISO[master]/MOSI[Slave])

  • クロック信号 (SCLK,CLK,SCK[Serial Clock])

  • スレーブ選択 (CS[Chip Select]/SS[Slave Select]/SYNC/ENABLE)

  • 信号線の呼称が複数あって初心者の心を折りがち

  • 各線の役割と配線の理解

  • MISO/MOSIのみそもしは、Master in Slave Out /Master Out Slave inの略なので配線方法がわかりやすい。2つの機器のピンアサインのMISO同士、MOSI同士をつなぐ

  • スレーブ選択CSがlowの場合→繋がれたスレーブが起きる。マスターとの通信が有効になる。highの場合→スレーブが寝てマスターからのデータを無視。これが複数機器を接続できる仕組み。

  • マスター側のスレーブ選択CSはスレーブ機器の台数分必要。一方、MISO/MOSI/SCKは数珠繋ぎにできる。

  • スレーブが1台の場合はスレーブ選択CSを設定で固定することで省略可。

  • 通信手順など
     + 1回のSPI通信でやりとりするデータの塊とそのやりとりをトランザクション(Transaction)と呼ぶ。
     + マスターの要求(コマンド送信)→スレーブが応答(コマンド送信)の順。
     + 実際には上記より細かい手順が発生しているが、送受信はほぼ同時となる。
     + マスターが常に情報を送り続ければ、疑似的にスレーブからの要求発信も可能。
     + クロック信号のどのタイミングで送受信を行うかのモード設定(MODE0~3)がある。

  • 設定や接続でのポイント
     + 通信速度の設定はマスター側のみでOK。
     + 信号線の電圧レベル違い(5V信号を3.3V用の信号線に入力)で機器を壊しがち
     + 5Vトレラントと明記されていれば大丈夫。そうでない場合は電圧の回路を挟む。
     + クロック周波数は機器のクロックに合わせて分周して決定すると通信が安定する。

参考)SPIのモードやクロックの分周比について
https://www.lapis-tech.com/lazurite-jp/contents/reference/spi.html

Teensy4.0とESP32をSPIでつなぎたい

今回はTeensy4.0とESP32を、Arduino IDEを使ってSPI通信で連携させてみたいと思います。
というのも、Teensy4.0は処理速度が非常に高速で使い勝手がよいのですが無線を搭載しておらず、一方のESP32はWifiやBlueToothを搭載して処理速度がなかなかではあるものの、シリアル出力の数や拡張性、処理速度においてはTeensy4.0が優っているためです。
この2つが高速通信で連携できると、今やりたいプロジェクトがグッと前に進みます。

ちなみにArduinoとSPIの機器をつなぎたい場合は「Arduio SPI 機器名(SDなど)」検索すれば情報が出てきやすいと思います。SDカードぐらいであればさほどがんばる必要はなく、比較的簡単に接続が成功します。

簡単かと思ったら一筋縄ではいかない

調べてびっくりしたのですが、実はTeensy4.0はSPIスレーブにはまだ公式対応していないようです。
3.xまでのライブラリは作られているようですが、4.0でもうまく動くかは不明です。
https://github.com/tonton81/TSPISlave

またESP32も同様にSPIスレーブにはまだ公式対応していないようです。

なのでいきなり壁にぶち当たってしまうわけですが、ESP32のUARTシリアル通信では必要なスピードを出すのが難しく、どうしてもSPI通信が欲しくなります。

公式情報がありました (2021.07.16追記)

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_slave.html
ESP32側をスレーブにするための情報が公式から発表されていました。

さらにライブラリが発表されていました!!! (2021.07.17追記)

https://github.com/hideakitai/ESP32DMASPI (hidekitai氏)
なんとESP32側をスレーブにするためのライブラリが発表されていました。
できたてホヤホヤのようです。
こちらをありがたく使わせていただき、その内容に沿って以下のスクリプトと解説文を全面的に書き直しました。

まず配線をします

接続ピンはデフォルト設定のまま行います。

Teensy4.0 SPI線 ESP32 Dev Module
10番ピン CS 15番ピン
11番ピン MOSI 13番ピン
12番ピン MISO 12番ピン
13番ピン SCK 14番ピン
GND GND GND
5V 5V 5V

Teensy4.0側の線は設定で変更可能ですが、条件がいくつかあるようなので、そのままがラクです。
また、ESP側の設定はやや事情が複雑ですが、上記のデフォルトで動きます。

ちなみにESP側のSPIには、SPI,HSPI,VSPIの3種類のモジュールが搭載されており、また呼称もHSPI,VSPIをそれぞれSPI2,SPI3と別名があるようです。
SPIは内部フラッシュとの接続に使われているため、外部デバイスとのSPI接続ではHSPIもしくはVSPIの回路の利用となるそうです。

ESPのSPI通信のデフォルトのピンアサインは、
HSPI(SPI2) - SCK,MISO,MOSI,SS = 14,12,13,15
VSPI(SPI3) - SCK,MISO,MOSI,SS = 18,19,23,5
となります。

また、GND同士だけではなく双方の5Vピン同士も接続するとどういうわけか通信が安定しました。(2021.07.20追記)
(お互いに5Vをリファレンス的に扱っているかどうかはまだ調べきれていません。)

Teensy4.0側(SPIマスター)で動くスクリプト

さて本題のスクリプトです。
マスター側はシンプルなので自前のスクリプトでなんとかしていきます。

今回は20個の配列データの送受信を行ってみます。データの型はChar型(uint8_t型)になります。
簡易的なデータの正誤確認のチェックサムとして、データの0~18番目までを積算し、合計値をビット反転させた下位2バイトを配列の最後に格納しています。
これをSPI通信にて1秒ごとに送受信していきます。

そして以下がはじめてSPI通信を使う人にとって大事なポイントです。
SPI通信に使う送信データは送信毎にバッファに書き込むことになるわけですが、
送信している瞬間にバッファのデータはどんどん消されて空になっていきます。
ほぼ同時にその空になったバッファに受信データが上書きされていきます。
送信用に使った配列バッファに受信データも格納されるというところがわかりにくいところですよね。
ここがSPI通信最大のポイントだと思っています。
そこで、このスクリプトでは受け取ったデータを別の受信データ用の配列に書き写しています。

Teensy4.0
#include <SPI.h>  // SPIライブラリを導入

//変数の設定
static const int MSG_SIZE = 20;//データのサイズ(何バイト分か)
uint8_t s_message_buf[MSG_SIZE];//送信データバッファ用の配列
uint8_t r_message_buf[MSG_SIZE];//受信データバッファ用の配列
int checksum;//チェックサム計算用

//SPI通信設定のインスタンスを立てる
SPISettings mySPISettings = SPISettings(6000000, MSBFIRST, SPI_MODE3);

void setup() {
  //SPI通信とシリアル通信の初期設定
  Serial.begin(115200);
  pinMode (SS, OUTPUT);  // スレーブ機器を起こす
  SPI.begin();   // SPIの初期化
  delay(100); //シリアルの起動を安定させる(要調整)
  Serial.println("SPI Master Start."); //シリアル始動のご挨拶

  //送受信バッファのリセット
  memset(s_message_buf, 0, MSG_SIZE);
  memset(r_message_buf, 0, MSG_SIZE);
}

void loop() {
  Serial.println();//シリアルモニタ改行

  //送信データの作成
  checksum = 0;
  for (int i = 0; i < MSG_SIZE-1; i++) {
    s_message_buf[i] = uint8_t (i & 0xFF);
    checksum += int(s_message_buf[i]);    
  }
  checksum = (checksum ^ 0xFF) & 0xFF; //合計値を反転し、下位2ビットを取得
  s_message_buf[MSG_SIZE-1] = uint8_t (checksum);//末尾にチェックサムを追加

  //送信データの表示
  Serial.print("  [Send] ");
  for (int i = 0; i < MSG_SIZE; i++) {
    Serial.print(uint8_t (s_message_buf[i]));
    Serial.print(",");
  }
  Serial.println();

  //SPI通信の開始
  SPI.beginTransaction(mySPISettings);//通信開始
  digitalWrite(SS, LOW); //スレーブ機器を起こす

  //送信の実施と同時に受信データを受信データ用配列に書き写す
  for ( int i = 0; i < MSG_SIZE; i++) {
    r_message_buf[i] = SPI.transfer(s_message_buf[i]);  //※送信と同時に受信データが返り値になる
    delayMicroseconds(50);//送受信時間調整用のディレイ
  }

  //受信データの表示
  Serial.print("  [Rsvd] ");
  for (int i = 0; i < MSG_SIZE; i++) {
    Serial.print(uint8_t (r_message_buf[i]));
    Serial.print(",");
  } Serial.println();

  //受信データのチェックサム確認
  checksum = 0;
  for (int i = 0; i < MSG_SIZE - 1; i++) {//受信データの末尾-1番までの値を合計
    checksum += int(r_message_buf[i]);
  }
  checksum = (checksum ^ 0xFF) & 0xFF; //合計値を反転し、下位2ビットを取得

  Serial.print("  CKSUM: "); // チェックサムの正解を表示
  Serial.println(uint8_t (r_message_buf[MSG_SIZE - 1]));

  if (uint8_t (checksum) == uint8_t (r_message_buf[MSG_SIZE - 1])) {//チェックサムの正誤を表示
    Serial.print("    OK!: "); Serial.println(uint8_t (checksum));
  } else {
    Serial.print("****NG*: "); Serial.println(uint8_t (checksum));
  }

  digitalWrite(SS, HIGH);//スレーブ機器を終了
  SPI.endTransaction();//SPIを解放
  delay(1000);
}

SPI Slave Driverの解説を読み、ESP32にライブラリを導入する

まず、公式サイトのSPI Slave Driverを読み、内容をある程度理解します。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_slave.html

次に、以下のgithubよりhideakitai氏によるライブラリをありがたく導入させていただきます。
https://github.com/hideakitai/ESP32DMASPI

DLしたライブラリのArduino IDEでの使用方法

1. ESP32を導入済みのArduinoIDEを開きます
2. githubのESP32DMASPIをcodeのボタンからzip形式でDLします
3. ArduinoIDEのメニューから「スケッチ」→「.zip形式のライブラリをインストール」でDLしたzipを選択します
4. 念の為ArduinoIDEを再起動します。
5. ArduinoIDEのメニュー 「ライブラリのインクルード」の先に 「ESP32DMASPI」が表示されていればインストール成功です。

スレーブ側のスクリプト

ライブラリのサンプルを参考にしつつ、自分の理解のために修正しています。

ESP32
#include <ESP32DMASPISlave.h>

ESP32DMASPI::Slave slave;

static const int MSG_SIZE = 20;
uint8_t* s_message_buf;
uint8_t* r_message_buf;
int checksum;

void setup()
{
  Serial.begin(115200);
  delay(500);//シリアルの開始を待ち安定化させるためのディレイ(要調整)

  Serial.println("SPI Slave Start.");//シリアルモニタの確認用。

  // DMAバッファを使う設定 これを使うと一度に送受信できるデータ量を増やせる
  s_message_buf = slave.allocDMABuffer(MSG_SIZE); //DMAバッファを使う
  r_message_buf = slave.allocDMABuffer(MSG_SIZE); //DMAバッファを使う

  // 送受信バッファをリセット
  memset(s_message_buf, 0, MSG_SIZE);
  memset(r_message_buf, 0, MSG_SIZE);

  //送信データを作成してセット
  checksum = 0;
  for (int i = 0; i < MSG_SIZE - 1; i++) //配列の末尾以外をデータを入れる
  {
    uint8_t rnd = random(0, 255);
    s_message_buf[i] = rnd;
    checksum += rnd; //チェックサムを加算
  }
  s_message_buf[MSG_SIZE - 1] = uint8_t(checksum & 0xFF ^ 0xFF); //データ末尾にチェックサムにする

  slave.setDataMode(SPI_MODE3);
  slave.setMaxTransferSize(MSG_SIZE);
  slave.setDMAChannel(2); // 専用メモリの割り当て(1か2のみ)
  slave.setQueueSize(1); // キューサイズ とりあえず1
  // HSPI(SPI2) のデフォルトピン番号は CS: 15, CLK: 14, MOSI: 13, MISO: 12
  slave.begin(); // 引数を指定しなければデフォルトのSPI(SPI2,HSPIを利用)
}

void loop()
{

  // キューが送信済みであればセットされた送信データを送信する。
  if (slave.remained() == 0) {
    slave.queue(r_message_buf, s_message_buf, MSG_SIZE);
  }

  // マスターからの送信が終了すると、slave.available()は送信サイズを返し、
  // バッファも自動更新される
  while (slave.available())
  {
    Serial.print(" Send : ");
    for (uint32_t i = 0; i < MSG_SIZE; i++)
    {
      Serial.print(s_message_buf[i]);
      Serial.print(",");
    }
    Serial.println();

    // show received data
    Serial.print(" Rsvd : ");
    for (size_t i = 0; i < MSG_SIZE; ++i) {
      Serial.print(r_message_buf[i]);
      Serial.print(",");
    }
    Serial.println();
    slave.pop();//トランザクションを終了するコマンドらしい

    //受信データのチェックサム確認
    checksum = 0;
    for (int i = 0; i < MSG_SIZE - 1 ; i++) { //受信データの末尾-1番までの値を合計
      checksum += int(r_message_buf[i]);
    }
    checksum = (checksum ^ 0xff) & 0xff; //合計値を反転し、下位2バイトを取得
      Serial.print(" cksum: "); Serial.println(uint8_t (r_message_buf[MSG_SIZE - 1]));

    
    if (uint8_t (checksum) == uint8_t(r_message_buf[MSG_SIZE - 1])) {
      Serial.print("   OK!: "); Serial.println(uint8_t (checksum));
    } else {
      Serial.print("**ERR*: "); Serial.println(uint8_t (checksum));
    }
    Serial.println();
  }
}


実行

Teensy4.0, ESP32それぞれにスクリプトを書き込み、シリアルモニタで結果を確認します。

Teensy4.0側の画面例

ESP32側の画面例

Teensy4.0側からは0~18+チェックサムのデータを送信し、
ESP32側からはセッティング時に作成したランダムな値を返信しています。
それぞれ、受信結果に対してチェックサムを実施し、結果を表示しています。

##TeensyではなくArduinoをマスターとして使いたい場合
ArduinoのSPIもちょっとややこしいですが理解のためのポイントがあります。

  • SPI専用のSPDR(SPI Data Register)というメモリがあるのでそれを使う
  • SPCRという8ビットのメモリ(レジスタ)があり、各ビットのオンオフでSPIの設定を行う
  • 信号線の電圧レベル差に注意 (Arduino UNOは5Vレベル、ESP32は3.3Vレベル)
    この2つを前提条件として理解できていれば、検索でなんとかなると思います。

ESP-WROOM-32に関するTIPS
https://trac.switch-science.com/wiki/esp32_tips#SPI%E9%80%9A%E4%BF%A1

このあとやること

データ量を増やし、通信速度を安定して使える限界までアップさせていきます。
SPIには一つのバッファを送受信で上書きするという特性があるので、通信時間や書き換えタイミングなどに注意しつつ、攻めていきたいと思います。
またマスター側からの送信は現在1バイトずつになっていますが、データの長さを指定することで配列まるごとの書き換えも可能のようなのでそのあたりも調整してみたいと思います。

おわりに

初心者向けの動かせるSPI解説の資料が少なく、今回はかなり大変でしたが、
ライブラリを作られる方がいたり、それを導入し解説してくださる方がいて本当に助かりました。ありがとうございました。

今回はTeensy4.0とESP32をSPIで接続してみましたが、これからWifiを搭載したTeensy4.0が出るか、超高速でシリアルを沢山積んだESP32が出たりするとまた色々と面白くなりそうですね。

また、スクリプトは解説で変なところがありましたらぜひお教えいただきたく、よろしくお願いいたします。

参考資料

SPI Slave Driver EPS32の公式資料

上記を使いやすくするためのライブラリ (hideakitai氏)

ESP32をSPI通信のスレーブで使用する (中村氏)
ESP32でSPIスレーブ通信するときの注意点
SPI通信 (ちっちゃいおっさん方式での説明)
ArduinoでSPI通信を行う方法
Arduino の SPI接続
[AN-1248アプリケーション・ノート]
(https://www.analog.com/media/jp/technical-documentation/application-notes/AN-1248_jp.pdf)
メモ:ESP32のSPIについて混乱していることの整理
ESP32 で SPI スレーブ通信するときの注意点

以上を参考にさせていただきました。ありがとうございました。

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8