SoundFlowerbedに音量コントロールをつけてみた。

MacでアプリやYoutubeの音を録音するのにSoundFlowerが便利でよく使っています。

http://cycling74.com/products/soundflower/

SoundFlower

SoundFlowerはいわゆる仮想サウンドバイスで、SoundFlowerに出力すると、SoundFlowerの入力側に入ってくるという仕組みです。AudacityAbleton Live, その他録音ができるアプリで入力にSoundFlowerを指定すると、iTunes/Youtube/その他アプリから出ている音はほぼなんでも録音できるというやつです。プロテクトがかかってようが関係なし。Windowsでも似たようなのがあります。

SoundFlowerbed

で、SoundFlowerに出力してるだけの状態だと、Macのスピーカからは音がでないので、それをモニターするSoundFlowerbedというツールがついてきます。
このツールで、モニター先のオーディオデバイスを選べます。

音量が。。

そこまでは良いのですが、システムのサウンドの出力先をSoundFlowerにすると、メニューバーの音量アイコンやキーボードの音量キーではSoundFlowerの音量しか調整できなくて、不便に感じていたわけです。

一見SoundFlowerの音量が調整できるなら良いじゃないかとおもいますが、録音レベルを最大にしつつ、小さい音量で聞きたい場合などは、モニター先のオーディオデバイスの音量も調整したいわけで。。

改良!

で、最近になってSoundFlowerがオープンソースなことを知りました。なので、SoundFlowerbedを改良してモニター先の音量も調整できるようにしてみました。

音量調整に困っている方はお試し歓迎です。
https://www.dropbox.com/s/qlgozdjimbnxeie/Soundflowerbed_volumecontrol.zip


本家に取り込まれるようgithubにてpull-requestしようと思ってますが、別件のパッチのpull-requestに反応がないのでちょっと待機中。

mrubyのメモリ使用量(リベンジ)の続き

mrubyのメモリ使用量(リベンジ) - kyabの日記では、参考サイトを元にmrb_open()直後までのメモリ使用量を約30kb減らすことができました。

total allocated size = 77858
malloc count = 1458
max memory usage = : 8381936

ただ、これはMac上のllvm-gccでの結果でした。

壊れたchipKIT Max32の2代目が到着したので、chipKIT Max32上で調べてみたら、次の結果になりました。

total allocated size = 77858
malloc count = 1458
max memory usage = : 62024

トータルのメモリ確保サイズはもちろん同じなのですが、max memory usageが多分正しい値で出ています。これはmallocで返ったアドレスの一番大きな値と一番小さい値の差です。
要するに128kb中、半分しか使っていないってことですね。これは良い感じです。まぁスタックとglobal/static変数領域も必要ですが。

mrubyのメモリ使用量(リベンジ)

2014/2/20追記
以下のメモリ削減はあくまで当時のmrubyに対する変更です。今でも有効かわかりませんし、#pragma packを使うと動作しない場合もあるとかないとか、、試す方は注意ください。また当時はmrbconf.hの#define を直接編集していますが、現在はbuild_config.rb内でcc.definesを使用するのが適切です。mrubyソースのexampleが参考になります。


少し前に、chipKIT Max32上でのメモリ使用量を調べたのですが、どうも間違ってるぽいので、やり直し+メモリ(RAM)削減を試みてみました。

前回:mrubyのメモリ使用量 on chipKit Max32 - kyabの日記

測り方

chipKIT上で計測するとアップロードに時間がかかって面倒なので、Mac上のllvm-gcc 4.2 32bitで試行錯誤。

次のようなコードで検証しました。mrb_open()には、mrb_open_allocf()というメモリアロケータを指定できるバージョンがあるので、それを利用して、mrb_open()が終わるまでの総mallocサイズを計算しています。
freeされた分がわからないのですが、まぁ、雰囲気程度はつかめるだろうと。あと、mrb_open()の前と後でmalloc()で返ってくるアドレスを比較するという方法もやってみたのですが、フラグメンテーションとかがあるのか、ちゃんとした値になりませんでした。

#include <stdio.h>
#include <mruby.h>
void *heap_bottom;
size_t total_size = 0;
void *max_reached = 0;
int malloc_count = 0;

void *myallocf(struct mrb_state *mrb, void *p, size_t size, void *ud){
    if (size == 0){
        free(p);
        return NULL;
    }else{
        malloc_count++;
        total_size += size;
        void *ret = realloc(p, size);

        if (ret > max_reached){
            max_reached = ret;
        }
        return ret;
    }
}
int main(int argc, const char * argv[])
{
    heap_bottom = realloc(NULL,1);
    
    mrb_state *mrb = mrb_open_allocf(myallocf, NULL);
    mrb_close(mrb);
    
    printf("total allocated size = %lu\n",total_size);
    printf("malloc count = %d\n",malloc_count );
    printf("max memory usage = : %lu\n", (unsigned long)max_reached - (unsigned long)heap_bottom);
    return 0;
}

結果

total allocated size = 107521
malloc count = 1523
max memory usage = : 7567280

1523回malloc(またはrealloc)されて、トータル108kb位確保されていますね。max memory usageはヒープの一番下のアドレスから、確保された一番大きいアドレスまでのサイズですが、多分大きいサイズと小さいサイズの領域は離れていたりするので、約に立たない値とみなして、以降無視します。

chipKIT Max32のRAMは128kbなので、(108kb-途中でfreeされた分)が使えるわけで、実際ラジコンの自動走行までは出来たのですが、あまり大きいプログラムだと厳しいです。なので、省メモリ化をやってみました。

省メモリ化

以下のサイトを参考にしました。
mruby-NXTプロジェクトの挑戦 No.2 RAM 64kbの環境で動かされています。ソースは公開されていない様子。
mruby for embedded systems ソースも公開されていますね。

どちらも内部のハッシュのデータ構造やアルゴリズムを変えたりしていますが、mrubyの内部コードを見始めたばかりの自分にはそこまでは真似できそうにないので、表面的に手を出せそうなところだけ。ソースも置いておきます。

https://github.com/kyab/mruby_min

以下、やってみたことと、それぞれ108kbからどれだけ減ったかの経緯です。

1.MRB_USE_FLOAT

include/mrbconf.h

/* add -DMRB_USE_FLOAT to use float instead of double for floating point numbers */
#define MRB_USE_FLOAT

浮動小数点にdoubleではなくfloatを使うようにします。約3.5kb削減できました。

2.MRB_HEAP_PAGE_SIZE

include/mrbconf.h

/* number of object per heap page */
#define MRB_HEAP_PAGE_SIZE 8

ページ毎のオブジェクトサイズ(う〜ん、ページ単位でメモリ確保して、幾つかオブジェクトを入れるような内部構造なんでしょうか?)を減らします。約8.5kb削減できました。

3 コア機能の幾つかを削る

include/mrbconf.h

/* -DDISABLE_XXXX to drop the feature */
#define DISABLE_REGEXP	        /* regular expression classes */
#define DISABLE_SPRINTF	/* Kernel.sprintf method */
//#define DISABLE_MATH		/* Math functions */
#define DISABLE_TIME		/* Time class */
#define DISABLE_STRUCT	/* Struct class */
#define DISABLE_STDIO		/* use of stdio */

TIMEはどちらにせよ使えないのですが、SPRINTF,STRUCT,STDIOも削ってみました。約3.5kb削減。

4 khashのデフォルトサイズを小さく

include/mrbconf.h

/* default size of khash table bucket */
//#define KHASH_DEFAULT_SIZE 32
#define KHASH_DEFAULT_SIZE 8

内部のハッシュ?のデフォルトサイズを小さくします。これも約3.5kb削減。

5 mrb_value構造体を小さく

include/mruby/value.h

#pragma pack(1)
typedef struct mrb_value {
  union {
    mrb_float f;
    void *p;
    mrb_int i;
    mrb_sym sym;
  } value;
  enum mrb_vtype tt:8;
} mrb_value;
#pragma pack()

#pragma packで詰めます。3kb削減。

6 シンボル名の長さ制限

src/symbol.c

#pragma pack(1)
typedef struct symbol_name {
  unsigned char len;
  const char *name;
} symbol_name;
#pragma pack()

シンボル名を255文字までに制限します。3kb削減。

7 インスタンス変数の持ち方をセグメンテーションリストに。

@takahashimさんに教えてもらいました。
include/mrbconf.h

/* use segmented list for IV table */
//#define MRB_USE_IV_SEGLIST
#define MRB_USE_IV_SEGLIST

/* initial size for IV khash; ignored when MRB_USE_IV_SEGLIST is set */
//#define MRB_IVHASH_INIT_SIZE 8
#define MRB_IVHASH_INIT_SIZE 3

これは5kb削減。

ここまでを全部適用した結果です。

total allocated size = 77858
malloc count = 1458
max memory usage = : 8381936

大分減りましたね。108kb - 78kb = 30kb稼げました。これで当面いけそうです。

番外編1

mruby-NXTプロジェクトの挑戦 No.2 の(6)mrblib の isec はROMを参照する もやってみました。rom_iseqブランチにコードを置いてます。
これは、8.5kb削減と、なかなかの検討ですが、エンディアン依存のコードになってしまったので、とりあえず今は外してます。

番外編2

これも@takahashimさんに教えてもらったのですが、mrblib/*.rbを削除するという手です。これは結構強力で、40kb位削減できました。しかしこれらのファイルを削るとArrayとかKernelとかHashとか、Rubyのコアクラス達への影響が大きいのでこれもやらないことにしました。でも結局ここで使ってない機能を削るのが一番手っ取り早いですね。

chipKIT Max32上で試す

さて、メモリ削減バージョンだとどれくらいのサイズのコードがchipKIT Max32上で動くのか試します。


・・・と言いたいところですが、違う回路の実験中、配線ミスでchipKITを壊してしまいました(笑)。今2台目を注文中です・・・。

chipKit Max32でのmrubyビルド再考

引き続きArduino互換機chipKit Max32でmrubyを動かすシリーズです。
今まではchipKit Max32のIDEであるMPIDEのライブラリフォルダにmrubyのソースを丸ごとコピーするという方法を取って来ましたが、MPIDEのライブラリフォルダは*.c/*.hの配置に制限があったりして、イマイチスマートではなかったです。
Arduino互換機 chipKIT Max32でmrubyを動かす - kyabの日記

最近もっとましなやり方を見つけたので書いてみます。mrubyのビルドもrubyを使う方法になって、クロスコンパイルなどもスマートになったのでその辺やりたい人の参考にもなるかも。

あ、Mac前提で書いてます。

mrubyの入手

githubから落とします。

git clone https://github.com/mruby/mruby.git

MPIDEの入手

以下からMac OSX用をダウンロード
https://github.com/chipKIT32/chipKIT32-MAX/downloads

以下、mrubyは/work/mrubyに、MPIDEは/Applications/Mpide.appに置いたものとして説明。

build_config.rbの編集

/work/mruby/build_config.rbの最後に以下を追記。chipKit Max32用のクロスコンパイルをするための設定です。書き方とどのようにビルドされるかはここに載ってますね。コンパイルオプションはMPIDE上でシフトを押しながらビルドすると詳細が出てくるので、それを参考にしました。
2013/6/21追記 mruby本体にexampleとして入りました。以下はmruby本体の examples/targets/chipKitMax32.rbをコピペでOK.

MRuby::CrossBuild.new("chipKitMax32") do |conf|
 	toolchain :gcc

 	MPIDE_PATH = "/Applications/mpide.app"
 	MRUBY_PATH = "/work/mruby"

 	conf.cc do |cc|
 		cc.command="#{MPIDE_PATH}/Contents/Resources/Java/hardware/pic32/compiler/pic32-tools/bin/pic32-gcc"
 		cc.include_paths = ["#{MPIDE_PATH}/Contents/Resources/Java/hardware/pic32/cores/pic32",
 							"#{MPIDE_PATH}/Contents/Resources/Java/hardware/pic32/variants/Max32",
 						    "#{MRUBY_PATH}/include"]
 		cc.flags << "-O2 -mno-smart-io -w -ffunction-sections -fdata-sections -g -mdebugger -Wcast-align " +
 				"-fno-short-double -mprocessor=32MX795F512L -DF_CPU=80000000L -DARDUINO=23 -D_BOARD_MEGA_ " +
 				"-DMPIDEVER=0x01000202 -DMPIDE=23"
 		cc.compile_options = "%{flags} -o %{outfile} -c %{infile}"
 	end

 	conf.archiver do |archiver|
 		archiver.command = "#{MPIDE_PATH}/Contents/Resources/Java/hardware/pic32/compiler/pic32-tools/bin/pic32-ar"
 		archiver.archive_options = 'rcs %{outfile} %{objs}'
 	end

 	conf.bins = []

 end

mrubyをコンパイル

cd /work/mruby
make clean
make

これで、/work/mruby/build以下にビルドされます。host/bin以下にmirb,mrbc,mrubyが出来ていて、chipKitMax32/lib以下にlibmruby.aが出来ているはず。

MPIDEの設定

mrubyのヘッダとライブラリをMPIDEから参照するようにします。
/Applications/Mpide.app/Contents/Resources/Java/hardware/pic32/platforms.txtを開いて、以下の行を編集

...
pic32.recipe.c.combine.pattern={0}{1}::{2}::{3}{4}::-o::{5}{6}.elf::{7}::{8}::-L/work/mruby/build/chipKitMax32/lib::-lmruby::-L{9}::-lm::-T::{10}/{11}
...
pic32.compiler.cpp.flags=-O2::-c::-mno-smart-io::-w::-fno-exceptions::-ffunction-sections::-fdata-sections::-g::-mdebugger::-Wcast-align::-fno-short-double::-I/work/mruby/include

保存して、MPIDEを再起動すればOKです。

コンパイルできるか確認

MPIDEで適当なスケッチを書いて、ビルドしてみます。

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

#include "mruby.h"

CHANGE_HEAP_SIZE(100*1024);  //ヒープサイズを100kbにする。

void setup(){
  Serial.println(9600);
  mrb_state *mrb = mrb_open();
  Serial.println("mruby initialized.");
  mrb_close(mrb);
}

void loop(){
}

"Done compilling."と表示されればビルド成功です! あとはアップロードして実行するだけ。

これでmrubyを更新したときは、単にmrubyをmakeしなおせば良いですね。

mrubyのメモリ使用量 on chipKit Max32

01/26追記: 測り方間違ってるぽいです! こちらを参照ください。mrubyのメモリ使用量(リベンジ) - kyabの日記 以下誤り!
chipKit Max32上でのmrubyのメモリ使用量を調べてみました。といってもmrb_open()しただけの状態です。

mrbconf.hはこんな感じで、TIME,GEMSをDisableにしてます。

/* -DDISABLE_XXXX to drop the feature */
#define DISABLE_REGEXP	        /* regular expression classes */
//#define DISABLE_SPRINTF	/* Kernel.sprintf method */ //ok
//#define DISABLE_MATH		/* Math functions */ //ok
#define DISABLE_TIME		/* Time class */   //ng
//#define DISABLE_STRUCT	/* Struct class */ //ok
//#define DISABLE_STDIO		/* use of stdio */ //ok

/* Now DISABLE_GEMS is added as a command line flag in Rakefile, */
/* we do not need to set it here. */

#define DISABLE_GEMS

検証コード。mrb_open()の直前と直後でmallocして、アドレスを出しています。スタックの底、ヒープの範囲はHow to Measure Available Memory?を参考にしました。

#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>

extern __attribute__((section("linker_defined"))) char _heap;
extern __attribute__((section("linker_defined"))) char _min_heap_size;

void setup(){
  CHANGE_HEAP_SIZE(100*1024);
  Serial.begin(9600);
  
  int *p = (int *)malloc(sizeof(int));
  Serial.print("before mrb_open(): allocated addres = ");
  Serial.println((unsigned int)p, HEX);
  
  mrb_state *mrb = mrb_open();
  
  int *p2 = (int *)malloc(sizeof(int));
  Serial.print("after mrb_open(): allocated addres = ");
  Serial.println((unsigned int)p2, HEX);
  
  byte *pTopHeap = (byte *)&_heap + (unsigned int) &_min_heap_size;
  Serial.print ('\n');
  Serial.print("Bottom of stack: ");
  Serial.println((unsigned int) &pTopHeap, HEX);
  Serial.print("top of heap: ");
  Serial.println((unsigned int) pTopHeap, HEX);
  Serial.print("Bottom of heap: ");
  Serial.println((unsigned int) &_heap, HEX);  
  
}

void loop(){
}

結果

before mrb_open(): allocated addres = A0000FD8
after mrb_open(): allocated addres = A00075F0
Bottom of stack: A001FFC8
top of heap: A0019FD0
Bottom of heap: A0000FD0

0xA00075F0 - 0xA0000FD8 = 26148byte消費していることになります。
試しにSPRINTF,STRUCT,STDIOをDisableにしてみたのですが、500byte程減っただけでした。

う〜ん、25kbですか。chipKit Max32のRAMは128kbなので、大分余裕があるように思えます。
でも、mruby-NTXのプロジェクトの挑戦 を見ると、最初98kbで、それをチューニングして64kbのRAMに収めたとある・・。測り方間違ってるのかなぁ。

あと、実際の動作コードでどれくらいの事ができるのか(どれくらい呼び出しネストできる?、どれくらいオブジェクト作れる?)も調べたいところ。

Arduino互換機+mrubyでラジコンを自動走行してみた

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



前回までで、chipKIT Max32上でmrubyプログラムをさくさく開発できるようになりました。何かやることは無いかと考えて、昔遊んでいたラジコンのモータとサーボが制御できることがわかったので、自動走行してみました。

最初に円を描く走行、その次は8の字運転です。

・・・残雪多すぎ+シャフトが外れて8の字は散々(笑)
どちらもmrubyのコードで制御しています。雪がない時に調整すればもう少しまともに走るはず。

円走行(circle.rb)

Serial.println("loading circle.rb....")

class Circle
	include Arduino

	def initialize
		@servo = Servo.new
		@servo.attach(5)
	end

	def idleloop

	end

	def run
		Serial.println("circle started")
		delay(5000)

		@servo.write(60)
		analogWrite(3, 220)
		delay(10 * 1000)

		analogWrite(3, 128)
		@servo.write(90)
		delay(1000)

	end
end

Circle.new

八の字(eight.rb)

Serial.println("loading eight.rb...")

class Eight
	include Arduino

	def initialize
		@servo = Servo.new
		@servo.attach(5)
	end

	def idleloop
	end

	def angle(val)
		@servo.write(val)
	end

	def speed(val)
     #モータは3番PINに接続
		analogWrite(3, val)
	end

	#八の字運転
	def draw_8
		
		#straight
		angle 90
		speed  240
		delay 1000

		#turn left
		angle  0
		speed  210
		delay 1500

		#straight
		angle  90
		speed  240
		delay 1000

		#turn right
		angle  180
		speed  210
		delay 1500
	end		

	def run
		Serial.println("eight started")
		delay(5000)

		3.times do
			draw_8
		end

		angle 90
		speed 128

	end
end

Eight.new

angle()は舵角の調整です。0で左いっぱい、90で中央(直進)、180で右いっぱい。
speed()はモータ回転の調整です。最大255で、コーナリング時は少し遅くしています。

digitalWrite()、pinMode()、Servoクラス等のArduinoのAPIのうち、使うものだけmrubyから呼び出せるようにC側で関数を用意してます(mrbgem化したい→mruby-arduinoとしてmrbgem化しました。)。また、今回はスイッチが押されたらrun()がC側から呼ばれます。

かなり長いですが、C側のコードも一応。

#include <Servo.h>


#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>
#include <mruby/data.h>
#include <mruby/class.h>



//extern const char initialcode[];

mrb_state *mrb;
char code[4048];
mrb_value sketchObject;

mrb_value mrb_serial_begin(mrb_state *mrb, mrb_value self){
  mrb_int speed = 0;
  int n = mrb_get_args(mrb,"i",&speed);
  Serial.begin(speed);
  return mrb_nil_value();
}

mrb_value mrb_serial_println(mrb_state *mrb, mrb_value self){
  mrb_value s;
  mrb_get_args(mrb,"S", &s);
  Serial.println(RSTRING_PTR(s));
  return mrb_nil_value();
}

void mrb_servo_free(mrb_state *mrb, void *ptr){
  delete (Servo *)ptr;
}

struct mrb_data_type mrb_servo_type = {"Servo", mrb_servo_free};

mrb_value mrb_servo_initialize(mrb_state *mrb, mrb_value self){
  Servo *newServo = new Servo();
  Serial.println("newServo");
  Serial.println((int)newServo);
  DATA_PTR(self) = newServo;  
  DATA_TYPE(self) = &mrb_servo_type;  
  return self;
}

mrb_value mrb_servo_attach(mrb_state *mrb, mrb_value self){
  Servo *servo = (Servo *)mrb_get_datatype(mrb, self, &mrb_servo_type);
  
  mrb_int pin = 0;
  int n = mrb_get_args(mrb, "i", &pin);
  servo->attach(pin);
  return mrb_nil_value();
}

mrb_value mrb_servo_write(mrb_state *mrb, mrb_value self){
  Servo *servo = (Servo *)mrb_get_datatype(mrb, self, &mrb_servo_type);
  mrb_int angle = 0;
  int n = mrb_get_args(mrb, "i", &angle);
  servo->write(angle);
  return mrb_nil_value();
}

mrb_value mrb_arduino_pinMode(mrb_state *mrb, mrb_value self){
  mrb_int pin, mode;
  int n = mrb_get_args(mrb, "ii", &pin, &mode);
  pinMode(pin, mode);
  return mrb_nil_value();
}

mrb_value mrb_arduino_digitalWrite(mrb_state *mrb, mrb_value self){
  mrb_int pin, value;
  int n = mrb_get_args(mrb, "ii", &pin, &value);
  digitalWrite(pin, value);
  return mrb_nil_value();
}

mrb_value mrb_arduino_digitalRead(mrb_state *mrb, mrb_value self){
  mrb_int pin;
  int n = mrb_get_args(mrb, "i", &pin);
  int val = digitalRead(pin);
  return mrb_fixnum_value(val);
}

mrb_value mrb_arduino_analogWrite(mrb_state *mrb, mrb_value self){
  mrb_int pin, value;
  int n = mrb_get_args(mrb, "ii", &pin, &value);
  analogWrite(pin, value);
  return mrb_nil_value();
}

mrb_value mrb_arduino_analogRead(mrb_state *mrb, mrb_value self){
  mrb_int pin;
  int n = mrb_get_args(mrb, "i", &pin);
  int val = analogRead(pin);
  return mrb_fixnum_value(val);
}
  
mrb_value mrb_arduino_delay(mrb_state *mrb, mrb_value self){
  mrb_int ms;
  int n = mrb_get_args(mrb, "i", &ms);
  delay(ms);
  return mrb_nil_value();
}

mrb_value mrb_arduino_millis(mrb_state *mrb, mrb_value self){
  return mrb_fixnum_value(millis());
}

mrb_value mrb_arduino_map(mrb_state *mrb, mrb_value self){
  mrb_int value, fromLow, fromHigh, toLow, toHigh;
  int n = mrb_get_args(mrb, "iiiii", &value, &fromLow, &fromHigh, &toLow, &toHigh);
  mrb_int ret = map(value, fromLow, fromHigh, toLow, toHigh);
  return mrb_fixnum_value(ret);
}


void init_arduino_library(mrb_state *mrb){
  RClass *serialClass = mrb_define_class(mrb, "Serial", mrb->object_class);
  mrb_define_class_method(mrb, serialClass,"begin",mrb_serial_begin, ARGS_REQ(1));
  mrb_define_class_method(mrb, serialClass,"println", mrb_serial_println, ARGS_REQ(1));


  RClass *servoClass = mrb_define_class(mrb, "Servo", mrb->object_class);
  MRB_SET_INSTANCE_TT(servoClass, MRB_TT_DATA);
  mrb_define_method(mrb, servoClass,"initialize", mrb_servo_initialize, ARGS_NONE());
  mrb_define_method(mrb, servoClass,"attach", mrb_servo_attach, ARGS_REQ(1));
  mrb_define_method(mrb, servoClass,"write", mrb_servo_write, ARGS_REQ(1));
  
  RClass *arduinoModule = mrb_define_module(mrb, "Arduino");
  mrb_define_module_function(mrb, arduinoModule, "pinMode", mrb_arduino_pinMode, ARGS_REQ(2));
  mrb_define_module_function(mrb, arduinoModule, "digitalWrite", mrb_arduino_digitalWrite, ARGS_REQ(2));
  mrb_define_module_function(mrb, arduinoModule, "digitalRead", mrb_arduino_digitalRead, ARGS_REQ(1));
  mrb_define_module_function(mrb, arduinoModule, "analogWrite", mrb_arduino_analogWrite, ARGS_REQ(2));
  mrb_define_module_function(mrb, arduinoModule, "analogRead", mrb_arduino_analogRead, ARGS_REQ(1));
  mrb_define_module_function(mrb, arduinoModule, "delay", mrb_arduino_delay, ARGS_REQ(1));
  mrb_define_module_function(mrb, arduinoModule, "millis", mrb_arduino_millis, ARGS_NONE());
  mrb_define_module_function(mrb, arduinoModule, "map", mrb_arduino_map, ARGS_REQ(5));
  mrb_define_const(mrb, arduinoModule, "HIGH", mrb_fixnum_value(1));
  mrb_define_const(mrb, arduinoModule, "LOW", mrb_fixnum_value(0));
  mrb_define_const(mrb, arduinoModule, "INPUT", mrb_fixnum_value(0));
  mrb_define_const(mrb, arduinoModule, "OUTPUT", mrb_fixnum_value(1));
  mrb_define_const(mrb, arduinoModule, "INPUT_PULLUP", mrb_fixnum_value(2));
}  

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_rc_runner)");   
  
  return readedLen;
}

const int MOTOR_PIN = 3;
const int START_PIN = 2;
const int SERVO_PIN = 5;

const int SPEED_CONTROL_PIN = 0;
const int ANGLE_CONTROL_PIN = 1;

Servo servo;

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(100*1024);
  
  Serial.begin(9600);
  
  pinMode(START_PIN, INPUT);
  pinMode(MOTOR_PIN, OUTPUT);
  analogWrite(MOTOR_PIN, 128);

  
  mrb = NULL;
  
}



void loop(){
  
  if (Serial.available() > 0 ){
    /*  upload mode  */
    
    if (unsigned short codeLen = receiveNewCode()){
      if (mrb) mrb_close(mrb);
      mrb = mrb_open();
      init_arduino_library(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 (digitalRead(START_PIN)){
    if (!mrb) return;
    mrb_value result = mrb_funcall(mrb, sketchObject, "run", 0);
    if (mrb->exc){
      Serial.println("exception occured in run");
    }
  }else{
    
    if (mrb) {
      mrb_value result = mrb_funcall(mrb, sketchObject, "idleloop", 0);
      if (mrb->exc){
        Serial.println("exception occured in idle loop");
      }
    }
    
    Serial.println("idle loop");
    int speed = analogRead(SPEED_CONTROL_PIN);
    speed /= 4;
    if (speed > 127){
      if (speed > 250){
        speed = 250;
      }
    } else if (speed > 64){
      //backward slow
      speed = 2;
    }else{
      //backward fast
      speed = 1;
    }
    analogWrite(MOTOR_PIN, speed);

    int angle = analogRead(ANGLE_CONTROL_PIN);
    angle = map(angle, 0, 1024, 180, 0);
    
    /*servo.attach(SERVO_PIN);
    servo.write(angle);  
    delay(500);
    servo.detach();
    */
    delay(100);
    
  }
}

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を使ってみる人の参考になればと思って記事にしてみました。