LoginSignup
2
0

【チュートリアル】mruby/cをSTM32マイコンで動かす Chapter02: mruby/cでElChika(えるちか)

Last updated at Posted at 2024-05-24

しまねソフト研究開発センター(略称 ITOC)にいます、東です。

mruby/cをSTマイクロエレクトロニクス社製の32bitマイコンSTM32で動かす記事
今回はその第2回、mruby/c のポーティングを行い、ElChika1(えるちか = LEDの点滅)を行います。

目標

ST社製マイコン評価ボード 「Nucleo-F401RE」 のオンボードLEDを、mruby/c のコードを使って点滅させる。

前提知識

mruby/c は、RubyのソースコードをPC上であらかじめバイトコード(中間コード)に変換し、それをターゲットマイコン上の VM(Virtual machine) を使って実行します。

STM32Tuto02-mrubyc動作フロー2.png

バイトコードはmrubyとコンパチブルなので、mrubyプロジェクトから配布されているコンパイラ (mrbc.exe) をそのまま利用します。

STM32Tuto02-mrubyc動作フロー.png

VM自体は、C言語(C99) を使って書いてあります。
mruby/c プログラムを動作させるやり方は、以下の2種類が考えられます。

  1. あらかじめVMをROMに書き込んでおき、あとからバイトコードだけを書き込んで実行する方法
  2. 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 上でリンクする方式とする
  • スケジューラを使う
  • ハードウェアタイマーを使わない

準備するもの

mruby/c は、release3.3 のソースコードをダウンロードします。
mrubyコンパイラは、バージョン 3.3.0 を、ダウンロードします。

前回の記事 を参考に、STM32CubeIDE の環境構築を終わらせておきます。

作業手順

ポーティングから mruby/c サンプルプログラムを動かすまでの手順は、以下の順番で行います。

  1. mruby/c のソースコードを開発環境へ持ってくる
  2. タイマー未使用を宣言する
  3. mruby/c 用の hal ファイルを作る
  4. mruby/c を実行するC関数を作る
  5. Ruby (mruby/c) のプログラムを作る
  6. mrbc.exe を使って、mrubyバイトコードファイルを作る
  7. 全体をビルドし、ターゲットデバイスに書き込む

途中、フォルダの作成など、現時点でのベストプラクティスと思う方法を紹介します。
それではやっていきます。

1. mruby/c のソースコードを開発環境へ持ってくる

CubeIDEの左ペイン Project Explorer 上、Core の上で右クリックし、New > Folder を選びます。
STM32Tuto02-スクリーンショット 2024-05-20 17.16.12.png

New Folder ダイアログで、Folder name 欄へ、mrubyc_src と入力し、[Finish] をクリックし、フォルダを作ります。
STM32Tuto02-スクリーンショット 2024-05-20 17.18.38.png

ダウンロードした mruby/c ソースコードを任意の場所へ展開します。

展開したフォルダの src フォルダにある全ファイルを、先ほどの mrubyc_src フォルダへコピーします。
コピーの方法は、ドラッグ&ドロップでも良いですし、[Ctrl]-[A] → [Ctrl]-[C] → [Ctrl]-[V] でもできます。
STM32Tuto02-スクリーンショット 2024-05-20 17.22.46.png

2. タイマー未使用を宣言する

この章では簡単のため、ハードウェアタイマーを使わずに動かします。そのための宣言をプロジェクト設定に追加します。

メニューから、Project > Properties を選び、ダイアログを開きます。

ダイアログ左ペインの C/C++ General > Paths and Symbols をクリックし、右ペインの Symbols タブをクリックします。

Languages が GNU C になっていることを確認し、[Add...] ボタンをクリックします。

Add symbol ダイアログが開くので、Name欄に、MRBC_NO_TIMER と入力して [OK] をクリックします。
STM32Tuto02-スクリーンショット 2024-05-20 18.46.44.png

[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] をクリックします。
STM32Tuto02-スクリーンショット 2024-05-20 19.01.13.png

ファイルの内容を、以下の通り入力します。

Core/mrubyc_src/hal.h
#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 /* MRUBYC_SRC_HAL_H_ */

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 と入力します。
STM32Tuto02-スクリーンショット1 2024-05-20 18.02.48.png

以下の内容を入力します。

Core/mrubyc/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 sample1[];
  mrbc_create_task( sample1, 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 関数をコールするように書き換えます。

Core/Src/main.c
  /* USER CODE BEGIN 2 */
  void start_mrubyc(void);
  start_mrubyc();
  /* USER CODE END 2 */

5. Ruby (mruby/c) のプログラムを作る

mruby/c のソースファイル sample1.rb を作ります。

左ペイン Project Explorer 上、mrubyc の上で右クリックし、New > File を選びます。

Create New File ダイアログで、File name 欄へ、sample1.rb と入力し、[Finish] をクリックし、ファイルを作ります。

以下の内容を入力します。

Core/mrubyc/sample1.rb
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 -Bsample1 sample1.rb

Rubyソースコードがバイトコードに変換されて、sample1.c ファイルが生成されます。
sample1.c ファイルを表示して、どのように出力されたかを確認しておくと良いでしょう。

sample1.c
#include <stdint.h>
#ifdef __cplusplus
extern
#endif
const uint8_t sample1[] = {
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,
};

ここでできた sample1 配列が、「4. mruby/c を実行するC関数を作る」 の箇所で記述したプログラムのタスクの登録部で使われます。

7. 全体をビルドし、ターゲットデバイスに書き込む

ビルド

メニューから、Project > Build Project を選びます。
下部コンソールペインに、Build Finished. と表示されるのを確認します。いくつか Warning が出るかと思いますが、一旦ここは無視しても良いです。

書き込み

メニューから、Run > Run を選んでターゲットへ書き込みます。

オンボードのLEDが点滅したでしょうか。

STM32Tuto02-Demo1.gif

mruby/c プログラムの書き換え

プログラムを書き換えて、点滅の速度をもっと早くしてみましょう。

sample1.rb を、以下のように書き換えて保存します。

Core/mrubyc/sample1.rb
while true
  led_write( 1 )
  sleep 0.1        # ここと
  led_write( 0 )
  sleep 0.5        # ここ
end

保存するのを忘れないようにしましょう。
次に、コマンドプロンプトで、書き換えたrubyソースコードをバイトコードに変換します。

mrbc.exe -Bsample1 sample1.rb

CubeIDE で、ビルドしてターゲットへ書き込みます。

メニューから、Run > Run を選んでターゲットへ書き込みます。
先ほどは、ビルドと書き込みを別々に行っていましたが、じつは書き込み前に自動的にビルドされるので、書き込みの操作だけでOKです。

実行が始まると、LEDが先ほどより早く点滅します。

STM32Tuto02-Demo2.gif

おわりに

今回は、とりあえず動かすことを目標に、mruby/c をポーティングしました。
このままでは、mruby/c のプログラムを書き換えたら都度 mrbc.exe を動かさないといけないので、不便ですね。また、最初のプログラムでは、sleep 1 と指定しているのに、明らかに 1秒より長い周期で点滅しているように見えます。

次回はこのあたりのことを、解決していこうと思います。

  1. ElChika : Electro Luminescence Chika Chika ですんで、LED (Light Emitting Diode) に限らずそれっぽいものを点滅させてます。

  2. このほかにも、同 Properties ダイアログの、C/C++ Build > Settings > Tool Settings > MCU GCC Compiler > Preprocessor > Define symbols の画面でも同様の定義ができます。試した結果、どちらでも同じ効果でした。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0