tana_ash's diary

プログラミングや電子工作など。やってみたこと、わかったことをまとめておく場所。

mrubyをSTM32 Nucleoボードで動作させる

今年、STマイクロから新しいSTM32マイコンボード、Nucleo(
STM32 MCU Nucleo - STMicroelectronics)が発売されました。Arduino互換のピンを持ち、最大で96KBのRAMを搭載しています。なおかつmbedに対応しドラッグ&ドロップで書き込みが可能で、そしてそれが1500円です(秋月電子にて)。

私はこのボードを知った時から、mrubyを動作させることを考えていました。そして先週、秋月電子にてついにNucleoを購入することが叶い、早速mrubyを動作させてみることに決めました。

目標

Cortex-M4プロセッサ、512KBのFlash、96KBのSRAMを搭載したNucleo F401REボードでmrubyを動作させる。

(他のNucleoボードではRAMが不足する可能性があるので、今回はF401RE用とします。)

手順

mbedのオフラインコンパイル環境を準備する

まず最初に、Cortex-M/Cortex-R用のCコンパイラとして、GCC ARM Embedded(https://launchpad.net/gcc-arm-embedded)を導入します。基本的にはダウンロードしたアーカイブを展開してパスを通すだけで完了です。

次に、mbed SDK(https://github.com/mbedmicro/mbed)を、mbed tools - Handbook | mbed を参考に準備します。

# 必要なライブラリ
pip install jinja2
# ライブラリをビルド
python workspace_tools/build.py -m NUCLEO_F401RE -t GCC_ARM

ところで、mbed SDKにはmakeでコンパイルできるようにプロジェクト全体をエクスポートする機能があるのですが、GCC ARM向けに書き出す機能はNucleoでは未実装のようです。しかし、ライブラリのディレクトリを覗いてみるとNucleo用のターゲット依存ファイルはGCC ARM用のファイルが用意されているようなので、今回は無理矢理エクスポートしてみます。
まず、似たような(と私が思っている)Cortex-M4のSTM32F407を積んだDISCOVERYボード用のテンプレートファイル(?)を、そのままコピーします。

cp workspace_tools/export/gcc_arm_disco_f407vg.tmpl workspace_tools/export/gcc_arm_nucleo_f401re.tmpl

次に、workspace_tools/export/gccarm.py の、GccArmクラスのTARGETS変数(配列)の最後に

'NUCLEO_F401RE'

という文字列を追加しておきます。

これによって、mbedプロジェクトをGCC ARM用に書き出すことができるようになりました。次のコマンドラインで、mbed SDKのテストプログラムを書き出してみます。(この手順については http://embeddedworldweb.blogspot.jp/2013/12/how-to-use-mbed-exporters-tutorial.html が詳しいです。)

python workspace_tools/project.py -m NUCLEO_F401RE -p0 -i gcc_arm

build/exportディレクトリにzipファイルが作られます。これを展開してmakeすると.binファイルが生成されます。今回は、これをベースにmrubyを組み込んでいきます。

mrubyのチューニングとビルド設定

そのままのmrubyはRAMの消費が大きいので、ここではビルド時のオプションでチューニングするとともに、標準ライブラリをいくつか削ってしまいます。
標準ライブラリを削ることで多くのメソッドが使用できなくなってしまいますが、RAM消費量の削減効果は絶大です。
今回はmrblibディレクトリのenum.rb、hash.rb、range.rb、string.rbの拡張子を変更して、組み込まれないようにしました。
さらに、チューニングオプションとARM用のクロスコンパイル設定をbuild_config.rbに追加しました。

MRuby::CrossBuild.new('nucleo') do |conf|
  toolchain :gcc
  conf.cc.command = "arm-none-eabi-gcc"
  conf.linker.command = "arm-none-eabi-gcc"
  conf.archiver.command = "arm-none-eabi-ar"

  conf.cc.flags << "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -ffunction-sections -fdata-sections"
  conf.cc.defines = %w(MRB_WORD_BOXING MRB_HEAP_PAGE_SIZE=8 MRB_USE_IV_SEGLIST KHASH_DEFAULT_SIZE=8 MRB_STR_BUF_MIN_SIZE=20 DISABLE_STDIO)
  conf.linker.flags << "-Wl,--gc-sections"

  conf.build_mrbtest_lib_only
  conf.bins = []

  #conf.gem 'examples/mrbgems/c_and_ruby_extension_example'

  #conf.test_runner.command = 'env'

end

この状態でmakeします。

使ってみる

続いて、先ほどエクスポートしたGCC ARM用のmbedプロジェクトに、mrubyのライブラリとヘッダファイルをコピーします。
今回は、次のディレクトリ構成にしました。

Makefile
main.cpp
env/ (エクスポートした時にもとからある)
mbed/ (mbedライブラリ)
mruby/
  include/ (mrubyのincludeディレクトリをそのまま持ってくる)
  lib/
    libmruby.a (mrubyのビルド時に生成されるbuild/nucleo/libディレクトリからコピー)

そして、Makefileを次のように変更します。

  • INCLUDE_PATHS変数の末尾に -I./mruby/include を追加
  • 空になっているLIBRARIES変数にmrubyライブラリを追加 LIBRARIES = ./mruby/lib/libmruby.a
  • CC_SYMBOLS変数の末尾に -DMRB_WORD_BOXING を追加 (mrubyビルド時のオプションと合わせる)

次に、いよいよmrubyを組み込んだプログラムを作成します。今回は次のmain.cppを使用します。シリアル通信とLED用のメソッドを追加しておきました。
このプログラムはCの配列としてtest.cに書き込まれたmrubyバイトコードを実行します。

#include "mbed.h"
#include <mruby.h>
#include <mruby/dump.h>
#include <mruby/string.h>
#include <stdint.h>
#include "test.c"

DigitalOut myled(LED1);
Serial pc(SERIAL_TX, SERIAL_RX);

extern const uint8_t bytecode[];

mrb_value mrb_serial_puts(mrb_state *mrb, mrb_value self) {
  mrb_value str;
  mrb_get_args(mrb, "S", &str);
  pc.printf("%s\n", RSTRING_PTR(str));
  return mrb_nil_value();
}

mrb_value mrb_wait(mrb_state *mrb, mrb_value self) {
  mrb_float seconds;
  mrb_get_args(mrb, "f", &seconds);
  wait(seconds);
  return mrb_nil_value();
}

mrb_value mrb_set_led(mrb_state *mrb, mrb_value self) {
  mrb_int state;
  mrb_get_args(mrb, "i", &state);
  myled = state;
  return mrb_nil_value();
}

int main(void) {
  mrb_state *mrb;
  struct RClass *kernel;
  mrb = mrb_open();
  kernel = mrb->kernel_module;
  mrb_define_method(mrb, kernel, "serial_puts", mrb_serial_puts, MRB_ARGS_REQ(1));
  mrb_define_method(mrb, kernel, "wait", mrb_wait, MRB_ARGS_REQ(1));
  mrb_define_method(mrb, kernel, "set_led", mrb_set_led, MRB_ARGS_REQ(1));
  mrb_load_irep(mrb, bytecode);
  mrb_close(mrb);
  return 0;
}

そして、次のRubyプログラムを動作させます。(test.rbというファイル名で保存します)

serial_puts "program start"
while true
  set_led 1
  serial_puts "led on"
  wait 0.5
  set_led 0
  serial_puts "led off"
  wait 0.5
end

次のコマンドでバイトコードを生成します。(/path/to/mruby/bin/mrbcはmrubyをビルドしたディレクトリによって変わります)

/path/to/mruby/bin/mrbc -Bbytecode test.rb

いよいよビルドです。makeコマンドを実行するとバイナリが生成されます。
生成されたMBED_A1.binをNucleoのドライブにドラッグ&ドロップで書き込むと、シリアルポートに文字列を出力しながらLEDが点滅します。

TODO