カスタムアロケータとC++のnew

mruby Advent Calendar 2013 - Qiitaの19日目の記事です。

mrubyでカスタムアロケータを使う

mrubyを使うときは、まずmrb_open()を呼ぶわけですが、mrb_open_allocf()という独自のアロケータを指定できる亜種があります。

mrb_state* mrb_open_allocf(mrb_allocf, void *ud);

mrb_allocfは次のようにtypedefされています

typedef void* (*mrb_allocf) (struct mrb_state *mrb, void*, size_t, void *ud);

mrb_allocf()の引数ですが、2番目のvoid *はreallocする場合の元の領域へのポインタ(新しい領域ならNULL), size_tには確保したいサイズ(バイト数。0の場合は解放しろという意味)が渡されます。void *udはユーザデータでmrb_open_allocf()に渡したものがそのまま渡されて、自由に使えるようです。

では、何も考えずにmrb_open()を呼び出した場合はどうなっているのでしょうか?
mrb_open()は内部ではmrb_open_allocf()を読んでいるだけで、その際に最初の引数として内部で定義されているallocf()を渡しています。で、allocf()は次のようになっています。

static void*
allocf(mrb_state *mrb, void *p, size_t size, void *ud)
{
  if (size == 0) {
    free(p);
    return NULL;
  }
  else {
    return realloc(p, size);
  }
}

ふむふむ。素直にrealloc()とfree()を使っています。

で、まぁmrb_allocf()を自分で定義してmrb_open_allocf()に指定してやると独自のアロケータが使えるわけです。mruby内のあらゆるメモリ確保は独自のアロケータ経由になりますし、mrb_malloc()などのメモリ確保関数も内部的には独自のアロケータを呼んでくれます。

メモリの少ないシステムや、診断などをしたい場合に使えそうですね。

mrbgemでのメモリ確保

mrubyを使う側がカスタムアロケータを使う可能性があるので、いろんな人に使ってもらうmrbgemを書く際はメモリ確保にmalloc()を直接呼ぶのではなく、mrb_malloc()を呼ぶのが基本のようです。

mrbgemでC++のnewは?

んじゃmrbgemでC++のnew使うときはどうすんのよという話がでてきますが、placement new(配置new)を使えば良さそうに思えます。

C++のクラスFooをラップしたRubyのクラスFooをmrbgemで定義する例を書いてみます。あ、ちょうど2日前に構造体をラップする方法をtsaharaさんが解説されているので、mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法 - Qiitaを参考にします。

#include <new>

#include "mruby/class.h"
#include "mruby/value.h"
#include "mruby/data.h"

class Foo;   //どこかで定義されていることにする

void foo_free(mrb_state *mrb, void *ptr){
  Foo *foo = (Foo *)ptr;
  foo->~Foo();   //デストラクタを明示的に呼び出し
  mrb_free(mrb, foo); //メモリ解放
}

//Fooのタイプ定義
const static struct mrb_data_type mrb_foo_type = { "Foo", foo_free };

mrb_value foo_init(mrb_state *mrb, mrb_value self){
  void *p = mrb_malloc(mrb, sizeof(Foo));
  Foo *newFoo = new(p) Foo();  //placement newを使ってFoo::Foo()を呼ぶ
  DATA_PTR(self) = newFoo;  
  DATA_TYPE(self) = &mrb_foo_type;  
  return self;
}


void
mrb_mruby_foo_gem_init(mrb_state *mrb)
{
  struct RClass *fooClass;

  fooClass = mrb_define_class(mrb, "Foo", mrb->object_class);
  MRB_SET_INSTANCE_TT(fooClass, MRB_TT_DATA);
  mrb_define_method(mrb, fooClass, "initialize", foo_init, MRB_ARGS_NONE());
}

void
mrb_mruby_foo_gem_final(mrb_state *mrb)
{
}

placement newで本当に大丈夫?

Fooが単純なクラスの場合はこれで良さそうです。実際mruby-arduinoではArduinoのServoクラスをこの方法でラップしています。

でもFooの実装の中で普通のnewを使っている場合には対応できませんね。。複雑なライブラリでnewしまくっている場合やSTLまで使われている場合はどうすりゃいいんでしょう?

そのような場合はoperator newをオーバーロードしてやれば良さそうです。ただ、そうすると今度はmrubyを含むプログラムのnewが全部置き換わっちゃうので、それはそれで問題かも。うーむ。

STM32F4DICOVERY+Wi-Fiモジュール+mrubyでWebサーバ

STM32F4DISCOVERYでmrubyを動かせましたが、次になにかしたいなと思っていたところ、簡単にマイコンに接続できるWi-Fiモジュールの存在を知りました。

GAINSPAN GS1011シリーズ
http://www.sugakoubou.com/doku/lib/exe/fetch.php?hash=2c0bd5&media=http%3A%2F%2Fwww.sugakoubou.com%2Fstore%2Fimages%2Froboba023.jpg

このモジュールは、マイコンとシリアル通信上でATコマンドを使って簡単にWirelessの設定、TCP/IP通信ができるというものです。菅工房さんのページに詳しく解説されています。

菅工房 低消費電流 Wi-Fiモジュール
ATコマンドの詳細なリファレンスの入手には本来NDA契約の手続きが必要で、自分は契約書のコピーをファイルで送ったりしたのですが、うんともすんとも連絡が来ないので、WebでPDFを見つけて来ました^^;

で、このGS1011とSTM32F4DISCOVERYを接続し、簡易Webサーバを書いて基板上にある4色のLEDを制御してみました。

実装

GS1011とのシリアル通信を含むTCPライブラリをCで書き(かなり適当)、その上にこれまた適当なWebサーバをC/mrubyの混合ででっち上げ、その上のWebアプリ(?)とLEDの制御をmrubyで書きました。

一番上のWebアプリ的なものはSinatraライクに書けるようにしてあります。こんな感じ。多少はRubyらしいでしょうか?

include RouterDSL  #sinatraの超簡易版 get()
include Arduino     #mruby-arduino。pinMode()やdigitalWrite()

#各LEDのPIN番号
LED_GREEN = 60 
LED_ORANGE = 61
LED_RED = 62
LED_BLUE = 63

#LEDの初期化
[LED_GREEN, LED_ORANGE, LED_RED, LED_BLUE].each do |led|
	pinMode(led, OUTPUT);
end

html = <<EOS
	<html>
		<head><title>STM32F4-Discovery LED Controller</title></head>
		<body align=center>
		 <p>Hello mruby Wifi Web Server</p>
		 <li><a href="/control/green/on"> green on</a></li>
		 <li><a href="/control/green/off"> green off</a></li>
		 <li><a href="/control/orange/on"> orange on</a></li>
		 <li><a href="/control/orange/off"> orange off</a></li>
		 <li><a href="/control/red/on"> red on</a></li>
		 <li><a href="/control/red/off"> red off</a></li>
		 <li><a href="/control/blue/on"> blue on</a></li>
		 <li><a href="/control/blue/off"> blue off</a></li>
		</body>
	</html>
EOS

get "/" do
	html
end
 
get "/control/:color/:onoff" do |color, onoff|
	pin = case color
		when "green"
			LED_GREEN
		when "orange"
			LED_ORANGE
		when "red"
			LED_RED
		when "blue"
			LED_BLUE
		else
			return "I don't have color:#{color}"
		end

	if (onoff == "on")
		digitalWrite(pin, HIGH)
	elsif (onoff == "off")
		digitalWrite(pin, LOW)
	else
		return "Bad control:#{onoff}. should be on or off"
	end

	html
end

HTTPヘッダの解析なんかも正規表現ライブラリmasamitsu-murase/mruby-hs-regexpを使わせてもらってかなり楽ができました。

で、ブラウザからアクセスするとこんな感じのページが表示されます。

"green on"をクリックすると緑色のLEDが点灯、"orange off"をクリックするとオレンジ色のLEDが消灯、、といった感じです。この基板は青色LEDが綺麗です。

メモリ

で、本当はLEDの状態に合わせてHTMLも変えたいのですが、文字列を色々操作しているとRAM不足に陥りました。
ただ、STM32F4DISCOVERYのもう一つのメモリ空間(64KB)がほとんど余っています。ここにベースとなるHTMLのテンプレートを置いたり、mrubyの初期化で確保している大きめの領域(20kb程度)を置けばまだいける!はず?

¥1,650で買えるARM基板STM32F4DISCOVERYでmrubyを動かす

今までchipKIT Max32でmrubyを動かして遊んでいたわけですが、最近STMicroelectronicsのSTM32F4DISCOVERYという評価ボードを知りました。

STM32F4DISCOVERY

http://akizukidenshi.com/img/goods/3/M-05313.JPG

  • ARM Coretex-M4 STM32F407VGT6 Microcontroller
  • 1MB Flash
  • 128KB + 64KB RAM

秋月電子で\1,650で販売されています。色々人気なようで、動画プレイヤーを作っている方もいます。すごい・・。

この基板、RAMは合計で192KBあるのですが、メモリマップ上で128KBと64KBが分断されています。このためヒープ(mallocで使う場所)はどちらかしか使えないのですが、スタックを64KB側に置いたり色々工夫できるので、mrubyを動かすには今までより余裕がありそうです。

で、既にyamanekkoさんがmrubyを動かしています。参考にさせて頂きました。
https://github.com/yamanekko/mruby-on-stm32f4discovery/wiki

yamanekkoさんは、STMicroelectronicsが提供しているライブラリを使っているようですが、私はArduino互換APIを持つlibmapleのSTM32F4DISCOVERYポート版を使いました。

libmapleというのは、Arduino互換ボードのMapleを販売しているleaflabsが開発したArduino互換APIライブラリ(実際には低レベルAPIArduino互換API)ですが、これがAeroQuadというこれまたArduinoベースのクアッドコプター向けに移植されて、その際にSTM32F4DISCOVERY向けにもポートされています。

もともとのlibmapleはコチラに、AeroQuad向けにポートされたものがコチラです。

少しややこしいですが、とにかくArduino互換APIが使えるわけで、mruby-arduinoをUpdateして、このポートされたlibmapleでも動くようにして使うことにしました。

mrubyのビルド

build_config.rbはこんな感じになりました。

クロスビルドに使うツールチェインはMaple用のIDEであるMAPLE IDEに入っているものを使います(ARM_TOOLCHAIN_PATHは適当に書き換えてください)。
MAPLE IDE : http://leaflabs.com/docs/maple-ide-install.html#maple-ide-install

また、libmapleはコチラからダウンロードしたものを使います。こちらはLIBMAPLE_PATHを適当に書き換えてください。

AeroQuad:https://github.com/AeroQuad/AeroQuad

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

  ARM_TOOLCHAIN_PATH = "/Applications/MapleIDE.app/Contents/Resources/Java/hardware/tools/arm/bin"

  LIBMAPLE_PATH = "/Users/koji/tools/AeroQuad/Libmaple/libmaple"

  conf.cc do |cc|
    cc.command = "#{ARM_TOOLCHAIN_PATH}/arm-none-eabi-gcc"
    cc.include_paths << ["#{LIBMAPLE_PATH}/libmaple", 
                        "#{LIBMAPLE_PATH}/wirish",
                        "#{LIBMAPLE_PATH}/wirish/comm",
                        "#{LIBMAPLE_PATH}/wirish/boards",
                        "#{LIBMAPLE_PATH}/libraries"
                        ]
    cc.flags = %w(-Os -g3 -gdwarf-2  -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=softfp -ffunction-sections 
                  -nostdlib -fdata-sections -Wl,--gc-sections -DBOARD_discovery_f4 -DMCU_STM32F406VG 
                  -DSTM32_HIGH_DENSITY -DSTM32F2 -DF_CPU=168000000UL -D__FPU_PRESENT=1)

    #some adjustment to reduce heap usage.
    #cc.flags << %w(-DMRB_USE_FLOAT)
    cc.defines << %w(MRB_HEAP_PAGE_SIZE=64)
    cc.defines << %(MRB_IREP_ARRAY_INIT_SIZE=128u)
    cc.defines << %w(MRB_USE_IV_SEGLIST)
    cc.defines << %w(KHASH_DEFAULT_SIZE=8)
    cc.defines << %w(MRB_STR_BUF_MIN_SIZE=20)
    #cc.defines << %w(DISABLE_STDIO) #if you dont need
    cc.defines << %w(MRB_GC_STRESS)   #no document
    cc.defines << %w(POOL_PAGE_SIZE=1000) #effective only for use with mruby-eval
  end
  
  conf.cxx do |cxx|
    cxx.command = conf.cc.command.dup
    cxx.include_paths = cc.include_paths.dup
    cxx.flags = conf.cc.flags.dup << %w(-fno-rtti -fno-exceptions)
    cxx.defines = conf.cc.defines.dup
    cxx.compile_options = conf.cc.compile_options.dup
  end

  conf.archiver do |archiver|
    archiver.command = "#{ARM_TOOLCHAIN_PATH}/arm-none-eabi-ar"
  end

  conf.bins = []

  #do not build executable test
  conf.build_mrbtest_lib_only

  conf.gem :core => "mruby-print" 
  conf.gem :core => "mruby-toplevel-ext"

  conf.gem :github => "kyab/mruby-arduino", :branch => "master" 
end

MRuby::Build.new do |conf|
  # load specific toolchain settings
  toolchain :clang

  # include the default GEMs
  conf.gembox 'full-core'

  conf.cc.flags << [ENV['CFLAGS'] || %w( -g -O0)] 
  conf.cc.compile_options = "%{flags} -o %{outfile} -c %{infile}" 

end

ビルドは

$cd /path/to/mruby
$make

/path/to/mruby/build/STM32F4/lib にlibmruby.aができていればOK。

プログラム本体

main.cppを書いて、libmapleに入っているexampleのMakefileをちょこっと編集して使います。

main.cpp。mrubyで書かれたクラスBlinkerのインスタンスを生成して、loopの中で呼んでます。

#include "wirish.h"  //Arduino.hのlibmaple版

#include <errno.h>

#include "mruby.h"
#include "mruby/class.h"
#include "mruby/value.h"
#include "mruby/irep.h"

#define LED_ORANGE Port2Pin('D',13)
#define LED_RED    Port2Pin('D',14)    
#define LED_BLUE   Port2Pin('D',15)

mrb_state *mrb;
mrb_value blinker_obj;
int ai;
extern const uint8_t blinker[];

size_t total_size = 0;

// stubs for newlib
extern "C" {
    void _exit(int rc){
        while(1){

        }
    }
    int _getpid(){
        return 1;
    }
    
    int _kill(int pid, int sig){
        errno = EINVAL;
        return -1;
    }

    extern void free(void *ptr);
    extern void *realloc(void *ptr, size_t size);

}


void printlnSize(size_t size){
    char str[15];
    sprintf(str,"%u", size);
    Serial2.println(str);
}

// custom allocator to check heap shortage.
void *myallocf(mrb_state *mrb, void *p, size_t size, void *ud){
    if (size == 0){
        free(p);
        return NULL;
    }

    //Serial2.println("realloc");
    void *ret = realloc(p, size);
    if (!ret){
        Serial2.print("memory allocation error. size:");
        printlnSize(size);
    }
    total_size += size;
}


void setup() {
    // initialize the digital pin as an output:
    pinMode(BOARD_LED_PIN, OUTPUT);
    pinMode(LED_BLUE, OUTPUT);

    Serial2.begin(9600);
    Serial2.println("Hello world!");
    //delay(1000);

    //starting up mruby
    mrb = mrb_open_allocf(myallocf, NULL);

    Serial2.println("mrb_open done. total:");
    printlnSize(total_size);
    mrb_load_irep(mrb, blinker);
    

    Serial2.println("mruby initialized");

    //Get Blinker class and create instance.
    //equivalent to ruby: blinker_obj = Blinker.new(13,1000)
    RClass *blinker_class = mrb_class_get(mrb, "Blinker");
    if (mrb->exc){
        Serial2.println("failed to load class Blinker");
    }

    mrb_value args[2];
    args[0] = mrb_fixnum_value(LED_ORANGE);     //pin Number
    args[1] = mrb_fixnum_value(1000);   //interval
    blinker_obj = mrb_class_new_instance(mrb, 2, args, blinker_class);

    //is exception occure?
    if (mrb->exc){
        Serial2.println("failed to create Blinker instance");
        return;
    }

    ai = mrb_gc_arena_save(mrb);
}

void loop() {

    // Serial2.println("loop");

    mrb_funcall(mrb, blinker_obj,"run",0);
    if (mrb->exc){
        Serial2.println("failed to run!");
        mrb->exc = 0;
        delay(1000);
    }
    mrb_gc_arena_restore(mrb, ai);
}


// Force init to be called *first*, i.e. before static object allocation.
// Otherwise, statically allocated objects that need libmaple may fail.
__attribute__((constructor)) void premain() {
    init();
}

int main(void) {
    setup();

    while (true) {
        loop();
    }
    return 0;
}

blinker.rb。Blinkerクラスを定義してます。Blinker#runで指定された秒数ごとにチカチカ

#To (re)compile C bytecode:
#
#/path/to/mruby/bin/mrbc -Bblinker -oblinker.c blinker.rb
#

class Blinker
	include Arduino
	attr_accessor :interval ,:pin
	
	def initialize(pin,interval_ms)
		Serial2.println("Blinker initialized")
		@pin = pin
		@interval = interval_ms
		pinMode(@pin, OUTPUT)
	end

	def run
		Serial2.println("blink! discovery!")

		digitalWrite(@pin, HIGH)
		delay(@interval)
		digitalWrite(@pin, LOW)
		delay(@interval)
	end
end

Makefile。無駄な行がいっぱいありますが、

# Try "make help" for more information on BOARD and MEMORY_TARGET;
# these default to a Maple Flash build.
#BOARD ?= maple
#BOARD ?= aeroquad32
#BOARD ?= aeroquad32f1
BOARD ?= discovery_f4
#BOARD ?= aeroquad32mini
#BOARD ?= freeflight

#V=1

.DEFAULT_GOAL := sketch

LIB_MAPLE_HOME ?= /Users/koji/tools/AeroQuad/Libmaple/libmaple
MRUBY_HOME ?= /Users/koji/work/mruby/mruby

MRUBY_INCLUDES = -I$(MRUBY_HOME)/include
MRUBY_LIB = -L$(MRUBY_HOME)/build/STM32F4/lib

##
## Useful paths, constants, etc.
##

ifeq ($(LIB_MAPLE_HOME),)
SRCROOT := .
else
SRCROOT := $(LIB_MAPLE_HOME)
endif
BUILD_PATH = build
LIBMAPLE_PATH := $(SRCROOT)/libmaple
WIRISH_PATH := $(SRCROOT)/wirish
SUPPORT_PATH := $(SRCROOT)/support
# Support files for linker
LDDIR := $(SUPPORT_PATH)/ld
# Support files for this Makefile
MAKEDIR := $(SUPPORT_PATH)/make

# USB ID for DFU upload
VENDOR_ID  := 1EAF
PRODUCT_ID := 0003

##
## Target-specific configuration.  This determines some compiler and
## linker options/flags.
##

MEMORY_TARGET ?= jtag

# $(BOARD)- and $(MEMORY_TARGET)-specific configuration
include $(MAKEDIR)/target-config.mk

##
## Compilation flags
##

GLOBAL_FLAGS    := -D$(VECT_BASE_ADDR)					     \
		   -DBOARD_$(BOARD) -DMCU_$(MCU)			     \
		   -DERROR_LED_PORT=$(ERROR_LED_PORT)			     \
		   -DERROR_LED_PIN=$(ERROR_LED_PIN)			     \
		   -D$(DENSITY) -D$(MCU_FAMILY) 

ifeq ($(BOARD), freeflight)
GLOBAL_FLAGS += -DDISABLEUSB
endif

ifeq ($(BOARD), aeroquad32)
GLOBAL_FLAGS += -DF_CPU=168000000UL
endif

ifeq ($(BOARD), discovery_f4)
GLOBAL_FLAGS += -DF_CPU=168000000UL
endif

GLOBAL_FLAGS += -D__FPU_PRESENT=1

ifeq ($(MCU_FAMILY), STM32F2)
	EXTRAINCDIRS += \
		$(LIB_MAPLE_HOME)/libmaple/usbF4/STM32_USB_Device_Library/Core/inc \
		$(LIB_MAPLE_HOME)/libmaple/usbF4/STM32_USB_Device_Library/Class/cdc/inc \
		$(LIB_MAPLE_HOME)/libmaple/usbF4/STM32_USB_OTG_Driver/inc \
		$(LIB_MAPLE_HOME)/libmaple/usbF4/VCP
endif

		   
#GLOBAL_FLAGS += -DDISABLEUSB
#GLOBAL_FLAGS += -DUSB_DISC_OD
		   
GLOBAL_CFLAGS   := -Os -g3 -gdwarf-2  -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=softfp \
		   -nostdlib -ffunction-sections -fdata-sections	     \
		   -Wl,--gc-sections $(GLOBAL_FLAGS)
GLOBAL_CXXFLAGS := -fno-rtti -fno-exceptions -Wall $(GLOBAL_FLAGS)
GLOBAL_ASFLAGS  := -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=softfp		     \
		   -x assembler-with-cpp $(GLOBAL_FLAGS)
LDFLAGS  = -T$(LDDIR)/$(LDSCRIPT) -L$(LDDIR)    \
            -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=softfp -Xlinker     \
            --gc-sections -Wall# -Xlinker --allow-multiple-definition

##
## Build rules and useful templates
##

include $(SUPPORT_PATH)/make/build-rules.mk
include $(SUPPORT_PATH)/make/build-templates.mk

##
## Set all submodules here
##

# Try to keep LIBMAPLE_MODULES a simply-expanded variable
ifeq ($(LIBMAPLE_MODULES),)
	LIBMAPLE_MODULES := $(SRCROOT)/libmaple
else
	LIBMAPLE_MODULES += $(SRCROOT)/libmaple
endif
LIBMAPLE_MODULES += $(SRCROOT)/wirish
# Official libraries:
LIBMAPLE_MODULES += $(SRCROOT)/libraries/Servo
LIBMAPLE_MODULES += $(SRCROOT)/libraries/LiquidCrystal
LIBMAPLE_MODULES += $(SRCROOT)/libraries/Wire

# Experimental libraries:
LIBMAPLE_MODULES += $(SRCROOT)/libraries/FreeRTOS
LIBMAPLE_MODULES += $(SRCROOT)/libraries/mapleSDfat

# Call each module's rules.mk:
$(foreach m,$(LIBMAPLE_MODULES),$(eval $(call LIBMAPLE_MODULE_template,$(m))))

##
## Targets
##

# main target
include $(SRCROOT)/build-targets.mk

$(BUILD_PATH)/$(BOARD).elf: $(BUILDDIRS) $(TGT_BIN) $(BUILD_PATH)/main.o $(BUILD_PATH)/blinker.o
	$(SILENT_LD) $(CXX) $(LDFLAGS) -o $@ $(BUILD_PATH)/main.o $(BUILD_PATH)/blinker.o $(TGT_BIN) $(MRUBY_LIB) -lmruby -Wl,-Map,$(BUILD_PATH)/$(BOARD).map

WIRISH_INCLUDES += -I$(SRCROOT)/libraries

build_dir : $(BUILD_PATH)
	mkdir -p $<

$(BUILD_PATH)/main.o: main.cpp
	$(CXX) $(CFLAGS) $(CXXFLAGS) $(LIBMAPLE_INCLUDES) $(WIRISH_INCLUDES) $(MRUBY_INCLUDES) -o $@ -c $< 

blinker.c: blinker.rb
	mrbc -Bblinker $<

$(BUILD_PATH)/blinker.o: blinker.c
	$(CC) $(CFLAGS) -o $@ -c $< 

.PHONY: install sketch clean help debug cscope tags ctags ram flash jtag doxygen mrproper

# Target upload commands
UPLOAD_ram   := $(SUPPORT_PATH)/scripts/reset.py && \
                sleep 1                  && \
                $(DFU) -a0 -d $(VENDOR_ID):$(PRODUCT_ID) -D $(BUILD_PATH)/$(BOARD).bin -R
UPLOAD_flash := $(SUPPORT_PATH)/scripts/reset.py && \
                sleep 1                  && \
                $(DFU) -a1 -d $(VENDOR_ID):$(PRODUCT_ID) -D $(BUILD_PATH)/$(BOARD).bin -R
UPLOAD_jtag  := $(OPENOCD_WRAPPER) flash

all: library

# Conditionally upload to whatever the last build was
install: INSTALL_TARGET = $(shell cat $(BUILD_PATH)/build-type 2>/dev/null)
install: $(BUILD_PATH)/$(BOARD).bin
	@echo Install target: $(INSTALL_TARGET)
	$(UPLOAD_$(INSTALL_TARGET))

# Force a rebuild if the target changed
PREV_BUILD_TYPE = $(shell cat $(BUILD_PATH)/build-type 2>/dev/null)
build-check:
ifneq ($(PREV_BUILD_TYPE), $(MEMORY_TARGET))
	$(shell rm -rf $(BUILD_PATH))
endif

sketch: build-check MSG_INFO $(BUILD_PATH)/$(BOARD).bin

clean:
	rm -rf build
	rm blinker.c

mrproper: clean
	rm -rf doxygen

help:
	@echo ""
	@echo "  libmaple Makefile help"
	@echo "  ----------------------"
	@echo "  "
	@echo "  Programming targets:"
	@echo "      sketch:   Compile for BOARD to MEMORY_TARGET (default)."
	@echo "      install:  Compile and upload code over USB, using Maple bootloader"
	@echo "  "
	@echo "  You *must* set BOARD if not compiling for Maple (e.g."
	@echo "  use BOARD=maple_mini for mini, etc.), and MEMORY_TARGET"
	@echo "  if not compiling to Flash."
	@echo "  "
	@echo "  Valid BOARDs:"
	@echo "      maple, maple_mini, maple_RET6, maple_native"
	@echo "  "
	@echo "  Valid MEMORY_TARGETs (default=flash):"
	@echo "      ram:    Compile sketch code to ram"
	@echo "      flash:  Compile sketch code to flash"
	@echo "      jtag:   Compile sketch code for jtag; overwrites bootloader"
	@echo "  "
	@echo "  Other targets:"
	@echo "      debug:  Start OpenOCD gdb server on port 3333, telnet on port 4444"
	@echo "      clean: Remove all build and object files"
	@echo "      help: Show this message"
	@echo "      doxygen: Build Doxygen HTML and XML documentation"
	@echo "      mrproper: Remove all generated files"
	@echo "  "

debug:
	$(OPENOCD_WRAPPER) debug

cscope:
	rm -rf *.cscope
	find . -name '*.[hcS]' -o -name '*.cpp' | xargs cscope -b

tags:
	etags `find . -name "*.c" -o -name "*.cpp" -o -name "*.h"`
	@echo "Made TAGS file for EMACS code browsing"

ctags:
	ctags-exuberant -R .
	@echo "Made tags file for VIM code browsing"

ram:
	@$(MAKE) MEMORY_TARGET=ram --no-print-directory sketch

flash:
	@$(MAKE) MEMORY_TARGET=flash --no-print-directory sketch

jtag:
	@$(MAKE) MEMORY_TARGET=jtag --no-print-directory sketch

doxygen:
	doxygen $(SUPPORT_PATH)/doxygen/Doxyfile

ビルドとアップデート。USBを接続(mini-USBの方)して

$ make jtag
$ st-flash write build/discoveryf4.bin 0x08000000 #2014/2/28 引数の順番間違え修正

これでボード上のオレンジ色のLEDがチカチカするはずです。

というか、アップロード時間がchipKIT Max32の10倍以上速い!

まとめ

  • STM32F4DISCOVERYで無事mrubyが動きました。メモリマップの工夫次第ではそれなりの大きさのデータも扱えそうです。
  • mruby-arduinoもこの基盤基板に対応したので、Arduino的プログラミングがmrubyでできます。
  • Arduino互換ボードに比べるとちょっと慣れた人向けだと思いますが、mrubyを含む200kb超のプログラムをアップロードしても10数秒しかかからないので、超快適になりました。
  • 安いので壊れたら買い直せばいいだけ
  • シリアル通信には変換基板が必要ぽいです。USBが二系統あって、1つはMini-USBでjtag専用(アップロードとかgdbデバッグ)。もう一つはMicro-USBでUSBデバイスにすることもできる?ようですがよくわかりません。とりあえずデバッグ用のシリアル通信には自分はこれを使っています。

速いし安いしで、これは結構オススメかも知れません。

mrubyのmrb_gc_arena_save()/mrb_gc_arena_restore()の使い方がまだよくわからないので実験してみた

Matzがmrubyのarenaの使い方について書かれています。

Matzにっき(2013-07-31) _mrubyのmrb_gc_arena_save()/mrb_gc_arena_restore()の使い方

自分も時々arena_overflowになって悩まされていたのですが、いまいちわからないままあやしいところをmrb_gc_arena_save()とmrb_gc_arena_restore()で囲って、動けばいいや的なノリで切り抜けてました。

Matzの記事ではCで拡張(mrbgem)を書く場合を例に説明されてますが、自分はmrb_funcallなど、C側からmrubyを呼ぶ場合にどうすれば良いかもよくわかっていなかったので、いろいろ実験してみました。

arena_overflowとやらをおこしてみる

とりあえず何も考えずに適当なクラスのインスタンスを作りまくってみます。

#include <stdio.h>
#include "mruby.h"

// Foo#func: 引数なし、適当なStringを返す。以下ずっと中身は同じなので省略
mrb_value func(mrb_state *mrb,mrb_value self)
{
    printf("Foo#func called\n");
    mrb_value val = mrb_str_new_cstr(mrb, "value_created_in_func");
    return val;
}

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
    }
    
    mrb_close(mrb);
    return 0;
}

結果

$ ./arena
index = 0
index = 1
...
index = 97
index = 98

Abort trap: 6

index=98、つまり99個目のインスタンスを作ろうとして落ちました。あれ?100個分あるんじゃなかったの?98個しか出来ねえじゃんというのはあるのですが、そこは後で判明したのでとりあえずこのまま進みます。というか、以前はarena overflowとかメッセージが出たような気がするのですが・・。

mrb_gc_arena_save(), mrb_gc_arena_restore()でarena_overflowを防ぐ

で、噂のmrb_gc_arena_save()とmrb_gc_arena_restore()をつかってみます。

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    int ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        mrb_gc_arena_restore(mrb, ai);
    }
    
    mrb_close(mrb);
    return 0;

結果

$ ./arena
index = 0
index = 1
...
index = 108
index = 109

お〜、これでOKぽいですね。でも、これじゃ意味がありません。mrb_gc_arena_restore()で全部元に戻しちゃったら必要なオブジェクトが使えませんね。そういう必要なオブジェクトはmrb_gc_protect()で保護せよとのことなので、その通りにしてみます。

mrb_gc_protect()で必要なオブジェクトを保護

とりあえず、for文で作ったオブジェクト(Fooクラスのインスタンス)の最後だけは保護するということにしてみます。ついでにちゃんと使えるか確かめるために、funcというメソッドを呼び出します。

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    int ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        mrb_gc_arena_restore(mrb, ai);
    }
    
    //最後のインスタンスを保護して、使ってみる
    mrb_gc_protect(mrb, foo);
    mrb_funcall(mrb,last_foo, "func", 0, NULL);
    
    mrb_close(mrb);
    return 0;
}

結果

$ ./arena
index = 0
index = 1
...
index = 108
index = 109
Foo#func called

これで良さそうです。ですが、mrb_funcallでFoo#funcを呼び出すとStringが(mrb_valueで)返ってきますが、こいつは大丈夫なんでしょうか。こんどはFoo#funcを100回以上呼び出してみます。

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    int ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        mrb_gc_arena_restore(mrb, ai);
    }
    
    //最後のインスタンスを保護して、100回以上使ってみる
    mrb_gc_protect(mrb, foo);
    for (int i = 0; i < 110; i++){
        printf("index(func) = %d\n", i);
        mrb_value ret = mrb_funcall(mrb, foo, "func", 0, NULL );
    }
    
    mrb_close(mrb);
    return 0;
}

結果

...
index(func) = 47
Foo#func called
index(func) = 48
Foo#func called

Abort trap: 6

あ〜、またarena overflowっぽいです。100回の半分程度で落ちてますね。Foo#fooの中で文字列を作っているのと、mrb_funcallで値を戻すので2倍消費するのでしょうか?・・・。まぁとりあえずmrb_funcall周りもmrb_gc_arena_save()とmrb_gc_arena_restore()で囲ってやったらよさそうです。

mrb_funcall()周りをmrb_gc_arena_save()とmrb_gc_arena_restore()で囲ってarene_overflowを防ぐ

こんな感じでどうでしょう(疲れてきた。。。)

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    int ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        mrb_gc_arena_restore(mrb, ai);
    }
    
    //最後のインスタンスを保護して、100回以上使ってみる
    mrb_gc_protect(mrb, foo);
    
    ai = mrb_gc_arena_save(mrb);
    for (int i = 0; i < 110; i++){
        printf("index(func) = %d\n", i);
        mrb_value ret = mrb_funcall(mrb, foo, "func", 0, NULL );
        mrb_gc_arena_restore(mrb, ai);
    }
    
    mrb_close(mrb);
    return 0;

結果

...
index(func) = 108
Foo#func called
index(func) = 109
Foo#func called

よしよし、これで良いんじゃないでしょうか?


・・・ただ気になるのは最初の時点で98個しかオブジェクトが作れなかったことです、どうも、クラスの登録とメソッドの登録もarena_indexを増加させてるんじゃないでしょうか?
なので、そこも囲ってみました。あとarena_indexの値も表示して確認してみます。

int main(int argc, const char * argv[])
{
    mrb_state *mrb = mrb_open();
    
    int ai = mrb_gc_arena_save(mrb);
    printf("arena index before registering class = %d\n", ai);
    
    //クラスFooとそのメソッドfuncを登録する
    struct RClass *foo_class = mrb_define_class(mrb, "Foo", mrb->object_class);
    mrb_define_method(mrb, foo_class, "func",func, MRB_ARGS_NONE());
    
    printf("arena index after registering class = %d\n", mrb_gc_arena_save(mrb));
    
    mrb_gc_arena_restore(mrb, ai);
   
   //ai = mrb_gc_arena_save(mrb);
    
    //Fooのインスタンスを作りまくる
    mrb_value foo;
    for (int i = 0; i < 110 ; i++){
        printf("index = %d\n", i);
        foo = mrb_class_new_instance(mrb, 0, NULL, foo_class);
        //mrb_gc_arena_restore(mrb, ai);
    }
    ...

結果

$ ./arena
arena index before registering class = 0
arena index after registering class = 2
index = 0
index = 1
...
index = 99
index = 100

Abort trap: 6

やっぱりmrb_define_class()とmrb_define_method()もarenaを増加させていたようです。この辺周りの関数を呼び出す時も注意ですね。

mrbgemでクラスやメソッドを登録する際

今回はmrbgemではなく、普通にmainの中でmrubyにクラスやメソッドを登録したのですが、それはarena_indexを増やすのでした。
ということはmrbgemを書く場合にもクラス登録時にmrb_gc_arena_save()やmrb_gc_arena_restore()で囲ってやる必要があるのかと心配になったのですが、それよきにはからってくれるっぽいです。
以下はmrubyのビルド中に作られるmruby-sprintfというmrbgemの初期化コードです。

...
void GENERATED_TMP_mrb_mruby_sprintf_gem_init(mrb_state *mrb) {
  int ai = mrb_gc_arena_save(mrb);
  mrb_mruby_sprintf_gem_init(mrb);
  mrb_gc_arena_restore(mrb, ai);
}

このように、実際にクラス、メソッドを登録するmrb_mruby_sprintf_gem_init()周りはmrb_gc_arena_save()とmrb_gc_arena_restore()で囲まれてるんですね。なるほど。

Safariをハイジャックしてmrubyを動かす

F-Scriptを真似して、SafariをハイジャックしてSafariの中でmrubyを動かしてみました。

ハイジャックするといってもgdbでプロセスにアタッチしてFrameworkを読みこませる方法なので、XCode必須。

今はメッセージの引数に使える型に制限がありますが、そのうちまとまったらどこかで公開する予定。

ホストベースなirbでmrubyとラジコンと戯れる その2

mrubyを使ったホストベースなirbその2です。

動画

コマンド

#fileコマンド

Rubyファイルを読み込んで、ボード上で実行させます。

#reconnectコマンド

一旦USBを抜いて、再度挿し直した時に復帰する機能です。

開発サイクル

この2つのコマンドを使えば、コードを編集→USB接続なしで自由な場所でラジコンを動かす→状態の問い合わせ、コードの再編集の無限ループです。らぴっどぷろとたいぴんぐ。

自分はラジコンカーで遊んでいますが、仕組みは汎用(のつもり)です。

ロボット的な機器にあちこち移動させて、返ってきてから機器が取ってきた情報をRubyのオブジェクトとして自由に弄り回してちょっと一部メソッドを書き換えて、、とかできます。RubySmalltalk的?CLOS的?オブジェクト指向バンザイ。

ホストベースなirbでmrubyとラジコンと戯れる

@bovensiepenさんという方がArduino Due上でmirbを動かしています。

http://blog.mruby.sh/201305201003.html

自分も以前chipKITMax32で試したのですが、メモリが厳しすぎてまともに動きませんでした。どうもコンパイルするときにメモリをかなり使ってしまうみたいなので、こんどはホストベースのmirbというのをやってみました。

動画

デモ動画。無駄に長いので、暇な人以外は後半だけどうぞ。無謀にも英語に挑戦してます。

説明

https://cacoo.com/diagrams/EmmKpYRK6YEvRwcE-44F09.png
図のまんまですが、ホスト側とボード側両方でmrubyを動かし、ホスト側でコンパイルしてボードに送ってます。ホストが(も)頑張るからホストベースと読んでいます。ボード側のメモリ消費量は多少抑えられたはず。
動画でタイピングは激しくミスってますが、文法エラーはホスト上のコンパイラで、実行時エラー(例外)はボード上で一応ちゃんと検知されます。

いじったところ

ちなみにmrubyのソースコードは本日最新版に一行だけ変更してます(バイトコードを読み込む部分ですが、いまいち自信なし)。→(2013/6/25追記:)mruby本体に同様の変更が取り込まれたので、修正なしで動くようになりました。

それ以外はbuild_config.rbでいくつかの機能をDisableにしました。

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

  # Mac OS X
  MPIDE_PATH = '/Applications/Mpide.app/Contents/Resources/Java'
  # GNU Linux
  # MPIDE_PATH = '/opt/mpide-0023-linux-20120903'

  PIC32_PATH = "#{MPIDE_PATH}/hardware/pic32"  

  conf.cc do |cc|
    cc.command = "#{PIC32_PATH}/compiler/pic32-tools/bin/pic32-gcc"
    cc.include_paths << ["#{PIC32_PATH}/cores/pic32",
                        "#{PIC32_PATH}/variants/Max32",
                        "#{PIC32_PATH}/libraries"]
    cc.flags = %w(-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)

    #some adjustment for mruby to reduce heap usage.
    #cc.flags << %w(-DMRB_USE_FLOAT) #動かん
    cc.flags << %w(-DMRB_HEAP_PAGE_SIZE=64)
    #cc.flags << %w(-DMRB_WORD_BOXING) #動かん
    cc.flags << %w(-DMRB_USE_IV_SEGLIST)
    cc.flags << %w(-DKHASH_DEFAULT_SIZE=8)
    cc.flags << %w(-DMRB_STR_BUF_MIN_SIZE=20)
    #cc.flags << %w(-DDISABLE_STDIO) #don't disable STDIO, to use mrb_read_irep_file()
    cc.flags << %w(-DMRB_GC_STRESS)   #no document
    cc.flags << %w(-DPOOL_PAGE_SIZE=1000) #effective only for use with mruby-eval
    cc.compile_options = "%{flags} -o %{outfile} -c %{infile}"
  end

  conf.cxx do |cxx|
    cxx.command = conf.cc.command.dup
    cxx.include_paths = conf.cc.include_paths.dup
    cxx.flags = conf.cc.flags.dup
    cxx.compile_options = conf.cc.compile_options.dup
  end

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

  #no executables
  conf.bins = []

  conf.gem :core => "mruby-print" 
  conf.gem :core => "mruby-math"
  conf.gem :core => "mruby-random"
  #conf.gem :core => "mruby-eval"   #一応動いた
  conf.gem :github => "masamitsu-murase/mruby-hs-regexp", :branch => "master" #動いた。素晴らしい 

  conf.gem :github => "kyab/mruby-arduino", :branch => "master"

  conf.build_mrbtest_lib_only

end

MRuby::Build.new do |conf|
  # load specific toolchain settings
  toolchain :clang

  # include the default GEMs
  conf.gembox 'default'

  conf.cc.flags << [ENV['CFLAGS'] || %w(-DENABLE_READLINE -g)]   #-arch i386 -arch x86_64
  conf.cc.compile_options = "%{flags} -o %{outfile} -c %{infile}"  #building universal binary dont allow -M

  conf.linker.libraries << "edit"   #OSX readline compatible library
  
  conf.gem :github => "kyab/mruby-bin-mirb-hostbased", :branch => "master"

end

ソース

動画の中で紹介しているmirb(mruby-bin-mirb-hostbased)とmruby-arduinoは以下に置いています。

https://github.com/kyab/mruby-bin-mirb-hostbased (MPIDE用のスケッチ付き)
https://github.com/kyab/mruby-arduino

*

いろいろやってるとそのうちメモリ不足で止まってしまいますが、どうもmrb_funcall()で返ってきたmrb_valueGCに開放させる方法がわかりません。。

あと、USBでつながったままでどうやってラジコンを走らせるんだという話もありますが、そこはちょっと工夫しています。それは次回。