¥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デバイスにすることもできる?ようですがよくわかりません。とりあえずデバッグ用のシリアル通信には自分はこれを使っています。
速いし安いしで、これは結構オススメかも知れません。