Compiling Julia code for a RISC-V ESP32 microcontroller

I wanted to share some experiments I did to run Julia on a ESP32 C3 using GPUCompiler and LLVM:

This microcontrollers use the open RISC-V instruction set. The ESP32 family of microcontrollers are quite popular because they quite inexpensive and jet rather powerful (dual core processor, WiFi and Bluetooth).

Once you have a Julia build with RISC-V enabled as LLVM target, GPUCompiler.jl can generate an object file with the compiled Julia code. I wrapped some functions from the Arduino API to manipulate the GPIO (General-Purpose Input/Output) pins and write to serial output. As it is done typically in Arduino sketches, there is a Julia function for setup() and one for loop() (both part of a Julia module). A single object file with these functions is then linked against the ESP32 IDF framework from the manufacturer of these microcontrollers.

What I got to work is quite basic like blinking a led, write to serial output. The most complex example that I got working is to control a led strip.

Only a tiny subset of Julia can be used however (no global variables, no heap allocation: no strings or arrays..). When I try to use global variable, the memory adresses of the host are stored into the RISC-V binary.

A work-around to access some global variables like the global Serial object is to create a function that returns it adress in LLVM IR. But for global variables declared by the user, this is more difficult to do.

I learned a lot from the post of @seelengrab (Running Julia baremetal on an Arduino) on Julia on Arduino, which works also for WASM (GitHub - Alexander-Barth/FluidSimDemo-WebAssembly) .

Any ideas how to handle module-level global variable such as a reference to an Int32 would be quite helpful (when cross-compiling).

6 Likes

Very cool! I also did some experiments with RISC-V, but never published anything about that because it’s mostly the same as the Arduino stuff but on a different chip. I couldn’t get global variables to work either, unfortunately :frowning: Only some constants like strings and such, because they lower somewhat nicely to LLVM module constants. Even this required custom passes over the IR though, to essentially “patch out” dynamic calls into the runtime.

1 Like