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

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