PicoRubyをESP32で動かすまでの記録
このディレクトリにて作業中である。
PicoRubyに関しては一旦forkしたものをSubmoduleとしている。
ある程度動作するようになった段階で、本家リポジトリへPull Requestを出す予定。
最初は mruby-esp32 の設定をPicoRuby用に書き換えてコンパイルを通す戦略を採った。
しかし、機械的ににmrubyをPicoRubyに置き換えていくだけではリンクエラーが解決しなかった。
そこで、一旦PicoRubyの build_config
ディレクトリに今回用のconfigである riscv-esp.rb
を追加し、まずは libmruby.a
の生成を試みる戦略に切り替えた。
最終的に次のようなconfigにて libmruby.a
が生成されることを確認した。
libmruby.a
が生成されたので、再度ESP-IDFでプロジェクトを作成し、単純にこの静的ライブラリをリンクできることを確認する。リンクしようとするとリンクエラーが発生した。
これは picoruby-machine
で提供している machine.c
の関数が足りていないためである。 R2P2 ではこのソースコードを直接 CMakeLists.txt
で取り込んでコンパイルしていたので、同様にする。
libmruby.a
の依存関係に picoruby-esp32
コンポーネントを 追加するのがポイント。これをしないとリンクエラーが解決しなかった。おそらく、最初に mruby-esp32
の設定を変えていたときに起こっていたエラーも同様だったと思われる。
ここまででビルドが通る状況になったので、ビルドだけではなく実機上での動作確認も並行しながら進めていく。動作確認にはこのデバイスを使っている。このデバイスでなければならない理由はなく、たまたま手元にあったため。
app_main()
を次のように実装し、Rubyファイルの実行ができるか確認する。
ビルドは通るが、実機上でWDTエラーが出た。WDTとはWatch Dog Timerの略で、タスクをスリープさせずに回し続けると起こるエラーである。ESP-IDFでWDTを無効化するのも手だが、スリープがない状態で回し続けることは不健全なので、きちんとスリープが入るようにする。
mrbc_run()
のコードを読むと、 hal_idle_cpu()
をいう関数を呼び出していることがわかる。
ここはおそらくRubyの実行が終わってこれ以上実行する命令がないときにたどり着くものと思われる。 hal_idle_cpu()
は先ほどコンパイル対象に追加した machine.c
に実装された関数であり、現状空である。
おそらく、ここでスリープが挟まらないことでWDTが発生しているものと推測されるため、ここの処理を記述する。ついでに hal_enable_irq()
や hal_disable_irq()
も mruby/cの実装 を参考に追加をしておいた。
これでWDTは回避できるようになった。
WDTは回避できたが Hello World!
が表示されない。puts
メソッドの実装を追っていくと、mruby/c内の [mrbc_putchar()] にたどり着いた。ここで hal_write()
を呼んでいる。
hal_write()
も先ほどの machine.c
に存在する関数のため、そこを追加実装する。要はコンソール上に文字列を出力する処理を書けばよく、ESP32の場合はシンプルに printf()
が使える。
ついでに hal_flush()
も fflush(stdout)
を呼ぶようにコードを追加しておいた。
これで無事 Hello World!
が出力できることを確認した。
メッセージを変えても問題なく出力できている。
次のステップとして、「標準入力を受け付けられること」を実現したい。
picoruby-io-console
というgemで get_char
をしてそうということがわかったのでこれを取り込む方向で作業する。
PicoRuby側のコードに以下のような修正を加えた。
diff --git a/mrbgems/picoruby-machine/ports/esp32/machine.c b/mrbgems/picoruby-machine/ports/esp32/machine.c
index 96904cc..d72fdb1 100644
--- a/mrbgems/picoruby-machine/ports/esp32/machine.c
+++ b/mrbgems/picoruby-machine/ports/esp32/machine.c
@@ -4,6 +4,7 @@
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
+#include <unistd.h>
#include <time.h>
#include <freertos/FreeRTOS.h>
@@ -50,18 +51,33 @@ int hal_flush(int fd) {
int
hal_read_available(void)
{
- return 0;
+ fd_set fds;
+ struct timeval timeout = { 0, 0 };
+
+ FD_ZERO(&fds);
+ FD_SET(STDIN_FILENO, &fds);
+
+ int ret = select(STDIN_FILENO + 1, &fds, NULL, NULL, &timeout);
+ return (ret > 0 && FD_ISSET(STDIN_FILENO, &fds));
}
int
hal_getchar(void)
{
- return 0;
+ if (hal_read_available()) {
+ return getchar();
+ }
+ return -1;
}
void
hal_abort(const char *s)
{
+ if(s) {
+ hal_write(1, s, strlen(s));
+ }
+
+ abort();
}
diff --git a/build_config/riscv-esp.rb b/build_config/riscv-esp.rb
index 6ad3a94..7948fc2 100644
--- a/build_config/riscv-esp.rb
+++ b/build_config/riscv-esp.rb
@@ -15,5 +15,6 @@ MRuby::CrossBuild.new("esp32") do |conf|
conf.cc.defines << "MRBC_TIMESLICE_TICK_COUNT=1"
conf.gem core: "picoruby-machine"
+ conf.gem core: "picoruby-io-console"
conf.picoruby
end
また、 /mrbgems/picoruby-io-console/ports/esp32/io-console.c b/mrbgems/picoruby-io-console/ports/esp32/io-console.c
を新規で作成し、次のように実装した。
#include <stdio.h>
#include <termios.h>
#include <fcntl.h>
#include <mrubyc.h>
static struct termios save_settings;
static int save_flags;
void
c_raw_bang(mrbc_vm *vm, mrbc_value *v, int argc)
{
struct termios settings;
tcgetattr(fileno(stdin), &save_settings);
settings = save_settings;
settings.c_iflag &= ~(BRKINT | ISTRIP | IXON);
settings.c_lflag &= ~(ICANON | IEXTEN | ECHO | ECHOE | ECHOK | ECHONL);
settings.c_cc[VMIN] = 1;
settings.c_cc[VTIME] = 0;
tcsetattr(fileno(stdin), TCSANOW, &settings);
save_flags = fcntl(fileno(stdin), F_GETFL, 0);
if (0 < argc) {
fcntl(fileno(stdin), F_SETFL, save_flags | O_NONBLOCK); /* add `non blocking` */
}
else {
fcntl(fileno(stdin), F_SETFL, save_flags);
}
}
void
c_cooked_bang(mrbc_vm *vm, mrbc_value *v, int argc)
{
struct termios settings;
tcgetattr(fileno(stdin), &save_settings);
settings = save_settings;
settings.c_iflag |= (BRKINT | ISTRIP | IXON);
settings.c_lflag |= (ICANON | IEXTEN | ECHO | ECHOE | ECHOK | ECHONL);
settings.c_cc[VMIN] = 1;
settings.c_cc[VTIME] = 0;
tcsetattr(fileno(stdin), TCSANOW, &settings);
save_flags = fcntl(fileno(stdin), F_GETFL, 0);
fcntl(fileno(stdin), F_SETFL, save_flags & ~O_NONBLOCK);
}
void
c__restore_termios(mrbc_vm *vm, mrbc_value *v, int argc)
{
fcntl(fileno(stdin), F_SETFL, save_flags);
tcsetattr(fileno(stdin), TCSANOW, &save_settings);
}
int
hal_getchar(void)
{
int c = getchar();
if (c == EOF) {
return -1;
} else {
return c;
}
}
static void
c_echo_eq(mrbc_vm *vm, mrbc_value *v, int argc)
{
struct termios settings;
tcgetattr(fileno(stdin), &settings);
if (v[1].tt == MRBC_TT_TRUE) {
settings.c_lflag |= ECHO;
}
else {
settings.c_lflag &= ~ECHO;
}
tcsetattr(fileno(stdin), TCSANOW, &settings);
// mrbc_incref(&v[0]);
SET_RETURN(v[1]);
}
static void
c_echo_q(mrbc_vm *vm, mrbc_value *v, int argc)
{
struct termios settings;
tcgetattr(fileno(stdin), &settings);
if (settings.c_lflag & ECHO) {
SET_TRUE_RETURN();
}
else {
SET_FALSE_RETURN();
}
}
void
io_console_port_init(mrbc_vm *vm, mrbc_class *class_IO)
{
mrbc_define_method(vm, class_IO, "raw!", c_raw_bang);
mrbc_define_method(vm, class_IO, "cooked!", c_cooked_bang);
mrbc_define_method(vm, class_IO, "_restore_termios", c__restore_termios);
mrbc_define_method(vm, class_IO, "echo=", c_echo_eq);
mrbc_define_method(vm, class_IO, "echo?", c_echo_q);
}
picoruby-esp32
を次のように修正した。
diff --git a/components/picoruby-esp32/CMakeLists.txt b/components/picoruby-esp32/CMakeLists.txt
index 30d1e33..eff62ed 100644
--- a/components/picoruby-esp32/CMakeLists.txt
+++ b/components/picoruby-esp32/CMakeLists.txt
@@ -1,7 +1,11 @@
idf_component_register(
SRCS
picoruby/mrbgems/picoruby-machine/ports/esp32/machine.c
- INCLUDE_DIRS "."
+ picoruby/mrbgems/picoruby-io-console/ports/esp32/io-console.c
+ INCLUDE_DIRS
+ "."
+ ${CMAKE_SOURCE_DIR}/components/picoruby-esp32/picoruby/mrbgems/picoruby-mrubyc/lib/mrubyc/src
+ ${CMAKE_SOURCE_DIR}/components/picoruby-esp32/picoruby/mrbgems/picoruby-machine/include
REQUIRES esp_driver_uart
)
diff --git a/mrblib/main_task.rb b/mrblib/main_task.rb
index 733c205..c707ff2 100644
--- a/mrblib/main_task.rb
+++ b/mrblib/main_task.rb
@@ -1 +1,10 @@
-puts 'Hello World!'
+puts "Press 's' to skip running app.mrb or app.rb"
+skip = false
+10.times do
+ if IO.getch == "s"
+ puts "Skip running app"
+ skip = true
+ break
+ end
+ sleep 0.1
+end
実機上で実行すると次のようなエラーが出る。一部文字化けはしているが uninitialized constant IO
とのこと。IOクラスは picoruby-io-console
で定義済だと思うが...
Press 's' to skip running app.mrb or app.rb
Exception(vm_id=|�1xception(vm_id=|�):ception(vm_id=|� :ception(vm_id=|�uninitialized constant IO (initialized constant IONameErrorized constant IO)
printデバッグする限り mrbc_io_console_init
が呼ばれていないように見える。
何か追加の手順が必要なのか
もしかすると picoruby-require
が必要なのかも。この辺の思想がmrubyとはちょっと違うような気がしている。
build_configに picoruby-require
を追加してみる。
diff --git a/build_config/riscv-esp.rb b/build_config/riscv-esp.rb
index 7948fc2..60855ec 100644
--- a/build_config/riscv-esp.rb
+++ b/build_config/riscv-esp.rb
@@ -14,6 +14,7 @@ MRuby::CrossBuild.new("esp32") do |conf|
conf.cc.defines << "MRBC_TICK_UNIT=10"
conf.cc.defines << "MRBC_TIMESLICE_TICK_COUNT=1"
+ conf.gem core: "picoruby-require"
conf.gem core: "picoruby-machine"
conf.gem core: "picoruby-io-console"
conf.picoruby
ビルドして実行すると以下のエラーが出る。
Exception(vm_id=��<1xception(vm_id=��<):ception(vm_id=��< :ception(vm_id=��<undefined local variable or method 'require' for Object (defined local variable or method 'require' for ObjectNoMethodErroral variable or method 'require' for Object)
requireを使うにももうひと手間必要ってこと?
なりほど、C言語側のエントリポイントでこれを呼ぶ必要があるのか。
picoruby_init_require()
はこのコミットで引数に mrbc_vm
を渡さないと動かなくなっている。
以下のように呼び出す。
mrbc_tcb *tcb = mrbc_create_task(main_task, 0);
mrbc_vm *vm = &tcb->vm;
picoruby_init_require(vm);
この対応をして、再度ビルドすると今度はリンクエラーが出る。
fat_file.c:(.text.c_physical_address+0x28): undefined reference to `FILE_physical_address'
collect2: error: ld returned 1 exit status
picoruby-filesystem-fat
にもESP32実装を加える必要があるのか。なるほど
mrb_value
と RString
の構造体にアクセスするとExceptionが起こる問題で2時間くらい溶かした。原因は片方のコンポーネントには以下の定義があるにもかかわらず、mainコンポーネントにはそれがなかったことが原因
add_definitions(
-DMRBC_USE_FLOAT=2
-DNDEBUG
)
picogem_init.c
を main.c
から include
していることを完全に失念していた。
io-console.c
の実装はmrubycのものを持ってきていたが、これだと getc
メソッドなどの実装が漏れていて標準入力まで至らないことがわかった。rp2040のソースをそのまま使ってみる。
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <mrubyc.h>
/*-------------------------------------
*
* IO::Console
*
*------------------------------------*/
static bool raw_mode = false;
static bool raw_mode_saved = false;
static bool echo_mode = true;
static bool echo_mode_saved = true;
void
c_raw_bang(mrb_vm *vm, mrb_value *v, int argc)
{
raw_mode_saved = raw_mode;
raw_mode = true;
}
void
c_cooked_bang(mrb_vm *vm, mrb_value *v, int argc)
{
raw_mode_saved = raw_mode;
raw_mode = false;
}
static void
c_echo_eq(mrb_vm *vm, mrb_value *v, int argc)
{
echo_mode_saved = echo_mode;
if (v[1].tt == MRBC_TT_FALSE) {
echo_mode = false;
} else {
echo_mode = true;
}
}
static void
c_echo_q(mrb_vm *vm, mrb_value *v, int argc)
{
if (echo_mode) {
SET_TRUE_RETURN();
} else {
SET_FALSE_RETURN();
}
}
void
c__restore_termios(mrb_vm *vm, mrb_value *v, int argc)
{
raw_mode = raw_mode_saved;
echo_mode = echo_mode_saved;
}
static void
c_gets(mrbc_vm *vm, mrbc_value *v, int argc)
{
mrb_value str = mrbc_string_new(vm, NULL, 0);
char buf[2];
buf[1] = '\0';
while (true) {
int c = hal_getchar();
if (c == 3) { // Ctrl-C
mrbc_raise(vm, MRBC_CLASS(IOError), "Interrupted");
return;
}
if (c == 27) { // ESC
continue;
}
if (c == 8 || c == 127) { // Backspace
if (0 < str.string->size) {
str.string->size--;
mrbc_realloc(vm, str.string->data, str.string->size);
hal_write(1, "\b \b", 3);
}
} else
if (-1 < c) {
buf[0] = c;
mrbc_string_append_cstr(&str, buf);
hal_write(1, buf, 1);
if (c == '\n' || c == '\r') {
break;
}
}
}
SET_RETURN(str);
}
static void
c_getc(mrbc_vm *vm, mrbc_value *v, int argc)
{
if (raw_mode) {
char buf[1];
int c = hal_getchar();
if (-1 < c) {
buf[0] = c;
mrb_value str = mrbc_string_new(vm, buf, 1);
SET_RETURN(str);
} else {
SET_NIL_RETURN();
}
}
else {
c_gets(vm, v, argc);
mrbc_value str = v[0];
if (1 < str.string->size) {
mrbc_realloc(vm, str.string->data, 1);
str.string->size = 1;
}
}
}
void
io_console_port_init(mrbc_vm *vm, mrbc_class *class_IO)
{
mrbc_define_method(vm, class_IO, "raw!", c_raw_bang);
mrbc_define_method(vm, class_IO, "cooked!", c_cooked_bang);
mrbc_define_method(vm, class_IO, "echo?", c_echo_q);
mrbc_define_method(vm, class_IO, "echo=", c_echo_eq);
mrbc_define_method(vm, class_IO, "_restore_termios", c__restore_termios);
mrbc_define_method(vm, class_IO, "getc", c_getc);
mrbc_define_method(vm, mrbc_class_object, "gets", c_gets);
}
また、 machine.c
の hal_read_avairable
が機能していなかったので、次のように書き換えた。
int
hal_read_available(void)
{
hal_idle_cpu();
return 1;
}
hal_idle_cpu()
をしているのはこれを入れないとWDTエラーとなるため。
ここまでで IO.getch
の動作確認は完了。
STDIN
は誰がセットしているのか考える必要がありそう。
と思いきや、自分が参照しているR2P2とバージョンに差分が出ている気もするので確認する。
最新バージョンではここで定数をセットすることになっていることがわかったので、同じようになってみる。
例外は出ないがプロンプトも出ないのでもう少し調べてみる。
PicoRubyの 現時点の最新版 をfetchしてマージしてみた。
起動と同時にpanicが発生するようになった。
I (359) main_task: Started on CPU0
I (359) main_task: Calling app_main()
Guru Meditation Error: Core 0 panic'ed (Load access fault). Exception was unhandled.
Core 0 register dump:
MEPC : 0x4200f13c RA : 0x4200f454 SP : 0x3fcc3400 GP : 0x3fc8be00
--- Stack dump detected
--- 0x4200f13c: search_builtin_symbol at symbol.c:?
0x4200f454: mrbc_search_symid at ??:?
TP : 0x3fcc3650 T0 : 0x7f7f7f7f T1 : 0x0000001b T2 : 0xffffffff
S0/FP : 0x3fcc3450 S1 : 0x3fc95178 A0 : 0x00000000 A1 : 0x3c091120
A2 : 0x3fc95178 A3 : 0x00000001 A4 : 0x00000000 A5 : 0x3fc95178
A6 : 0x00000000 A7 : 0x00000021 S2 : 0x3c091510 S3 : 0x3fc90838
S4 : 0x0000000f S5 : 0x0000000d S6 : 0x00000070 S7 : 0x3fc908f0
S8 : 0x3fc9518c S9 : 0x00000001 S10 : 0x00000000 S11 : 0x00000000
T3 : 0x00000002 T4 : 0x00000002 T5 : 0x5f28207c T6 : 0x5f28207c
MSTATUS : 0x00001881 MTVEC : 0x40380001 MCAUSE : 0x00000005 MTVAL : 0x00000000
--- 0x40380001: _vector_table at /Users/yuhei/esp/esp-idf/components/riscv/vectors_intc.S:54
MHARTID : 0x00000000
どのタイミングでpanicが起こっているのか切り分けていく。
この1行をコメントアウトすればpanicは回避できることがわかった。定数を文字列内に埋め込むときのシンボル探索で何か問題が起きているのだろうか...
とりあえずこの問題は後で調査するとして、当面はコメントアウトをしたまま作業を進めてみる。
ここで sleep_ms
を呼び出したとき、
内部的にはmruby/cのこの関数が呼ばれていることがわかった。
たぶん、ここは Machine#delay_ms
を呼ばせないといけないような気がする。
main_task.rb
にて Machine.using_delay
を呼ぶようにしてみた。
Machine.using_delay do
STDIN.echo = false
$shell = Shell.new(clean: true)
end
ターミナルの初期化がされたので、一歩前進した気がする!
ロゴを出力しようとしてエラーが出る件、このような文字列定数の中に別の定数を埋め込むだけでも再現することがわかった。
CONST1 = ""
CONST2 = "#{CONST1}aaa"
puts CONST2
search_builtin_symbol
でpanicが起こっているところまで特定したが、根本原因はわからない。
opensslのpath解決ができなくなったので、この記事を参考に環境変数を追加
定数の中に定数があるとエラーになる問題は一旦PicoRubyのバージョンを古いバージョンに戻したら発生しなくなった。
これでロゴの出力までは実現できた。
$shell.start
をすると、内部で Sandbox.new
したときに abort()
が呼ばれてしまう模様。次はこの問題を調査する。
abort() was called at PC 0x420341a7 on core 0
--- 0x420341a7: pm_constant_id_list_init_capacity at ??:?
--- Stack dump detected
Core 0 register dump:
MEPC : 0x4038084c RA : 0x40383992 SP : 0x3fcc37e0 GP : 0x3fc8be00
--- 0x4038084c: panic_abort at /Users/yuhei/esp/esp-idf/components/esp_system/panic.c:463
0x40383992: __ubsan_include at /Users/yuhei/esp/esp-idf/components/esp_system/ubsan.c:311
TP : 0x3fcc3af0 T0 : 0x37363534 T1 : 0x7271706f T2 : 0x33323130
S0/FP : 0x00000004 S1 : 0x3fcc3844 A0 : 0x3fcc380c A1 : 0x3fcc3842
A2 : 0x00000000 A3 : 0x3fcc3839 A4 : 0x00000001 A5 : 0x3fcc2000
A6 : 0x7a797877 A7 : 0x76757473 S2 : 0x3fcc45dc S3 : 0x3fcc4790
S4 : 0x00000001 S5 : 0x3fcc38b8 S6 : 0x0000002f S7 : 0x00000000
S8 : 0x00000001 S9 : 0x00000000 S10 : 0x00000000 S11 : 0x00000000
T3 : 0x6e6d6c6b T4 : 0x6a696867 T5 : 0x66656463 T6 : 0x62613938
MSTATUS : 0x00001881 MTVEC : 0x40380001 MCAUSE : 0x00000007 MTVAL : 0x00000000
--- 0x40380001: _vector_table at /Users/yuhei/esp/esp-idf/components/riscv/vectors_intc.S:54
abort()
が呼ばれる問題、allocがうまく機能していないことが原因ということがわかった。
build configに定数を追加することで、PicoRubyで実装されたallocが呼ばれるようになる。
conf.cc.defines << "MRC_CUSTOM_ALLOC"
これでほぼ動いたが、改行コードが文字化けしていることが判明したので、この定数を追加してCRLF変換するようにする。
conf.cc.defines << "MRBC_CONVERT_CRLF=1"
echoコマンドが動いた!