Cross-compiling for Embedded arm32 Target (RP2040 / Cortex-M0+)

Hi, I recently got a hold of a Raspberry Pi Pico, and was looking into whether it would be possible to compile native embedded arm32 executables to run on the platform (which features a Cortex-M0±based RP2040 with 264kB of RAM and 2MB of flash). Most of what I’ve found on Discourse deals with compiling for arm64 platforms with an existing OS, which is not quite my use case. However, I did find a nearly 10 year old Stack Overflow question that does deal with essentially the same situation I’m interested in.

But before I started delving into what is likely a months-long process of hacking things together and porting other things, does any know if an existing toolchain let me accomplish this task? If not, are there any suggestions on where to start developing one? I’m willing to provide more details on what I’m looking for if the question isn’t clear enough.

Reference materials:
Raspberry Pi Pico Documentation (including Pi Pico and RP2040 datasheets

Probably the closes to get an OCaml project on a microcontroller is the project GitHub - stevenvar/OMicroB: An OCaml generic virtual machine for microcontrollers

However, OMicroB runs bytecode not native.

1 Like

You could also taker a look at @timada’s work on github for solo5-embedded - there’s an rpi related project - https://github.com/TImada/raspi4_freertos

Please do bear in mind that native code support for ARM32 and i386 have been removed in OCaml trunk (ocaml/ocaml#11904) and so OCaml 5.1 onwards will only support 32-bit targets in bytecode mode.

Now, bytecode is actually pretty good for embedded targets since it compresses quite well (I once had a Mirage DNS server in bytecode in <100kB), but it might not meet your performance requirements. You can continue to use OCaml 4.x for this in native code, of course, depending on what your own constraints are.

264kB of RAM is probably not enough to run nontrivial OCaml programs. Back in the days of Caml Light in the early 1990’s, we were able to run on 640 kB PCs, but it was already very limited. I don’t think you can do anything useful in OCaml these days without a couple of MB of RAM.

Actually, looking at the current range of Raspberry Pi boards explains well why we’re dropping native-code support for ARM 32 bits. All the boards with enough RAM to run Linux and OCaml have 64-bit ARMv8 processors, even the $15 Pi Zero. It’s only the Pi Pico boards that have 32-bit processors and cannot run OCaml anyway by lack of RAM.

Plus, ARMv7 (32 bits) has a weird memory model that makes it difficult if not impossible to support Multicore OCaml. ARMv8, on the other hand, is a perfect match for Multicore OCaml.

4 Likes

Wow, those were prompt replies! Sorry for the delay, and thanks for all the responses.

264kB of RAM is probably not enough to run nontrivial OCaml programs. Back in the days of Caml Light in the early 1990’s, we were able to run on 640 kB PCs, but it was already very limited. I don’t think you can do anything useful in OCaml these days without a couple of MB of RAM.

@xavierleroy From asking this question I’ve noticed that I’ve opened up quite the can of worms and maybe should have done some more research first! From my understanding, microcontroller programs usually reside in ROM or flash and are not loaded into RAM during execution, so I should be able to offload a lot of the total memory usage to flash, I think; do native-code OCaml executables generally require a lot of runtime memory in addition to the code size (maybe for things like garbage collection?)

Now, bytecode is actually pretty good for embedded targets since it compresses quite well (I once had a Mirage DNS server in bytecode in <100kB), but it might not meet your performance requirements. You can continue to use OCaml 4.x for this in native code, of course, depending on what your own constraints are.

@avsm Thanks for the info! I guess I should look into projects like what Leonardo linked earlier and investigate whether native code is even worth the trouble. Honestly, my intuition previously would have been the opposite; that native code would be easier to get running on an embedded platform than a bytecode runtime!

Note that you probably should take a look on gilbraltar which is a full OCaml runtime for Rasperry Pi 4 (arm64). We already did several works on this ocaml toolchain, a “guirlande OS” or a music player OS via Jack.

2 Likes

what puzzles me a bit is that my Raspberry Pi 400 running a 32bit raspian for compatibilty sake says

$ uname -m
armv7l

while you mention armv8. Do I have to run a 64bit OS to see that?

Yes, the default raspian for Raspberry Pi 4 was a 32-bits system at the beginning (now, it seems that the official raspbian is a 64-bits system since a year). More precisely, the Cortex-A53 (RPi 3) or the Cortex-72 (RPi 4, RPi 400) are a 64-bits processor (armv8).

No problem, it’s a useful discussion to have here, that’s what this forum is for.

Yes, the OCaml runtime system uses nontrivial amounts of RAM for the memory heap and its garbage collector. most of this RAM is allocated on demand, but the initial sizes are in the 1Mb range (to give an order of magnitude). Moreover, these initial sizes tend to increase over time, as OCaml picks up new features (like multicore support in version 5) and gets tuned for heavy, server-like workloads.

I guess the point I’m trying to make is that OCaml is not designed for microcontrollers. It squarely targets more conventional computers, of the kind that can run a full OS and load programs into RAM. But the Raspberry Pi Zero that I mentioned is an example of such a conventional computer, yet it costs only $15, i.e. not much more than the $4 Raspberry Pi Pico you’re considering. An extra $11 buys you a tremendous amount of computing power!

Quite possibly. At any rate, you’ll need a 64-bit OS to be able to use ARM64 native-code compilation in OCaml. But there are plenty to choose from for the Raspberry Pi 4 : several Linux distributions, plus FreeBSD, maybe others.

1 Like

Since OCaml drops 32bit support, and due to the very small resources of your target, this is exactly the case where Rust would make much more sense than OCaml:

(hope raspi is not too OT)

after flipping the switch

$ tail -n1 /boot/config.txt
arm_64bit=1
$ uname -m
aarch64
$ make clean ; dune build --profile release bin/seppo.exe
...
$ file _build/default/bin/seppo.exe 
_build/default/bin/seppo.exe: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

I still get a 32bit binary - I guess at least until I rebuild the toolchain (opam, namely).

As I am out for static musl builds which OCaml 5 will drop :frowning: I guess I will change to Alpine on the metal later this year. I hate containers.

You need to rebuild your entire userspace, not just opam. You can flip the Linux kernel to 64-bit, but that hasn’t changed the fact that all your previous distro binaries are still building 32-bit binaries.

What makes you say this? Static musl builds should work fine with OCaml 5.x as far as I’m aware.

1 Like
$ opam switch list-available | fgrep musl | tail -n 1
ocaml-variants                         4.11.2+musl+static+flambda             Official release 4.11.2, compiled with musl-gcc -static and with flambda activated

and I remember the recommendation to do docker (alpine) builds for later versions. But can’t find it right now.

Since 4.12.0 and Experimental new layout for the ocaml-variants packages in opam-repository the way to install those compilers is:

opam switch create 5.0.0-static \
  --packages=ocaml-variants.5.0.0+options,ocaml-option-static
2 Likes