2017年8月26日土曜日

Arduino UnoをSPI Slaveとして使う。

なぜだか分からないが、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

0 件のコメント:

コメントを投稿