今回の「技術情報」は、今どきのモノづくりに欠かせない存在である組込み系エンジニアの皆様に役立てていただけそうな、SPI通信の話題をご提供。
「PICのSPI通信が、うまくいかない」「動作しない」・・・。そんな話を、たびたび耳にします。
そこで、最近PICで加速度計を制御することになった当社エンジニアが、SPI通信に四苦八苦した過程を率直に明かし、正しく通信できるようにするためのポイントを、皆様にお話しします。
by Tadaharu Inoue 2014/01/22
今回の[技ラボ~技術情報]は、家電、モバイル機器、ロボット、自動車など、今どきのモノづくりに欠かせない存在である組込み系エンジニアの皆様に、役立てていただけそうな話題です。
「PICのSPI通信が、うまくいかない」「動作しない」・・・。そんな話を、たびたび耳にします。
対象となるデバイスごとにクセがあったり、プログラムには間違いがなさそうでも、どういうわけか通信できなかったりとか・・・。
ISPのソフトウェア技術者である私は最近、とある業務で加速度計を制御することになり、PICで加速度計を扱う機会を得ました。そこでこの業務を、SPI通信に関する試行錯誤の“モデルケース”にしようと考えたのです。設定や実装の場面で、SPIならではのクセなどを実際に体感し、通信に行き詰まってしまう典型的なプロセスや、正しく通信できるようにするためのポイントを、皆さまにお話ししようと思います。
PICと加速度センサ間の通信に、SPIを活用する
加速度計を、制御する――。その第一歩として、「あるしきい値を超えた加速度を検出すると、LEDが点灯する」という単純なプログラムを作成し、実際に動作させてみます。
「PIC―加速度計」間の通信には、先に述べたSPIを使用したのですが、正しく通信できるようになるまでには、やはり苦労しました。その試行錯誤のプロセスも、のちほど詳しくご紹介します。
使用したデバイス
使用した加速度計およびPICは、以下のものです。
回路図
PICと加速度計は、以下の図のように繋いでいます(電源などは省略しています)。
各デバイスのピンの詳細については、データシートを参照してください。
PIC24FJ64GA004 Family Data Sheet
ADXL345 データシート
開発環境
開発には、以下の環境を使用しました。
- IDE:MAPLAB IDE v8.90
- Compiler:MAPLAB C Compiler for PIC24 and dsPIC v3.31
SPI通信とは
実装方法を記述するにあたって、あらためてSPI通信について、簡単に説明しておきます。
SPIとは、「シリアル・ペリフェラル・インタフェース(Serial Peripheral Interface)」の略で、比較的近距離に配置されているデバイス同士をつなぐバスのことです。今回のケースでは1対1の通信を行いますが、1(マスター)対N(スレーブ)の通信も可能です。信号は送受信2本(SDI, SDO)、シリアルクロック(SCK)と、複数スレーブを制御するためのSS信号の、合計4本を使用します。(1対1の通信の場合、SSは省略することもできます)。
以下に、SPI通信の概要図を示します。
SPI通信を行うと、結果的にマスター、スレーブで保持しているSPI専用バッファの内容が交換されることになります。続いてSPI通信の仕組みを、順を追って見ていきましょう。
- マスターのSPIバッファを、1bitシフトします。
- マスター側で1bitシフトすることで溢れてしまった最上位ビットを、SDIから送出します。
- スレーブ側は、マスターから送出されるクロック(SCK)と同期して、同じようにSPIバッファを1bitシフトします。
- スレーブ側でも溢れてしまった最上位ビットを、SDIから送出します。
- それぞれのSDIから送出された1bitは、それぞれのSDOで受信されてSPIバッファの最下位ビットに格納にされます。
以上の操作を8回繰り返すことで、合計8bitのデータを送受信し、結果的にお互いのSPIバッファの内容が入れ替わります。これがSPI通信の原理です。
SPI通信についてざっくり理解していただいたところで、さっそくソースコードを書いていきましょう。まずは、加速度計と繋がっているPICのポート設定を行います。ここは入出力の設定だけなので、詳細な説明は省きます。RP5などがどのピンに対応しているかは、データシートを参照して下さい。また、PPSInput()などの関数は、pps.hで定義されています。
// ポートの設定
PPSInput (PPS_SDI1 ,PPS_RP5);
PPSOutput(PPS_RP6 ,PPS_SDO1);
PPSOutput(PPS_RP21 ,PPS_SCK1OUT);
PPSOutput(PPS_RP20 ,PPS_SS1OUT);
次に、ADXL345のCSピンを初期化します。ADXL345データシートの、SPI通信の項目に、以下の説明があります。
CSはシリアル・ポート・イネーブル・ラインであり、SPI マスターが制御します。図36 に示すように、このラインは、伝送開始時にローレベル、伝送終了時にハイレベルにする必要があります。
(ADXL345データシートから引用)
つまり、SPI通信中はCSピンをロー、通信していないときはハイにする必要があるということ。そしてADXL345側は常にスレーブになるので、CSピンはマスターのPIC側で制御する必要があります。PIC起動時は、もちろんSPI通信中ではないので、CSピンはハイにしておきます。
SPI通信部分の実装
そしていよいよSPIを初期化します。ここでもし設定を間違えると、マスター/スレーブ間で息のあった通信ができず、正しいデータのやり取りができません。今回はスレーブ側のADXL345にPIC側が合わせる形になるので、ADXL345のデータシートをよく読む必要があります。SPI通信を行う上で特に注意すべきポイントは、以下の3点です。
- クロックのエッジでサンプリングするか、中央でサンプリングするか
- クロック立ち上がりでサンプリングするか、立ち下がりでサンプリングするか
- クロックの通常時はLowか、Highか
ADXL345のデータシートを見ると、
データは、SCLKの立上がりエッジでサンプリングしてください。
(DXL345データシートから引用)
—とあります。また、同じくデータシートのSPIタイミング図のクロック波形(赤枠で囲まれているところ)を見てみると、Active時がLowであることが分かります。
ちなみにクロックのサンプリングタイミングや、通常時がHighかLowかの表現方法は、各社でまったく異なる表現を使っています。ADXL345のデータシートのような説明方法もありますし、ModeやCKP、CKEなどの単語を用いて表現する会社もあります。SPIを調査する上で、私自身も非常に混乱しましたので、調査中に見た表現方法を以下にまとめておきます。ADXL345の場合はMode3、またはMode1,1、もしくはCKP=1,CKE=0となります。非常に紛らわしいですね。
表現方法 | 意味 | ||
---|---|---|---|
Mode1 | Mode0,1 | CKP=0 CKE=0 | AクロックがHighのときをActiveとする。 クロックがActiveのときにサンプリング。 |
Mode3 | Mode1,1 | CKP=1 CKE=0 | クロックがLowのときをActiveとする。 クロックがActiveのときにサンプリング。 |
Mode0 | Mode0,0 | CKP=0 CKE=1 | クロックがHighのときをActiveとする。 クロックがIdleのときにサンプリング。 |
Mode2 | Mode1,0 | CKP=1 CKE=1 | クロックがLowのときをActiveとする。 クロックがIdleのときにサンプリング。 |
以上のことから、SPIの設定を以下のように行いました。
OpenSPI1(
// config1
ENABLE_SCK_PIN & // SPICLKを有効にする
ENABLE_SDO_PIN & // SDOピンをモジュールで制御する
SPI_MODE8_ON & // データ長を8bitとする
SPI_SMP_ON & // クロックのエッジでサンプリング
SPI_CKE_OFF & // クロックActive遷移時にデータ変化
SLAVE_ENABLE_OFF & // SSをモジュールで制御しない
CLK_POL_ACTIVE_LOW & // CLKをActive = Lowとする
MASTER_ENABLE_ON & // Masterとして使う
SEC_PRESCAL_2_1 &
PRI_PRESCAL_1_1,
// config2
FRAME_ENABLE_OFF,
// config3
SPI_ENABLE &
SPI_IDLE_CON &
SPI_RX_OVFLOW_CLR
);
SPIの設定ができたところで、SPIの書き込みと読み込みの部分を関数化しておこうと思います。まず、書き込み関数は以下のように実装しました。
// SPI書き込み
void WriteRegister(char registerAddress, char value)
{
unsigned char dummy = 0;
// SPI開始時にLOW
_RC4 = 0;
// 書き込みアドレス指定
putsSPI1(1, &registerAddress);
Delay(5);
dummy = getcSPI1();
// 書き込み
putsSPI1(1, &value);
Delay(5);
dummy = getcSPI1();
// SPI終了時にHIGH
_RC4 = 1;
}
SPI書き込み関数は、ADXL345レジスタへの書き込みに使用します。最初に書き込み予定のレジスタアドレスを送信し、その後に書き込む値を送信するため、2回のSPI通信が発生します。SPI通信はデータを1byte送信すると、相手からも1byteのデータが送られてきますので、上記ソースコードのようにputをした後にgetを行う必要があります。最初はputのみを記述していたのですが、どうも受信したデータは1度getしに行かないと、次回のputが正常にできないようです。
今回は、データ送信にputsSPI1()、受信にgetcSPI1()を使用しました。これらの関数はspi.hで定義されているものです。そのほかにも、SPIに関する送受信関数には以下のものがあります。
- riteSPI1() ・・・ SPIバッファにデータを書き込む
- ReadSPI1() ・・・ SPIバッファからデータを読み込む
- putcSPI1() ・・・ WriteSPI1()のマクロ
- getcSPI1() ・・・ ReadSPI1()のマクロ
- putsSPI1() ・・・ 複数バイトのデータをSPIバッファに書き込む
- getsSPI1() ・・・ 複数バイトのデータをSPIバッファから読み込む
putとgetの間に適当なDelayを入れていますが、これがないとタイミングの問題で正しく動作してくれません。本来は、DataRdySPI1()などを使用してデータ受信フラグを監視すべきだと思うのですが、このフラグの使い方が、いまいちよくわかりませんでした。たとえばputとgetの間に以下のようなコードを入れると、無限ループに陥ってしまいます。
While(!DataRdySPI1());
この原因を調べてみると、データ受信フラグ(送信完了フラグ)はバッファからデータを読み出すまでクリアされないらしいのですが、どうもそんな動きはしていないような・・・? オシロスコープで見てみたり、デバッガでレジスタをダンプしてみたりと、いろいろ試行錯誤をしたのですが、結局謎は解けずじまい。・・・というわけで今回はDataRdySPI1()を使うのは諦めて、Delayで対応することにしました。
ところで、なぜ送信にput”s”SPI1()を使っているのに、受信にはget”s”SPI1()ではなくget”c”SPI1()を使用しているのか、疑問に思った方もおられると思います。じつはget”s”SPI1()の実装を見てみると、内部でDataRdySPI1()が使われています。そのため、get”s”SPI1()を使った場合も、この部分で無限ループに陥ってしまいました。このような理由で、今回は最終的にput”s”SPI1()とget”c”SPI1()の組み合わせで落ち着いています。
インターネット上でSPI通信を検索すると、正しくDataRdySPI1()を使用できている例もありましたので、他シリーズのPICでは正しく動くのかもしれません。DataRdySPI1()の正しい使い方に関しては、今後の課題にしておきます。
同じようにして、読み込み関数も実装しました。読み込み関数はADXL345のデータレジスタから加速度データを読み出すときに使用します。
// SPI読み込み
void ReadRegister(char registerAddress, int numBytes)
{
char dat[10];
unsigned int i = 0;
unsigned char dummy = 0x00;
char dummyDat;
// 複数バイト読み出し時のお作法
char address = 0x80 | registerAddress;
if(numBytes > 1)address = address | 0x40;
// SPI開始時にLOW
_RC4 = 0;
// 読み出し先アドレスの指定
putsSPI1(1, &address);
Delay(5);
dummyDat = getcSPI1();
Delay(5);
// 値の読み出し
for(i = 0; i < numBytes; i++)
{
putsSPI1(1, &dummy);
Delay(5);
dat[i] = getcSPI1();
Delay(5);
}
// SPI終了時にHIGH
_RC4 = 1;
for(i = 0; i < numBytes; i++)
{
Adxl345_buf[i] = dat[i];
}
}
読み出し時も読み出し先レジスタアドレスを送信後、データを読み出すことになります。ただし、加速度計のデータシートによると、複数バイトの読み出しを行う場合はレジスタアドレス指定時のお作法があるようなので、それに従ってレジスタアドレス値を変更しておきます。読み出したデータは、グローバルに定義したバッファに格納するようにしました。
ようやくこれで、SPI通信のための準備は完了です(ここまで来るのに、実際は3日くらいかかりました)。
加速度計ADXL345の、レジスタ設定
続いて、加速度計ADXL345の初期化を行います。今回はとりあえずの動作確認なので、細かい設定は行いません。データレンジ±16gの10bit深度。低消費電力モードで50Hzのサンプリングレートとしてみました。
// ADXL-345初期化
// 加速度センサレジスタ設定
WriteRegister(DATA_FORMAT, 0x03); // +-16g 10bit
WriteRegister(BW_RATE, 0x19);
WriteRegister(POWER_CTL, 0x08); //Measurement mode
さっそく、先ほど実装したWriteRegister()が活躍しています。ADXL345は、この他にも割り込み設定、割り込みのしきい値設定、スリープ/スタンバイモードの切り替えなどの設定も、レジスタへの書き込みで行うことができます。
加速度モニタリング部分の実装
最後に、PICのメインループを実装します。ADXL345から加速度を読み出し、Z方向に5.0G以上の加速度が検出された場合に、LEDをONするようにしています。
// メインループ
while(1)
{
// 加速度受信
ReadRegister(DATAX0, 6);
// 加速度計算
Adxl345_x = ((int)Adxl345_buf[1]<<8)|(int)Adxl345_buf[0];
Adxl345_y = ((int)Adxl345_buf[3]<<8)|(int)Adxl345_buf[2];
Adxl345_z = ((int)Adxl345_buf[5]<<8)|(int)Adxl345_buf[4];
Adxl345_xg = Adxl345_x * 0.03125;
Adxl345_yg = Adxl345_y * 0.03125;
Adxl345_zg = Adxl345_z * 0.03125;
// Z方向に5g以上の加速度を検知したらLED点灯
if(Adxl345_zg >= 5.0)
{
LATBbits.LATB10 = 0;
}
}
加速度のデータは、ADXL345のDATAX0(0x32)~DATAZ1(0x37)のレジスタに格納されています。このレジスタは最初に設定したサンプリングレートで更新され、更新されたときにDATA_READYの割り込みが飛ぶようになっています。今回は特に割り込みは使用していませんが、割り込みを使えば、データ更新時にのみ加速度を取得するようなコードにすることも可能です。
データレジスタから加速度の生データを読み出した後は、データを意味のある値(加速度)に変換する必要があります。加速度データはデータレンジ±16gの10bit深度で来ているはずなので、まずは2byteに分かれているデータをくっつけます。その後、スケール係数を乗算する必要がありますが、スケール係数Gsは以下の式で与えられます。
Gs = [データレンジ幅]/2^[データビット深度] = 16 * 2 / 2^10 = 0.03125
ここまで来て、ようやく加速度計から加速度データを取得することができます。
以上のプログラムを書き込んで、実際に動かしてみました。電源を入れても何も起こらないのですが、基盤を手に持って軽く振ってみると、無事にLEDが点灯しました!
・・・ということで、PICからADXL345の基本的な制御は、これで完成です。加速度計からの割り込みも利用すれば、いろいろなことに使えそうですね。
今回ご紹介したケースと全く同じデバイス/回路図でSPIを使われる人がどのくらい存在するのかはわかりませんが、動作させたソースコードをダウンロードできるようにしておきます。