Arduino Uno / atmega328p の SPI Slave ライブラリを自作する

## 目標

表題の通り。SPI 関係のレジスタをいい感じにいじってくれて、データ受信処理をいい感じにやってくれる、そんなライブラリを手作りします。

結果としては Arduino のライブラリ不使用で済んだので、Arduino でない素の AVR マイコンでも使えるものができました。

:::note warn
**Arduino 標準の SPI ライブラリ `SPI.h` は Master 動作のみ**

`SPI.h` では Slave 動作をさせることができません。AVR レジスタを直接いじる、または何らかのライブラリを導入する必要があります。
:::

:::note
レジスタ: (ここでは) マイコンの各種機能を制御できる指定のメモリ領域

「マイコン レジスタ」で調べれば解説が出るので、ここでは詳しくは扱いません。
:::

## 前提知識

* C の基本文法が分かる
* マイコンのレジスタが何なのか、また扱い方が分かる
* 割り込みの概念を初歩レベルは分かる
* C/C++ のヘッダファイルの書き方、ソースの複数ファイル分割のやり方が分かる

## 雑に設計検討

```plantuml
@startuml
skinparam componentStyle rectangle

component main

component Library {
  port spislave_buf_remain
  port spislave_fetch
  component FIFO
  "SPI Receive Interrupt" -up--> FIFO
}

spislave_buf_remain --> main
spislave_fetch -up--> main

FIFO --> spislave_buf_remain
FIFO --> spislave_fetch
@enduml
```

イメージとしては、Arduino の `Serial` の受信処理みたいな感じ。`Serial.available()` で受信バッファのデータ数、`Serial.read()` でデータを 1つ取得できる、というやつ。

FIFO データ構造を活用すれば、難しい要素は無さそうです。

:::note
FIFO: First In First Out

データの格納について、先に入れられたものが先に取り出される方式のデータ構造。感覚としては店などの行列待ちと同じ。
:::

## SPI 制御のレジスタ

データシートからレジスタの情報を確かめます。

https://www.microchip.com/en-us/product/ATMEGA328P

SPI 関係で主なレジスタは `SPCR` (140ページ) です。

| レジスタ | 意味 | 設定値 |
| :---: | --- | :---: |
| SPIE | 1 で SPI の割り込みが有効 | 1 |
| SPE | 1 で SPI の機能全体が有効 | 1 |
| DORD | データの伝送方式設定 | 0 |
| MSTR | 0 で Slave, 1 で Master 動作 | 0 |
| CPOL | データの伝送方式設定 | 0 |
| CPHA | *同上* | 0 |
| SPR1 | SPI クロック周波数設定 | 0 |
| SPR2 | *同上* | 0 |

※ いずれも初期値 0, 読み込み/書き込み可

* データ伝送方式は、初期状態 (全部 0) が最も標準的な方式です
  * SPI の伝送方式の種類については省略
* SPI クロック設定は、Slave 動作時は無効です
  * Master 側がクロックを制御するため

以上から、`SPCR` の設定は次のようにすればよいことになります。

```c
SPCR = 0b11000000;
```

それからもう一つ、`SPDR` というレジスタがあります。これは送受信データのやりとりをする領域です。受信したら、ここを読み取れば良いわけです。

```c
int value = SPDR;
```

## SPI 受信割り込み

割り込みについて詳しく踏み込むと、ベクターがどうこう･･･という話になって少し難しいです。ここは AVR 標準ライブラリに頼ります。

以下のように書けば、「SPI 受信時にこの動作をする」というのが指定できます。

```c
#include <avr/interrupt.h>

ISR(SPI_STC_vect) {
  // 受信時の処理を書く
}
```

## FIFO の設計

難しく身構える必要はなく、単純な配列をうまく扱えば簡単に実装できます。イメージはこんな感じ。

![fifo-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540894/3ba8aca8-3018-f198-75fa-067fcc13154f.png)

1つの配列上に 2つのカーソルを走らせることで FIFO を実現します。あとは空読み、ストック数限界超えの処理を加えれば OK。

今回は最大 256 個まで格納できる設計にします。

```c
#define FIFO_SIZE 256

uint8_t fifo_body[FIFO_SIZE];
uint16_t fifo_remain = 0;
uint8_t fifo_enque_cursor = 0;
uint8_t fifo_deque_cursor = 0;

void fifo_enque(uint8_t data) {
  if(fifo_remain < FIFO_SIZE) {
    fifo_body[fifo_enque_cursor] = data;
    fifo_remain++;

    // Cursor back
    if(fifo_enque_cursor < (FIFO_SIZE - 1))
      fifo_enque_cursor++;
    else
      fifo_enque_cursor = 0;
  }
}


uint8_t fifo_deque() {
  uint8_t ret_tmp = 0;

  if(fifo_remain > 0) {
    ret_tmp = fifo_body[fifo_deque_cursor];
    fifo_remain--;

    // Cursor back
    if(fifo_deque_cursor < (FIFO_SIZE - 1))
      fifo_deque_cursor++;
    else
      fifo_deque_cursor = 0;
  }

  return ret_tmp;
}
```

## `static` によるカプセル化

ライブラリ外から FIFO のパラメータ (配列の書き込み・読み出し位置など) はいじられたくありません。FIFO データ構造に関わる部分に `static` をつけて、スコープをファイル内のみにする、つまりライブラリ外からいじれないようにします。

`static` によるスコープの制限は、例えば以下のように働きます。

```main.c
#include "any_lib.h"

int main() {
  external_func();  // 使える

  internal_func();  // 使えない
  internal_val = 1; // アクセスできない

  return 0;
}
```

```any_lib.h
#ifndef ANY_LIB_H
#define ANY_LIB_H

void external_func(); // プロトタイプ宣言

#endif /* ANY_LIB_H */
```

```any_lib.c
#include "any_lib.h"

static int internal_val = 1;

static void internal_func();  // プロトタイプ宣言

void external_func() {
  internal_func();  // 使える
  internal_val = 2; // アクセスできる
}

static void internal_func() {
}
```

## 完成品

レジスタ設定、受信時の割り込み処理定義、FIFO の構築、それからライブラリ外とやりとりする関数を実装して完成です。

<details><summary>C ソース</summary>

:::note warn
~~PlatformIO の Arduino ボードのプロジェクトで使う場合、拡張子を `.c` でなく `.cpp` にする必要があります。`.h` はそのままで良いです。~~

ヘッダファイルの `extern "C"` により、`.c` で問題無いことに気付きました (詳細後述)。
:::

```spi_slave.c
/**
 * @file spi_slave.c
 *
 * @author aKuad
 *
 * @copyright CC0
 */

#include "spi_slave.h"

static uint8_t fifo_body[SPISLAVE_BUF_SIZE];
static uint16_t fifo_remain = 0;
static uint8_t fifo_enque_cursor = 0;
static uint8_t fifo_deque_cursor = 0;

static void fifo_enque(uint8_t data);
static uint8_t fifo_deque();


/**
 * @brief Init SPI config registers
 */
void spislave_init() {
  SPCR = 0b11000000;  // SPI interrupt enable, SPI operation enable, slave mode
}


/**
 * @brief Return buffered data count
 * @return Buffered data count
 */
uint16_t spislave_buf_remain() {
  return fifo_remain;
}


/**
 * @brief Fetch data from internal FIFO buffer
 * @return Data value
 */
uint8_t spislave_fetch() {
  return fifo_deque();
}


/* On data receive interrupt */
ISR(SPI_STC_vect) {
  fifo_enque(SPDR);
}


/**
 * @brief Internal FIFO buffer enqueue
 * @param[in] data Data to enqueue
 */
static void fifo_enque(uint8_t data) {
  if(fifo_remain < SPISLAVE_BUF_SIZE) {
    fifo_body[fifo_enque_cursor] = data;
    fifo_remain++;

    // Cursor back
    if(fifo_enque_cursor < (SPISLAVE_BUF_SIZE - 1))
      fifo_enque_cursor++;
    else
      fifo_enque_cursor = 0;
  }
}


/**
 * @brief Internal FIFO buffer dequeue
 * @return Dequeued data
 */
static uint8_t fifo_deque() {
  uint8_t ret_tmp = 0;

  if(fifo_remain > 0) {
    ret_tmp = fifo_body[fifo_deque_cursor];
    fifo_remain--;

    // Cursor back
    if(fifo_deque_cursor < (SPISLAVE_BUF_SIZE - 1))
      fifo_deque_cursor++;
    else
      fifo_deque_cursor = 0;
  }

  return ret_tmp;
}
```
</details>

<details><summary>ヘッダ</summary>

:::note
(詳細は省きますが) `extern "C"` があることによって、C++ からでも C のソースが使えるようになります。
:::

```spi_slave.cpp
/**
 * @author aKuad
 *
 * @copyright CC0
 */
#ifndef SPI_SLAVE_H
#define SPI_SLAVE_H

#include <avr/io.h>
#include <avr/interrupt.h>

#define SPISLAVE_FIFO_SIZE 256

#ifdef __cplusplus
extern "C" {
#endif

void spislave_init();
uint16_t spislave_buf_remain();
uint8_t spislave_fetch();

#ifdef __cplusplus
}
#endif

#endif /* SPI_SLAVE_H */
```

</details>

<details><summary>ライブラリ使用例</summary>

```cpp
/**
 * @author aKuad
 *
 * @copyright CC0
 */

#include "spi_slave.h"

void setup() {
  spislave_init();
  Serial.begin(9600);
}

void loop() {
  if(spislave_buf_remain()) {
    Serial.println(spislave_fetch());
  }
  delay(1);
}
```

</details>