Arduinoで複数のジャイロセンサとSPI通信

概要

今回は、MPU9250を複数用いてArduinoとSPI通信していきたいと思います。具体的には、SPI通信によってI2Cでは不可能なMPU9250の複数制御を試したので、その記録をここに残します。複数と言っても、ここでは二つのジャイロセンサで実践します。でも、この方法であれば二つだけではなく、三つでも四つでもほぼ同時に通信が可能です。そのあたりの説明は後程説明します。
使用する部品は以下のものです。

f:id:PsyTouSan:20210925090655j:plain
MPU9250

こちらがMPU9250です。3軸加速度、3軸角速度、3軸方位角の計9軸を測定することができるIMUで、コンパクトにまとまっており、価格も数百円程度と安価に入手できます。

MPU9250の仕様

MPU9250は主に、I2C通信とSPI通信の二種類の通信形態をサポートしています。特に重要なのは以下の二つです。

  • SPI通信は、加速度センサとジャイロセンサのみサポート
  • それぞれのデータは上位7ビットと下位7ビットにアドレスが分けられている

特に、一つ目の仕様に関しては致命的なレベルです。要するに、SPI通信をする際は、地磁気センサの使用ができない(頑張ればできるかもしれない)ということになります。なので、今回は加速度と角加速度の計6軸の出力で実装したいと思います。

詳しいことは、以下のデータシートを参考にしてください。

https://invensense.tdk.com/wp-content/uploads/2015/02/PS-MPU-9250A-01-v1.1.pdf

SPI通信を選んだ理由

というのも、複数のセンサを扱う場合I2C通信では不都合なことが発生します。

以下は、I2C通信の仕組みを簡略化した図です。

f:id:PsyTouSan:20210929145302p:plain
I2C通信の簡単な仕組み


上の図のSDAが、送受信するデータですが、このうちの青枠がマスターからスレーブ、黄枠がスレーブからマスターに送信されるデータです。

このうちの最初の7ビットは、通信を開始したいスレーブのアドレスを持っています。

要するに、最初の7ビットで、どのスレーブとやりとりするかを決定します。次のビットで、書き込みをするか、読み込みをするかを送ります。この時、書き込みをする場合は1が送信されます。すると、このアドレスと一致したスレーブから応答を得ることができます。それが、ACKです。ただ、ここで一つ問題が発生します。複数の同じスレーブ(今回の場合はMPU9250)と通信をする場合、互いにスレーブアドレスが一致してしまうため、応答が衝突してしまい、うまく動作しないということです。

一応、上記のデータシートによれば、MPU9250は二種類のスレーブアドレスを切り替えることができるみたいです。これが意味することは、I2C通信で二つのMPU9250と通信ができるということです。しかし、MAXで二種類のアドレスしか持つことができないため、I2C通信では三つ以上の同じセンサを扱うことができません。しかし、SPI通信であれば、SSピンのデジタル信号を切り替える(通信したい相手のみをLOWに保つ)ことで、選択的に通信することができます。

f:id:PsyTouSan:20211001135454p:plain
SPI通信

なので、SSピンのHIGHとLOWを切り替えて、切り替えるたびにMISOとMOSIを読み取るということをすれば、複数のセンサと同時に通信することができるというわけです。

Arduinoで実装する

さて、実際にコードを書いていきます。使用するのはArduino MEGA 2560です。
それ以外のArduinoでも問題ありませんが、MEGAとはピン配置や番号が異なっているため、注意が必要です。今回は、手元にあるArduino MEGA 2560を使用して実装していきます。また、細かい説明に関しては、後ほど説明します。
接続は次のとおりです

f:id:PsyTouSan:20210929210802p:plain
配線方法

こんな感じです。NCS以外は並列につないでいます。ちなみにVccは3.3Vです。
上の図は、三つがつ結線されていますが、いくつで行っても問題ありません。これよりも多くつなぐ場合でも、NCSピン以外を並列になるようにつないで、NCSピンだけを他のデジタルピンにつなげば大丈夫です。ただし、あまり多く結線してしまうと、それだけ処理がArduinoへの負担も大きくなりますから、そこは注意ですね。
今回は、二つで行ってみたいと思います。

f:id:PsyTouSan:20211002090943j:plain
結線の様子

少しわかりにくいですが、実際につないでみた写真です。

それでは、Arduino IDEを用いてプログラムを記述していきたいと思います。
以下に示すのはソースコードです。

#include <SPI.h>

int data[6];
int GX,GY,GZ; //角速度を格納
int AX,AY,AZ; //加速度を格納
float ax,ay,az,gx,gy,gz;

void setup() {
  pinMode(49,OUTPUT);
  pinMode(48,OUTPUT);
  SPI.setBitOrder(MSBFIRST);
  SPI.setClockDivider(SPI_CLOCK_DIV4);
  SPI.setDataMode(SPI_MODE3);
  SPI.begin();  
  
  Serial.begin(2000000);
}

void loop() {
  dataRead(49,data);   //ssピン49のセンサの六つの値を取得
  AX = data[0];
  AY = data[1];
  AZ = data[2];
  GX = data[3];
  GY = data[4];
  GZ = data[5];

  Serial.print("A:");
  Serial.print(GX); Serial.print(",");
  Serial.print(GY); Serial.print(",");
  Serial.print(GZ); Serial.print(",");

  dataRead(48,data);   //ssピン48のセンサの六つの値を取得
  AX = data[0];
  AY = data[1];
  AZ = data[2];
  GX = data[3];
  GY = data[4];
  GZ = data[5];

  Serial.print("B:");
  Serial.print(GX); Serial.print(",");
  Serial.print(GY); Serial.print(",");
  Serial.print(GZ); Serial.println("");
}

//加速度センサの測定値を取得するだけの関数
int dataRead(int ss,int data[6])
//第一引数から、読み取るセンサのcsピン番号,読み取りデータ格納用の配列
{
  byte dataH = Read(ss,0x3B);
  byte dataL = Read(ss,0x3C);
  ax = dataH << 8 | dataL;
  data[0] = ax;

  dataH = Read(ss,0x3D);
  dataL = Read(ss,0x3E);
  ay = dataH << 8 | dataL;
  data[1] = ay;
  
  dataH = Read(ss,0x3F);
  dataL = Read(ss,0x40);
  az = dataH << 8 | dataL;
  data[2] = az;

  dataH = Read(ss,0x43);
  dataL = Read(ss,0x44);
  gx = dataH << 8 | dataL;
  data[3] = gx;
  
  dataH = Read(ss,0x45);
  dataL = Read(ss,0x46);
  gy = dataH << 8 | dataL;
  data[4] = gy;
  
  dataH = Read(ss,0x47);
  dataL = Read(ss,0x48);
  gz = dataH << 8 | dataL;
  data[5] = gz;

  return 0;
  
}

//ピンを読み込む関数
byte Read(int ss,byte address)
//第一引数から、読み取るセンサのcsピン番号,アドレス
{
  digitalWrite(ss,LOW);
  SPI.transfer(address | 0x80);
  byte data = SPI.transfer(0);
  digitalWrite(ss,HIGH);
  return data;
}

コードの簡単な説明

SPIライブラリ

ArduinoIDEには、SPI通信をするのに必要なインターフェースを簡単に揃えることができるライブラリがあります。それが、SPI.hライブラリです。
11行目から14行目までは、SPI通信をする際に必要な設定をしています。

//最上位ビットを転送する
  SPI.setBitOrder(MSBFIRST);
//SPI通信の動作スピード
  SPI.setClockDivider(SPI_CLOCK_DIV4);
//SPI通信の動作モード
  SPI.setDataMode(SPI_MODE3);
//SPI通信を開始する
  SPI.begin();  
転送する順序

SPI.BitOrder()関数は、ビットの送受信する順番を「最上位ビットを最初に送る」のか、「最下位ビットを最初に送る」のかを決定する。今回の場合は、MSBFIRSTとすることで、最上位ビットを先に転送している。

動作スピード

SPI.setClockDriver()関数で、システムのクロックの速さに応じたクロックを設定できます。要するに、システムクロックが32MHzの時に、SPI_CLOCK_DIV4とすれば、32MHzを四分割した8MHzとなりますし、SPI_CLOCK_DIV8とすれば、八分割の4MHzとなります。分割数は、最後の数字を変えることで変化できますが、「2、4、8、16、32、64、128」の7つに限定されています。

動作モード

SPI.setDataMode()関数で、SPI通信の動作モードを選択できますが、SPI_MODE0、SPI_MODE1、SPI_MODE2、SPI_MODE3の4つしかない。この4つはそれぞれ極性モードと送受信するタイミングが異なる。詳しい内容はここでは割愛する。

ジャイロデータの取得

まず通信する相手を選択

今回の場合、2つのMPU9250を使用しましたが、前述したとおりどちらか一方のSSピンをHIGHに保つことでそのセンサとの通信を遮断することが必要になってきます。それを行っている関数が、86行目に示したbyte Read関数です。この関数が呼び出された時、その第一引数に渡された番号をLOWに変化させ、SPI通信のトランザクションを開始します。そして、それが終了したらすぐにHIGHに戻します。

次いでデータの取得

ソースコードの51行目から54行目を例に見てみます。以下はその部分の切り取りです。

  byte dataH = Read(ss,0x3B);
  byte dataL = Read(ss,0x3C);
  ax = dataH << 8 | dataL;
  data[0] = ax;

MPU9250から転送されてくるジャイロデータは、上位8ビットと下位8ビットに分かれて送られてきます。なので、それらをdataHとdataLの2つの変数に分けて受け取っています。それらをビットシフトすることで、ジャイロデータを取得していることになります。

結果

以下のスクショにはセンサAとセンサBの3軸角速度をそれぞれ表示しています。(加速度も表示してしまうと、ごちゃごちゃしてよくわからなくなってしまうので。)

f:id:PsyTouSan:20211003223438p:plain
取得した結果

センサAのみを軽く振動させている状態です。生値のままですが、実際に2つのジャイロセンサの値を取得出来ていることがわかります。
以上です。