Skip to content

Commit 516052f

Browse files
committed
pico: add support for flash-based JIT stream
Introduce jit_stream_flash.c common implementation that leverages (common) flash behavior that can be written from 1 to 0. Signed-off-by: Paul Guyot <pguyot@kallisys.net>
1 parent 63570e9 commit 516052f

File tree

18 files changed

+2337
-13
lines changed

18 files changed

+2337
-13
lines changed

.github/workflows/build-and-test.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,19 @@ jobs:
535535
ulimit -c unlimited
536536
./tests/test-heap
537537
538+
- name: "Test: test-jit_stream_flash with valgrind"
539+
if: matrix.library-arch == ''
540+
working-directory: build
541+
run: |
542+
ulimit -c unlimited
543+
valgrind --error-exitcode=1 ./tests/test-jit_stream_flash
544+
545+
- name: "Test: test-jit_stream_flash"
546+
working-directory: build
547+
run: |
548+
ulimit -c unlimited
549+
./tests/test-jit_stream_flash
550+
538551
- name: "Test: test-mailbox with valgrind"
539552
if: matrix.library-arch == ''
540553
working-directory: build

doc/src/atomvm-internals.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Following BEAM, there are two flavors of the emulator: jit and emu, but eventual
137137
- Native: the VM only runs native code and all code must be precompiled on the desktop using the JIT compiler (which effectively is a AOT or Ahead-of-Time compiler). In this mode, it is not necessary to bundle the jit compiler on the embedded target.
138138
- Hybrid: the VM can run native code as well as emulated BEAM code and some code is precompiled on the desktop.
139139

140-
JIT is available on some platforms (currently only x86_64 and aarch64) and compiles Erlang bytecode at runtime. Erlang bytecode is never interpreted. EMU is available on all platforms and Erlang bytecode is interpreted.
140+
JIT is available on some platforms (currently only x86_64, aarch64 and armv6m) and compiles Erlang bytecode at runtime. Erlang bytecode is never interpreted. EMU is available on all platforms and Erlang bytecode is interpreted.
141141

142142
Modules can include precompiled code in a dedicated beam chunk with name 'avmN'. The chunk can contain native code for several architectures, however it may only contain native code for a given version of the native interface. Current version is 1. This native code is executed by the jit-flavor of the emulator as well as the emu flavor if execution of precompiled is enabled.
143143

@@ -158,6 +158,27 @@ A backend implementation is required for each architecture. The backend is calle
158158

159159
A stream implementation is responsible for streaming the machine code, especially in the context of low memory. Two implementations currently exist: `jit_stream_binary` that streams assembly code to an Erlang binary, suitable for tests and precompilation on the desktop, and `jit_stream_mmap` that streams assembly code in an `mmap(2)` allocated page, suitable for JIT compilation on Unix.
160160

161+
### Embedded JIT and Native
162+
163+
On embedded devices, Native mode means the code is precompiled on the desktop and executed natively on the device. This currently works on all ARMv6M devices (Pico and STM32).
164+
165+
The default partition scheme on all platforms is optimized for the Emulated VM which is larger than the JIT or Native VM, and for the Emulated atomvmlib (with no native code for estdlib and no jit library) which is smaller than the JIT atomvmlib (that includes native code for estdlib and jit library).
166+
167+
JIT mode means the Erlang bytecode is compiled to native code directly on the device. This actually is possible on Raspberry Pi Pico by using the flash to store the native code. The first time the code is executed, it is compiled and streamed to flash, and for next runs (including at a future boot), the native code is directly executed.
168+
169+
To achive embedded JIT, it is required to flash the device with the JIT compiler for armv6m which is part of the jit library. This library is quite large, so for Pico boards that come with 2MB of flash, it is required to remove jit modules for other backends. It is also required to change the way code is partitioned.
170+
171+
For example, it is possible to have the following offsets defined in `src/platforms/rp2/src/main.c`:
172+
173+
```
174+
#define LIB_AVM ((void *) 0x10060000)
175+
#define MAIN_AVM ((void *) 0x101B0000)
176+
```
177+
178+
To fit in the lib partition, all networking modules should also be removed (the Pico doesn't have any networking capacity).
179+
180+
After the first run, compiled modules in flash are used unless there is a version mismatch or the application avm or the library avm have been updated on the device. AVM packages end with a section called "end" (0x656E64). When the JIT compiler flashes native code, it changes this name to "END" (0x454E44), by effectively clearing 3 bits in the flash, which is possible without erasing any flash block. Any rewrite of these avm packages will overwrite the section names to "end".
181+
161182
## The Scheduler
162183

163184
In SMP builds, AtomVM runs one scheduler thread per core. Scheduler threads are actually started on demand. The number of scheduler threads can be queried with [`erlang:system_info/1`](./apidocs/erlang/estdlib/erlang.md#system_info1) and be modified with [`erlang:system_flag/2`](./apidocs/erlang/estdlib/erlang.md#system_flag2). All scheduler threads are considered equal and there is no notion of main thread except when shutting down (main thread is shut down last).

libs/jit/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ set(ERLANG_MODULES
2626
jit
2727
jit_precompile
2828
jit_stream_binary
29+
jit_stream_flash
2930
jit_stream_mmap
3031
jit_aarch64
3132
jit_aarch64_asm

libs/jit/src/jit_stream_flash.erl

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
%
2+
% This file is part of AtomVM.
3+
%
4+
% Copyright 2025 Paul Guyot <pguyot@kallisys.net>
5+
%
6+
% Licensed under the Apache License, Version 2.0 (the "License");
7+
% you may not use this file except in compliance with the License.
8+
% You may obtain a copy of the License at
9+
%
10+
% http://www.apache.org/licenses/LICENSE-2.0
11+
%
12+
% Unless required by applicable law or agreed to in writing, software
13+
% distributed under the License is distributed on an "AS IS" BASIS,
14+
% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
% See the License for the specific language governing permissions and
16+
% limitations under the License.
17+
%
18+
% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
19+
%
20+
21+
-module(jit_stream_flash).
22+
23+
% Stream implementation using flash, suitable for MCUs with programmable flash
24+
25+
-export([
26+
new/1,
27+
offset/1,
28+
append/2,
29+
replace/3,
30+
map/4,
31+
flush/1
32+
]).
33+
34+
%% Additional nif
35+
-export([
36+
read/3
37+
]).
38+
39+
-export_type([stream/0]).
40+
41+
-type stream() :: binary().
42+
43+
%%-----------------------------------------------------------------------------
44+
%% @returns An empty binary
45+
%% @param MaxSize maximum size of the stream
46+
%% @doc Create a new stream, i.e. return an empty binary
47+
%% @end
48+
%%-----------------------------------------------------------------------------
49+
-spec new(MaxSize :: pos_integer()) -> stream().
50+
new(_MaxSize) ->
51+
erlang:nif_error(undefined).
52+
53+
%%-----------------------------------------------------------------------------
54+
%% @param Stream stream to get the offset from
55+
%% @returns The current offset
56+
%% @doc Get the current offset in the stream
57+
%% @end
58+
%%-----------------------------------------------------------------------------
59+
-spec offset(stream()) -> non_neg_integer().
60+
offset(_Stream) ->
61+
erlang:nif_error(undefined).
62+
63+
%%-----------------------------------------------------------------------------
64+
%% @param Stream stream to append to
65+
%% @param Binary binary to append to the stream
66+
%% @returns The updated stream
67+
%% @doc Append a binary to the stream
68+
%% @end
69+
%%-----------------------------------------------------------------------------
70+
-spec append(stream(), binary()) -> stream().
71+
append(_Stream, _Binary) ->
72+
erlang:nif_error(undefined).
73+
74+
%%-----------------------------------------------------------------------------
75+
%% @param Stream stream to update
76+
%% @param Offset offset to update from
77+
%% @param Replacement binary to write at offset
78+
%% @returns The updated stream
79+
%% @doc Replace bytes at a given offset
80+
%% @end
81+
%%-----------------------------------------------------------------------------
82+
-spec replace(stream(), non_neg_integer(), binary()) -> stream().
83+
replace(_Stream, _Offset, _Replacement) ->
84+
erlang:nif_error(undefined).
85+
86+
%%-----------------------------------------------------------------------------
87+
%% @param Stream stream to update
88+
%% @param Offset offset to update from
89+
%% @param Length length of the section to update
90+
%% @param MapFunction function that updates the binary
91+
%% @returns The updated stream
92+
%% @doc Replace bytes at a given offset calling a map function
93+
%% @end
94+
%%-----------------------------------------------------------------------------
95+
-spec map(stream(), non_neg_integer(), pos_integer(), fun((binary()) -> binary())) -> stream().
96+
map(Stream, Offset, Length, MapFunction) ->
97+
Binary = ?MODULE:read(Stream, Offset, Length),
98+
Mapped = MapFunction(Binary),
99+
?MODULE:replace(Stream, Offset, Mapped).
100+
101+
%%-----------------------------------------------------------------------------
102+
%% @param Stream stream to read from
103+
%% @param Offset offset to read from
104+
%% @param Length number of bytes to read
105+
%% @returns The binary data read from the stream
106+
%% @doc Read bytes at a given offset
107+
%%
108+
%% @end
109+
%%-----------------------------------------------------------------------------
110+
-spec read(stream(), non_neg_integer(), pos_integer()) -> binary().
111+
read(_Stream, _Offset, _Length) ->
112+
erlang:nif_error(undefined).
113+
114+
%%-----------------------------------------------------------------------------
115+
%% @param Stream stream to flush
116+
%% @returns The stream flushed
117+
%% @doc Flush the stream. Typically invalidates instruction cache.
118+
%%
119+
%% @end
120+
%%-----------------------------------------------------------------------------
121+
-spec flush(stream()) -> stream().
122+
flush(_Stream) ->
123+
erlang:nif_error(undefined).

0 commit comments

Comments
 (0)