なぜだか分からないが、ArduinoのライブラリはSPIのSlaveに対応していない。I2CはSlaveでも使えるのに何故なんだろう?
今まで素のAVRも含めてSPIのスレーブとして使ったことが無かったので、直接レジスタを叩いてやってみた。
Arduino UnoをSlaveにする
配線図
写真のオフィシャルのArduino Uno Rev3(緑色)をMaster、aitendoのびんぼうでいーの(黄土色)をSlaveとして使っている。
SPIの信号線の間にブレッドボードを入れているのはオシロで測定しやすくするためで、必要がなければArduinoのPin同士を直結した方がいい。以前mbedでSPIの接続を悪条件にしてテストした時エラーが多発した。(参考「
Nucleo(mbed OS5)のSPI通信を検証してみる。」)
動作としては、Masterから1ビットずつシフトした8bit値をSlaveに送り(MOSI)、Slaveは受信したデータを8個のLEDに表示する。LEDは流れるように点灯する。
Slaveは1づつインクリメントした8bit値をMasterに送り(MISO)、Master側は受信したデータをSerialで出力する。←Arduino IDEのシリアルモニタでMasterが受信したデータを表示できる。
※Arduinoを2個使うとArduino IDEで操るのがそこそこややこしいです(^q^;
Masterのスケッチ
SPI_Master_LEDx8.ino
#include <SPI.h>
#define SPI_CS_PIN (10)
uint8_t cnt = 1;
void setup()
{
Serial.begin(9600);
Serial.println("SPI_Master_LEDx8 Test");
pinMode(SPI_CS_PIN, OUTPUT);
digitalWrite(SPI_CS_PIN, HIGH);
SPI.begin();
}
void loop()
{
digitalWrite(SPI_CS_PIN, LOW);
uint8_t rdata = SPI.transfer(1 << cnt);
digitalWrite(SPI_CS_PIN, HIGH);
cnt++;
if (cnt > 8) {
cnt = 0;
}
Serial.println(rdata);
delay(100);
}
Master側のSPIの設定はデフォルトで、Clock:4MHz、MSB First、Mode:0になる。
Slaveのスケッチ
SPI_Slave_LEDx8
volatile uint8_t sdata = 0;
void setup()
{
// LEDs output
DDRD = 0xFF;
PORTD = 0xFF;
_delay_ms(100);
// LEDs check
for (int i = 0; i < 8; i++) {
PORTD = 1 << i;
_delay_ms(100);
}
PORTD = 0x00;
// SPI slave
DDRB = 0x10; // MISO(PB4)をoutputに設定
// SPCRのビット設定
// SPI有効:1 | SPI割り込み有効:1 | MSBから送信:0 | スレーブ:0
// クロック極性:0 | クロック位相:0 | SPR1:0 | SPR0:0
SPCR = (1 << SPIE) | (1 << SPE);
}
ISR (SPI_STC_vect)
{
// 受信データをLEDに出力
PORTD = SPDR;
// 次に送信するデータをセット
SPDR = sdata;
sdata++;
}
void loop()
{
}
Slave側のスケッチは、まぎれが無いように基本的にArduinoの関数は使っていない。
「//LED check」の部分は、起動時に接続しているLEDを順番に全点灯して配線ミスがないか確認できるようにしている。
ATMega328PのハードウェアSPIは
SCK: PB5 (D13)
MISO: PB4 (D12)
MOSI: PB3 (D11)
CS(SS): PB2 (D10)
を使うようになっていて、Slaveの場合MISOだけはPinの機能をOUTPUTに指定する必要があるので、
// SPI slave
DDRB = 0x10; // MISO(PB4)をoutputに設定
としている。
「ISR (SPI_STC_vect){}」は割り込みハンドラで、SPCRレジスタのSPIEビットを1に設定すると、SPI通信が完了したときに呼び出されるようになる。
SlaveからMasterに送信するデータはSPDRレジスタに設定すればよく、「ISR (SPI_STC_vect){}」内で、受信データをSPDRレジスタから読み出した後、SPDRに書き込んでいる。このデータは、次回SPI通信が行われたときにMISOから出力されてMasterに送信される。
SPI通信は割り込みで処理しているので、メインループの「loop()」では何もしていない。
SPI通信の状態
MOSI
ch1:MOSI ch2:SCK
SPIクロックは4MHz
MISO
ch1:MISO ch2:SCK
ATMega88VをSlaveにする
同じことをATMega328Pとピン互換で、安価なATMega88Vでもやってみた。
配線図
配線図ではatemega168となっているが、実際はATMega88Vを使って内蔵RC/8MHzで動作させている。
Fuse Bit
hFuse: DFh
lFuse: A2h (動作確認のためPB0からクロック出力)
eHuze: 01h
Slave(ATMega88V)のソースコード
Atmel Studio7 (Version: 7.0.1417)でビルド
/*
* SPI_Slave_LEDx8.c
*
* Created: 2017/08/26 9:16:53
* Author : gizmo
*/
#define F_CPU (8000000UL)
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
volatile uint8_t sdata = 0;
ISR (SPI_STC_vect)
{
// 受信データをLEDに出力
PORTD = SPDR;
// 次に送信するデータをセット
SPDR = sdata;
sdata++;
}
int main(void)
{
// LEDs output
DDRD = 0xFF;
PORTD = 0xFF;
_delay_ms(100);
// LEDs check
for (int i = 0; i < 8; i++) {
PORTD = 1 << i;
_delay_ms(100);
}
PORTD = 0x00;
// SPI slave
DDRB = 0x10; // MISO(PB4)をoutputに設定
// SPCRのビット設定
// SPI有効:1 | SPI割り込み有効:1 | MSBから送信:0 | スレーブ:0
// クロック極性:0 | クロック位相:0 | SPR1:0 | SPR0:0
SPCR = (1 << SPIE) | (1 << SPE);
sei(); // 割り込み許可
while (1)
{
}
}
基本的にはArduinoのスケッチと同じだが、ハマってしまった点。
#include <interrupt.h>
これを指定しなくても、Atmel Studio 7ではWarningは出るがBuild出来てしまう。「int ISR (SPI_STC_vect){}」という普通の関数と解釈してコンパイルしてしまうからだろう。プログラムの中で「ISR()」は呼び出されていないのでリンクも出来てしまう。
sei();
Arduinoでは(全体の)割り込みはデフォルトで許可されているが、素のAVRのプログラムでは明示的にsei()を呼び出して、割り込みを有効化しないとダメ。
また
SPIクロックも問題があって、Arduino同士で使ったMasterのスケッチではLEDの点灯動作がおかしくなった。
SlaveはATMega88Vを8MHz駆動させていて、SPIクロックの最大周波数はマスタークロックの1/2なので、Masterが送ってくるSPIクロックが4MHzだと処理が追いつかないんだと思う。
SPIクロックを1MHzにしたMasterのスケッチ
#include <SPI.h>
#define SPI_CS_PIN (10)
uint8_t cnt = 1;
void setup()
{
Serial.begin(9600);
Serial.println("SPI_Master_LEDx8 Test");
pinMode(SPI_CS_PIN, OUTPUT);
digitalWrite(SPI_CS_PIN, HIGH);
SPI.begin();
}
void loop()
{
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(SPI_CS_PIN, LOW);
uint8_t rdata = SPI.transfer(1 << cnt);
digitalWrite(SPI_CS_PIN, HIGH);
SPI.endTransaction();
cnt++;
if (cnt > 8) {
cnt = 0;
}
Serial.println(rdata);
delay(100);
}
どこが違うかというと、
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
として、SPIクロックを1MHzにしている点。SPI.beginTransaction()しているので、SPI通信の終了時にSPI.endtransmission()している。
SPIの通信状態
MOSI
ch1:MOSI ch2:SCK
SPIクロックは1MHz
MISO
ch1:MISO ch2:SCK
Github:
https://github.com/ryood/Arduino_SPI_Slave