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 |
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かも。 実際に使い始めると、不具合が出て来るかも知れないので、今後は実用的に使って行きたい。