しまねソフト研究開発センター(略称 ITOC)にいます、東です。
mruby/cをSTマイクロエレクトロニクス社製の32bitマイコンSTM32で動かす記事、
今回はその第2回、mruby/c のポーティングを行い、ElChika1(えるちか = LEDの点滅)を行います。
目標
ST社製マイコン評価ボード 「Nucleo-F401RE」 のオンボードLEDを、mruby/c のコードを使って点滅させる。
前提知識
mruby/c は、RubyのソースコードをPC上であらかじめバイトコード(中間コード)に変換し、それをターゲットマイコン上の VM(Virtual machine) を使って実行します。
バイトコードはmrubyとコンパチブルなので、mrubyプロジェクトから配布されているコンパイラ (mrbc.exe) をそのまま利用します。
VM自体は、C言語(C99) を使って書いてあります。
mruby/c プログラムを動作させるやり方は、以下の2種類が考えられます。
- あらかじめVMをROMに書き込んでおき、あとからバイトコードだけを書き込んで実行する方法
- mruby/cのバイトコードをC言語の配列へ出力し、他のC言語ファイルとともにビルドしマイコンに書き込む方法
今回は、2の方法を説明します。
スケジューラ
mruby/cの特徴の一つとして、スケジューラを内包し、OSなしでも複数のrubyプログラムの同時実行が可能です。ですがスケジューラとmruby/c VM本体は疎結合であるように設計しており分離可能です。すなわち、mrubyのように VM 本体だけでも使う事ができます。具体的方法は、sample_c フォルダの sample_no_scheduler.c を見てください。
今回は、スケジューラを使う方針にします。
ハードウェアタイマー
スケジューラは、プリエンプティブな動作を実現するために、ハードウェアタイマーを必要とします。しかしポーティングの初期段階でいきなりフルスペック動作をさせるのはハードルが高いので、ハードウェアタイマーなしで疑似動作させるためのモードがあります。
今回は、ハードウェアタイマーを使わずに動作確認をすることにします。
(ハードウェアタイマーを使うのは次回以降とします)
(参考)
このほかにも、PicoRuby (https://github.com/picoruby/picoruby) という、
- コンパイラを独自開発
- VMは mruby/c を使う
という処理系もあります。
今回の方針
- mruby/c のバイトコードをC言語のバイト列へ変換し、STM32CubeIDE 上でリンクする方式とする
- スケジューラを使う
- ハードウェアタイマーを使わない
準備するもの
- STM32マイコン評価ボード Nucleo-F401RE
(https://www.st.com/ja/evaluation-tools/nucleo-f401re.html) - 開発環境 STM32CubeIDE (ver 1.15.1)
(https://www.st.com/ja/development-tools/stm32cubeide.html) - mruby/c ソースコード
(https://github.com/mrubyc/mrubyc/releases) - mrubyコンパイラ mrbc.exe
(https://www.s-itoc.jp/support/technical-support/mrubyc/mrubyc-download/)
mruby/c は、release3.3 のソースコードをダウンロードします。
mrubyコンパイラは、バージョン 3.3.0 を、ダウンロードします。
前回の記事 を参考に、STM32CubeIDE の環境構築を終わらせておきます。
作業手順
ポーティングから mruby/c サンプルプログラムを動かすまでの手順は、以下の順番で行います。
- mruby/c のソースコードを開発環境へ持ってくる
- タイマー未使用を宣言する
- mruby/c 用の hal ファイルを作る
- mruby/c を実行するC関数を作る
- Ruby (mruby/c) のプログラムを作る
- mrbc.exe を使って、mrubyバイトコードファイルを作る
- 全体をビルドし、ターゲットデバイスに書き込む
途中、フォルダの作成など、現時点でのベストプラクティスと思う方法を紹介します。
それではやっていきます。
1. mruby/c のソースコードを開発環境へ持ってくる
CubeIDEの左ペイン Project Explorer 上、Core
の上で右クリックし、New > Folder を選びます。
New Folder ダイアログで、Folder name 欄へ、mrubyc_src
と入力し、[Finish] をクリックし、フォルダを作ります。
ダウンロードした mruby/c ソースコードを任意の場所へ展開します。
展開したフォルダの src フォルダにある全ファイルを、先ほどの mrubyc_src フォルダへコピーします。
コピーの方法は、ドラッグ&ドロップでも良いですし、[Ctrl]-[A] → [Ctrl]-[C] → [Ctrl]-[V] でもできます。
2. タイマー未使用を宣言する
この章では簡単のため、ハードウェアタイマーを使わずに動かします。そのための宣言をプロジェクト設定に追加します。
メニューから、Project > Properties を選び、ダイアログを開きます。
ダイアログ左ペインの C/C++ General > Paths and Symbols をクリックし、右ペインの Symbols タブをクリックします。
Languages が GNU C
になっていることを確認し、[Add...] ボタンをクリックします。
Add symbol ダイアログが開くので、Name欄に、MRBC_NO_TIMER
と入力して [OK] をクリックします。
[Apply and Close] をクリックしてダイアログを閉じます。2
3. mruby/c 用の hal ファイルを作る
STM32環境にもHALがあり、ややこしいですがそれとは別物です。
以下の手順で、Core/mrubyc_src フォルダへ、hal.h
ファイルを作ります。
CubeIDEの左ペイン Project Explorer の、先ほど作った mrubyc_src
フォルダの上で右クリックし、New > Header File を選びます。
New Header File ダイアログが開くので、Header file: 欄へ、hal.h
と入力して [Finish] をクリックします。
ファイルの内容を、以下の通り入力します。
#ifndef MRBC_SRC_HAL_H_
#define MRBC_SRC_HAL_H_
#define MRBC_TICK_UNIT 1
#define MRBC_TIMESLICE_TICK_COUNT 10
#define hal_init() ((void)0)
#define hal_enable_irq() ((void)0)
#define hal_disable_irq() ((void)0)
#define hal_idle_cpu() (HAL_Delay(MRBC_TICK_UNIT), mrbc_tick())
int hal_write(int fd, const void *buf, int nbytes);
int hal_flush(int fd);
void hal_abort(const char *s);
#endif
mruby/c で必要とする hal は、4つのマクロ (define) と、3つの関数、たったこれだけです。
今回のレポートではとりあえず動かすことを重要視し、ダミーで定義して後々拡張します。
名前から大まかな動作は想像できると思いますが、簡単にそれぞれを説明します。
- hal_init
- hal独自の初期化が必要になる場合、このマクロに定義する。
- hal_enable_irq
- CPUの割り込み許可処理関数を記述する。
- hal_disable_irq
- CPUの割り込み禁止処理関数を記述する。
- hal_idle_cpu
- CPUをアイドルモードにするための処理関数を記述する。
- int hal_write(int fd, const void *buf, int nbytes)
- mruby/c の putc などの標準出力先を記述する。
- int hal_flush(int fd)
- 標準出力にバッファを持つ場合のバッファフラッシュを記述する。
- void hal_abort(const char *s)
- なんらかの原因で abort する場合にコールする関数を記述する。
hal_idle_cpu マクロに関して、今回のようにハードウェアタイマーを使用しないモードでCPUを止めてしまうと、起きるためのトリガーがなくなって本当に動作が完全停止してしまうので、1ミリ秒の時間待ちと、併せて mruby/c のスケジューラ内部処理に必要な関数 mrbc_tick()
を記述します。
4. mruby/c を実行するC関数を作る
mruby/c を実行する関数を、新たに Core/mrubyc/start_mrubyc.c ファイルを作って記述します。
先ほどと同じように、CubeIDEの左ペイン Project Explorer 上、Core
の上で右クリックし、New > Folder を選びます。
New Folder ダイアログで、Folder name 欄へ、mrubyc
と入力し、[Finish] をクリックし、フォルダを作ります。
できた mrubycフォルダ上で右クリックし、New > Source File と選びます。
New Source File ダイアログの Source file 欄へ、start_mrubyc.c
と入力します。
以下の内容を入力します。
#include "main.h"
#include "../mrubyc_src/mrubyc.h"
static void c_led_write(mrbc_vm *vm, mrbc_value v[], int argc);
/* mruby/c プログラムが使うワークメモリの確保 */
#define MRBC_MEMORY_SIZE (1024*30)
static uint8_t memory_pool[MRBC_MEMORY_SIZE];
/*! mruby/c プログラムの実行開始
*/
void start_mrubyc( void )
{
mrbc_init(memory_pool, MRBC_MEMORY_SIZE);
// ユーザ定義メソッドの登録
mrbc_define_method(0, 0, "led_write", c_led_write);
// タスクの登録
extern const uint8_t task1[];
mrbc_create_task( task1, 0 );
// 実行開始
mrbc_run();
}
/*! オンボードLED ON/OFF メソッドの実装
*/
static void c_led_write(mrbc_vm *vm, mrbc_value v[], int argc)
{
int on_off = GET_INT_ARG(1);
HAL_GPIO_WritePin( GPIOA, GPIO_PIN_5, on_off );
}
/*! HAL(ダミー)
*/
int hal_write(int fd, const void *buf, int nbytes)
{
return 0;
}
int hal_flush(int fd)
{
return 0;
}
void hal_abort(const char *s)
{
}
前回、C言語で動作確認をした時に書き換えた Core/Src/main.c を、以下の通り start_mrubyc 関数をコールするように書き換えます。
/* USER CODE BEGIN 2 */
void start_mrubyc(void);
start_mrubyc();
/* USER CODE END 2 */
5. Ruby (mruby/c) のプログラムを作る
mruby/c のソースファイル task11.rb
を作ります。
左ペイン Project Explorer 上、mrubyc
の上で右クリックし、New > File を選びます。
Create New File ダイアログで、File name 欄へ、task1.rb
と入力し、[Finish] をクリックし、ファイルを作ります。
以下の内容を入力します。
while true
led_write( 1 )
sleep 1
led_write( 0 )
sleep 1
end
6. mrbc.exe を使って、mrubyバイトコードファイルを作る
ダウンロードした mruby コンパイラのフォルダから、mrbc.exe を、先ほどの mrubyc フォルダへコピーします。
コマンドプロンプトを起動し、先のフォルダへ cd します。
cd \PATH\TO\PROJECT\tuto01\Core\mrubyc
mrbc.exe コンパイラで、rubyソースコードをバイトコードに変換します。
mrbc.exe -Btask1 task1.rb
Rubyソースコードがバイトコードに変換されて、task1.c
ファイルが生成されます。
task1.c ファイルを表示して、どのように出力されたかを確認しておくと良いでしょう。
#include <stdint.h>
#ifdef __cplusplus
extern
#endif
const uint8_t task1[] = {
0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x00,0x70,0x4d,0x41,0x54,0x5a,
(snip...)
0x00,0x05,0x73,0x6c,0x65,0x65,0x70,0x00,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};
ここでできた task1
配列が、「4. mruby/c を実行するC関数を作る」 の箇所で記述したプログラムのタスクの登録部で使われます。
7. 全体をビルドし、ターゲットデバイスに書き込む
ビルド
メニューから、Project > Build Project を選びます。
下部コンソールペインに、Build Finished. と表示されるのを確認します。いくつか Warning が出るかと思いますが、一旦ここは無視しても良いです。
書き込み
メニューから、Run > Run を選んでターゲットへ書き込みます。
オンボードのLEDが点滅したでしょうか。
mruby/c プログラムの書き換え
プログラムを書き換えて、点滅の速度をもっと早くしてみましょう。
task1.rb を、以下のように書き換えて保存します。
while true
led_write( 1 )
sleep 0.1 # ここと
led_write( 0 )
sleep 0.5 # ここ
end
保存するのを忘れないようにしましょう。
次に、コマンドプロンプトで、書き換えたrubyソースコードをバイトコードに変換します。
mrbc.exe -Btask1 task1.rb
CubeIDE で、ビルドしてターゲットへ書き込みます。
メニューから、Run > Run を選んでターゲットへ書き込みます。
先ほどは、ビルドと書き込みを別々に行っていましたが、じつは書き込み前に自動的にビルドされるので、書き込みの操作だけでOKです。
実行が始まると、LEDが先ほどより早く点滅します。
おわりに
ファイル全体は、github リポジトリにありますので、そちらをご覧ください。
今回は、とりあえず動かすことを目標に、mruby/c をポーティングしました。
このままでは、mruby/c のプログラムを書き換えたら都度 mrbc.exe を動かさないといけないので、不便ですね。また、最初のプログラムでは、sleep 1
と指定しているのに、明らかに 1秒より長い周期で点滅しているように見えます。
次回はこのあたりのことを、解決していこうと思います。