ch32funのmruby/cでCH32X035のUART


CH32X035には、I2CやSPIは一つずつしかないが、USARTは四つある。 WeActStudioの開発ボードの限られた端子で、この四つをUARTとして有効に活用できるかを考えてみた。 すると、flow制御を行わずRXとTXのみを使う場合には、remappingも使うと、USART1のTXを除いて利用が可能であることが分かった。 これを下の表にまとめる。

pin TX RX remapping
USART1 [PB10] PB11 0,2
USART2 PA2 PA3 0
USART3 PC18(DIO) PC19(DCK) 1
USART4 PB0 PB1 0
角括弧は端子が存在しないことを意味するが、ピンの数の多いパッケージなら使えるだろう。 そこで、これらの四つのUARTを、ch32funのmruby/cで使えるようにしてみた。

TXやRXを扱うには、主に三つの方法がある。 最も単純なのは、読み書きを指示したら、それが完了するまで待って、次に移る方法である。 プログラムは短くなるが、その処理の間はCPUを占有することになる。 次の方法は、割り込みを利用する方法であり、割り込みがかかるまではCPUがフリーになるが、それぞれの割り込み処理を書く必要がある。 そして、DMAを使う方法である。 UARTとバッファのやりとりをDMAに任せることができるが、処理がいつ完了したかなどを意識する必要がある。 CH32X035のDMAには8つのchannelがあり、下の表のように、どのUSARTに割り当てられるかが決まっているが、四つのUSARTのすべてにDMAを割り当てることもできる。

DMA TX RX
USART1 4 5
USART2 7 6
USART3 2 3
USART4 1 8

mruby/cで、メモリやflashをできるだけ節約する形で、UARTを実装するにはどうしたら良いのかを考えたが、TXは単純にデータを送り、RXはDMAを使うことにした。 TXでDMAを使うためには、送信が終了するまでデータをバッファーにためて置く必要があり、mruby/cで臨時に作ったバッファーが廃棄されてしまうと駄目になってしまうので、仮にDMAを使う場合でも、すべての転送が終わるまで、そのサブルーチンを終了できず、DMAを使うメリットはあまり無い。 一方、RXはいつ来るか分からないデータを待って、それらを監視または割り込みで処理するのは面倒なので、受信バッファを用意しないといけないが、DMAを使うことにして、必要なときに、受信状態を調べることにした。 多量のデータが来て、バッファが一杯になる可能性もあり、それをどう処理するかが問題であるが、今回はそれは見送ることにした。

4つのUARTを区別しないといけないので、mrbc_uart.hの中で、UART_HANDLEを定義した。

#ifndef _MRBC_UART_H
#define _MRBC_UART_H

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

#define UART_BUF_SIZE 128

typedef struct UART_HANDLE {
  uint8_t unit_num;
  uint8_t rx_index;
  char rx_buf[UART_BUF_SIZE];
} UART_HANDLE;

void mrbc_init_class_uart(void);

#endif

一つのUARTを定義すると、バッファの分だけメモりを消費するが、CH32X035はそれなりにメモリが多いので、大丈夫だろう。

メインのmrbc_uart.cは、予想通りかなり長くなってしまった。 四つのUSARTとDMAの設定に加えて、ピンやremappingを扱うので、仕方無いだろう。 それでも、PWMのときのポインタを使った手法で、USARTの番号やDMAのチャンネルについて、一つのコードで動くように書けた。

#include "mrbc_uart.h"

static volatile uint32_t usart_base[]={USART1_BASE,USART2_BASE,USART3_BASE,USART4_BASE};
static volatile uint32_t dma_rx_base[]={DMA1_Channel5_BASE,DMA1_Channel6_BASE,DMA1_Channel3_BASE,DMA1_Channel8_BASE};
static volatile uint8_t rx_pin_num[]={27,3,51,17};
static volatile uint8_t tx_pin_num[]={26,2,50,16};

#define UART_TIMEOUT_MAX 100000
int usart_write(UART_HANDLE *handle, const char *buf, int size){
  USART_TypeDef *usart = ((USART_TypeDef *) usart_base[handle->unit_num-1]);
  for(int i = 0; i < size; i++){
    uint32_t timeout = UART_TIMEOUT_MAX;
    while( !(usart->STATR & USART_FLAG_TXE) )
    { if (--timeout == 0) return -1;}
    usart->DATAR = *buf++;
  }
  return size;
}
int usart_available(DMA_Channel_TypeDef *dma_rx, uint8_t rx_index){
  uint8_t size =(UART_BUF_SIZE - dma_rx->CNTR); //%UART_BUF_SIZE;
  size -= rx_index;
  size %= UART_BUF_SIZE;
  return size;
}
int usart_read(UART_HANDLE *handle, char *buf, int size){
  DMA_Channel_TypeDef *dma_rx = ((DMA_Channel_TypeDef *) dma_rx_base[handle->unit_num-1]);
  for(int i = 0; i < size; i++){
    while(!usart_available(dma_rx,handle->rx_index));
    buf[i] = handle->rx_buf[handle->rx_index];
    handle->rx_index = (handle->rx_index + 1)%UART_BUF_SIZE;
  }
  buf[size] = '\0';
  return size;
}
int usart_can_read_line(UART_HANDLE *handle){
  DMA_Channel_TypeDef *dma_rx = ((DMA_Channel_TypeDef *) dma_rx_base[handle->unit_num-1]);
  uint8_t index = handle->rx_index;
  uint8_t last = (UART_BUF_SIZE - dma_rx->CNTR)%UART_BUF_SIZE;
  while( index != last ) {
    if( handle->rx_buf[index++] == '\n' )return (index - handle->rx_index)%UART_BUF_SIZE;
    index%=UART_BUF_SIZE;
  }
  return 0;
}

void c_uart_setmode(mrbc_vm *vm, mrbc_value v[], int argc){
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  MRBC_KW_ARG( baudrate, baud, data_bits, stop_bits, parity, flow_control, txd_pin, rxd_pin, rts_pin, cts_pin );
  int32_t baud_rate = 9600;
  int datb = 8;
  int stpb = 1;
  int prty = 0;
  if( MRBC_KW_ISVALID(baudrate) ) baud_rate = mrbc_integer(baudrate);
  if( MRBC_KW_ISVALID(baud) ) baud_rate = mrbc_integer(baud);
  if( MRBC_KW_ISVALID(data_bits) ) datb = mrbc_integer(data_bits);
  if( MRBC_KW_ISVALID(stop_bits) ) stpb = mrbc_integer(stop_bits);
  if( MRBC_KW_ISVALID(parity) ) prty = mrbc_integer(parity);
  USART_TypeDef *usart = ((USART_TypeDef *) usart_base[handle->unit_num-1]);
  usart->CTLR1 &= ~( CTLR1_UE_Set );
  usart->BRR = ( ( FUNCONF_SYSTEM_CORE_CLOCK ) + baud_rate/2 ) / baud_rate;
  usart->CTLR3 = USART_DMAReq_Rx | USART_HardwareFlowControl_None | USART_CTLR3_DMAR;
  usart->CTLR2 = (stpb&2)<<12; // 1:1bit, 2:2bit, //3:0.5, 4:1.5  0213
  usart->CTLR1 = USART_WordLength_9b * (prty>0 && datb>7) | (prty&3)<<9 | USART_Mode_Tx | USART_Mode_Rx | CTLR1_UE_Set;
  MRBC_KW_DELETE( baudrate, baud, data_bits, stop_bits, parity, flow_control, txd_pin, rxd_pin, rts_pin, cts_pin );
}

void c_uart_new(mrb_vm *vm, mrb_value *v, int argc){
  v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(UART_HANDLE));
  int unit_num = 1;
  if( argc > 0 ) unit_num = GET_INT_ARG(1);
  MRBC_KW_ARG( unit );
  if( MRBC_KW_ISVALID(unit) ) unit_num = mrbc_integer(unit);
  MRBC_KW_DELETE( unit );
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  handle->unit_num = unit_num;
  handle->rx_index = 0;
  if(handle->unit_num==1){
    RCC->APB2PCENR |= RCC_APB2Periph_USART1; //1<<14
  }else{
    RCC->APB1PCENR |= 1<<(15+handle->unit_num);
  }
  uint8_t tx=tx_pin_num[handle->unit_num-1];
  uint8_t rx=rx_pin_num[handle->unit_num-1];
  if(handle->unit_num == 3){ //disable SW and remap1
    AFIO->PCFR1 &= ~AFIO_PCFR1_SWJ_CFG;
    AFIO->PCFR1 |= AFIO_PCFR1_SWJ_CFG_DISABLE;
    AFIO->PCFR1 &= ~AFIO_PCFR1_USART3_REMAP;
    AFIO->PCFR1 |= AFIO_PCFR1_USART3_REMAP_0;
    GPIOC->CFGXR &= ~( 0xF << 4*(tx-48) );
    GPIOC->CFGXR |= GPIO_CFGLR_OUT_50Mhz_AF_PP << 4*(tx-48);
    GPIOC->CFGXR &= ~( 0xF << 4*(rx-48) );
    GPIOC->CFGXR |= GPIO_CFGLR_IN_FLOAT << 4*(rx-48);
  }else{
    funPinMode(tx,GPIO_CFGLR_OUT_50Mhz_AF_PP);
    funPinMode(rx,GPIO_CFGLR_IN_FLOAT);
  }
  USART_TypeDef *usart = ((USART_TypeDef *) usart_base[handle->unit_num-1]);
  DMA_Channel_TypeDef *dma_rx = ((DMA_Channel_TypeDef *) dma_rx_base[handle->unit_num-1]);
  RCC->AHBPCENR |= RCC_AHBPeriph_DMA1;
  dma_rx->CFGR &= ~DMA_CFGR1_EN;
  dma_rx->MADDR = (uint32_t)handle->rx_buf;
  dma_rx->PADDR = (uint32_t)&usart->DATAR;
  dma_rx->CNTR = UART_BUF_SIZE;
  dma_rx->CFGR = DMA_CFGR1_MINC | DMA_CFGR1_CIRC | DMA_CFGR1_EN;
  c_uart_setmode( vm, v, argc );
}
  
void c_uart_write(mrb_vm *vm, mrb_value *v, int argc){
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  int bufsiz = 0;
  if(argc>0){
    const char *s = mrbc_string_cstr(&v[1]);
    int len = mrbc_string_size(&v[1]);
    bufsiz=usart_write(handle,s,len);
  }
  SET_RETURN( mrbc_integer_value(bufsiz) );
}

static void c_uart_puts(mrbc_vm *vm, mrbc_value v[], int argc){
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  char zero[]="\n";
  char *s=zero;
  int len=1;
  if(argc>0){
    s = mrbc_string_cstr(&v[1]);
    len = mrbc_string_size(&v[1]);
    if( len == 0 || s[len-1] != '\n' ) s[len++]='\n';
  }
  usart_write(handle,s,len);
  SET_NIL_RETURN();
}

void c_uart_read(mrb_vm *vm, mrb_value *v, int argc){
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  mrbc_value ret = mrbc_nil_value();
  if(argc>0){
    int size = GET_INT_ARG(1);
    ret = mrbc_string_new(vm, 0, size);
    char *buf = mrbc_string_cstr(&ret);    
    size = usart_read( handle, buf, size );
  }
  SET_RETURN(ret);
}

static void c_uart_gets(mrbc_vm *vm, mrbc_value v[], int argc){
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  mrbc_value ret = mrbc_nil_value();
  int size=0;
  while( !size ) size = usart_can_read_line(handle);
  ret = mrbc_string_new(vm, 0, size);
  char *buf = mrbc_string_cstr(&ret);
  size = usart_read( handle, buf, size );
  SET_RETURN(ret);
}

static void c_uart_bytes_available(mrbc_vm *vm, mrbc_value v[], int argc){
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  DMA_Channel_TypeDef *dma_rx = ((DMA_Channel_TypeDef *) dma_rx_base[handle->unit_num-1]);
  int value=usart_available( dma_rx, handle->rx_index);
  SET_INT_RETURN(value);
}

static void c_uart_bytes_to_write(mrbc_vm *vm, mrbc_value v[], int argc){
  SET_INT_RETURN( 0 );
}

static void c_uart_can_read_line(mrbc_vm *vm, mrbc_value v[], int argc){
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  int pos=usart_can_read_line(handle);
  SET_INT_RETURN( pos );
}

static void c_uart_clear_rx_buffer(mrbc_vm *vm, mrbc_value v[], int argc){
  UART_HANDLE *handle = (UART_HANDLE *)v[0].instance->data;
  DMA_Channel_TypeDef *dma_rx = ((DMA_Channel_TypeDef *) dma_rx_base[handle->unit_num-1]);
  handle->rx_index=(UART_BUF_SIZE - dma_rx->CNTR)%UART_BUF_SIZE;
}

static void c_uart_nop(mrbc_vm *vm, mrbc_value v[], int argc){
}

void mrbc_init_class_uart(void){
  mrb_class *uart = mrbc_define_class(0, "UART",  mrbc_class_object);
  mrbc_define_method(0, uart, "new", c_uart_new);
  mrbc_define_method(0, uart, "setmode", c_uart_setmode);
  mrbc_define_method(0, uart, "read", c_uart_read);
  mrbc_define_method(0, uart, "write", c_uart_write);
  mrbc_define_method(0, uart, "gets", c_uart_gets);
  mrbc_define_method(0, uart, "puts", c_uart_puts);
  mrbc_define_method(0, uart, "bytes_available", c_uart_bytes_available);
  mrbc_define_method(0, uart, "bytes_to_write", c_uart_bytes_to_write);
  mrbc_define_method(0, uart, "can_read_line", c_uart_can_read_line);
  mrbc_define_method(0, uart, "flush", c_uart_nop);
  mrbc_define_method(0, uart, "clear_rx_buffer", c_uart_clear_rx_buffer);
  mrbc_define_method(0, uart, "clear_tx_buffer", c_uart_nop);
  mrbc_define_method(0, uart, "send_break", c_uart_nop);
  mrbc_set_class_const(uart, mrbc_str_to_symid("NONE"), &mrbc_integer_value(0));
  mrbc_set_class_const(uart, mrbc_str_to_symid("EVEN"), &mrbc_integer_value(2));
  mrbc_set_class_const(uart, mrbc_str_to_symid("ODD"), &mrbc_integer_value(3));
}

コンパイルサイズは約2.3kとなり、PWMとI2Cの合計とほぼ同じ容量である。 もう少し小さくしたいけど、無理かな。

様々なバグに苦労したが、 気付くのに最も時間がかかったのは mrbc_instance_newでsizeof(UART_HANDLE)の指定を忘れていた点である。 それ以外には、orを忘れたり、andとorを間違ったり、parityを指定するときに9bitにしないといけなかったり、RCCの有効化でミスをしたりといったところかな。

単純にecho backするプログラムはこんな感じ。

sleep 2
u=UART.new(3,baud:115200)
while true
  sleep 1
  l=u.bytes_available
  if l>0
    s=u.read(l)
    u.write(s)
  end
end

baudrateの設定が正しいかをチェックするために、別のICとも通信してみたので、おそらく大丈夫だろう。 baudrateなどは保存していないので、setmodeではdefaultでない値は一度に指定する必要がある。 まあ、UARTだと基本的には一対一の通信なので、最初に設定したら、それを変更することはあまり無いかな。

これで、CH32X035のmruby/cで動かしたいと思っていた機能は組み込み終った。 また、最低限の動作チェックもしたので、多少は使えるものになっていると思いたい。 PWMのプログラムが一番苦労したかな。 それともUARTかも。 実際に使い始めると、不具合が出て来るかも知れないので、今後は実用的に使って行きたい。