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より書くのが難しそうだ。