様々なマイコンでmruby/cを使う方法
あるマイコンでmruby/cを使うためには、mruby/cをコンパイルするだけでは駄目で、GPIO, ADC, PWM, I2C, SPI, UARTなどの機能を使えるようにプログラムを作る必要がある。 このことがmruby/cを使う敷居を上げているように思われる。 私がmruby/cを初めて使った時には、様々なマイコンを利用できるArduinoを活用して、ESP8266やESP32やCH32V203に機能を組み込んだ。 その後、ch32シリーズに限れば、サイズが小さくなる上に細かな操作もできるch32funの方が良いことに気が付き、CH32V203とCH32X035とCH32V006でmruby/cを動かすことに成功した。 そして最近、GD32VF103でmruby/cを動かしてみた。 この際には、ch32fun用のコードを変更して作ったのだが、思ったよりも簡単だったが、それなりに面倒ではあった。 異なるマイコン用にmruby/c用のプログラムを書いたことによって、その特徴が多少は理解できて、プログラムを作る効率を上げる仕組みを思い付いたので、そのことについて説明したい。 それがうまく行けば、新たなマイコンでmruby/cに機能を追加する手間が減り、mruby/cの敷居が下るのではないかと期待している。
例えばGPIOについては、ArduinoではdigitalWrite、ch32funではfunDigitalWriteなどを使って扱うことができ、これらの関数を使うとmruby/cからGPIOを使えるようにするのは比較的に簡単である。 他のマイコンについても、mrbcDigitalWriteという関数を作れば、ほぼ同様のやり方でプログラムを作ることができる。 これをさらに進めれば、マイコン固有の書き方をしなければいけない部分を、mrbcDigitalWriteなどの関数に押し込んで、mruby/c固有の書き方をする部分と分離することができる。 これまでは、それぞれの機能について、ヘッダファイルとC言語のソースファイルの二つを作っていたので、これらをそれぞれのマイコンについて書き換えなければならなかった。 しかし、上記のようにすれば、新たなマイコンでmruby/cを動かすときには、mruby/c固有の部分は共通のものを使い、マイコン固有のmrbcDigitalWriteなどの関数のみを書き換えれば良くなるはずである。
GPIOについてのmruby/c固有の部分を作ってみたが、以下のような感じになった。 ピンは番号で指定することにして、GPIOを扱う関数とモード用の定数を使って記述した。 また、エラー処理なども省いて、できるだけコンパクトにした。 さらに、FloatとStringの両方を使わないときには、できるだけ小さくするために、methodの数を減らすようにした。
#ifndef _MRBC_GPIO_H
#define _MRBC_GPIO_H
#include "mrbc_api.h"
#include "mrubyc.h"
typedef struct GPIO_HANDLE {
uint8_t pin_num;
} GPIO_HANDLE;
static void c_gpio_new(mrb_vm *vm, mrb_value *v, int argc){
v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(GPIO_HANDLE));
GPIO_HANDLE *handle = (GPIO_HANDLE *)v[0].instance->data;
if(argc>0){
handle->pin_num = GET_INT_ARG(1);
}
if(argc>1) mrbcPinMode( handle->pin_num, GET_INT_ARG(2) );
}
static void c_gpio_setmode(mrb_vm *vm, mrb_value *v, int argc){
uint8_t pin=0xff;
uint8_t mode;
if( v[0].tt == MRBC_TT_OBJECT ){ //Instance method mode.
GPIO_HANDLE *handle = (GPIO_HANDLE *)v[0].instance->data;
if(argc>0){ pin=handle->pin_num; mode=GET_INT_ARG(1); }
}else{ // Class method mode.
if(argc>1){ pin=GET_INT_ARG(1); mode=GET_INT_ARG(2); }
}
if(pin!=0xff) mrbcPinMode( pin, mode );
}
static void c_gpio_write(mrb_vm *vm, mrb_value *v, int argc){
GPIO_HANDLE *handle = (GPIO_HANDLE *)v[0].instance->data;
if(argc>0) mrbcDigitalWrite( handle->pin_num, GET_INT_ARG(1) );
}
static void c_gpio_read(mrb_vm *vm, mrb_value *v, int argc){
GPIO_HANDLE *handle = (GPIO_HANDLE *)v[0].instance->data;
SET_INT_RETURN( mrbcDigitalRead(handle->pin_num) );
}
#if MRBC_USE_FLOAT || MRBC_USE_STRING
static void c_gpio_write_at(mrb_vm *vm, mrb_value *v, int argc){
if(argc>1) mrbcDigitalWrite( GET_INT_ARG(1), GET_INT_ARG(2) );
}
static void c_gpio_high(mrb_vm *vm, mrb_value *v, int argc){
GPIO_HANDLE *handle = (GPIO_HANDLE *)v[0].instance->data;
SET_BOOL_RETURN( mrbcDigitalRead(handle->pin_num) >0 );
}
static void c_gpio_low(mrb_vm *vm, mrb_value *v, int argc){
GPIO_HANDLE *handle = (GPIO_HANDLE *)v[0].instance->data;
SET_BOOL_RETURN( mrbcDigitalRead(handle->pin_num) ==0 );
}
static void c_gpio_read_at(mrb_vm *vm, mrb_value *v, int argc){
if(argc>0) SET_INT_RETURN( mrbcDigitalRead( GET_INT_ARG(1) ) );
}
static void c_gpio_high_at(mrb_vm *vm, mrb_value *v, int argc) {
if(argc>0) SET_BOOL_RETURN( mrbcDigitalRead( GET_INT_ARG(1) ) >0 );
}
static void c_gpio_low_at(mrb_vm *vm, mrb_value *v, int argc) {
if(argc>0) SET_BOOL_RETURN( mrbcDigitalRead( GET_INT_ARG(1) ) ==0 );
}
#endif
void mrbc_init_class_digital(void){
mrb_class *gpio = mrbc_define_class(0, "GPIO", mrbc_class_object);
mrbc_define_method(0, gpio, "new", c_gpio_new);
mrbc_define_method(0, gpio, "setmode", c_gpio_setmode);
mrbc_define_method(0, gpio, "write", c_gpio_write);
mrbc_define_method(0, gpio, "read", c_gpio_read);
#if MRBC_USE_FLOAT || MRBC_USE_STRING
mrbc_define_method(0, gpio, "write_at", c_gpio_write_at);
mrbc_define_method(0, gpio, "high?", c_gpio_high);
mrbc_define_method(0, gpio, "low?", c_gpio_low);
mrbc_define_method(0, gpio, "read_at", c_gpio_read_at);
mrbc_define_method(0, gpio, "high_at?", c_gpio_high_at);
mrbc_define_method(0, gpio, "low_at?", c_gpio_low_at);
#endif
mrb_value mrbc_gpio_in=mrbc_integer_value(MRBC_GPIO_IN);
mrb_value mrbc_gpio_out=mrbc_integer_value(MRBC_GPIO_OUT);
mrb_value mrbc_gpio_in_pu=mrbc_integer_value(MRBC_GPIO_IN_PU);
mrb_value mrbc_gpio_in_pd=mrbc_integer_value(MRBC_GPIO_IN_PD);
mrb_value mrbc_gpio_out_od=mrbc_integer_value(MRBC_GPIO_OUT_OD);
mrbc_set_class_const(gpio, mrbc_str_to_symid("IN"), &mrbc_gpio_in);
mrbc_set_class_const(gpio, mrbc_str_to_symid("OUT"), &mrbc_gpio_out);
mrbc_set_class_const(gpio, mrbc_str_to_symid("IN_PU"), &mrbc_gpio_in_pu);
mrbc_set_class_const(gpio, mrbc_str_to_symid("IN_PD"), &mrbc_gpio_in_pd);
mrbc_set_class_const(gpio, mrbc_str_to_symid("OUT_OD"), &mrbc_gpio_out_od);
}
#endif
あとは、それぞれのマイコンについてmrbcPinModeとmrbcDigitalWriteとmrbcDigitalReadの三つの関数とモード用の定数MRBC_GPIO_*を定義すれば良いのである。 例えば、Arduinoについては、非常に単純で、以下のように書ける。
#define MRBC_GPIO_IN INPUT #define MRBC_GPIO_OUT OUTPUT #define MRBC_GPIO_IN_PU INPUT_PULLUP #if defined(INPUT_PULLDOWN) #define MRBC_GPIO_IN_PD INPUT_PULLDOWN #else #define MRBC_GPIO_IN_PD INPUT #endif #if defined(OUTPUT_OD) #define MRBC_GPIO_OUT_OD OUTPUT_OD #elif defined(OUTPUT_OPEN_DRAIN) #define MRBC_GPIO_OUT_OD OUTPUT_OPEN_DRAIN #else #define MRBC_GPIO_OUT_OD OUTPUT #endif #define MRBC_GPIO_IN_ANALOG INPUT #define mrbcPinMode(pin, mode) pinMode(pin,mode) #define mrbcDigitalWrite(pin, val) digitalWrite(pin,val) #define mrbcDigitalRead(pin) digitalRead(pin)
このようにすると、 mruby/cに固有な部分のプログラムを、GPIO, ADC, PWM, I2C, SPI, UARTのそれぞれについて作り、マイコン固有な部分をまとめたプログラムと組み合せることにより、簡単にmruby/cにそれぞれの機能を追加することができる。 また、このマイコン固有な部分をまとめたプログラムは、mruby/cだけに限らず、別のプログラムから呼び出すことも可能であり、そういう意味でも有用であろう。 欠点としては、共通化することで、余分な処理も増えるので、コンパイルサイズが若干増えるということがあるだろう。
Arduinoとch32funを使ってmruby/cを使うためのプログラムを、現在ホームページで公開しているが、将来的にはこれらを上記のようなやり方のプログラムに統一したい。 それぞれの環境できちんとコンパイルできるかや、動作の確認もしないといけないので、それなりには手間がかかるだろうが。