カスタムアロケータと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モジュールの存在を知りました。
このモジュールは、マイコンとシリアル通信上で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
- 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ライブラリ(実際には低レベルAPIとArduino互換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()で囲まれてるんですね。なるほど。
ホストベースなirbでmrubyとラジコンと戯れる
@bovensiepenさんという方がArduino Due上でmirbを動かしています。
http://blog.mruby.sh/201305201003.html
自分も以前chipKITMax32で試したのですが、メモリが厳しすぎてまともに動きませんでした。どうもコンパイルするときにメモリをかなり使ってしまうみたいなので、こんどはホストベースのmirbというのをやってみました。
動画
デモ動画。無駄に長いので、暇な人以外は後半だけどうぞ。無謀にも英語に挑戦してます。
説明
図のまんまですが、ホスト側とボード側両方で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