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は一つしか無いし、周波数ぐらいしか設定するパラメータが無いので、プログラムの書き方には、あまり工夫する余地も無いだろう。 ハードウェアに依存する部分は、関数として分離したので、別のハード用に変更することも、比較的簡単にできるだろう。