しまねソフト研究開発センター(略称 ITOC)にいます、東です。
mruby/cペリフェラルライブラリのSTM32マイコンへの実装の記事、今回はその第6回、SPIクラスを実装します。
目標
SPIクラスのAPIガイドライン に従って、STM32マイコン(Nucleo F401RE) 向けの実装を完了させる。
今回の方針
- ボード上のシルク SCK,MISO,MOSI は無視して CN7の1,2,3 ピンを使う
- CubeIDEで設定したユニット SPI3 のみサポートする
- SPIマスタ、8ビットモードのみサポートする(ガイドラインがそのように定義)
- チップセレクトはサポートしない(ガイドラインがそのように定義)
- C言語のみで実装する
SPIユニットの仕様と、使用方法調査
当ボードに搭載されている STM32F401RET6 は、3つのSPIユニットを持っています。
DS10086: Table 9. Alternate function mapping を参照すると、ボード上のシルク SCK,MISO,MOSI に対応するのは、SPI1ユニットであることが分かりますが、SPI1のSCKを割り当てられる PA5とPB3 ピンは、このボードではそれぞれ LED, SWOへの固定的な割り当てがなされており、CubeIDE上でも SPI1ユニットは使用不可である旨が表示されます。よって、シルク印刷は無視することにして、SPI3ユニットを使う事にします(既に第1回で、そのように割り当てています)
CN | pin | SILK | GPIO | Usage |
---|---|---|---|---|
CN7 | 1 | PC10 | SPI3_SCK | |
CN7 | 2 | PC11 | SPI3_MISO | |
CN7 | 3 | PC12 | SPI3_MOSI |
また、同資料によるとピン割り当ての自由度があまり無いので、I2Cと同様、ピンアサイン対応はやめてピン番号は決め打ちで実装します。
クロックは、ベースが 42MHz で、これをプリスケーラによって分周した値が使われます。分周は、 $ F = 1 / 2^n $ (n=1..8) であるので、$ 1/2 $ から $ 1/256 $ までとなり、ガイドラインが規定するデフォルト値 (1MHz) は設定することができません。よって、デフォルト値は1MHzを超えない値の中で最大値、42MHz / 64 ≃ 656kHz とします。
HALライブラリ調査
メーカー製 HAL リファレンスマニュアル (UM1725) や、CubeIDEでのコード自動生成により、HALでSPIを使う方法を調査します。
その結果、以下の手順に従えば実現できそうであることがわかりました。
- ポーリング、割り込み、DMAの3種類の方法があるが、当面は最も簡単に使えるポーリングモードで実装すればよさそう
- 送信 (HAL_SPI_Transmit) 、受信 (HAL_SPI_Receive)、送受信 (HAL_SPI_TransmitReceive) が用意されている
- モード設定等は、HAL_SPI_Init() 関数を使えば、周波数、モード(CPOL,CPHA)、MSB or LSB first 等、設定できる
- モード設定・変更時は、SPIをDISABLEにしてから変更することが必要。(RM0368)
作業手順
では、実際の作業に入ります。
SPI3の初期パラメータの調整
ガイドラインで示されたデフォルト値に、CubeIDE を使って設定します。
Project Explorer から、(プロジェクト名).ioc をダブルクリックし、コンフィグレーション画面を開き、Connectivity > SPI3 をクリックします。
Parameter Settings 画面が表示されるので、Clock Parameters 欄を以下の通り設定します。
雛形の作成
GPIOクラス実装編 と同様に、雛形のファイルを用意してから、これに肉付けしていきます。
#include "main.h"
#include "../mrubyc_src/mrubyc.h"
extern SPI_HandleTypeDef hspi3;
static void c_spi_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
}
static void c_spi_setmode(mrbc_vm *vm, mrbc_value v[], int argc)
{
}
static void c_spi_read(mrbc_vm *vm, mrbc_value v[], int argc)
{
}
static void c_spi_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
}
static void c_spi_transfer(mrbc_vm *vm, mrbc_value v[], int argc)
{
}
void mrbc_init_class_spi(void)
{
mrbc_class *cls = mrbc_define_class(0, "SPI", 0);
mrbc_define_method(0, cls, "new", c_spi_new);
mrbc_define_method(0, cls, "setmode", c_spi_setmode);
mrbc_define_method(0, cls, "read", c_spi_read);
mrbc_define_method(0, cls, "write", c_spi_write);
mrbc_define_method(0, cls, "transfer", c_spi_transfer);
mrbc_set_class_const(cls, mrbc_str_to_symid("MSB_FIRST"), &mrbc_integer_value(0));
mrbc_set_class_const(cls, mrbc_str_to_symid("LSB_FIRST"), &mrbc_integer_value(1));
}
SPIも、今後作る他の機能へ提供する機能は無いので、簡略化のためにCヘッダファイルを作りません。
また、ガイドラインで規定されたクラス定数 MSB_FIRST, LSB_FIRST を初期化関数内で定義します。数値の0, 1 に深い意味は無く、識別できれば良いです。
次に、start_mrubyc()
関数へ、今回作成したSPI初期化用関数 mrbc_init_class_spi() をコールするよう書き足します。
/*! mruby/c プログラムの実行開始
*/
void start_mrubyc( void )
{
mrbc_init(memory_pool, MRBC_MEMORY_SIZE);
// 各クラスの初期化
...snip...
void mrbc_init_class_spi(void); // 追加
mrbc_init_class_spi(); // 追加
この段階で、一度正しくビルドができるか確認します。
さらに注意深く確認するには、Rubyスクリプトでも SPI クラスが使えるようになったかを確認するコードを書いておくと良いと思います。
spi = SPI.new()
puts "DONE"
コンストラクタの実装
今回の方針では、SPI3ユニットしか使わないので、I2Cと同様にコンストラクタは不要にしたいところですが、モード設定パラメータがあるので、そういうわけにもいきません。一方、setmode
メソッドも存在し、コンストラクタと全く同じキーワードでSPIの動作モードを設定します。
ですので、コンストラクタではインスタンスの生成のみ行い、モード設定は setmode
メソッドへ委譲します。
static void c_spi_new(mrbc_vm *vm, mrbc_value v[], int argc)
{
// allocate instance
v[0] = mrbc_instance_new(vm, v[0].cls, 0);
c_spi_setmode( vm, v, argc );
}
setmode メソッドの実装
SPIのモード設定、周波数設定等を行うメソッドで、キーワード引数のみ受け付けるよう設計されています。
キーワード引数
param | type | description |
---|---|---|
unit | --- | SPIユニットの指定 |
frequency | Integer | 周波数 (default 1MHz) |
mode | Integer | 0 to 3 (default 0) |
first_bit | Constant | SPI::MSB_FIRST or SPI::LSB_FIRST (default MSB_FIRST) |
setmode メソッドでは、これらキーワード引数を取得して、実際の処理は spi_setmode 関数を作って委譲します。
static void c_spi_setmode(mrbc_vm *vm, mrbc_value v[], int argc)
{
MRBC_KW_ARG( unit, frequency, mode, first_bit );
if( !MRBC_KW_END() ) goto RETURN;
int32_t spi_freq = -1;
int spi_mode = -1;
int spi_first_bit = -1;
if( MRBC_KW_ISVALID(frequency) ) spi_freq = mrbc_integer(frequency);
if( MRBC_KW_ISVALID(mode) ) spi_mode = mrbc_integer(mode);
if( MRBC_KW_ISVALID(first_bit) ) spi_first_bit = mrbc_integer(first_bit);
if( spi_setmode(&hspi3, spi_freq, spi_mode, spi_first_bit) < 0 ) {
mrbc_raise(vm, MRBC_CLASS(ArgumentError), 0);
}
RETURN:
MRBC_KW_DELETE( unit, frequency, mode, first_bit );
}
spi_setmode関数
//================================================================
/*! set SPI mode and clock
@param hspi pointer to HAL SPI_Handle
@param freq clock frequency (Hz)
@param mode mode (0..3)
@param msb_lsb first bit msb = 0 or lsb = 1
@return zero is no error.
*/
static int spi_setmode( SPI_HandleTypeDef *hspi,
int32_t freq, int mode, int msb_lsb )
{
static uint32_t const TBL_PRESCALER[] = {
SPI_BAUDRATEPRESCALER_2, SPI_BAUDRATEPRESCALER_4,
SPI_BAUDRATEPRESCALER_8, SPI_BAUDRATEPRESCALER_16,
SPI_BAUDRATEPRESCALER_32, SPI_BAUDRATEPRESCALER_64,
SPI_BAUDRATEPRESCALER_128, SPI_BAUDRATEPRESCALER_256,
};
static const int NUM_TBL_PRESCALER = sizeof(TBL_PRESCALER) / sizeof(TBL_PRESCALER[0]);
if( freq > 0 ) {
int prescaler = 2;
int n;
for( n = 0; n < NUM_TBL_PRESCALER; n++, prescaler *= 2 ) {
uint32_t fcalc = SPI_BASEFREQ / prescaler;
if( freq >= fcalc ) break;
}
hspi->Init.BaudRatePrescaler = TBL_PRESCALER[n];
}
switch( mode ) {
case 0:
hspi->Init.CLKPolarity = SPI_POLARITY_LOW;
hspi->Init.CLKPhase = SPI_PHASE_1EDGE;
break;
case 1:
hspi->Init.CLKPolarity = SPI_POLARITY_LOW;
hspi->Init.CLKPhase = SPI_PHASE_2EDGE;
break;
case 2:
hspi->Init.CLKPolarity = SPI_POLARITY_HIGH;
hspi->Init.CLKPhase = SPI_PHASE_1EDGE;
break;
case 3:
hspi->Init.CLKPolarity = SPI_POLARITY_HIGH;
hspi->Init.CLKPhase = SPI_PHASE_2EDGE;
break;
}
switch( msb_lsb ) {
case 0:
hspi->Init.FirstBit = SPI_FIRSTBIT_MSB;
break;
case 1:
hspi->Init.FirstBit = SPI_FIRSTBIT_LSB;
break;
}
__HAL_SPI_DISABLE(hspi);
if (HAL_SPI_Init(hspi) != HAL_OK) return -1;
__HAL_SPI_ENABLE(hspi);
return 0;
}
ポイントは、リファレンスマニュアルに書いてあるとおり、実際に設定変更をする前に、__HAL_SPI_DISABLE()
でSPIをDisableにしてから HAL_SPI_Init()
を呼んでいる事です。
また、このサブ関数を作らなくても c_spi_setmode
メソッド関数内に処理を書くこともできますが、今後SPI3以外への適用も考えてサブ関数を作って呼ぶようにしています。
read メソッドの実装
readメソッドの仕様を確認します。
readメソッド仕様
read( read_bytes ) -> String
- SPIバスから read_bytes バイトのデータを読み込む。
- 同時に出力されるデータは、0が出力される。
この仕様から、HAL_SPI_Receive
関数を使って実装できそうです。しかし実際に実装してテストプログラムを書き、アナライザを使って確認すると、Read時にMOSI端子からゴミデータ出力がされるようです。SPI規格には恐らく違反していませんが、ガイドラインではゼロを出力することが求められていますので、HAL_SPI_Receive
ではなく、HAL_SPI_TransmitReceive
を使ってゼロを出力しながらリードするようにします。
static void c_spi_read(mrbc_vm *vm, mrbc_value v[], int argc)
{
if( v[1].tt != MRBC_TT_INTEGER ) {
mrbc_raise(vm, MRBC_CLASS(ArgumentError), 0);
return;
}
int read_bytes = mrbc_integer(v[1]);
mrbc_value ret = mrbc_string_new(vm, 0, read_bytes);
uint8_t *buf = (uint8_t *)mrbc_string_cstr(&ret);
memset(buf, 0, read_bytes);
HAL_StatusTypeDef sts;
sts = HAL_SPI_TransmitReceive(&hspi3, buf, buf, read_bytes, SPI_TIMEOUT_ms );
if( sts != HAL_OK ) {
mrbc_raisef(vm, 0, "HAL layer error (status code %d)", sts);
}
SET_RETURN(ret);
}
出力と入力のバッファを同じにしているのが少し気がかりですが、少なくとも現バージョンのHALでは問題無いことが確認できているので、メモリ節約のためにそうしています。
write メソッドの実装
writeメソッド仕様
write( *outputs ) -> nil
- SPIバスへ、outputs で指定したデータを出力する。
- outputsは、Integer, Array もしくは String で指定する。
使用例
spi.write( 0x30, 0xa2 )
spi.write( "\x30\xa2" )
i2c.write( 0x02, 0xee, 0xad, 0x00, data_string ) # useful for EEPROM
この仕様は、I2Cの write
と同じです。よって、I2Cクラス実装時に作ったサブルーチン make_output_buffer()
を、使って実装します。
uint8_t * make_output_buffer(mrb_vm *vm, mrb_value v[], int argc, int start_idx, int *ret_bufsiz);
static void c_spi_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
int bufsiz;
uint8_t *buf = make_output_buffer(vm, v, argc, 1, &bufsiz );
if( !buf ) return;
HAL_StatusTypeDef sts;
sts = HAL_SPI_Transmit(&hspi3, buf, bufsiz, SPI_TIMEOUT_ms );
mrbc_free( vm, buf );
if( sts != HAL_OK ) {
mrbc_raisef(vm, 0, "HAL layer error (status code %d)", sts);
}
SET_NIL_RETURN();
}
transfer メソッドの実装
transferメソッド仕様
transfer( outputs, additional_read_bytes = 0 ) -> String
- SPIバスへ outputs で指定したデータを出力しながら同時に入力する(汎用転送)
- outputs は、Integer, Array もしくは String で指定する。
- additional_read_bytes を指定すると、そのバイト数分の 0x00 を output に続いて出力する。
使用例
s = spi.transfer( 0b0000_0101, 1 ) # s は 2バイトの String が返る
writeしながら同時にreadするメソッドです。そのまま HAL_SPI_TransmitReceive
に相当します。先ほど実装したreadおよびwriteメソッドと同様の手段を使い、それらを組み合わせてやれば容易に実装できます。
static void c_spi_transfer(mrbc_vm *vm, mrbc_value v[], int argc)
{
uint8_t *buf = 0;
int bufsiz;
if( argc == 0 ) goto ERROR_ARGUMENT;
buf = make_output_buffer(vm, v, 1, 1, &bufsiz );
if( !buf ) return;
if( argc >= 2 ) {
if( v[2].tt != MRBC_TT_INTEGER ) goto ERROR_ARGUMENT;
int additional_read_bytes = mrbc_integer(v[2]);
uint8_t *buf2 = mrbc_realloc(vm, buf, bufsiz + additional_read_bytes);
if( !buf2 ) {
mrbc_free(vm, buf);
mrbc_raise(vm, 0, 0);
return;
}
buf = buf2;
memset(buf + bufsiz, 0, additional_read_bytes);
bufsiz += additional_read_bytes;
}
mrbc_value ret = mrbc_string_new_alloc(vm, buf, bufsiz);
HAL_StatusTypeDef sts;
sts = HAL_SPI_TransmitReceive(&hspi3, buf, buf, bufsiz, SPI_TIMEOUT_ms );
if( sts != HAL_OK ) {
mrbc_raisef(vm, 0, "HAL layer error (status code %d)", sts);
}
SET_RETURN(ret);
return;
ERROR_ARGUMENT:
mrbc_raise(vm, MRBC_CLASS(ArgumentError),0);
}
これで実装は終了です。
テスト
熱電対温度センサーIC MAX31855
MAXIM社の熱電対用温度センサーIC MAX31855 を接続してみます。
センサICのデータシートを確認すると、このICには設定はなにも無く、読めば値が返ってくる仕様です。そのため、MOSIは無く MISO のみです。
SPIのモード等を確認します。
- クロック最大周波数は、5MHz
- アイドル時のクロックはLow、サンプリングエッジは立ち上がり1 => mode=0
- MSB first で出力される
よって、すべてSPIクラスのデフォルト値でOKです。
ChipSelect には、GPIOの"PB6" 端子を使います。
#
# MAX31855
# Cold-Junction Compensated Thermocouple-to-Digital Converter
#
# https://www.maximintegrated.com/en/products/interface/sensor-interface/MAX31855.html
#
# Breakout board.
# https://www.switch-science.com/catalog/864/
#
# Pin assign.
# CN pin GPIO Usage
# CN7 1 PC10 SCK
# CN7 2 PC11 MISO
# CN5 3 PB6 CS (SS)
puts "MAX31855 Thermo meter."
spi = SPI.new()
cs = GPIO.new("PB6", GPIO::OUT)
cs.write( 1 )
sleep_ms 100
while true
cs.write( 0 )
s = spi.read(4)
cs.write( 1 )
# DATA[31:18] * 0.25
temp_tc = (((s.getbyte(0) << 6) | (s.getbyte(1) >> 2)) -
((s.getbyte(0) & 0x80) << 7)) * 0.25
# DATA[15:4] * 0.0625
temp_std = (((s.getbyte(2) << 4) | (s.getbyte(3) >> 4)) -
((s.getbyte(2) & 0x80) << 5)) * 0.0625
printf "TC=%.1f ℃ std=%.1f ℃\n", temp_tc, temp_std
sleep 1
end
実行結果
MAX31855 Thermo meter.
TC=28.5 ℃ std=29.0 ℃
TC=28.5 ℃ std=29.0 ℃
TC=28.5 ℃ std=29.0 ℃
...
おわりに
ファイル全体は、github リポジトリにありますので、そちらをご覧ください。
今回は、SPIクラスを実装しました。一部ガイドラインと合致しない部分もありましたが、概ねmruby/c のメソッドとHALライブラリの関数が対応しており、実装は難しくありませんでした。
次回は、UARTクラスを実装します。
-
なお、旧版のデータシートには「クロックの立ち下がりでデータを読む」旨が記載されていたようですが、最新版(Revision5)では削除されており、タイミング図をみても実際の波形を確認しても、明らかに立ち上がりで読むべきです。 ↩