Open30

PicoRubyをESP32で動かすまでの記録

Y_uuuY_uuu

libmruby.a が生成されたので、再度ESP-IDFでプロジェクトを作成し、単純にこの静的ライブラリをリンクできることを確認する。リンクしようとするとリンクエラーが発生した。

これは picoruby-machine で提供している machine.c の関数が足りていないためである。 R2P2 ではこのソースコードを直接 CMakeLists.txt で取り込んでコンパイルしていたので、同様にする。

libmruby.a の依存関係に picoruby-esp32 コンポーネントを 追加するのがポイント。これをしないとリンクエラーが解決しなかった。おそらく、最初に mruby-esp32 の設定を変えていたときに起こっていたエラーも同様だったと思われる。

https://github.com/yuuu/picoruby-esp32/blob/9fe7de144bc60da8739f7aa62730a799a15c85df/components/picoruby-esp32/CMakeLists.txt

Y_uuuY_uuu

app_main() を次のように実装し、Rubyファイルの実行ができるか確認する。

https://github.com/yuuu/picoruby-esp32/blob/9fe7de144bc60da8739f7aa62730a799a15c85df/main/main.c

ビルドは通るが、実機上でWDTエラーが出た。WDTとはWatch Dog Timerの略で、タスクをスリープさせずに回し続けると起こるエラーである。ESP-IDFでWDTを無効化するのも手だが、スリープがない状態で回し続けることは不健全なので、きちんとスリープが入るようにする。

mrbc_run() のコードを読むと、 hal_idle_cpu() をいう関数を呼び出していることがわかる。

https://github.com/mrubyc/mrubyc/blob/ff9ec5a55427dc2ed28b13d8d947c69e0ae76c6e/src/rrt0.c#L367

ここはおそらくRubyの実行が終わってこれ以上実行する命令がないときにたどり着くものと思われる。 hal_idle_cpu() は先ほどコンパイル対象に追加した machine.c に実装された関数であり、現状空である。
おそらく、ここでスリープが挟まらないことでWDTが発生しているものと推測されるため、ここの処理を記述する。ついでに hal_enable_irq()hal_disable_irq()mruby/cの実装 を参考に追加をしておいた。

https://github.com/picoruby/picoruby/blob/326413efd9f16965a07c5996a4b1346fad8aeb6f/mrbgems/picoruby-machine/ports/esp32/machine.c

これでWDTは回避できるようになった。

Y_uuuY_uuu

WDTは回避できたが Hello World! が表示されない。puts メソッドの実装を追っていくと、mruby/c内の [mrbc_putchar()] にたどり着いた。ここで hal_write() を呼んでいる。

https://github.com/mrubyc/mrubyc/blob/ff9ec5a55427dc2ed28b13d8d947c69e0ae76c6e/src/console.c#L123

hal_write() も先ほどの machine.c に存在する関数のため、そこを追加実装する。要はコンソール上に文字列を出力する処理を書けばよく、ESP32の場合はシンプルに printf() が使える。

https://github.com/picoruby/picoruby/blob/326413efd9f16965a07c5996a4b1346fad8aeb6f/mrbgems/picoruby-machine/ports/esp32/machine.c#L43

ついでに hal_flush()fflush(stdout) を呼ぶようにコードを追加しておいた。

これで無事 Hello World! が出力できることを確認した。
メッセージを変えても問題なく出力できている。

https://x.com/Y_uuu/status/1870597480028762325

Y_uuuY_uuu

次のステップとして、「標準入力を受け付けられること」を実現したい。
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);
}
Y_uuuY_uuu

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
Y_uuuY_uuu

実機上で実行すると次のようなエラーが出る。一部文字化けはしているが 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)
Y_uuuY_uuu

もしかすると picoruby-require が必要なのかも。この辺の思想がmrubyとはちょっと違うような気がしている。

Y_uuuY_uuu

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を使うにももうひと手間必要ってこと?

Y_uuuY_uuu

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実装を加える必要があるのか。なるほど

Y_uuuY_uuu

mrb_valueRString の構造体にアクセスするとExceptionが起こる問題で2時間くらい溶かした。原因は片方のコンポーネントには以下の定義があるにもかかわらず、mainコンポーネントにはそれがなかったことが原因

add_definitions(
  -DMRBC_USE_FLOAT=2
  -DNDEBUG
)

picogem_init.cmain.c から include していることを完全に失念していた。

Y_uuuY_uuu

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.chal_read_avairable が機能していなかったので、次のように書き換えた。

int
hal_read_available(void)
{
  hal_idle_cpu();
  return 1;
}

hal_idle_cpu() をしているのはこれを入れないとWDTエラーとなるため。

Y_uuuY_uuu

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が起こっているのか切り分けていく。

Y_uuuY_uuu

main_task.rb にて Machine.using_delay を呼ぶようにしてみた。

  Machine.using_delay do
    STDIN.echo = false
    $shell = Shell.new(clean: true)
  end

ターミナルの初期化がされたので、一歩前進した気がする!

Y_uuuY_uuu

ロゴを出力しようとしてエラーが出る件、このような文字列定数の中に別の定数を埋め込むだけでも再現することがわかった。

CONST1 = ""
CONST2 = "#{CONST1}aaa"
puts CONST2

search_builtin_symbol でpanicが起こっているところまで特定したが、根本原因はわからない。

Y_uuuY_uuu

$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
Y_uuuY_uuu

abort() が呼ばれる問題、allocがうまく機能していないことが原因ということがわかった。
build configに定数を追加することで、PicoRubyで実装されたallocが呼ばれるようになる。

  conf.cc.defines << "MRC_CUSTOM_ALLOC"

これでほぼ動いたが、改行コードが文字化けしていることが判明したので、この定数を追加してCRLF変換するようにする。

  conf.cc.defines << "MRBC_CONVERT_CRLF=1"