Arduino MKR ZEROでSPIを使う際の注意点まとめ

Arduino MKR ZERO を購入したので ATsamd系 Arduino における SPI の使い方をまとめておきます。ここでは SPI が何者であるかについては書きません。

# ATmega系 Arduino との違い

Arduino UNO に代表される ATmega系 の Arduino とは SPI の利用方法にいくつか差異があります。

## SPI.beginTransaction を使う

MKR ZERO にて `setBitOrder`, `setClockDivider`, `setDataMode` を呼び出すとハングし処理が停止します。よって ATmega系 のスケッチをそのまま使用できず、書き換えが必要になります。

```c++
#include "SPI.h"
#define CS_PIN 7

void setup() {
  pinMode(CS_PIN, OUTPUT);
  SPI.setBitOrder(MSBFIRST);            // <--- ここでハング
  SPI.setClockDivider(SPI_CLOCK_DIV2);
  SPI.setDataMode(SPI_MODE0);
  SPI.begin();
}
...
```

正しく動作させるためには `SPI.beginTransaction` を用いて設定を書き込みます。
（後述するクロックの項でも触れます）

```c++
#include "SPI.h"
#define CS_PIN 7

void setup() {
  pinMode(CS_PIN, OUTPUT);
  SPI.begin();
}

void loop() {
  digitalWrite(CS_PIN, LOW);
  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
  SPI.transfer(0x42);
  SPI.endTransaction();
  digitalWrite(CS_PIN, HIGH);

  while (1) ;
}
```

## SSピン（CSピン）

ATmega系 Arduino には SPI の SS（スレーブセレクト, CS）ピンが存在しますが MKR ZERO には存在しません。よって SS として使用するピンを明示的に指定し制御しなくてはなりません。

`digitalWrite` はそのまま使えますが、速度を求めるならば直接レジスタの操作ができます。
以下に MKR ZERO における指定方法を示します。無論、以下のレジスタ指定は MKR ZERO（ATSAMD21）用になっており、ATmega系 Arduino とは指定方法が異なります。

```c++
// 以下3つとも7番ピンにHIGHを出力
// digitalWrite(7, HIGH);
// PORT->Group[g_APinDescription[7].ulPort].OUTSET.reg = (1ul << g_APinDescription[7].ulPin);
digitalPinToPort(7)->OUTCLR.reg = digitalPinToBitMask(7);

// (transfer)

// 以下3つとも7番ピンにLOWを出力
// digitalWrite(7, LOW);
// PORT->Group[g_APinDescription[7].ulPort].OUTSET.reg = (1ul << g_APinDescription[7].ulPin);
digitalPinToPort(7)->OUTSET.reg = digitalPinToBitMask(7);
```

## クロック

SPI のクロック周波数(Hz)は `SPI.beginTransaction` で指定します。
指定できるクロックは 12 MHz 以下かつ 48 MHz を整数分周したものとなり、それ以外は近い周波数に近似されます。

MKR ZEROの場合は周波数が高い順に 12 MHz, 8 MHz, 6 MHz, 4 MHz, ... となります。48 MHz や 24 MHz を指定すると 12 MHz のクロックが出力されます。

```c++
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));  // use 8 MHz as clock
SPI.transfer(0x42);
SPI.endTransaction();
```

また、従来の `SPI_CLOCK_DIV*` は互換性のために残されており**使用してはいけません**。以下は [SPI.h](https://github.com/arduino/ArduinoCore-samd/blob/0411b6b0679e8ebae6fc9974003a603ecec846ce/libraries/SPI/SPI.h#L178-L188) からの引用ですが、ここに注意書きがあります。

```c++
// For compatibility with sketches designed for AVR @ 16 MHz
// New programs should use SPI.beginTransaction to set the SPI clock
#if F_CPU == 48000000
  #define SPI_CLOCK_DIV2   6
  #define SPI_CLOCK_DIV4   12
  #define SPI_CLOCK_DIV8   24
  #define SPI_CLOCK_DIV16  48
  #define SPI_CLOCK_DIV32  96
  #define SPI_CLOCK_DIV64  192
  #define SPI_CLOCK_DIV128 255
#endif
```

## microSDカード

MKR ZERO には microSD スロットが搭載されており内部的には SPI で読み書きを行っているはずです。
しかし MKR ZERO から出ている SPI ピンには信号は出力されませんでした。

| ![microSD側](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/25481b7c-a7b0-91b5-3a8f-d0110310e273.png) | ![ピンアウト側](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/412cdfc0-802a-e73e-fe38-d0f0d829dc52.png) |
|:-:|:-:|
| **microSDスロット側** | **ピンアウト側** |

[公式の回路図](https://store.arduino.cc/usa/arduino-mkrzero)を見ると microSD スロットとピンアウトの SPI は使用ポートが違うようです。
また、[MKR ZERO用のヘッダファイル](https://github.com/arduino/ArduinoCore-samd/blob/92efd74540bc834d228c0364cf6112488df3a4d3/variants/mkrzero/variant.h#L92-L128)を見るとソフトウェア側も別のポートが指定されています。

MKR ZERO のスペック表を見ると `SPI: 1` となっていますが、これは microSD スロットはカウントされていないようです。


# 転送方法による違い
## SPI.transfer

上記の注意点をまとめると以下のスケッチで SPI を使用できます。

```c++
#include "SPI.h"
#define CS_PIN 7
#define DATA_LENGTH 4
#define TRANSFER_LENGTH 4

uint8_t source_memory[DATA_LENGTH];

void setup() {
  pinMode(CS_PIN, OUTPUT);
  SPI.begin();
}

void loop() {
  source_memory[0] = 0x42;
  source_memory[1] = 0x9F;
  source_memory[2] = 0x35;
  source_memory[3] = 0x0C;

  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));

  digitalPinToPort(7)->OUTCLR.reg = digitalPinToBitMask(7);
  SPI.transfer(source_memory, TRANSFER_LENGTH);
  digitalPinToPort(7)->OUTSET.reg = digitalPinToBitMask(7);

  SPI.endTransaction();
}
```

## DMA

`SPI.transfer` を使った通信では通信完了までブロックされます。この挙動は ATmega系 Arduino も同じです。MKR ZERO では SPI 転送に DMA を利用できるため、特に多バイト転送では転送効率が上がります。

ライブラリは [**Adafruit_ZeroDMA**](https://github.com/adafruit/Adafruit_ZeroDMA) が使えます。
ただしサンプルプログラムは Arduino ZERO 用ですので `sercom4` -> `sercom1` と読み替えます。

```c++
#include <Adafruit_ZeroDMA.h>
#include "SPI.h"
#include "utility/dma.h"
#define CS_PIN 7
#define DATA_LENGTH 4
#define TRANSFER_LENGTH 4

Adafruit_ZeroDMA myDMA;
uint8_t source_memory[DATA_LENGTH];
volatile bool transfer_is_done = false;

void dma_callback([[maybe_unused]] Adafruit_ZeroDMA *dma) {
  // 転送完了のコールバックで CS は HIGH (disable)
  digitalPinToPort(7)->OUTSET.reg = digitalPinToBitMask(7);
  transfer_is_done = true;
}

void setup() {
  pinMode(CS_PIN, OUTPUT);
  SPI.begin();
  myDMA.setTrigger(SERCOM1_DMAC_ID_TX);
  myDMA.setAction(DMA_TRIGGER_ACTON_BEAT);
  myDMA.allocate();

  myDMA.addDescriptor(source_memory,                     // move data from here
                      (void *)(&SERCOM1->SPI.DATA.reg),  // to here (M0)
                      TRANSFER_LENGTH,     // this many...
                      DMA_BEAT_SIZE_BYTE,  // bytes/hword/words
                      true,                // increment source addr?
                      false);              // increment dest addr?
  myDMA.setCallback(dma_callback);
}

void loop() {
  source_memory[0] = 0x42;
  source_memory[1] = 0x9F;
  source_memory[2] = 0x35;
  source_memory[3] = 0x0C;
  transfer_is_done = false;

  SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));

  digitalPinToPort(7)->OUTCLR.reg = digitalPinToBitMask(7);
  myDMA.startJob();
  while (!transfer_is_done) ;

  SPI.endTransaction();
}
```

## 比較
最後に、CLK = 8 MHz 時の書き込みについてオシロで波形を確認しながら比較してみました。

### 4バイトのとき

4バイト転送時は DMA よりもわずかに `SPI.transfer` が速いです。しかし1バイトずつ送っているため、実質の転送時間は DMA のほうが既に高速です。

|| SPI.transfer | DMA |
|---|---|---|
|**転送時の波形**|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/11af0429-bbd3-21a1-74a6-3373cb40f2e8.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/743bfb65-6dac-9d44-870f-b235d8a879de.png)|
|**総転送時間**|10.44 μs|11.32 μs|
|**CS立ち下がりからCLK開始**|1.34 μs|4.06 μs|
|**CLK終了からCS立ち上がり**|1.30 μs|3.50 μs|
|**実質転送時間**|**7.80 μs**|**3.76 μs**|
|**ビットレート**|**512.8 kbps**|**1,063.8 kbps**|

### 1024バイトのとき

転送サイズが大きいほど DMA のほうが高速になります。
波形画像はどちらも同じ時間幅（500μs/div）です。時間あたりの転送量は DMA のほうが多くなっています。

|| SPI.transfer | DMA |
|---|---|---|
|**転送時の波形**|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/e28c8567-75ea-9341-4821-53a2aa72dc38.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30370/17ec1f47-c4ef-93ae-3e96-c0e36b6d1d0f.png)|
|**総転送時間**|2.365 ms|1.025 ms|
|**CS立ち下がりからCLK開始**|1.30 μs|3.90 μs|
|**CLK終了からCS立ち上がり**|1.26 μs|1.94 μs|
|**実質転送時間**|**2.362 ms**|**1.019 ms**|
|**ビットレート**|**433.5 kbps**|**1,004.9 kbps**|