mruby/cでstdin

様々なマイコンで、簡単に mruby/c を使えるように出来るのではないかと思って、 mrubyc_arduino を拡張して、 これまでに、GPIO,ADC,PWM,I2C,SPIは動くようになった。 また、必要なflashの容量を節約するために、FLOATを使わなくする機能のバグを修正したり、マルチタスクを無効にすることが出来るようになった。 次はUARTを組み込もうかなとも思ったが、関連する機能を別の方法で実現してみた。 mruby/cでは、puts,print,printfなどのstdoutに出力するコマンドを、hal_writeを介して使うことができる。 一方、gets,getcなどのstdinから入力するコマンドは、 mrubyにはあるようだが、mruby/cには無い。 mrubyc_arduinoでは、hal_writeはSerialから出力するように指定してあり、Serialからの入力もできるようになれば、UARTを組み込まなくてもSerialを使えるようになることになる。 そこで、stdinからの入力コマンドをどのようにすれば実現できるかを試してみた。 まずやったのが、putsなどと同様にしてgetbyteなどを定義できるのでは無いかと予想して、 c_object.cや_autogen_class_object.hやhal.hに必要だと思われる修正をしたが、メソッドが無いとエラーが出てしまう。 autogen_class*.hで定義された定数をどこかでmethodとして定義できるようにしているのだと思っていたが、kernelのコマンドとして動かす場合は、別のやり方が必要なようだ。 どうなっているのだろう。 仕方が無いので、sleepなどを定義しているのと同じ方法で、mrbc_define_methodを使ってやってみたら、動作させることに成功した。 Arduinoで一文字読み出すようにhal_readを定義する。 unsigned char hal_read(int fd) { while (Serial.available() == 0); return Serial.read(); } 標準入力を組み込むためのヘッダファイルmrbc_get.hを作る。 #ifndef _MRBC_GET_H #define _MRBC_GET_H #include <Arduino.h> #ifdef __cplusplus extern "C" { #endif #include "mrubyc.h" void mrbc_init_class_get(void); #ifdef __cplusplus } #endif #endif //extern getbyteはhal_readの値をそのまま返すように書けば良いが、getcやgetsはC言語でどう書くのか考えていたが、getbyteを使ってrubyで書いて、それをmrbcのコードとして組み込んじゃえば良いということになった。 そのためのrubyのスクリプトget.rbがこれである。 def getc getbyte.chr end def gets() s="" s<<c=getc until c=="\n" s end 面倒だったので、getsは引数を取れるようにはしていない。 これをmrbcで処理してbyteコードを取り出す。
Read more...

mrubyc_arduinoにSPI

I2Cに続いて、mrubyc_arduinoにSPIを組み込んでみた。 Arduinoでは、SPIは通常はSPI.hとSPI.cppに書かれており、 これをmruby/cから呼び出す。 ArduinoでのSPIの設定の仕方が、以前とは変っていたので、少し手間取ったが、おそらく実装できただろう。 ヘッダファイルは、spi.hにしようと思ったが、ArduinoのSPI.hなどと干渉する可能性があるので、mbc_spi.hとした。 その内容は以下の通り、初期化関数だけを宣言した。 #ifndef _MRBC_SPI_H #define _MRBC_SPI_H #include <Arduino.h> #ifdef __cplusplus extern "C" { #endif #include "mrubyc.h" void mrbc_init_class_spi(void); #ifdef __cplusplus } #endif #endif SPIでは、周波数、ビットの順番、モードなどを設定する必要がある。 また、通信時にreadとwriteが同時に行われるという特徴をもつ。 そのため、I2Cよりは少し複雑になったが、 SPIクラス実装編 を参考にして、試行錯誤の末に完成した mrbc_spi.cppは、以下のとおりである。 #include "mrbc_spi.h" #include <SPI.h> extern "C" { static SPISettings mySPISettings = SPISettings(1000000, MSBFIRST, SPI_MODE0); extern uint8_t * make_output_buffer(mrb_vm *vm, mrb_value v[], int argc, int start_idx, int *ret_bufsiz); void c_spi_setmode(mrbc_vm *vm, mrbc_value v[], int argc){ MRBC_KW_ARG( unit, frequency, mode, first_bit ); int32_t spi_freq = 1000000; int spi_mode = SPI_MODE0; int spi_first_bit = MSBFIRST; if( MRBC_KW_ISVALID(frequency) ) spi_freq = mrbc_integer(frequency); if( MRBC_KW_ISVALID(first_bit) ) spi_first_bit = mrbc_integer(first_bit); if( MRBC_KW_ISVALID(mode) ){ switch(mrbc_integer(mode)){ case 1: spi_mode = SPI_MODE1; break; case 2: spi_mode = SPI_MODE2; break; case 3: spi_mode = SPI_MODE3; break; } } mySPISettings = SPISettings(spi_freq, spi_first_bit, spi_mode); MRBC_KW_DELETE( unit, frequency, mode, first_bit ); } void c_spi_new(mrb_vm *vm, mrb_value *v, int argc){ v[0] = mrbc_instance_new(vm, v[0].
Read more...

mruby/cでI2C

mrubyc_arduinoにI2Cを組み込んだので、ESP8266を使ってテストしてみた。 少し困ったのが、ピン番号が分かり難いことだった。 Arduinoのファイルの中を見て、 I2Cでどのピンが使われているのかを調べたら、以下のように定義されていた。 #define PIN_WIRE_SDA (4) #define PIN_WIRE_SCL (5) しかし、今度はその番号がどのピンか分からないので、これも調べたら、以下のようになっていた。 static const uint8_t D1 = 5; static const uint8_t D2 = 4; つまり、D1がSCLでSDAがSDAのようだ。 不思議なナンバリングな気がする。 I2Cで通信する相手としては、AHT10という温湿度計を用いた。 mruby/c用のプログラムは以下のようにした。 g=GPIO.new 2,GPIO::OUT i=I2C.new loop{ print "reading\n" g.write 1 i.write 0x38,0xba sleep_ms 20 i.write 0x38,0xe1,0x08,0x00 sleep_ms(10) while ( i.read(0x38,1).ord & 0x80 > 0) i.write 0x38,0xac,0x33,0x00 sleep_ms(10) while ( i.read(0x38,1).ord & 0x80 > 0) d=i.read(0x38,6).bytes h=(d[1]<<12)+(d[2]<<4)+(d[3]>>4) printf( "humidity %.2f\n",h*100.0/0x100000 ) t=((d[3] & 0x0f)<<16)+(d[4]<<8)+d[5] printf( "temperature %.
Read more...

mrubyc_arduinoにI2C

mrubyc_arduinoには、 GPIO, ADC, PWMはすでに組み込んだので、 mruby, mruby/cのI/O APIガイドラインに規定されているもので、 残っているのはI2C, SPI, UARTの三つである。 その内から、I2Cを組み込んでみた。 Arduinoでは、I2Cは通常はWire.hとWire.cppに書かれており、 C++を使っているので、C言語で書かれているmruby/cと組み合せるのに少し苦労したが、 なんとか動くようになった。 まず、i2c.hだが、これは以下のようにした。 #ifndef _I2C_H #define _I2C_H #include <Arduino.h> #ifdef __cplusplus extern "C" { #endif #include "mrubyc.h" void mrbc_init_class_i2c(void); #ifdef __cplusplus } #endif #endif mruby/cの他のマイコンへの実装では、 writeのときに数値や配列などの様々な形で指定されたものを出力するために、 make_output_bufferという関数を使っている。 この関数はSPIなどでも使うので、outputbuffer.cという別のファイルを作って、cppからも呼び出せるように以下のおまじないで囲んでおいた。 #ifdef __cplusplus extern "C" { #endif #ifdef __cplusplus } #endif mrbc_allocは(void *)なのだが、エラーが出ることがあったので、(uint8_t *)に型変換をして用いるという一箇所だけ変更を加えた。 i2c.cppは、ArduinoのWireの使い方を使って、以下のように書いた。 #include "i2c.h" #include <Wire.h> extern "C" { extern uint8_t * make_output_buffer(mrb_vm *vm, mrb_value v[], int argc, int start_idx, int *ret_bufsiz); void c_i2c_new(mrb_vm *vm, mrb_value *v, int argc){ Wire.
Read more...

mrubyc_ardinoのdigitalをAPI準拠

2026/4/13のブログで書いたmrubyc_arduino用のGPIOのプログラムは、APIガイドラインに完全には準拠しておらず、またマイコンの種類によっては定数が異なるという欠点があった。 折角なので、書き直してみた。 digital.hは、定数などが無くなったので、すっきりした。 #ifndef _MRBC_GPIO_H #define _MRBC_GPIO_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); #ifdef __cplusplus } #endif #endif digital.cは、以前よりはmethodが増えた分だけ長くなった。 #include "mrbc_gpio.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; if(argc>0) handle->pin_num = GET_INT_ARG(1); if(argc>1) pinMode( handle->pin_num, GET_INT_ARG(2) ); } static void c_gpio_setmode(mrb_vm *vm, mrb_value *v, int argc){ if( v[0].
Read more...

ch32x035でmrubyc_arduinoを動かす

ch32v203をmrubyc_arduinoから動かすことが出来たので、ch32x035でも出来るだろうと思って、やってみた。 しかし、両者には二つの大きな違いがあることが判明した。 一つは、flashの容量がch32v203が64kなのに対して、ch32x035は少し小さくて62kであることである。 2kぐらいは、工夫すればなんとかなるだろうと予想した。 もう一つの大きな違いが、ch32v203にあった隠しflashがch32x035には無いということである。 mrbのコードを隠しflashに置くことによって、flashが節約できる上に、後からそこだけを書き換えるという使い方が出来たのだが、ch32x035では同じことが出来無い。 これらを以下のように対応して、ch32x035でも、Arduinoを使ってmruby/cから制御できるようになった。 まず、ch32v203と同様に、vm_config.hの設定は、以下のようにした。 #define MAX_VM_COUNT 1 #define MRBC_USE_FLOAT 0 #define MRBC_REQUIRE_32BIT_ALIGNMENT 最適化はsmallest+LTOとしてコンパイルすると、digital,adc,pwmを組込んだ状態で、ギリギリflashには収まるようになったが、mrb用の領域や今後の拡張も考えると、もう少し工夫したい。 そこで、昨日のブログに書いたように、同時に一つのmrbしか動かさないことにして、rrt0をいじってみた。 rrt0.hは、MRBC_TASK_NAME_LENとmrbc_tcbとmrbc_initの部分だけを残した。 rrt0.cは、c_sleepやc_sleep_msからはarduinoのdelayを呼び出すようにして、mrbc_initは以下のように最低限のコマンドだけにした。 void mrbc_init(void *heap_ptr, unsigned int size) { hal_init(); mrbc_init_alloc(heap_ptr, size); mrbc_init_global(); mrbc_init_class(); mrbc_define_method(0, 0, "sleep", c_sleep); mrbc_define_method(0, 0, "sleep_ms", c_sleep_ms); } そして、_autogen_class_rrt0.hは削除した。 mrbのコードを走らせるコマンドは、以下のコマンドの替わりに、mrbc_run_mrblibを用いた。 mrbc_create_task( MRB_CODE_ADRS, 0 ); mrbc_run(); これで、一つのタスクしか動かせなくなったが、3kほど容量に余裕ができた。 mrbのコードは、余っているflashに置くことになるのだが、Arduinoには組み込まないようにしたい。 また、ch32x035ではUSBからのプログラムの書き込みが出来るので、それを使いたい。 これらの要請を、どのようにしたら実現できるかを考えていたが、以下のようにすれば可能となることに気が付いた。 まず、mrbのコードはスケッチには組み込まずに、例えば以下のように指定して、コンパイルしたバイナリを出力する。 mrbc_run_mrblib( 0x8000000+57976 ); そして、そのbinファイルのバイトサイズを先の+以降に指定して、もう一度コンパイルする。 binのサイズが変っていないことを確認する。 もしbinのサイズが変ったら、変らなくなるまでこれを繰り返す。 この直後にmrbのコードを置けば、それが実行される仕組みである。 そのために、実行したいrubyスクリプトを用意して、mrbcで処理してできたmrbをbinファイルと結合する。 mrbc test.rb cat ch32.ino.CH32X035G8U.bin test.mrb >test.bin これをflashに書き込めば良いのである。 USBの書き込みは、wchispを用いたが、最初はverifyに失敗して苦労したが、何度も書き込んだらうまく行った。 ch32x035ボードを使ったのだが、書き込みモードにするためにBOOTボタンを押しながらUSBに接続して、すぐに以下のコマンドを実行する。
Read more...

ch32v203でmrubyc_arduinoを動かす

ch32v203は64kのflashを持つので、多少の工夫をすれば、mrubyc_arduinoを使えそうだと思い、試してみた。 なかなか動かなくて苦労したが、最終的には隠しflashにmrbcのコードを書き込むことによって、便利に使えることが分かった。 mrubyc_arduinoにdigital,adc,pwmを組込むと、flashには収まりきらないが、以前のブログに書いたようにfloatをoffにすると、ぎりぎり入るようになる。 このとき、最適化はsmallest+LTOが良い。 これでch32v203に書き込んだら、うまく動いてくれないかなと思ったが、動かない。 シリアルにも繋げていない状態だったので、ピンの電圧でしか状態をモニタできなかったので、原因を見付けるのに苦労した。 mrbc_init中で呼ばれているmrbc_init_classで止まっているところまで分かって、 さらに mrbc_run_mrblib、mrbc_load_mrb、mrbc_load_irep、load_irep、load_irep_1と辿って、念の為にbin_to_uint16を見ていたら、ようやく原因が明らかになった。 ch32v20xは、32bit-alignmentになっているようで、 vm_config.hでコメントアウトを外して、以下のように定義しておかないといけないことが判明した。 #define MRBC_REQUIRE_32BIT_ALIGNMENT ESP8266でもなかなか動かせなくて困っていたが、同じ原因のようだ。 ようやく動くようになったので、いろいろと試していたが、ch32v203には、64kのflashの後に、アクセスの遅い160kのflashがあり、mrbのコードはここに配置すると良いということに気が付いた。 そこで、拡張flashに置いたmrbのコードを走らせるArduinoのスケッチを以下のように書いてみた。 #include <mrubyc.h> #define MEMORY_SIZE (1024*10) static uint8_t memory_pool[MEMORY_SIZE]; int hal_write(int fd, const void *buf, int nbytes) { return (nbytes); } int hal_flush(int fd) { return 0; } void setup() { mrbc_init(memory_pool, MEMORY_SIZE); mrbc_init_class_digital(); mrbc_init_class_adc(); mrbc_init_class_pwm(); mrbc_create_task( 0x8010000, 0 ); mrbc_run(); } void loop() {} ch32v203とWCH-LinkEと接続して、バイナリとして出力したbinを以下のコマンドで書き込む。 ./minichlink -w sketch.bin flash -b mrbのコードは、以下ように作成して、拡張flashに書き込む。 mrbc test.rb mv test.
Read more...

mrubyc_arduinoでのflashの節約

mrubyc_arduinoを小さなflashやメモリのマイコンで動かすために、どのようにすれば良いかを考えている。 GPIO, ADC, PWMを組み込んだ状態で、例えばボードとしてArduino Nano Everyを指定してコンパイルすると、flashが35kほどオーバーする。 この条件のもとで、 最低限のrubyの機能を残した上で、必要なflashのサイズを減らそうと試みてみた。

2026/4/15の記事で書いたように、適切な修正をして、vm_config.hで以下のようにFloatを無効にすると、 5.5kほど削減できる。

MRBC_USE_FLOAT 0

また、debug機能が有効になっているようなので、vm_config.hで以下のようにすると、さらに2kほど減少する。

//#define MRBC_DEBUG

NDEBUGを定義しても同じ効果が現れるのかと思ったが、この定数は特殊な意味を持つようで、逆に大きくなってしまった。

methodを使えるようにするために、プログラムが大きくなるので、最低限のmethodだけを残して、残りのmethodを消せば、さらに小さくできるはずである。 そのためには、_autogen_class_*.hの中のmethod_symbols_*とmethod_functions_*の一部をコメントアウトすると、対応するmethodだけを無効にできる。 array,hash,range,string,rrt0について、多くのmethodを無効にしたら、さらに15kほど減少した。

あと12kぐらい減らせば、Arduino Nano Everyのflashに収まる。 上記に加えてできそうな工夫としては、 objectの不要なmethodの削除や、 taskは一つしか走らせない前提でのrrt0.cの関数の簡略化などが試せるだろう。 flashが少し大きいch32v203などでは、上記の工夫でなんとかflashに入るかも知れないので、いつか試してみたい。

ソースを眺めていて感じたのだが、rubyでは同じ機能のmethodに複数の名前があるのだが、これはマイコンにとっては無駄になっている。 例えば、Arrayの要素数を求めるmethodには、count, size, lengthの三つが定義されている。 mrbcでコンパイルする際にこれらをsizeに統一してしまって、マイコンにはsizeのみを定義するようにすれば、多少ではあるが、無駄が減るように思う。 また、"%d,%d\n"%[a,b]という表現も、mrbcでprintf("%d,%d\n",a,b)に変換するようにすれば、マイコンへの負荷を変えずに、rubyの表現が広がる気がするけど、難しいかな。

2026/4/19追記 使わないであろうmethodを削って、flashの使用量を減らそうとしていたが、おそらくこれはうまくいかないことが判明した。 mrblib.cでmrubyのコードが定義されており、この中で使っているmethodを消してしまうと、エラーが生じる。 例えば、empty?なんかは使わないと思っていたが、消したら駄目のようだ。 または、mrblib.cも同時にいじる必要がある。

Read more...

mrubyc_arduinoでPWM

mrubyc_arduinoにGPIOとADCを組込むことに成功したので、次はPWMだろうということでやってみたが、思ったよりも苦労した。 mruby/cのPWMでは、ONの割合と周波数を変えられるようになっている。 しかし、ArduinoのanalogWriteでは割合のみで、toneでは周波数のみしか変えられ無い。 仕方が無いので、これらを独立に変えるのは諦めて、割合のみか周波数のみを、それぞれanalogWriteとtoneで指定することにした。 次に問題だったのが、toneは本来は音をならすためのもので、音の継続時間を無しでも指定しても動くようにするために、C++でtone関数がオーバーロードされている。 そのため、C言語から呼び出すのが難しく、cppとした。 まず、pwm.hは、externをどこに入れるべきか試行錯誤したが、次のようにした。 #ifndef _PWM_H #define _PWM_H #include <Arduino.h> #ifdef __cplusplus extern "C" { #endif #include "mrubyc.h" typedef struct PWM_HANDLE { uint8_t pin_num; uint16_t period; uint8_t duty; } PWM_HANDLE; void mrbc_init_class_pwm(void); #ifdef __cplusplus } #endif #endif 問題のpwm.cppだが、これらの関数はc言語から呼ばれるので、全体をexternで囲って、次のようにした。 #include "pwm.h" extern "C" { void c_pwm_new(mrb_vm *vm, mrb_value *v, int argc){ v[0] = mrbc_instance_new(vm, v[0].cls, sizeof(PWM_HANDLE)); PWM_HANDLE *handle = (PWM_HANDLE *)v[0].instance->data; handle->pin_num = GET_INT_ARG(1); MRBC_KW_ARG(frequency, freq, duty); if( MRBC_ISNUMERIC(duty) ) analogWrite( handle->pin_num, MRBC_TO_INT(duty)*255/100.
Read more...

mruby/cのFloatについての小さなバグ

Arduinoを利用してESP32でmruby/cからGPIOとADCを使うことができるようにはなった。 私がよく使うマイコンは、ESP32よりもflashやメモリの小さなものなので、そのようなマイコンでmruby/cを動かせないかと思っている。 Arduino ZeroやXiao SAMD21に使われているATSAMD21G18Aとか、 WCHマイコンの中では高性能なch32v305などが、次のターゲットであろう。 これらは、今のやり方で動きそうに思うが、残念ながらこれらは手元には無いので、確かめられない。 さらにその次には、 ch32v203とか、 Arduino Nano Everyで使われているatmega4809などかな。 これらは、メモリも少ないが、flashも足りていない。 そこで、Arduino Nano Everyを例にして、mrubyc_arduinoのflashの削減ができないかを検討していたら、mruby/cの小さなバグを見付けたので、そのことについて書くことにする。

Arduino Nano Everyのflashは、48kである。 ADCを使った簡単なプログラムをmrubyc_arduinoでコンパイルすると、82k程となる。 flashの使用量をかなり減らさないといけない。 vm_config.hを見ると、以下のような指定がされている。

MRBC_USE_FLOAT 1
MRBC_USE_MATH 0
MRBC_USE_STRING 1

機能を制限して、flashやメモリの使用量を減らすオプションのようだ。 Floatをoffにするために、0にしてみたが、コンパイルでエラーが出てしまう。 試行錯誤の末、以下のような修正をすると、これを0にしても動くようにできた。 boxing_no.h, value.cの中には、".d"や"->d"などが使われているが、これらはfloatを使わない場合には使えなくなっているので、それらの使われている場所を以下で囲って無効にする。

#if MRBC_USE_FLOAT
#endif

そして、_autogen_builtin_class.hの中の以下の行も、同様に囲む。

extern struct RBuiltinClass mrbc_class_Float;

最後に、class.cの中のclassの定義を以下のようにする。

#if MRBC_USE_FLOAT
  MRBC_CLASS(Float),            // MRBC_TT_FLOAT     = 5,
#else
  0,
#endif

これで、floatをoffにしてもコンパイルが通るようになる。 Arduino Nano Everyの場合には、flashの使用量が5.5kぐらい減った。 同じくらい効果のある削減を、あと5個ぐらい組合せれば、動くようになるかも知れない。 mruby/cでFloatをoffにすることはあまり無いのかも知れないが、0にしたときにエラーが出るのは、一応のバグと言えるのでは無いだろうか。 Stringをoffにしようとしても、エラーが出るが、こちらはややこしそうなので、あまり手を付けていない。

さらにflashを減らす試みとして、あまり使わないclassを取り除けないかも試してみた。 具体的には、Range,Proc,Hash,Arrayなどがあまり使わないclassかなと思う。 十分には検証していないが、rangeではflashは1.5kしか小さくならなかった。 Proc,Hash,Arrayは、取り除いてみたが、vmのcoreの部分で使っている感じなので、うまく動かない可能性が高いと思う。 たとえ、これらを全て取り除いても、10kぐらいしか減らないことが分った。 今後、Rangeは殆ど消して、Proc,Hash,Arrayでは不要なmethodを取り除いたときに、どうなるかを確かめてみたいと思う。 また、rrt0.cの中には、複数のタスクを動かすためのプログラムが書かれているが、一つのタスクが動けば良いと考えれば、工夫するとflashの節約が出来る可能性もあるのでは無いだろうか。

2026/4/16追記 value.hにもマクロで".d"を使っている部分があったので、これも修正した。

Read more...