Spinelでマイコンで動かすライブラリを作れるようにしています。Rubyで書いたメソッドをSpinelでCにして、ボードのクロスコンパイラでファームウェアに組み込み、ボードのCから呼びます。
Spinelは、RubyのプログラムをCにするAOTコンパイラです。基本的にSpinelのランタイムはPOSIXに向けて書かれていて、マイコンでよく使われるCライブラリのnewlibには足りない部分があります。そのための足りない分を補うlib/newlib/を、forkのブランチで開発しています。
https://github.com/bash0C7/spinel/tree/newlib-runtime-pr
qemuの上では動いてて、これから実機でドッグフーディングしていきます。PRはその後検討します。
つかったもの
- Spinel
- arm-none-eabi-gcc 14.2.1(newlib)
- qemu-arm 10.0(Cortex-A9、Cortex-M33、Cortex-M0+)
- Claude Code
Rubyで書いてCから呼ぶ
ブランチのtest/newlib/kernel.rbは、μ-lawの音声を16bitのPCMにするメソッドです。
module NewlibKernel
#: (String) -> String
def self.ulaw_decode(src)
n = src.bytesize
out = "\0" * (n * 2)
i = 0
while i < n
u = (~src.getbyte(i)) & 0xFF
t = ((u & 0x0F) << 3) + 0x84
t = t << ((u & 0x70) >> 4)
v = ((u & 0x80) != 0 ? (0x84 - t) : (t - 0x84)) & 0xFFFF
out.setbyte(i * 2, v & 0xFF)
out.setbyte(i * 2 + 1, v >> 8)
i += 1
end
out
end
end
SpinelでCにするとき、--ext-entryにCから呼ぶメソッドを、--ext-initにランタイムを初期化する関数の名前を渡します。test/newlib/host.cからは、こう呼んでいます。
Init_newlib_kernel();
sp_NewlibKernel_s_ulaw_decode(SP_STR_HDL(sp_str_from_bytes(raw, 2048)));
引数と戻り値のCの型は、SpinelがCと一緒に出すヘッダーに書かれます。
ビルドは、ブランチをcheckoutしたディレクトリで、出てきたCとランタイムのソースをまとめてコンパイルします。$CCはボードのクロスコンパイラ、$BOARD_FLAGSは-mcpuなどのボードのフラグ、your_board.oはボード側のコードです。
spinel --cc=$CC kernel.rb -c --ext-init Init_newlib_kernel --ext-entry NewlibKernel.ulaw_decode -o kernel.c
$CC -c $BOARD_FLAGS -O2 -ffunction-sections -fdata-sections -ffp-contract=off \
-Ilib/newlib -DSP_NO_SIGNALS -DSP_NO_TERMIOS \
-include lib/newlib/sp_newlib_config.h \
-Ilib -Ilib/regexp -Ilib/regexp/shim \
$(spinel --cc=$CC kernel.rb --print-build | sed -n 's/^define //p') \
kernel.c lib/*.c lib/regexp/*.c lib/newlib/*.c
$CC $BOARD_FLAGS -Wl,--gc-sections *.o your_board.o -lm -o firmware.elf
--ccには、ボードのコンパイラを渡します。SpinelはそこからIntegerの幅を取るので、32bitのボードでは32bitになります。
RAMに収める
ランタイムの表の大きさとGCの最初のしきい値は、PC向けの値です。lib/newlib/sp_newlib_config.hを強制includeすると、数百KBのRAMに合う値になります。どれも-Dで上書きできます。
| .bss | |
|---|---|
| sp_newlib_config.hなし | 880,904バイト |
| sp_newlib_config.hあり | 18,248バイト |
kernel.rbを--gc-sectionsでリンクしたときの値です。
これから
stackchan-picoruby(スタックチャンをPicoRubyで動かすもの。ESP32-S3)と、sendairk03_instrument(PicoRubyで演奏ルールと音色を書くiPhone用の楽器。音源の処理をSpinelでCにしている)、そこで使っている自作のgemで、このブランチを使ってみます。
PRの文面(下書き)
PRを出すときは、この文面にするつもりです。
## What this changes
The runtime is written against POSIX, and newlib leaves part of it out: on master, 14 of the 43 runtime sources do not compile with arm-none-eabi-gcc. `lib/newlib/` supplies what newlib leaves out, as `lib/win32/` does for MinGW-w64.
- Like the Windows port (#7260), it is community-maintained and best effort. Neither CI nor the gate runs it.
- Only a build that passes `-Ilib/newlib` reads it.
- Existing code changes where a header cannot reach:
- Two table sizes and three GC triggers get `#ifndef` defaults, so a board can make them smaller. `test/newlib/kernel.rb` has 18248 bytes of .bss with `lib/newlib/sp_newlib_config.h` and 880904 without.
- `SP_NO_SIGNALS` also leaves out the fiber fault handler. The handler reads `si_addr`, and newlib's `siginfo_t` has no such field.
- `SP_NO_TERMIOS` takes the path `sp_io.c` has for WASI and Windows. newlib's `<termios.h>` includes a `<sys/termios.h>` that newlib does not ship.
- A stand-in for it would need `struct termios` and failing `tcgetattr` and `tcsetattr`, and would end at the same `ENOTTY`.
- Spinel refuses `Thread`, `Mutex`, `Queue` and `ConditionVariable` at `--print-build` and the link when the C compiler's newlib has no pthreads, as it does for `--target=wasm32-wasi`.
- The POSIX build does not change. On Linux with gcc 14.2.0, against master 7982f32b3:
- Of the 332 object files and archives, only `build/csrc/main.o` differs. It holds the refusal above.
- optcarrot's C and binary are byte-identical (checksum 59662).
- When a program uses threads and the C compiler is not the default `cc`, Spinel runs that compiler's preprocessor once more, on `<unistd.h>`.
- `make newlib-test` builds `test/newlib/` and runs it under qemu-arm:
- on Cortex-A9, and on Cortex-M33 and Cortex-M0+ with the `NEWLIB_ARCH` and `NEWLIB_RUN` in `docs/newlib.md`;
- it passes on all three.
## `make gate` (on this branch merged with current master)
(結果うめる)
