ch32funのmruby/cでCH32X035のSPI


I2Cだと電源などを合わせても、4本の配線があれば通信できるのに対して、SPIだと6本必要になる。 また、I2Cは同じ配線で複数のICとの通信が可能であるが、SPIではIC毎にセレクト信号を使うなどの工夫をしなければいけない。 少ない配線で多くのICと通信できる方が楽なので、SPIよりはI2Cを使うことが多く、私はSPIはあまり使わない。 しかし、SPIを使わないといけない場合もあるだろう。 そこで、ch32funのmruby/cからch32x035のSPIを使えるようにしてみた。

CH32X035にはSPIは一つしかないので、 mrbc_spi.hは単純に以下のようにした。

#ifndef _MRBC_SPI_H
#define _MRBC_SPI_H

#include "ch32fun.h"
#include "mrubyc.h"

void mrbc_init_class_spi(void);

#endif

通信する相手毎に設定が異なる場合には、その設定を記憶しておいた方が使い易くなるかも知れないが、メモリやflashを減らすために、単純な仕様にした。

標準のAPIのガイドラインに従って、SSはmruby/c側で制御することにして、 mrbc_spi.cで必要な関数などを定義した。

#include "mrbc_spi.h"

extern uint8_t * make_output_buffer(mrb_vm *vm, mrb_value v[], int argc,
                             int start_idx, int *ret_bufsiz);

void spi_init(){
  RCC->APB2PCENR |= RCC_APB2Periph_SPI1;
  //PA4 NSS
  funPinMode(5,GPIO_CFGLR_OUT_50Mhz_AF_PP); //PA5 SCK
  funPinMode(6,GPIO_CFGLR_IN_FLOAT); //PA6 MISO
  funPinMode(7,GPIO_CFGLR_OUT_50Mhz_AF_PP); //PA7 MOSI
}
#define SPI_TIMEOUT_MAX 100000
void spi_transfer(uint8_t *buf, uint32_t len) {
  uint32_t timeout;
  for (uint32_t i = 0; i < len; i++) {
    timeout = SPI_TIMEOUT_MAX;
    while (!(SPI1->STATR & SPI_STATR_TXE))
    { if (--timeout == 0) return; }
    SPI1->DATAR = buf[i];
    timeout = SPI_TIMEOUT_MAX;
    while (!(SPI1->STATR & SPI_STATR_RXNE))
    { if (--timeout == 0) return; }
    buf[i] = SPI1->DATAR;
  }
  timeout = SPI_TIMEOUT_MAX;
  while (SPI1->STATR & SPI_STATR_BSY)
  { if (--timeout == 0) return; }
}

void c_spi_setmode(mrbc_vm *vm, mrbc_value v[], int argc){
  MRBC_KW_ARG( unit, frequency, mode, first_bit );
  uint32_t spi_freq = 1000000;
  int spi_mode = 0;
  int spi_first_bit = 0;
  if( MRBC_KW_ISVALID(frequency) ) spi_freq = mrbc_integer(frequency);
  if( MRBC_KW_ISVALID(first_bit) ) spi_first_bit = mrbc_integer(first_bit);
  if( MRBC_KW_ISVALID(mode) ) spi_mode = mrbc_integer(mode);
  uint32_t ratio = (FUNCONF_SYSTEM_CORE_CLOCK / spi_freq); //FHCLK
  uint8_t scale=0;
  ratio--;
  while(ratio>>=1) scale++;
  if(scale>7) scale=7;
  uint16_t val=0;
  val |= SPI_CTLR1_MSTR;
  val |= (scale<<3); // SPI_CTLR1_BR
  val |= spi_mode & 3; // SPI_CTLR1_CPHA | SPI_CTLR1_CPOL
  val |= SPI_CTLR1_LSBFIRST * (spi_first_bit & 1) ;
  val |= SPI_CTLR1_SSM | SPI_CTLR1_SSI; // software SS
  SPI1->CTLR1 = val | SPI_CTLR1_SPE;
  MRBC_KW_DELETE( unit, frequency, mode, first_bit );
}

void c_spi_new(mrb_vm *vm, mrb_value *v, int argc){
  v[0] = mrbc_instance_new(vm, v[0].cls, 0);
  spi_init();
  c_spi_setmode( vm, v, argc );
}
  
void c_spi_write(mrb_vm *vm, mrb_value *v, int argc){
  uint8_t *buf = 0;
  int bufsiz = 0;
  if(argc>0){
    buf = make_output_buffer( vm, v, argc, 1, &bufsiz );
    spi_transfer(buf, bufsiz);
    mrbc_free( vm, buf );
  }
  SET_RETURN( mrbc_integer_value(bufsiz) );
}

void c_spi_read(mrb_vm *vm, mrb_value *v, int argc){
  mrbc_value ret = mrbc_nil_value();
  if(argc>0){
    int size = GET_INT_ARG(1);
    ret = mrbc_string_new(vm, 0, size);
    uint8_t *buf = (uint8_t *)mrbc_string_cstr(&ret);
    memset(buf, 0, size);
    spi_transfer(buf, size);
  }
  SET_RETURN(ret);
}

void c_spi_transfer(mrb_vm *vm, mrb_value *v, int argc){
  mrbc_value ret = mrbc_nil_value();
  uint8_t *buf = 0;
  int bufsiz = 0;
  if(argc>0){
    buf = make_output_buffer( vm, v, 1, 1, &bufsiz );
    if(argc>1){
      int additional_read_bytes = GET_INT_ARG(2);
      uint8_t *buf2 = (uint8_t *) mrbc_realloc(vm, buf, bufsiz + additional_read_bytes);
      buf = buf2;
      memset(buf + bufsiz, 0, additional_read_bytes);
      bufsiz += additional_read_bytes;
    }
    ret = mrbc_string_new_alloc(vm, buf, bufsiz);
    spi_transfer(buf, bufsiz);
  }
  SET_RETURN(ret);
}

void mrbc_init_class_spi(void){
  mrb_class *spi = mrbc_define_class(0, "SPI",  mrbc_class_object);
  mrbc_define_method(0, spi, "new", c_spi_new);
  mrbc_define_method(0, spi, "setmode", c_spi_setmode);
  mrbc_define_method(0, spi, "read", c_spi_read);
  mrbc_define_method(0, spi, "write", c_spi_write);
  mrbc_define_method(0, spi, "transfer", c_spi_transfer);
  mrbc_set_class_const(spi, mrbc_str_to_symid("LSB_FIRST"), &mrbc_integer_value(1));
  mrbc_set_class_const(spi, mrbc_str_to_symid("MSB_FIRST"), &mrbc_integer_value(0));
}

念の為にtimeout処理も付けたが、数が少ないので、マクロにはしなかった。 SPI_TIMEOUT_MAXが定義されている時だけ、timeout処理をするように書いた方が良いかも知れない。 また、ハードウェア依存の部分を分離しようか迷ったが、 多くのパラメータを渡さないといけない設定の部分だけは、 面倒なので埋め込んで書いた。 周波数とmodeなど、いくつかの設定項目があったり、送信と受信を同時に行うなど、I2Cよりは少しプログラムは複雑だが、サイズは約1kで、I2Cと同じ位になった。

SPIのICを探したら、 温度計のADT7310を見付けたので、これを使って通信のテストをしてみた。 でも、六本の配線をするのは面倒だった。 引数の数を間違えた箇所があって、なかなか動かなくて苦労したが、動作チェックも出来た。 そのrubyスクリプトはこんな感じである。

cs=GPIO.new(4,GPIO::OUT)
s=SPI.new(frequency:100000,first_bit:SPI::MSB_FIRST,mode:3)
cs.write(0)
print "setting\n"
s.write(1<<3,0x80) #cotinuous
cs.write(1)
while true
  sleep 1
  print "reading\n"
  cs.write(0)
  ret=s.transfer("\x50",2).bytes
  cs.write(1)
  t=ret[1]*256+ret[2]
  printf "%d",t
  td=t/128
  tf=t%128*100/128
  printf "T = %d.%02d\n",td,tf
end

整数しか使えないことを意識して、温度を表示した。 first_bitの指定で、MSB_FIRSTとしていて、firstが被っているので冗長な気がするけど、仕様なので仕方無いだろう。

これまで、SPIは面倒なので動作チェックをサボっていたが、それをしたおかげで、バグを発見することができた。 PWMではLEDのみしかチェックしていないけど、CH32X035でこれまでmruby/cに実装して来た機能については、一応の動作チェックをしているので、最低限は動くものになっているだろう。 あとはUARTだけども、こちらはI2CやSPIより書くのが難しそうだ。