mrubyc_arduinoでGPIOとADC


先日、 mruby/cとArduinoを組み合せることによって、rubyからESP32マイコンを制御することに成功した。 その際には、 mrubyc_arduino を利用した。

そのままでは動かなかったので、若干の修正をしなければならず、 mruby mruby/c共通I/O APIガイドライン に完全には準拠しておらず、 少し無駄な部分もあったので、 STM32マイコンへの実装を参考にして、 GPIOとADCについて新たに書いてみた。

GPIO用のdigital.hは、定数などを含めて、以下のようにした。

#ifndef _DIGITAL_H
#define _DIGITAL_H

#include <Arduino.h>

#ifdef __cplusplus
extern "C" {
#endif

#include "mrubyc.h"

typedef struct GPIO_HANDLE {
  uint8_t pin_num;
} GPIO_HANDLE;

void mrbc_init_class_digital(void);

#define GPIO_IN                 0x01
#define GPIO_OUT                0x02
#define GPIO_ANALOG             0x04
#define GPIO_HIGH_Z             0x08
#define GPIO_PULL_UP            0x10
#define GPIO_PULL_DOWN          0x20
#define GPIO_OPEN_DRAIN         0x40

#ifdef __cplusplus
}
#endif

#endif

digital.cは、newでpinModeを設定できるようにして、以下のようにした。

#include "digital.h"

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;
  handle->pin_num = GET_INT_ARG(1);
  if(argc>1) pinMode( handle->pin_num, GET_INT_ARG(2) );
}

static void c_gpio_write(mrb_vm *vm, mrb_value *v, int argc) {
  GPIO_HANDLE *handle = (GPIO_HANDLE *)v[0].instance->data;
  digitalWrite( handle->pin_num, GET_INT_ARG(1) );
}

static void c_gpio_setmode(mrb_vm *vm, mrb_value *v, int argc) {
  GPIO_HANDLE *handle = (GPIO_HANDLE *)v[0].instance->data;
  pinMode( 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( digitalRead(handle->pin_num) );
}

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, "write", c_gpio_write);
  mrbc_define_method(0, gpio, "setmode", c_gpio_setmode);
  mrbc_define_method(0, gpio, "read", c_gpio_read);
  mrbc_set_class_const(gpio, mrbc_str_to_symid("IN"),         &mrbc_integer_value(GPIO_IN));
  mrbc_set_class_const(gpio, mrbc_str_to_symid("OUT"),        &mrbc_integer_value(GPIO_OUT));
  mrbc_set_class_const(gpio, mrbc_str_to_symid("HIGH_Z"),     &mrbc_integer_value(GPIO_HIGH_Z));
  mrbc_set_class_const(gpio, mrbc_str_to_symid("PULL_UP"),    &mrbc_integer_value(GPIO_PULL_UP));
  mrbc_set_class_const(gpio, mrbc_str_to_symid("PULL_DOWN"),  &mrbc_integer_value(GPIO_PULL_DOWN));
  mrbc_set_class_const(gpio, mrbc_str_to_symid("OPEN_DRAIN"), &mrbc_integer_value(GPIO_OPEN_DRAIN));
}

これで、APIガイドラインに準拠したはずである。

ADCについては、参照電圧と分解能がマイコンによって異なるので、それらの値をadc.hの中で定数として定義した。

#ifndef _ADC_H
#define _ADC_H

#include <Arduino.h>

#ifdef __cplusplus
extern "C" {
#endif

#include "mrubyc.h"
    
typedef struct ADC_HANDLE {
  uint8_t pin_num;
} ADC_HANDLE;

void mrbc_init_class_adc(void);

#define ADC_RANGE 4095 //1023
#define ADC_REF 3.3 //5.0

#ifdef __cplusplus
}
#endif

#endif

adc.cの中では、newのときに、pinModeで入力にして、生データと電圧の読取のmethodを定義した。

#include "adc.h"
#include "digital.h"

void c_adc_new(mrb_vm *vm, mrb_value *v, int argc){
  v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(ADC_HANDLE));
  ADC_HANDLE *handle = (ADC_HANDLE *)v[0].instance->data;
  handle->pin_num = GET_INT_ARG(1);
  pinMode( handle->pin_num, GPIO_IN );
}

void c_adc_read_raw(mrb_vm *vm, mrb_value *v, int argc){
  ADC_HANDLE *handle = (ADC_HANDLE *)v[0].instance->data;
  SET_INT_RETURN(analogRead( handle->pin_num ));
}

void c_adc_read_voltage(mrb_vm *vm, mrb_value *v, int argc){
  ADC_HANDLE *handle = (ADC_HANDLE *)v[0].instance->data;
  float adc_p = ADC_REF * analogRead( handle->pin_num ) / ADC_RANGE;
  SET_FLOAT_RETURN(adc_p);
}

void mrbc_init_class_adc(void){
  mrb_class *adc = mrbc_define_class(0, "ADC",  mrbc_class_object);
  mrbc_define_method(0, adc, "new", c_adc_new);
  mrbc_define_method(0, adc, "read_raw", c_adc_read_raw);
  mrbc_define_method(0, adc, "read_voltage", c_adc_read_voltage);
  mrbc_define_method(0, adc, "read", c_adc_read_voltage);
}

ArduinoのanalogReadResolution()で分解能を変更した時の対応などもしないといけないと思うのだが、今回は省略した。

あとは、inoに

mrbc_init_class_digital();
mrbc_init_class_adc();

を加えて、mrubyc.hの最後に

#include "digital.h"
#include "adc.h"

を加えたら完了である。

mruby/cのデータが、ポインターや構造体などで、どうなっているのか分からずに書いたので、多少の苦労はしたが、比較的すっきりとしたコードになったのでは無いかと思う。 LEDを点滅させながらADCを読み取るプログラムをコンパイルしてみたところ、メモリの使用量が170バイト程小さくなっていた。 不要な部分を削った効果なのだろう。 メモリやflashの使用量をもっと減らして、幅広いマイコンで動くように出来無いのかなと思う。 mruby/cの機能を限定して、 Arduino UNOで動かした例もあるようなので、ESP32以外のマイコンでも試してみたい。 この例は古いmruby/cを元にしているので、そのままでは活用できないが、どのような仕組みになっているのか、調べてみたいと思う。

2026/4/19追記 gpioについては、二つのミスに気が付いた。 readとwriteを実装して満足していたが、これだけだとAPIに準拠していない。 read_atなどのクラスメソッドをすっかり忘れていた。 しかし、これらのmethodを追加すると、使うflashの量が増えるので、今回はまあいいか。 もう一つ重要なのが、INやOUTなどの定数が、マイコンの種類によって異なることである。 digital.hの中で値を定義していたが、arduinoのINPUTやOUTPUTを直接使うことにした。 adc.c中でも同様である。