ch32funのmruby/cでCH32X035のI2C
私がこのところ使っているch32x035のボードは、WeActStudioの開発ボードである。 そこには、QFN20というパッケージのCH32X035F8U6が使われており、ピンの数が20個と少ない。 さらに、その中の2ピンはUSBに使われているので、他の用途には使えない。 このような制約の下に、ハードウェアI2Cを使おうとすると、PC18とPC19を使う以外の方法が無いことが分かる。 以前、mrubyc_arduinoからI2Cを使えるようにプログラムを作ったが、このCH32X035ボードではおそらくI2Cは使えないだろう。 今回、ch32funのmruby/cからch32x035のI2Cを使えるようにしてみた。
I2Cに使うPC18とPC19の二つのピンは、WCH-Linkでの書き込みに使うDIOとDCKに割り当てられている。 I2Cを有効にすると、WCH-Linkでの書き込みはできなくなるが、USBから書き込えるので問題無いだろう。 そして、DIOとDCKにGNDと3.3Vを合わせて書き込み用の4ピンのピンヘッダを取り付けられるようになっており、これをそのままI2Cに使えるというメリットがある。
mrbc_i2c.hはほとんど何も指定する必要は無いので、以下の内容である。
#ifndef _MRBC_I2C_H #define _MRBC_I2C_H #include "ch32fun.h" #include "mrubyc.h" void mrbc_init_class_i2c(void); #endif
mrbc_i2c.cの
#include "mrbc_i2c.h"
#define I2C_TIMEOUT_MAX 100000
#define WAIT_FOR_FLAG(reg, flag) { \
uint32_t timeout = I2C_TIMEOUT_MAX; \
while (!((reg) & (flag))) { \
if (--timeout == 0) return -1; \
} \
}
extern uint8_t * make_output_buffer(mrb_vm *vm, mrb_value v[], int argc,
int start_idx, int *ret_bufsiz);
void i2c_init(){
AFIO->PCFR1 &= ~AFIO_PCFR1_SWJ_CFG;
AFIO->PCFR1 |= AFIO_PCFR1_SWJ_CFG_DISABLE;
GPIOC->CFGXR &= ~(0xF << 8 | 0xF << 12); //PC18 DIO, PC19 DCK
GPIOC->CFGXR |= GPIO_CFGLR_OUT_50Mhz_AF_PP << 8 | GPIO_CFGLR_OUT_50Mhz_AF_PP << 12;
RCC->APB1PCENR |= RCC_APB1Periph_I2C1;
AFIO->PCFR1 &= ~AFIO_PCFR1_I2C1_REMAP;
AFIO->PCFR1 |= AFIO_PCFR1_I2C1_REMAP_1 | AFIO_PCFR1_I2C1_REMAP_0; //011
I2C1->CTLR1 &= ~I2C_CTLR1_PE;
I2C1->CTLR2 = (FUNCONF_SYSTEM_CORE_CLOCK / 1000000);
I2C1->CKCFGR = FUNCONF_SYSTEM_CORE_CLOCK / (100000 << 1); //100kHz
I2C1->CTLR1 |= I2C_CTLR1_PE;
I2C1->CTLR1 |= I2C_CTLR1_ACK;
}
int i2c_start(uint8_t adrs, uint8_t rw){ // rw=1 if read
uint32_t timeout = I2C_TIMEOUT_MAX;
while (I2C1->STAR2 & I2C_STAR2_BUSY) {
if (--timeout == 0) return -1;
}
I2C1->CTLR1 |= I2C_CTLR1_START;
WAIT_FOR_FLAG(I2C1->STAR1, I2C_STAR1_SB);
I2C1->DATAR = (adrs << 1) | rw;
WAIT_FOR_FLAG(I2C1->STAR1, I2C_STAR1_ADDR);
(void)I2C1->STAR1;
(void)I2C1->STAR2;
return 0;
}
int i2c_write(const uint8_t *buf, uint16_t len){
for (uint16_t i = 0; i < len; i++) {
WAIT_FOR_FLAG(I2C1->STAR1, I2C_STAR1_TXE);
I2C1->DATAR = buf[i];
}
WAIT_FOR_FLAG(I2C1->STAR1, I2C_STAR1_BTF);
return 0;
}
int i2c_read(uint8_t *buf, uint16_t len){
I2C1->CTLR1 |= I2C_CTLR1_ACK;
for (uint16_t i = 0; i < len; i++) {
if(i==len-1) I2C1->CTLR1 &= ~I2C_CTLR1_ACK;
WAIT_FOR_FLAG(I2C1->STAR1, I2C_STAR1_RXNE);
buf[i] = I2C1->DATAR;
}
I2C1->CTLR1 |= I2C_CTLR1_STOP;
return 0;
}
void c_i2c_new(mrb_vm *vm, mrb_value *v, int argc){
i2c_init();
}
void c_i2c_write(mrb_vm *vm, mrb_value *v, int argc){
uint8_t *buf = 0;
int bufsiz = 0;
if( argc>0 ){
uint8_t adrs = GET_INT_ARG(1);
i2c_start(adrs,0);
if(argc>1){
buf = make_output_buffer( vm, v, argc, 2, &bufsiz );
i2c_write(buf, bufsiz);
mrbc_free( vm, buf );
}
I2C1->CTLR1 |= I2C_CTLR1_STOP;
}
SET_RETURN( mrbc_integer_value(bufsiz) );
}
void c_i2c_read(mrb_vm *vm, mrb_value *v, int argc){
uint8_t *buf = 0;
int bufsiz = 0;
mrbc_value ret = mrbc_nil_value();
if( argc>1 ){
int adrs = GET_INT_ARG(1);
int size = GET_INT_ARG(2);
if( argc > 2 ) {
i2c_start(adrs,0);
buf = make_output_buffer( vm, v, argc, 3, &bufsiz );
i2c_write(buf, bufsiz);
mrbc_free( vm, buf );
}
i2c_start(adrs,1);
ret = mrbc_string_new(vm, 0, size);
uint8_t *p = (uint8_t *)mrbc_string_cstr(&ret);
i2c_read(p,size);
}
SET_RETURN(ret);
}
void mrbc_init_class_i2c(void){
mrb_class *i2c = mrbc_define_class(0, "I2C", mrbc_class_object);
mrbc_define_method(0, i2c, "new", c_i2c_new);
mrbc_define_method(0, i2c, "read", c_i2c_read);
mrbc_define_method(0, i2c, "write", c_i2c_write);
}
初期化では、 SWDを無効にして、GPIOとしての設定をして、I2Cを有効にして、remappingしてから、I2Cの設定をしている。 PC16以降のピンはch32funから設定するのは困難なので、レジスタに値を代入することによって操作している。 通信の部分ではフラグが変化するのを待つ操作が複数あるので、timeoutも組み込んでマクロにしてある。 コンパイルすると、1kぐらいと、比較的コンパクトである。
温度湿度計のAHT10との通信を行って、うまく行くことを確認した。 ただし、CH32X035ではflashの節約のためにFLOATを使えなくしなければならないので、値の表示に工夫が必要となる。
printf( "humidity %.2f\n",h*100.0/0x100000 )
とすれば良いところで、
h*=100 hd=h>>20 hf=((h-(hd<<20))*100)>>20 printf( "humidity %d.%02d%%\n",hd,hf )
とした。 もっとすっきりした書き方ができるかも知れないけど。
I2Cは一つしか無いし、周波数ぐらいしか設定するパラメータが無いので、プログラムの書き方には、あまり工夫する余地も無いだろう。 ハードウェアに依存する部分は、関数として分離したので、別のハード用に変更することも、比較的簡単にできるだろう。