Skip to content

Commit 0630183

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 64120d6 commit 0630183

File tree

15 files changed

+1158
-16
lines changed

15 files changed

+1158
-16
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,19 @@ jobs:
526526
ulimit -c unlimited
527527
./tests/test-heap
528528
529+
- name: "Test: test-jit_stream_flash with valgrind"
530+
if: matrix.library-arch == ''
531+
working-directory: build
532+
run: |
533+
ulimit -c unlimited
534+
valgrind --error-exitcode=1 ./tests/test-jit_stream_flash
535+
536+
- name: "Test: test-jit_stream_flash"
537+
working-directory: build
538+
run: |
539+
ulimit -c unlimited
540+
./tests/test-jit_stream_flash
541+
529542
- name: "Test: test-mailbox with valgrind"
530543
if: matrix.library-arch == ''
531544
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).

src/libAtomVM/module.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,13 @@ Module *module_new_from_iff_binary(GlobalContext *global, const void *iff_binary
353353
fprintf(stderr, "Native code chunk found but no compatible architecture or variant found\n");
354354
}
355355
}
356+
} else {
357+
ModuleNativeEntryPoint module_entry_point;
358+
uint32_t labels;
359+
uint16_t version;
360+
if (sys_get_cache_native_code(global, mod, &version, &module_entry_point, &labels) && version == JIT_FORMAT_VERSION) {
361+
module_set_native_code(mod, labels, module_entry_point);
362+
}
356363
}
357364
#endif
358365

src/libAtomVM/nifs.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5633,6 +5633,8 @@ static term nif_code_server_set_native_code(Context *ctx, int argc, term argv[])
56335633
VALIDATE_VALUE(argv[0], term_is_atom);
56345634
VALIDATE_VALUE(argv[1], term_is_integer);
56355635

5636+
avm_int_t labels_count = term_to_int(argv[1]);
5637+
56365638
term module_name = argv[0];
56375639
Module *mod = globalcontext_get_module(ctx->global, term_to_atom_index(module_name));
56385640
if (IS_NULL_PTR(mod)) {
@@ -5646,10 +5648,12 @@ static term nif_code_server_set_native_code(Context *ctx, int argc, term argv[])
56465648

56475649
SMP_MODULE_LOCK(mod);
56485650
if (mod->native_code == NULL) {
5649-
module_set_native_code(mod, term_to_int(argv[1]), entry_point);
5651+
module_set_native_code(mod, labels_count, entry_point);
56505652
}
56515653
SMP_MODULE_UNLOCK(mod);
56525654

5655+
sys_set_cache_native_code(ctx->global, mod, JIT_FORMAT_VERSION, entry_point, labels_count);
5656+
56535657
return OK_ATOM;
56545658
}
56555659
#endif

src/libAtomVM/sys.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,33 @@ void sys_free_platform(GlobalContext *global);
296296
*/
297297
ModuleNativeEntryPoint sys_map_native_code(const uint8_t *native_code, size_t size, size_t offset);
298298

299+
/**
300+
* @brief Get the cache (typically on flash) of native code for a given module
301+
*
302+
* @details If module is found in cache, return a pointer to the entry point.
303+
* Only implemented on platforms with JIT. Implementations on flash typically
304+
* check if the jit cache is valid (for lib or for app) and use the pointer to
305+
* code as a key.
306+
* @param global the global context
307+
* @param mod module to return the cache native code for
308+
* @param version version of the cache entry (for compatibility with the VM)
309+
* @param entry_point entry point to the module, if found
310+
* @param labels number of labels
311+
* @return \c true if the cache entry was found
312+
*/
313+
bool sys_get_cache_native_code(GlobalContext *global, Module *mod, uint16_t *version, ModuleNativeEntryPoint *entry_point, uint32_t *labels);
314+
315+
/**
316+
* @brief Add native code to cache for a given module
317+
*
318+
* @param global the global context
319+
* @param mod module to add the native code for
320+
* @param version version of the native code
321+
* @param entry_point entry point to the module
322+
* @param labels number of labels
323+
*/
324+
void sys_set_cache_native_code(GlobalContext *global, Module *mod, uint16_t version, ModuleNativeEntryPoint entry_point, uint32_t labels);
325+
299326
#ifdef __cplusplus
300327
}
301328
#endif

src/platforms/esp32/partitions.csv

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,5 @@
77
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
88
nvs, data, nvs, 0x9000, 0x6000,
99
phy_init, data, phy, 0xf000, 0x1000,
10-
factory, app, factory, 0x10000, 0x1C0000,
11-
boot.avm, data, phy, 0x1D0000, 0x40000,
12-
main.avm, data, phy, 0x210000, 0x100000
10+
factory, app, factory, 0x10000, 0x160000,
11+
main.avm, data, phy, 0x170000, 0x290000,

src/platforms/generic_unix/lib/sys.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,4 +853,24 @@ ModuleNativeEntryPoint sys_map_native_code(const uint8_t *native_code, size_t si
853853
return (ModuleNativeEntryPoint) (native_code + offset);
854854
#endif
855855
}
856+
857+
bool sys_get_cache_native_code(GlobalContext *global, Module *mod, uint16_t *version, ModuleNativeEntryPoint *entry_point, uint32_t *labels)
858+
{
859+
UNUSED(global);
860+
UNUSED(mod);
861+
UNUSED(version);
862+
UNUSED(entry_point);
863+
UNUSED(labels);
864+
return false;
865+
}
866+
867+
void sys_set_cache_native_code(GlobalContext *global, Module *mod, uint16_t version, ModuleNativeEntryPoint entry_point, uint32_t labels)
868+
{
869+
UNUSED(global);
870+
UNUSED(mod);
871+
UNUSED(version);
872+
UNUSED(entry_point);
873+
UNUSED(labels);
874+
}
875+
856876
#endif

src/platforms/rp2/src/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ else()
5555
target_compile_definitions(AtomVM PRIVATE PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS=20000)
5656
endif()
5757

58+
if (AVM_DISABLE_SMP)
59+
target_compile_definitions(AtomVM PRIVATE PICO_FLASH_ASSUME_CORE1_SAFE)
60+
endif()
61+
5862
if (AVM_WAIT_BOOTSEL_ON_EXIT)
5963
target_compile_definitions(AtomVM PRIVATE WAIT_BOOTSEL_ON_EXIT)
6064
endif()

src/platforms/rp2/src/lib/CMakeLists.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ set(HEADER_FILES
3131

3232
set(SOURCE_FILES
3333
gpiodriver.c
34-
jit_stream_flash.c
3534
networkdriver.c
3635
otp_crypto_platform.c
3736
platform_defaultatoms.c
@@ -110,4 +109,16 @@ if (PICO_CYW43_SUPPORTED)
110109
define_if_function_exists(libAtomVM${PLATFORM_LIB_SUFFIX} gethostname "unistd.h" PRIVATE HAVE_GETHOSTNAME)
111110
endif()
112111

112+
if (NOT AVM_DISABLE_JIT)
113+
target_sources(
114+
libAtomVM${PLATFORM_LIB_SUFFIX}
115+
PRIVATE
116+
jit_stream_flash_platform.c
117+
../../../../libAtomVM/jit_stream_flash.c
118+
jit_stream_flash_platform.h
119+
../../../../libAtomVM/jit_stream_flash.h
120+
)
121+
target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,jit_stream_flash_get_nif")
122+
endif()
123+
113124
target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif -Wl,-u -Wl,otp_crypto_nif")
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* This file is part of AtomVM.
3+
*
4+
* Copyright 2025 by 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+
#ifndef AVM_NO_JIT
22+
23+
#include "jit_stream_flash.h"
24+
25+
#include <hardware/flash.h>
26+
#include <pico/flash.h>
27+
#include <stdio.h>
28+
#include <stdlib.h>
29+
30+
#include "rp2_sys.h"
31+
32+
// Helper structures for flash_safe_execute
33+
struct EraseParams
34+
{
35+
uintptr_t addr;
36+
};
37+
38+
struct WriteParams
39+
{
40+
uintptr_t addr;
41+
const uint8_t *data;
42+
size_t len;
43+
};
44+
45+
static void __not_in_flash_func(do_erase_sector)(void *params_ptr)
46+
{
47+
struct EraseParams *params = (struct EraseParams *) params_ptr;
48+
flash_range_erase(params->addr - XIP_BASE, FLASH_SECTOR_SIZE);
49+
}
50+
51+
static void __not_in_flash_func(do_write_page)(void *params_ptr)
52+
{
53+
struct WriteParams *params = (struct WriteParams *) params_ptr;
54+
flash_range_program(params->addr - XIP_BASE, params->data, params->len);
55+
}
56+
57+
struct JSFlashPlatformContext *jit_stream_flash_platform_init(void)
58+
{
59+
return (struct JSFlashPlatformContext *) 1;
60+
}
61+
62+
void jit_stream_flash_platform_destroy(struct JSFlashPlatformContext *pf_ctx)
63+
{
64+
UNUSED(pf_ctx);
65+
}
66+
67+
bool jit_stream_flash_platform_erase_sector(struct JSFlashPlatformContext *pf_ctx, uintptr_t addr)
68+
{
69+
UNUSED(pf_ctx);
70+
71+
struct EraseParams params = {
72+
.addr = addr
73+
};
74+
75+
int r = flash_safe_execute(do_erase_sector, &params, UINT32_MAX);
76+
if (UNLIKELY(r != PICO_OK)) {
77+
fprintf(stderr, "flash_safe_execute (erase) failed with error %d\n", r);
78+
return false;
79+
}
80+
81+
return true;
82+
}
83+
84+
bool jit_stream_flash_platform_write_page(struct JSFlashPlatformContext *pf_ctx, uintptr_t addr, const uint8_t *data)
85+
{
86+
UNUSED(pf_ctx);
87+
88+
struct WriteParams params = {
89+
.addr = addr,
90+
.data = data,
91+
.len = FLASH_PAGE_SIZE
92+
};
93+
94+
int r = flash_safe_execute(do_write_page, &params, UINT32_MAX);
95+
if (UNLIKELY(r != PICO_OK)) {
96+
fprintf(stderr, "flash_safe_execute (write) failed with error %d\n", r);
97+
return false;
98+
}
99+
100+
return true;
101+
}
102+
103+
uintptr_t jit_stream_flash_platform_ptr_to_executable(uintptr_t addr)
104+
{
105+
// Set Thumb bit
106+
return addr | 0x1;
107+
}
108+
109+
uintptr_t jit_stream_flash_platform_executable_to_ptr(uintptr_t addr)
110+
{
111+
// Clear Thumb bit
112+
return addr & ~0x1UL;
113+
}
114+
115+
REGISTER_NIF_COLLECTION(jit_stream_flash, jit_stream_flash_init, NULL, jit_stream_flash_get_nif)
116+
117+
#endif // AVM_NO_JIT

0 commit comments

Comments
 (0)