目標
表題の通り。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 の設計
難しく身構える必要はなく、単純な配列をうまく扱えば簡単に実装できます。イメージはこんな感じ。
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
によるスコープの制限は、例えば以下のように働きます。
#include "any_lib.h"
int main() {
external_func(); // 使える
internal_func(); // 使えない
internal_val = 1; // アクセスできない
return 0;
}
#ifndef ANY_LIB_H
#define ANY_LIB_H
void external_func(); // プロトタイプ宣言
#endif /* ANY_LIB_H */
#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
で問題無いことに気付きました (詳細後述)。
/**
* @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 のソースが使えるようになります。
/**
* @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);
}