時間がとけるほど苦労した(けど最後はライブラリに救われました)
電子工作の世界3大通信規格
電子工作界で初心者がよく目にする通信規格といえば、
UART 、I2C、SPI の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通信最大のポイントだと思っています。
そこで、このスクリプトでは受け取ったデータを別の受信データ用の配列に書き写しています。
#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」が表示されていればインストール成功です。
スレーブ側のスクリプト
ライブラリのサンプルを参考にしつつ、自分の理解のために修正しています。
#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側からは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 スレーブ通信するときの注意点
以上を参考にさせていただきました。ありがとうございました。