1
0

mruby/cペリフェラルライブラリのSTM32マイコンへの実装 Chapter06: SPIクラス実装編

Posted at

しまねソフト研究開発センター(略称 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 欄を以下の通り設定します。

STM32mrbc06-CubeIDE1.png

雛形の作成

GPIOクラス実装編 と同様に、雛形のファイルを用意してから、これに肉付けしていきます。

Core/mrubyc/stm32f4_spi.c
#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() をコールするよう書き足します。

Core/mrubyc/start_mrubyc.c
/*! 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 クラスが使えるようになったかを確認するコードを書いておくと良いと思います。

Core/mrubyc/task1.rb
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のモード等を確認します。
STM32mrbc06-MAX31855_Timing.png

  • クロック最大周波数は、5MHz
  • アイドル時のクロックはLow、サンプリングエッジは立ち上がり1 => mode=0
  • MSB first で出力される

よって、すべてSPIクラスのデフォルト値でOKです。
ChipSelect には、GPIOの"PB6" 端子を使います。

STM32mrbc06-Demo01.JPG

Core/mrubyc/task1.rb
#
# 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クラスを実装します。

  1. なお、旧版のデータシートには「クロックの立ち下がりでデータを読む」旨が記載されていたようですが、最新版(Revision5)では削除されており、タイミング図をみても実際の波形を確認しても、明らかに立ち上がりで読むべきです。

1
0
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
0