chipKIT Max32上でのmruby中速開発

2014/2/20追記
下記はRSTRING_PTR()を誤った使い方をしています。mrb_valueにRSTRING_PTR()マクロを使った場合に取り出した文字列は終端がNULLとは限りませんのでご注意

前回、LEDの制御ができました。
しかし、バイトコードをCに埋め込む方法だと、毎回mruby丸ごとを含むスケッチのアップロードに5分ほどかかってしまいます。やってられません。

なんとかならないかと考えた上、Arduino APIのシリアル通信を使い、mrubyのバイトコードだけ送り込んでやれば、毎回スケッチをアップロードしなくてもいけるんじゃないかと考え、やってみました。作戦はこんな感じで。

  • chipKIT上で動くスケッチでは、loop()の中でシリアル入力を待ち受け、何か受信したらmrubyのバイトコードの先頭とみなし、全部受信して、ロードする。何も受信しなかった場合は、既にロードしたmrubyのloopを1回呼び出す。これが繰り返される。
  • Processingで、mrubyのバイトコードをchipKITに送り込むスケッチを書く。rubyコードを修正したときは、mrbcでコンパイルした*.mrbをこいつで送り込む。

あ、スケッチというのはArduinoやProcessing用語で、要するにソースコードです。

う〜ん、いまいちな動画です。ピントが合ってないし・・。2回目にfoo.rbを編集してchipKITに送った後、LEDが点滅しっぱなしに見えますが、実際は高速点滅してます(笑)

chipKIT上のスケッチは以下の様なものを用意しました。

#define CHANGE_HEAP_SIZE(size) __asm__ volatile ("\t.globl _min_heap_size\n\t.equ _min_heap_size, " #size "\n")

#include <mrbconf.h>
#include <mruby.h>

#include <mruby/irep.h>
#include <mruby/string.h>  
#include <mruby/value.h>
#include <mruby/dump.h>

mrb_state *mrb;
char code[4048];     //とりあえず
mrb_value sketchObject;

//print function
mrb_value cputs(mrb_state *mrb, mrb_value self){
  mrb_value val;
  mrb_get_args(mrb, "S", &val);
  Serial.println(RSTRING_PTR(val));
  return mrb_nil_value();
}

//digitalWrite function 
mrb_value cdigitalWrite(mrb_state *mrb, mrb_value self){
  mrb_int pin;
  mrb_int val;
  
  mrb_get_args(mrb, "ii", &pin, &val);
  digitalWrite(pin, val);
  return mrb_nil_value();
}

//delay function
mrb_value cdelay(mrb_state *mrb, mrb_value self){
  mrb_int val;
  mrb_get_args(mrb,"i", &val);
  delay(val);
  return mrb_nil_value();
}

unsigned short receiveNewCode(){
  unsigned short lengthH = Serial.read();
  while(Serial.available() == 0);
  unsigned short lengthL = Serial.read();
  unsigned short codeLength = (lengthH << 8) | lengthL;
  
  //send first ack("!")
  Serial.write(33);

  unsigned short readedLen = 0;
  while(readedLen < codeLength){
    for (int i = 0 ; i < 100; i++){
      while(Serial.available() == 0);
      code[readedLen] = Serial.read();
      readedLen++;
      if (readedLen == codeLength)
        break;
    }
    Serial.write(35);
  }
  Serial.println("uploaded(from mruby_runner)");   
  return readedLen;
}


void setup(){
  
  //the magic code to increase heap size to 100kb
  // see http://www.chipkit.org/forum/viewtopic.php?f=19&t=1565
  CHANGE_HEAP_SIZE(110*1024);
  
  Serial.begin(9600);
  pinMode(7, OUTPUT);        //set pin7 as OUTPUT
  
  mrb = NULL;
  
}

void define_methods(mrb_state *mrb){
  mrb_define_method(mrb, mrb->object_class, "cputs", cputs, ARGS_REQ(1));
  mrb_define_method(mrb, mrb->object_class, "cdigitalWrite", cdigitalWrite, ARGS_REQ(2));
  mrb_define_method(mrb, mrb->object_class, "cdelay", cdelay, ARGS_REQ(1));  
}

void loop(){
  
  if (Serial.available() > 0 ){
    /*  upload mode  */
    
    if (unsigned short codeLen = receiveNewCode()){
      if (mrb) mrb_close(mrb);
      mrb = mrb_open();
      define_methods(mrb);
      FILE *fp = fmemopen(code, codeLen, "rb");  //open buffer as FILE  stream. platform dependent.
      sketchObject = mrb_load_irep_file(mrb, fp);
     
      mrb_value result = mrb_funcall(mrb, sketchObject, "setup", 0);
      if (mrb->exc){
        Serial.println("exception occured in setup on new code");
      }
    }
    
  }else{
    if (!mrb) return;
    mrb_value result = mrb_funcall(mrb, sketchObject, "loop", 0);
    if (mrb->exc){
      Serial.println("exception occured in loop");
    }
  }
}

receiveNewCode()の中で100byte毎にackを送りつつ、新しいバイトコードを受け取っています。

Processingのコードは以下の様なものです。

import processing.serial.*;

Serial serial = new Serial(this, "/dev/cu.usbserial-A6009CM4",9600);
delay(6000);

byte bytecode[] = loadBytes("/path/to/*.mrb");


//at first we read all
for (int i = 0; i < 100; i++){
  while(serial.available() > 0){
    print(serial.readChar());
  }
  delay(10);  
}

println("mruby bytecode updating..." + bytecode.length + " (bytes) to upload"); 

int bytesWritten = 0;
char lengthH = (char) (bytecode.length >> 8);
char lengthL = (char)(bytecode.length & 0x00FF);
serial.write(lengthH);
serial.write(lengthL);

while(serial.available() == 0);
println(serial.readChar());    //receive first ack

for (int written = 0 ; written < bytecode.length;){
  for (int i = 0 ; i < 100; i++){
    serial.write(bytecode[written]);
    written++;
    if (written == bytecode.length)
      break;
  }
  while(serial.available() == 0);
  print(serial.readChar());
}

println("");
println("done! " + bytecode.length + " bytes uploaded");

//work as serial monitor(read only)
for (int i = 0 ; i < 1000000; i++){
  while(serial.available() > 0){
    print(serial.readChar());
  }
  delay(10);
}

こちらは100byte毎にackを待ちつつ、バイトコードを送信してます。chipKIT側のシリアルの受信バッファが127byteしかないらしいので、少しずつ送ることにしました。あと先頭の方にある6000ミリ秒のウェイトは、なんかシリアルのセットアップに時間がかかるっぽい?ので入れてます(これのせいで2時間悩みました・・)。

さて、肝心のmrubyコードですが、適当なクラスにsetup()とloop()を実装して、ファイルの最後で 適当なクラス.new()でインスタンスを返せば良いことにしています。chipKIT上のスケッチでは、chipKIT側のloop()のコードを見てもらうとわかると思いますが、最初一回だけsetupを呼んで、あとは繰り返しloopを読んでいます。

例えばこんな感じです。

HIGH = 1
LOW = 0

class Foo
	def setup
		@count = 0
	end
	def loop
		cdigitalWrite(7, HIGH);
		cdelay(1000);
		cdigitalWrite(7, LOW);
		cdelay(1000);
		cputs "count = " + @count.to_s
		@count += 1
	end
end
Foo.new

やっていることはほぼ前回同様で、1秒ごとに7番ピンをON/OFFしてます。

これをmrbcを使って
$ bin/mrbc foo.rb
とすると、foo.mrbができます。これがバイトコード(用語正しいの??)です。Processingのスケッチでこいつをuploadしてます。

これでサクッとruby側のコードを修正する環境はできました。コマンドライン用に整理すれば、makeと連動して使えそうです。、、が、ツッコミどころ満載です。

  • シリアル通信の準備待ち6秒が痛い・・これがなければ高速開発といえるのに・・
  • シリアル送信はできるけど、受信ができない。chipKIT側は何か受信するとmrubyのバイドコードと認識しちゃうので・・。
  • chipKIT Max32を再起動、USB挿し直しすると、何も動かなくなる。なぜならバイドコードの保存先がないので。USBを繋ぎ直すと、毎回バイトコードのアップロードしなおしです(笑)。まぁ、実験する時用ということで・・。
  • 結局USBでつながってないとダメなら、これって最初からFirmata使えば良いんじゃね?rubyバインディングもあるし

下2つはやったあと気付いちゃったのですが、mrubyを使ってみる人の参考になればと思って記事にしてみました。