LoginSignup
1
1

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

Last updated at Posted at 2023-11-12

目標

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

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

Arduino 標準の SPI ライブラリ SPI.h は Master 動作のみ

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

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

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

前提知識

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

雑に設計検討

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

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

FIFO: First In First Out

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

SPI 制御のレジスタ

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

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 の設定は次のようにすればよいことになります。

SPCR = 0b11000000;

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

int value = SPDR;

SPI 受信割り込み

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

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

#include <avr/interrupt.h>

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

FIFO の設計

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

fifo-image.png

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

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

#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 の構築、それからライブラリ外とやりとりする関数を実装して完成です。

C ソース

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;
}
ヘッダ

(詳細は省きますが) 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 */
ライブラリ使用例
/**
 * @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);
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1