diff --git a/CMakeLists.txt b/CMakeLists.txt
index 19405d42..bb661f9f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -152,6 +152,16 @@ get_filename_component(ProjectId ${CMAKE_CURRENT_LIST_DIR} NAME)
string(REPLACE " " "_" ProjectId ${ProjectId})
project(${ProjectId})
+# Fixed-address section for the native library C interface table. The address
+# is target-specific and must match VESC_IF in main/c_libs/vesc_c_if.h.
+idf_build_get_property(LIBIF_IDF_TARGET IDF_TARGET)
+set(LIBIF_LD "${CMAKE_CURRENT_SOURCE_DIR}/main/linker_libif_${LIBIF_IDF_TARGET}.ld")
+if(NOT EXISTS ${LIBIF_LD})
+ message(FATAL_ERROR "No native lib interface linker script for target ${LIBIF_IDF_TARGET} (expected ${LIBIF_LD})")
+endif()
+target_link_libraries(${PROJECT_NAME}.elf PRIVATE "-T${LIBIF_LD}")
+set_property(TARGET ${PROJECT_NAME}.elf APPEND PROPERTY LINK_DEPENDS ${LIBIF_LD})
+
include(cmake/git_rev_parse.cmake)
git_describe(GIT_COMMIT_HASH ".")
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index df8c14e5..b5faf49c 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -29,6 +29,7 @@ set(COMPONENT_SRCS
"hwconf/hw.c"
"lispif.c"
+"lispif_c_lib.c"
"lispif_events.c"
"lbm_vesc_utils.c"
"lispif_vesc_extensions.c"
diff --git a/main/c_libs/vesc_c_if.h b/main/c_libs/vesc_c_if.h
new file mode 100644
index 00000000..c045630d
--- /dev/null
+++ b/main/c_libs/vesc_c_if.h
@@ -0,0 +1,277 @@
+/*
+ Copyright 2026 Benjamin Vedder benjamin@vedder.se
+
+ This file is part of the VESC firmware.
+
+ The VESC firmware is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ The VESC firmware is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+/*
+ * Native library C interface for the VESC Express.
+ *
+ * This is a NEW interface, separate from the bldc (STM32 motor controller)
+ * vesc_c_if. It starts from the platform-neutral core - LispBM access,
+ * threads, timing, mutexes/semaphores, memory and printf - and contains no
+ * motor, CAN or peripheral functions. More slots will be appended over time.
+ *
+ * Compatibility rules:
+ * - New function pointers are ONLY ever added at the END of the struct, so
+ * libs built against older headers keep working on newer firmware. Added
+ * slots are null-pointers on firmware that predates them - a lib that
+ * wants to run on older firmware can check them for NULL.
+ * - VESC_C_IF_VERSION is bumped ONLY on a breaking layout change. INIT_START
+ * compares it against the firmware's table (first slot, read before any
+ * function in the table is called) and fails the lib init on mismatch.
+ */
+
+#ifndef VESC_C_IF_H
+#define VESC_C_IF_H
+
+#include
+#include
+#include
+
+// Layout version of the vesc_c_if struct below. Only bumped on breaking
+// changes; appended slots do NOT bump it.
+#define VESC_C_IF_VERSION 1
+
+#define NATIVE_LIB_MAGIC 0xCAFEBABE
+
+// Container magic for relocatable libs that the firmware copies into RAM
+// and patches at load time. Used on targets that cannot run position-
+// independent code in place (ESP32-S3 / Xtensa). Layout: magic,
+// version, code_size, data_size, entry_offset, reloc_count, relocs[],
+// code[], data[].
+#define NATIVE_LIB_RELOC_MAGIC 0xCAFEBABF
+
+// System tick, as returned by system_time_ticks
+typedef uint32_t systime_t;
+
+#ifdef IS_VESC_LIB
+// LBM types, provided by lispbm.h when compiling the firmware itself.
+typedef uint32_t lbm_value;
+typedef uint32_t lbm_type;
+typedef uint32_t lbm_cid;
+
+typedef uint32_t lbm_uint;
+typedef int32_t lbm_int;
+typedef float lbm_float;
+
+typedef struct {
+ uint8_t *buf;
+ lbm_uint buf_size;
+ lbm_uint buf_pos;
+} lbm_flat_value_t;
+
+typedef struct {
+ lbm_uint size; /// Number of elements
+ lbm_uint *data; /// pointer to lbm_memory array or C array.
+} lbm_array_header_t;
+
+typedef lbm_value (*extension_fptr)(lbm_value*,lbm_uint);
+
+// For double precision literals
+#define D(x) ((double)x##L)
+#endif
+
+typedef bool (*load_extension_fptr)(char*,extension_fptr);
+
+typedef void* lib_thread;
+typedef void* lib_mutex;
+typedef void* lib_semaphore;
+
+/*
+ * Function pointer struct. Always add new function pointers to the end in
+ * order to not break compatibility with old binaries. If a function is not
+ * available (e.g. in an old firmware) it will be a null-pointer.
+ */
+typedef struct {
+ // Interface layout version, always the first slot. Read by INIT_START
+ // before anything else in the table is used.
+ uint32_t if_version;
+
+ // LBM: extensions, symbols and errors
+ load_extension_fptr lbm_add_extension;
+ int (*lbm_set_error_reason)(char *str);
+ int (*lbm_add_symbol_const)(const char *, lbm_uint *);
+ int (*lbm_get_symbol_by_name)(const char *name, lbm_uint* id);
+
+ // LBM: evaluator control and messaging
+ void (*lbm_block_ctx_from_extension)(void);
+ bool (*lbm_unblock_ctx)(lbm_cid, lbm_flat_value_t*);
+ bool (*lbm_unblock_ctx_unboxed)(lbm_cid cid, lbm_value unboxed);
+ lbm_cid (*lbm_get_current_cid)(void);
+ int (*lbm_send_message)(lbm_cid cid, lbm_value msg);
+ void (*lbm_pause_eval_with_gc)(uint32_t num_free);
+ void (*lbm_continue_eval)(void);
+ bool (*lbm_eval_is_paused)(void);
+
+ // LBM: heap and values
+ lbm_value (*lbm_cons)(lbm_value car, lbm_value cdr);
+ lbm_value (*lbm_car)(lbm_value val);
+ lbm_value (*lbm_cdr)(lbm_value val);
+ lbm_value (*lbm_list_destructive_reverse)(lbm_value list);
+ bool (*lbm_create_byte_array)(lbm_value *value, lbm_uint num_elt);
+
+ // LBM: encoding
+ lbm_value (*lbm_enc_i)(lbm_int x);
+ lbm_value (*lbm_enc_u)(lbm_uint x);
+ lbm_value (*lbm_enc_char)(uint8_t x);
+ lbm_value (*lbm_enc_float)(float f);
+ lbm_value (*lbm_enc_u32)(uint32_t u);
+ lbm_value (*lbm_enc_i32)(int32_t i);
+ lbm_value (*lbm_enc_sym)(lbm_uint s);
+
+ // LBM: decoding
+ float (*lbm_dec_as_float)(lbm_value val);
+ uint32_t (*lbm_dec_as_u32)(lbm_value val);
+ int32_t (*lbm_dec_as_i32)(lbm_value val);
+ uint8_t (*lbm_dec_char)(lbm_value x);
+ char* (*lbm_dec_str)(lbm_value);
+ lbm_uint (*lbm_dec_sym)(lbm_value x);
+
+ // LBM: type checks
+ bool (*lbm_is_byte_array)(lbm_value val);
+ bool (*lbm_is_cons)(lbm_value x);
+ bool (*lbm_is_number)(lbm_value x);
+ bool (*lbm_is_char)(lbm_value x);
+ bool (*lbm_is_symbol)(lbm_value x);
+ bool (*lbm_is_symbol_nil)(lbm_uint);
+ bool (*lbm_is_symbol_true)(lbm_uint);
+
+ // LBM: symbol constants
+ lbm_uint lbm_enc_sym_nil;
+ lbm_uint lbm_enc_sym_true;
+ lbm_uint lbm_enc_sym_terror;
+ lbm_uint lbm_enc_sym_eerror;
+ lbm_uint lbm_enc_sym_merror;
+
+ // LBM: flat values
+ bool (*lbm_start_flatten)(lbm_flat_value_t *v, size_t buffer_size);
+ bool (*lbm_finish_flatten)(lbm_flat_value_t *v);
+ bool (*f_cons)(lbm_flat_value_t *v);
+ bool (*f_sym)(lbm_flat_value_t *v, lbm_uint sym);
+ bool (*f_i)(lbm_flat_value_t *v, lbm_int i);
+ bool (*f_b)(lbm_flat_value_t *v, uint8_t b);
+ bool (*f_i32)(lbm_flat_value_t *v, int32_t w);
+ bool (*f_u32)(lbm_flat_value_t *v, uint32_t w);
+ bool (*f_float)(lbm_flat_value_t *v, float f);
+ bool (*f_i64)(lbm_flat_value_t *v, int64_t w);
+ bool (*f_u64)(lbm_flat_value_t *v, uint64_t w);
+ bool (*f_lbm_array)(lbm_flat_value_t *v, uint32_t num_elts, uint8_t *data);
+
+ // Os: time and sleep
+ void (*sleep_ms)(uint32_t ms);
+ void (*sleep_us)(uint32_t us);
+ float (*system_time)(void); // Time since boot in seconds
+ float (*ts_to_age_s)(systime_t ts); // Age of timestamp in seconds
+ // Time since boot in system ticks (see SYSTEM_TICK_RATE_HZ). Use
+ // ts_to_age_s to get the age of a timestamp in seconds; it handles
+ // overflows.
+ systime_t (*system_time_ticks)(void);
+ void (*sleep_ticks)(systime_t ticks);
+ // High resolution timer for short busy-wait sleeps and time
+ // measurement, in microseconds.
+ uint32_t (*timer_time_now)(void);
+ float (*timer_seconds_elapsed_since)(uint32_t time);
+ void (*timer_sleep)(float seconds);
+
+ // Os: memory and IO
+ int (*printf)(const char *str, ...);
+ void* (*malloc)(size_t bytes);
+ void (*free)(void *ptr);
+
+ // Os: threads
+ lib_thread (*spawn)(void (*fun)(void *arg), size_t stack_size, const char *name, void *arg);
+ void (*request_terminate)(lib_thread thd);
+ bool (*should_terminate)(void);
+ // Set priority of current thread.
+ // Range: -5 to 5, -5 is lowest, 0 is normal, 5 is highest
+ void (*thread_set_priority)(int priority);
+ void** (*get_arg)(uint32_t prog_addr);
+
+ // Os: mutex
+ lib_mutex (*mutex_create)(void); // Use VESC_IF->free on the mutex when done with it
+ void (*mutex_lock)(lib_mutex);
+ void (*mutex_unlock)(lib_mutex);
+
+ // Os: semaphore
+ lib_semaphore (*sem_create)(void); // Use VESC_IF->free on the semaphore when done with it
+ void (*sem_wait)(lib_semaphore);
+ void (*sem_signal)(lib_semaphore);
+ bool (*sem_wait_to)(lib_semaphore, systime_t); // Returns false on timeout
+ void (*sem_reset)(lib_semaphore);
+} vesc_c_if;
+
+typedef struct {
+ void (*stop_fun)(void *arg);
+ void *arg;
+ uint32_t base_addr;
+} lib_info;
+
+// System tick rate. Can be used to convert system ticks to time
+#define SYSTEM_TICK_RATE_HZ 1000
+
+/*
+ * Address of the firmware-side C interface table. It must match the address
+ * of the .libif section in main/linker_libif_.ld for the target the
+ * firmware (and the native library) is built for.
+ *
+ * The firmware build picks the target up from sdkconfig automatically. When
+ * building a native library out of tree, define the CONFIG_IDF_TARGET_*
+ * macro matching the hardware you are building for, e.g.
+ * -DCONFIG_IDF_TARGET_ESP32C3=1. A library only works on the target it was
+ * built for.
+ */
+#if defined(__has_include)
+#if __has_include("sdkconfig.h")
+#include "sdkconfig.h"
+#endif
+#endif
+
+#if defined(CONFIG_IDF_TARGET_ESP32C3)
+#define VESC_IF ((vesc_c_if*)(0x3FCDBE00))
+#elif defined(CONFIG_IDF_TARGET_ESP32S3)
+#define VESC_IF ((vesc_c_if*)(0x3FCE8800))
+#elif defined(CONFIG_IDF_TARGET_ESP32C6)
+#define VESC_IF ((vesc_c_if*)(0x4087B800))
+#elif defined(CONFIG_IDF_TARGET_ESP32P4)
+#define VESC_IF ((vesc_c_if*)(0x4FF3A000))
+#else
+#error "Unknown ESP target. Define CONFIG_IDF_TARGET_ESP32C3, -S3, -C6 or -P4 when building a native library."
+#endif
+
+// Put this at the beginning of your source file
+#define HEADER volatile int __attribute__((__section__(".program_ptr"))) prog_ptr;
+
+// Init function
+#define INIT_FUN bool __attribute__((__section__(".init_fun"))) init
+
+// Put this at the start of the init function. The version check reads the
+// first table slot only - safe even when the firmware carries a different
+// interface layout.
+#define INIT_START (void)prog_ptr; \
+ if (VESC_IF->if_version != VESC_C_IF_VERSION) { \
+ return false; \
+ }
+
+// Address of this program in memory
+#define PROG_ADDR ((uint32_t)&prog_ptr)
+
+// The argument that was set in the init function (same as the one you get in stop_fun)
+#define ARG (*VESC_IF->get_arg(PROG_ADDR))
+
+extern volatile int prog_ptr;
+
+#endif // VESC_C_IF_H
diff --git a/main/linker_libif_esp32c3.ld b/main/linker_libif_esp32c3.ld
new file mode 100644
index 00000000..6e69ce08
--- /dev/null
+++ b/main/linker_libif_esp32c3.ld
@@ -0,0 +1,24 @@
+/* Places the native library C interface table (the .libif section, defined in
+ main/lispif_c_lib.c) at a fixed address so that
+ out-of-tree native libraries can find it through the VESC_IF macro in
+ main/c_libs/vesc_c_if.h.
+
+ The address here must match VESC_IF for this target, and the block is
+ excluded from the heap with SOC_RESERVE_MEMORY_REGION() in vesc_c_if.c.
+
+ ESP32-C3: top of the 0x3FCC0000 D/IRAM region, directly below the
+ ROM-reserved area (APP_USABLE_DRAM_END = 0x3FCDC710). Placing it at the
+ very top keeps the main DRAM heap region contiguous - a block in the
+ middle would split it and make large allocations (e.g. the LispBM
+ memory pool) fail even with plenty of total free heap. */
+
+SECTIONS
+{
+ .libif 0x3FCDBE00 (NOLOAD) :
+ {
+ _libif_start = ABSOLUTE(.);
+ KEEP(*(.libif))
+ . = ALIGN(4);
+ _libif_end = ABSOLUTE(.);
+ }
+}
diff --git a/main/linker_libif_esp32c6.ld b/main/linker_libif_esp32c6.ld
new file mode 100644
index 00000000..bbd15d42
--- /dev/null
+++ b/main/linker_libif_esp32c6.ld
@@ -0,0 +1,21 @@
+/* Places the native library C interface table (the .libif section, defined in
+ main/lispif_c_lib.c) at a fixed address so that
+ out-of-tree native libraries can find it through the VESC_IF macro in
+ main/c_libs/vesc_c_if.h.
+
+ The address here must match VESC_IF for this target, and the block is
+ excluded from the heap with SOC_RESERVE_MEMORY_REGION() in vesc_c_if.c.
+
+ ESP32-C6: top of the 0x40860000 HP SRAM region, below the ROM-reserved
+ area (APP_USABLE_DRAM_END = 0x4087C610). */
+
+SECTIONS
+{
+ .libif 0x4087B800 (NOLOAD) :
+ {
+ _libif_start = ABSOLUTE(.);
+ KEEP(*(.libif))
+ . = ALIGN(4);
+ _libif_end = ABSOLUTE(.);
+ }
+}
diff --git a/main/linker_libif_esp32p4.ld b/main/linker_libif_esp32p4.ld
new file mode 100644
index 00000000..906af2ac
--- /dev/null
+++ b/main/linker_libif_esp32p4.ld
@@ -0,0 +1,22 @@
+/* Places the native library C interface table (the .libif section, defined in
+ main/lispif_c_lib.c) at a fixed address so that
+ out-of-tree native libraries can find it through the VESC_IF macro in
+ main/c_libs/vesc_c_if.h.
+
+ The address here must match VESC_IF for this target, and the block is
+ excluded from the heap with SOC_RESERVE_MEMORY_REGION() in vesc_c_if.c.
+
+ ESP32-P4: below the lowest usable-DRAM end across chip revisions
+ (APP_USABLE_DIRAM_END = 0x4FF3AFC0 for rev < 3), so the same address
+ works on every revision. */
+
+SECTIONS
+{
+ .libif 0x4FF3A000 (NOLOAD) :
+ {
+ _libif_start = ABSOLUTE(.);
+ KEEP(*(.libif))
+ . = ALIGN(4);
+ _libif_end = ABSOLUTE(.);
+ }
+}
diff --git a/main/linker_libif_esp32s3.ld b/main/linker_libif_esp32s3.ld
new file mode 100644
index 00000000..bd877d20
--- /dev/null
+++ b/main/linker_libif_esp32s3.ld
@@ -0,0 +1,21 @@
+/* Places the native library C interface table (the .libif section, defined in
+ main/lispif_c_lib.c) at a fixed address so that
+ out-of-tree native libraries can find it through the VESC_IF macro in
+ main/c_libs/vesc_c_if.h.
+
+ The address here must match VESC_IF for this target, and the block is
+ excluded from the heap with SOC_RESERVE_MEMORY_REGION() in vesc_c_if.c.
+
+ ESP32-S3: top of the 0x3FCE0000 D/IRAM region, below the ROM-reserved
+ area (APP_USABLE_DRAM_END = 0x3FCE9710). */
+
+SECTIONS
+{
+ .libif 0x3FCE8800 (NOLOAD) :
+ {
+ _libif_start = ABSOLUTE(.);
+ KEEP(*(.libif))
+ . = ALIGN(4);
+ _libif_end = ABSOLUTE(.);
+ }
+}
diff --git a/main/lispif.c b/main/lispif.c
index 6ad77caf..c3deeff4 100644
--- a/main/lispif.c
+++ b/main/lispif.c
@@ -42,7 +42,7 @@
#define EXTENSION_STORAGE_SIZE 364
#endif
#ifndef USER_EXTENSION_STORAGE_SIZE
-#define USER_EXTENSION_STORAGE_SIZE 0
+#define USER_EXTENSION_STORAGE_SIZE 128
#endif
#define PROF_DATA_NUM 30
#define EXT_LOAD_CALLBACK_LEN 10
@@ -777,6 +777,8 @@ void lispif_stop(void) {
return;
}
+ lispif_stop_lib();
+
lispif_lock_lbm();
lbm_kill_eval();
@@ -932,9 +934,40 @@ bool lispif_restart(bool print, bool load_code, bool load_imports) {
int32_t offset = buffer_get_int32((uint8_t*)code_data, &ind);
int32_t len = buffer_get_int32((uint8_t*)code_data, &ind);
+ // Bounds check first
+ if (offset < 0 || len < 0
+ || (int64_t)offset + len > (int64_t)code_len) {
+ continue;
+ }
+
lbm_value val;
- if (lbm_share_array_const(&val, code_data + offset, len)) {
- lbm_define(name, val);
+ bool handled = false;
+
+ // Try native lib path if it's big enough to have a header
+ if (len > 12) {
+ uint32_t magic_be = 0;
+ memcpy(
+ &magic_be, (const uint8_t *)code_data + offset,
+ sizeof(magic_be)
+ );
+
+ if (magic_be == __builtin_bswap32(NATIVE_LIB_MAGIC) ||
+ magic_be == __builtin_bswap32(NATIVE_LIB_RELOC_MAGIC)) {
+ uint8_t *irom_base =
+ (uint8_t *)utils_drom_to_irom(code_data)
+ + offset;
+
+ lbm_value lib_addr = lbm_enc_u32((uint32_t)irom_base);
+ lbm_define(name, lib_addr);
+ handled = true;
+ }
+ }
+
+ // Fallback: normal Lisp import
+ if (!handled) {
+ if (lbm_share_array_const(&val, code_data + offset, len)) {
+ lbm_define(name, val);
+ }
}
}
}
diff --git a/main/lispif.h b/main/lispif.h
index fe59b264..6584ca1f 100644
--- a/main/lispif.h
+++ b/main/lispif.h
@@ -26,12 +26,16 @@
#include
#include "lispbm.h"
+#define NATIVE_LIB_MAGIC 0xCAFEBABE
+#define NATIVE_LIB_RELOC_MAGIC 0xCAFEBABF
+
// Functions
void lispif_init(void);
int lispif_get_restart_cnt(void);
void lispif_lock_lbm(void);
void lispif_unlock_lbm(void);
void lispif_stop(void);
+void lispif_stop_lib(void);
bool lispif_restart(bool print, bool load_code, bool load_imports);
void lispif_disable_all_events(void);
void lispif_free(void *ptr);
diff --git a/main/lispif_c_lib.c b/main/lispif_c_lib.c
new file mode 100644
index 00000000..a8e3dc22
--- /dev/null
+++ b/main/lispif_c_lib.c
@@ -0,0 +1,786 @@
+/*
+ Copyright 2022‑2026 Benjamin Vedder
+ Copyright 2022 Joel Svensson
+
+ This file is part of the VESC firmware and is released under the
+ terms of the GNU General Public License, version 3 (or any later).
+
+ -----------------------------------------------------------------
+ FreeRTOS / ESP‑IDF PORT
+ -----------------------------------------------------------------
+ All ChibiOS threading primitives have been replaced with their
+ FreeRTOS counterparts so that `(spawn …)` and related Lisp helpers
+ work unmodified on ESP32 targets.
+
+ This implements the VESC Express native lib interface (see
+ c_libs/vesc_c_if.h) - a fresh interface separate from the bldc
+ one, starting from the platform-neutral core: LispBM access,
+ threads, timing, mutexes/semaphores, memory and printf. New slots
+ are appended to the struct; VESC_C_IF_VERSION is only bumped on
+ breaking layout changes.
+*/
+
+#pragma GCC optimize("Os")
+#include
+#include
+#include
+#include "esp_timer.h"
+#include "esp_rom_sys.h"
+#include "esp_heap_caps.h"
+#include "esp_memory_utils.h"
+#include "heap_memory_layout.h"
+#include "commands.h"
+#include "extensions.h"
+#include "lbm_flat_value.h"
+#include "lispif.h"
+#include "lispbm.h"
+#include "utils.h"
+#include "c_libs/vesc_c_if.h"
+#include "freertos/task.h"
+
+_Static_assert(sizeof(vesc_c_if) <= 2048, "cif pad too small");
+
+typedef struct {
+ const char *name;
+ void *arg;
+ void (*func)(void*);
+ volatile bool should_terminate;
+ TaskHandle_t handle;
+ UBaseType_t base_prio;
+} lib_thd_info;
+
+#define LIB_MAX_THREADS 20
+static lib_thd_info *lib_thread_infos[LIB_MAX_THREADS] = {0};
+static size_t lib_thread_infos_cnt = 0;
+
+// Optional: protect lib_thread_infos[] edits/reads if accessed from multiple tasks
+static portMUX_TYPE lib_thr_mux = portMUX_INITIALIZER_UNLOCKED;
+#define LIB_THR_LOCK() portENTER_CRITICAL(&lib_thr_mux)
+#define LIB_THR_UNLOCK() portEXIT_CRITICAL(&lib_thr_mux)
+
+#define LIB_NUM_MAX 10
+
+static lib_info loaded_libs[LIB_NUM_MAX] = {0};
+
+// The flash (IROM) address of each loaded lib's container, i.e. the value
+// the lisp code passes to load-native-lib / unload-native-lib. For XIP libs
+// this equals base_addr; for RAM-loaded (relocated) libs it differs.
+static uint32_t lib_flash_addr[LIB_NUM_MAX] = {0};
+
+// Heap allocation backing a RAM-loaded lib, NULL for XIP libs.
+static void *lib_ram_alloc[LIB_NUM_MAX] = {0};
+
+// Second allocation backing a RAM-loaded lib's data region (S3), NULL
+// otherwise.
+static void *lib_ram_data[LIB_NUM_MAX] = {0};
+
+__attribute__((section(".libif"))) static volatile union {
+ vesc_c_if cif;
+ char pad[2048];
+} cif;
+
+// The .libif section is placed at a fixed, target-specific address by
+// main/linker_libif_.ld so that native libs can find the interface
+// table through the VESC_IF macro. Keep the heap allocator away from it.
+SOC_RESERVE_MEMORY_REGION((intptr_t)&cif, (intptr_t)&cif + sizeof(cif), vesc_libif);
+
+static bool lib_init_done = false;
+
+static bool lib_is_func_valid(void *func) {
+ return esp_ptr_executable(func);
+}
+
+static void lib_sleep_ms(uint32_t ms) {
+ vTaskDelay(pdMS_TO_TICKS(ms));
+}
+
+static void lib_sleep_us(uint32_t us) {
+ if (us >= 1000) {
+ vTaskDelay(pdMS_TO_TICKS(us / 1000));
+ us %= 1000;
+ }
+ if (us)
+ esp_rom_delay_us(us);
+}
+
+static float lib_system_time(void) {
+ return UTILS_AGE_S(0);
+}
+
+static float lib_ts_to_age_s(TickType_t ts) {
+ return UTILS_AGE_S(ts);
+}
+
+static void lib_thd(void *arg) {
+ lib_thd_info *t = (lib_thd_info*)arg;
+
+ // Set thread-local storage for should_terminate check
+ vTaskSetThreadLocalStoragePointer(NULL, 0, t);
+
+ t->func(t->arg);
+
+ // Task finished, remove from global tracking
+ for (size_t i = 0; i < lib_thread_infos_cnt; i++) {
+ if (lib_thread_infos[i] == t) {
+ // Shift down remaining entries
+ for (size_t j = i; j < lib_thread_infos_cnt - 1; j++) {
+ lib_thread_infos[j] = lib_thread_infos[j + 1];
+ }
+ lib_thread_infos[--lib_thread_infos_cnt] = NULL;
+ break;
+ }
+ }
+
+ lbm_free(t);
+ vTaskDelete(NULL); // clean self-termination
+}
+
+
+static bool lib_should_terminate(void) {
+ lib_thd_info *info = (lib_thd_info*) pvTaskGetThreadLocalStoragePointer(NULL, 0);
+ return info && info->should_terminate;
+}
+_Static_assert(
+ configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0,
+ "Need ≥1 TLS pointer for lib thread bookkeeping"
+);
+lib_thread lispif_spawn(void (*func)(void*), size_t stack_size, const char *name, void *arg) {
+ if (!lib_is_func_valid(func)) {
+ commands_printf_lisp("Invalid function address. Must be static.");
+ return 0;
+ }
+
+ if (lib_thread_infos_cnt >= LIB_MAX_THREADS) {
+ commands_printf_lisp("Thread limit reached.");
+ return 0;
+ }
+
+ lib_thd_info *info = lbm_malloc_reserve(sizeof(lib_thd_info));
+ if (!info) {
+ commands_printf_lisp("Failed to allocate thread info");
+ return 0;
+ }
+
+ info->arg = arg;
+ info->func = func;
+ info->name = name;
+ info->should_terminate = false;
+
+ TaskHandle_t thd = NULL;
+ BaseType_t result = xTaskCreatePinnedToCore(
+ lib_thd,
+ name ? name : "lib-thd",
+ stack_size,
+ info,
+ tskIDLE_PRIORITY + 5,
+ &thd,
+ tskNO_AFFINITY
+ );
+
+ if (result == pdPASS && thd != NULL) {
+ info->handle = thd;
+ info->base_prio = uxTaskPriorityGet(thd);
+ lib_thread_infos[lib_thread_infos_cnt++] = info;
+ return (lib_thread)thd;
+ } else {
+ commands_printf_lisp("Thread creation failed");
+ lbm_free(info);
+ return 0;
+ }
+}
+
+
+static void lib_request_terminate(lib_thread thd) {
+ TaskHandle_t handle = (TaskHandle_t)thd;
+
+ for (size_t i = 0; i < lib_thread_infos_cnt; i++) {
+ if (lib_thread_infos[i]->handle == handle) {
+ lib_thread_infos[i]->should_terminate = true;
+
+ // Wait for task to self-terminate
+ int timeout = 2000;
+ while (eTaskGetState(handle) != eDeleted && timeout-- > 0) {
+ vTaskDelay(pdMS_TO_TICKS(1));
+ }
+
+ if (timeout <= 0) {
+ commands_printf_lisp("Thread did not exit. Crashing...");
+ vTaskDelay(pdMS_TO_TICKS(20));
+ abort();
+ }
+
+ return;
+ }
+ }
+
+ commands_printf_lisp("Thread handle not found");
+}
+
+
+static inline UBaseType_t clamp_prio(int p) {
+ // ESP-IDF sets FREERTOS_MAX_PRIORITIES via sdkconfig; typical is 25.
+ const UBaseType_t maxp = configMAX_PRIORITIES - 1;
+ const UBaseType_t minp = tskIDLE_PRIORITY + 1;
+ if (p < (int)minp)
+ return minp;
+ if (p > (int)maxp)
+ return maxp;
+ return (UBaseType_t)p;
+}
+
+static void lib_thread_set_priority(int delta /* -5..5 */) {
+ // Find our bookkeeping record for the CURRENT task
+ lib_thd_info *info =
+ (lib_thd_info *)pvTaskGetThreadLocalStoragePointer(NULL, 0);
+ if (!info || info->handle != xTaskGetCurrentTaskHandle()) {
+ lbm_set_error_reason(
+ "thread_set_priority must be called from a lib thread"
+ );
+ return;
+ }
+
+ // Normalize the requested delta into absolute target priority
+ // 0 => baseline, +1 => one level higher than baseline, etc.
+ int target = (int)info->base_prio + delta;
+ UBaseType_t newp = clamp_prio(target);
+
+ vTaskPrioritySet(info->handle, newp);
+}
+
+static void **lib_get_arg(uint32_t prog_addr) {
+ uint32_t p = (uint32_t)utils_drom_to_irom((void *)prog_addr);
+
+ for (int i = 0; i < LIB_NUM_MAX; i++) {
+ uint32_t base = loaded_libs[i].base_addr;
+ if (!base)
+ continue;
+
+ if (p == base + 4u) {
+ return &loaded_libs[i].arg;
+ }
+ }
+ return NULL;
+}
+
+static bool lib_create_byte_array(lbm_value *value, lbm_uint num_elt) {
+ return lbm_heap_allocate_array(value, num_elt);
+}
+
+static bool lib_eval_is_paused(void) {
+ return lbm_get_eval_state() == EVAL_CPS_STATE_PAUSED;
+}
+
+static lib_mutex lib_mutex_create(void) {
+ SemaphoreHandle_t *m = lbm_malloc_reserve(sizeof(SemaphoreHandle_t));
+ if (!m)
+ return NULL;
+ *m = xSemaphoreCreateMutex();
+ if (!*m) {
+ lbm_free(m);
+ return NULL;
+ }
+ return (lib_mutex)m;
+}
+
+static void lib_mutex_lock(lib_mutex m) {
+ xSemaphoreTake(*((SemaphoreHandle_t *)m), portMAX_DELAY);
+}
+
+static void lib_mutex_unlock(lib_mutex m) {
+ xSemaphoreGive(*((SemaphoreHandle_t *)m));
+}
+
+static lib_semaphore lib_sem_create(void) {
+ SemaphoreHandle_t *s = lbm_malloc_reserve(sizeof(SemaphoreHandle_t));
+ if (!s)
+ return NULL;
+ *s = xSemaphoreCreateCounting(0xFFFF, 0);
+ if (!*s) {
+ lbm_free(s);
+ return NULL;
+ }
+ return (lib_semaphore)s;
+}
+
+static void lib_sem_wait(lib_semaphore s) {
+ xSemaphoreTake(*((SemaphoreHandle_t *)s), portMAX_DELAY);
+}
+
+static void lib_sem_signal(lib_semaphore s) {
+ xSemaphoreGive(*((SemaphoreHandle_t *)s));
+}
+
+static bool lib_sem_wait_to(lib_semaphore s, TickType_t timeout_ticks) {
+ return xSemaphoreTake(*((SemaphoreHandle_t *)s), timeout_ticks) == pdPASS;
+}
+
+static void lib_sem_reset(lib_semaphore s) {
+ SemaphoreHandle_t h = *((SemaphoreHandle_t *)s);
+ while (xSemaphoreTake(h, 0) == pdPASS) { /* drain */
+ }
+}
+
+static bool lib_add_extension(char *sym_str, extension_fptr ext) {
+ if (sym_str[0] != 'e' || sym_str[1] != 'x' || sym_str[2] != 't'
+ || sym_str[3] != '-') {
+ lbm_set_error_reason("Error: Extensions must start with ext-");
+ return false;
+ }
+
+ return lbm_add_extension(sym_str, ext);
+}
+
+static int lib_lbm_set_error_reason(char *str) {
+ lbm_set_error_reason(str);
+ return 1;
+}
+
+// High resolution timer for short busy-wait sleeps and time measurement
+uint32_t lib_timer_time_now() {
+ return (uint32_t)(esp_timer_get_time()); // microseconds
+}
+
+float lib_timer_seconds_elapsed_since(uint32_t time_us) {
+ uint32_t now_us = lib_timer_time_now();
+ return (now_us - time_us) / 1000000.0f;
+}
+
+void lib_timer_sleep(float seconds) {
+ if (seconds <= 0)
+ return;
+ uint32_t us = (uint32_t)(seconds * 1000000.0f);
+ while (us >= 2000) {
+ vTaskDelay(pdMS_TO_TICKS(1));
+ us -= 1000;
+ }
+ if (us)
+ esp_rom_delay_us(us);
+}
+
+void lispif_stop_lib(void) {
+ // 1) Call stop_fun for all loaded libs (mirrors STM32)
+ for (int i = 0; i < LIB_NUM_MAX; i++) {
+ if (loaded_libs[i].stop_fun) {
+ if (lib_is_func_valid(loaded_libs[i].stop_fun)) {
+ loaded_libs[i].stop_fun(loaded_libs[i].arg);
+ }
+ loaded_libs[i].stop_fun = NULL;
+ loaded_libs[i].base_addr = 0;
+ loaded_libs[i].arg = NULL;
+ lib_flash_addr[i] = 0;
+ }
+ }
+ // 2) Terminate remaining lib threads. Snapshot the handles under the
+ // lock, but request termination outside of it as lib_request_terminate
+ // blocks and blocking is not allowed in a critical section.
+ TaskHandle_t handles[LIB_MAX_THREADS];
+ size_t handle_cnt = 0;
+
+ LIB_THR_LOCK();
+ for (size_t i = 0; i < lib_thread_infos_cnt; i++) {
+ if (lib_thread_infos[i] && lib_thread_infos[i]->handle) {
+ handles[handle_cnt++] = lib_thread_infos[i]->handle;
+ }
+ }
+ LIB_THR_UNLOCK();
+
+ for (size_t i = 0; i < handle_cnt; i++) {
+ lib_request_terminate(handles[i]);
+ }
+
+ // 3) Free RAM-loaded lib images. Done last, after every lib thread is
+ // gone, as their code lives in these allocations.
+ for (int i = 0; i < LIB_NUM_MAX; i++) {
+ if (lib_ram_alloc[i]) {
+ heap_caps_free(lib_ram_alloc[i]);
+ lib_ram_alloc[i] = NULL;
+ }
+ if (lib_ram_data[i]) {
+ heap_caps_free(lib_ram_data[i]);
+ lib_ram_data[i] = NULL;
+ }
+ }
+}
+
+lbm_value ext_load_native_lib(lbm_value *args, lbm_uint argn) {
+ lbm_value res = lbm_enc_sym(SYM_EERROR);
+
+ // Expect a single numeric argument containing the IROM base address
+ if (argn != 1 || !lbm_is_number(args[0])) {
+ return res;
+ }
+
+ // The linker script and VESC_IF must agree on where the interface table
+ // lives, otherwise libs would read garbage function pointers.
+ if ((uintptr_t)&cif != (uintptr_t)VESC_IF) {
+ lbm_set_error_reason("Native lib interface address mismatch (firmware bug)");
+ return res;
+ }
+
+ if (!lib_init_done) {
+ // Zero the padding beyond the struct; slots appended to the
+ // interface after this firmware was built read as NULL.
+ memset((char *)cif.pad, 0, 2048);
+
+ cif.cif.if_version = VESC_C_IF_VERSION;
+
+ // LBM
+ cif.cif.lbm_add_extension = lib_add_extension;
+ cif.cif.lbm_block_ctx_from_extension = lbm_block_ctx_from_extension;
+ cif.cif.lbm_unblock_ctx = lbm_unblock_ctx;
+ cif.cif.lbm_get_current_cid = lbm_get_current_cid;
+ cif.cif.lbm_set_error_reason = lib_lbm_set_error_reason;
+ cif.cif.lbm_pause_eval_with_gc = lbm_pause_eval_with_gc;
+ cif.cif.lbm_continue_eval = lbm_continue_eval;
+ cif.cif.lbm_send_message = lbm_send_message;
+ cif.cif.lbm_eval_is_paused = lib_eval_is_paused;
+
+ cif.cif.lbm_cons = lbm_cons;
+ cif.cif.lbm_car = lbm_car;
+ cif.cif.lbm_cdr = lbm_cdr;
+ cif.cif.lbm_list_destructive_reverse = lbm_list_destructive_reverse;
+ cif.cif.lbm_create_byte_array = lib_create_byte_array;
+
+ cif.cif.lbm_add_symbol_const = lbm_add_symbol_const;
+ cif.cif.lbm_get_symbol_by_name = lbm_get_symbol_by_name;
+
+ cif.cif.lbm_enc_i = lbm_enc_i;
+ cif.cif.lbm_enc_u = lbm_enc_u;
+ cif.cif.lbm_enc_char = lbm_enc_char;
+ cif.cif.lbm_enc_float = lbm_enc_float;
+ cif.cif.lbm_enc_u32 = lbm_enc_u32;
+ cif.cif.lbm_enc_i32 = lbm_enc_i32;
+ cif.cif.lbm_enc_sym = lbm_enc_sym;
+
+ cif.cif.lbm_dec_as_float = lbm_dec_as_float;
+ cif.cif.lbm_dec_as_u32 = lbm_dec_as_u32;
+ cif.cif.lbm_dec_as_i32 = lbm_dec_as_i32;
+ cif.cif.lbm_dec_char = lbm_dec_char;
+ cif.cif.lbm_dec_str = lbm_dec_str;
+ cif.cif.lbm_dec_sym = lbm_dec_sym;
+
+ cif.cif.lbm_is_byte_array = lbm_is_array_r;
+ cif.cif.lbm_is_cons = lbm_is_cons;
+ cif.cif.lbm_is_number = lbm_is_number;
+ cif.cif.lbm_is_char = lbm_is_char;
+ cif.cif.lbm_is_symbol = lbm_is_symbol;
+
+ cif.cif.lbm_enc_sym_nil = ENC_SYM_NIL;
+ cif.cif.lbm_enc_sym_true = ENC_SYM_TRUE;
+ cif.cif.lbm_enc_sym_terror = ENC_SYM_TERROR;
+ cif.cif.lbm_enc_sym_eerror = ENC_SYM_EERROR;
+ cif.cif.lbm_enc_sym_merror = ENC_SYM_MERROR;
+
+ cif.cif.lbm_is_symbol_nil = lbm_is_symbol_nil;
+ cif.cif.lbm_is_symbol_true = lbm_is_symbol_true;
+
+ // Os
+ cif.cif.sleep_ms = lib_sleep_ms;
+ cif.cif.sleep_us = lib_sleep_us;
+ cif.cif.system_time = lib_system_time;
+ cif.cif.ts_to_age_s = lib_ts_to_age_s;
+ cif.cif.printf = commands_printf_lisp;
+ cif.cif.malloc = lbm_malloc_reserve;
+ cif.cif.free = lbm_free;
+ cif.cif.spawn = lispif_spawn;
+ cif.cif.request_terminate = lib_request_terminate;
+ cif.cif.should_terminate = lib_should_terminate;
+ cif.cif.get_arg = lib_get_arg;
+
+ // Mutex
+ cif.cif.mutex_create = lib_mutex_create;
+ cif.cif.mutex_lock = lib_mutex_lock;
+ cif.cif.mutex_unlock = lib_mutex_unlock;
+
+ // High resolution timer for short busy-wait sleeps and time measurement
+ cif.cif.timer_time_now = lib_timer_time_now;
+ cif.cif.timer_seconds_elapsed_since = lib_timer_seconds_elapsed_since;
+ cif.cif.timer_sleep = lib_timer_sleep;
+
+ // Flat values
+ cif.cif.lbm_start_flatten = lbm_start_flatten;
+ cif.cif.lbm_finish_flatten = lbm_finish_flatten;
+ cif.cif.f_b = f_b;
+ cif.cif.f_cons = f_cons;
+ cif.cif.f_float = f_float;
+ cif.cif.f_i = f_i;
+ cif.cif.f_i32 = f_i32;
+ cif.cif.f_i64 = f_i64;
+ cif.cif.f_lbm_array = f_lbm_array;
+ cif.cif.f_sym = f_sym;
+ cif.cif.f_u32 = f_u32;
+ cif.cif.f_u64 = f_u64;
+
+ // Unblock unboxed
+ cif.cif.lbm_unblock_ctx_unboxed = lbm_unblock_ctx_unboxed;
+
+ // System time
+ cif.cif.system_time_ticks = xTaskGetTickCount;
+ cif.cif.sleep_ticks = vTaskDelay;
+
+ // Semaphores
+ cif.cif.sem_create = lib_sem_create;
+ cif.cif.sem_wait = lib_sem_wait;
+ cif.cif.sem_signal = lib_sem_signal;
+ cif.cif.sem_wait_to = lib_sem_wait_to;
+ cif.cif.sem_reset = lib_sem_reset;
+
+ cif.cif.thread_set_priority = lib_thread_set_priority;
+
+ lib_init_done = true;
+ }
+
+ // Read IROM header base directly
+ uint32_t irom_base = lbm_dec_as_u32(args[0]);
+
+ // Basic pointer/alignment sanity
+ if (irom_base == 0 || (irom_base & 0x3) != 0) {
+ lbm_set_error_reason("Invalid IROM base pointer");
+ return res;
+ }
+
+ // Validate native header magic. Read through the DROM alias, as data
+ // reads through the instruction bus fault on some targets.
+ const uint8_t *container_drom = utils_irom_to_drom((void *)irom_base);
+ uint32_t magic_be = 0;
+ memcpy(&magic_be, container_drom, sizeof(magic_be));
+
+ bool is_reloc = magic_be == __builtin_bswap32(NATIVE_LIB_RELOC_MAGIC);
+ if (!is_reloc && magic_be != __builtin_bswap32(NATIVE_LIB_MAGIC)) {
+ lbm_set_error_reason("Magic number not found at IROM address");
+ return res;
+ }
+
+ // Duplicate check by container flash address
+ for (int i = 0; i < LIB_NUM_MAX; i++) {
+ if (loaded_libs[i].stop_fun != NULL
+ && lib_flash_addr[i] == irom_base) {
+ lbm_set_error_reason("Library already loaded");
+ return res;
+ }
+ }
+
+ int slot = -1;
+ for (int i = 0; i < LIB_NUM_MAX; i++) {
+ if (loaded_libs[i].stop_fun == NULL) {
+ slot = i;
+ break;
+ }
+ }
+ if (slot < 0) {
+ lbm_set_error_reason("Library table full");
+ return res;
+ }
+
+ // base_addr is where the lib image lives at runtime (prog_ptr at +4),
+ // entry_addr is the init function.
+ uint32_t base_addr;
+ uint32_t entry_addr;
+ void *ram_alloc = NULL;
+ void *ram_data = NULL;
+
+ if (is_reloc) {
+#if CONFIG_IDF_TARGET_ESP32S3 && CONFIG_ESP_SYSTEM_MEMPROT_FEATURE
+ // The exec-heap allocation below can never succeed with memory
+ // protection enabled, so fail with a message that says exactly
+ // what is wrong with this firmware build.
+ lbm_set_error_reason("This firmware was built with "
+ "CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y - native libs on the "
+ "ESP32-S3 need a build with it disabled");
+ return res;
+#elif CONFIG_IDF_TARGET_ESP32S3
+ // Xtensa cannot run position-independent code in place, so the
+ // container carries region-relative relocations and the image is
+ // copied to RAM in two parts: the code region goes to executable
+ // memory (any exec block works - including pure-IRAM without a
+ // data alias, since code is only word-accessed) and the data
+ // region to any byte-accessible internal RAM (including the
+ // DRAM-only spare). This keeps native libs out of the contested
+ // D/IRAM that LispBM needs. Container layout after the magic:
+ // version, code_size, data_size, entry_offset, reloc_count (all
+ // LE u32), relocs[], code[], data[].
+ uint32_t version, code_size, data_size, entry_offset, reloc_count;
+ memcpy(&version, container_drom + 4, 4);
+ memcpy(&code_size, container_drom + 8, 4);
+ memcpy(&data_size, container_drom + 12, 4);
+ memcpy(&entry_offset, container_drom + 16, 4);
+ memcpy(&reloc_count, container_drom + 20, 4);
+
+ if (version != 2) {
+ lbm_set_error_reason("Native lib container version mismatch - "
+ "rebuild the lib with the current vesc_pkg c_libs");
+ return res;
+ }
+
+ if (code_size < 4 || code_size > 0x40000 || (code_size & 3)
+ || data_size < 8 || data_size > 0x40000 || (data_size & 3)
+ || entry_offset >= code_size || (entry_offset & 3)
+ || reloc_count > (code_size + data_size) / 4) {
+ lbm_set_error_reason("Invalid native lib container");
+ return res;
+ }
+
+ uint32_t *code_ram = heap_caps_malloc(
+ code_size, MALLOC_CAP_EXEC | MALLOC_CAP_INTERNAL);
+ uint8_t *data_ram = heap_caps_malloc(
+ data_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
+ if (!code_ram || !data_ram) {
+ static char err_buf[96];
+ snprintf(err_buf, sizeof(err_buf),
+ "Out of memory for lib: code %u (largest %u), data %u (largest %u)",
+ (unsigned)code_size,
+ (unsigned)heap_caps_get_largest_free_block(
+ MALLOC_CAP_EXEC | MALLOC_CAP_INTERNAL),
+ (unsigned)data_size,
+ (unsigned)heap_caps_get_largest_free_block(
+ MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
+ if (code_ram) heap_caps_free(code_ram);
+ if (data_ram) heap_caps_free(data_ram);
+ lbm_set_error_reason(err_buf);
+ return res;
+ }
+
+ const uint8_t *relocs = container_drom + 24;
+ const uint8_t *code_src = relocs + reloc_count * 4;
+ const uint8_t *data_src = code_src + code_size;
+
+ // The code block may be pure IRAM, which only allows aligned
+ // 32-bit accesses - copy and patch it word-wise.
+ for (uint32_t i = 0; i < code_size / 4; i++) {
+ uint32_t w;
+ memcpy(&w, code_src + i * 4, 4);
+ code_ram[i] = w;
+ }
+ memcpy(data_ram, data_src, data_size);
+
+ // Relocation entries: bit31 = target is code, bit30 = the word
+ // sits in the data region, low bits = region-relative offset of
+ // the word. Stored words are region-relative target offsets.
+ bool patch_ok = true;
+ for (uint32_t r = 0; r < reloc_count; r++) {
+ uint32_t e;
+ memcpy(&e, relocs + r * 4, 4);
+ uint32_t off = e & 0x3FFFFFFF;
+ uint32_t add = (e & 0x80000000)
+ ? (uint32_t)code_ram : (uint32_t)data_ram;
+
+ if (e & 0x40000000) {
+ if ((off & 3) || off + 4 > data_size) {
+ patch_ok = false;
+ break;
+ }
+ uint32_t word;
+ memcpy(&word, data_ram + off, 4);
+ word += add;
+ memcpy(data_ram + off, &word, 4);
+ } else {
+ if ((off & 3) || off + 4 > code_size) {
+ patch_ok = false;
+ break;
+ }
+ code_ram[off / 4] += add;
+ }
+ }
+
+ uint32_t inner_magic = 0;
+ memcpy(&inner_magic, data_ram, 4);
+ if (!patch_ok || inner_magic != __builtin_bswap32(NATIVE_LIB_MAGIC)) {
+ heap_caps_free(code_ram);
+ heap_caps_free(data_ram);
+ lbm_set_error_reason("Invalid relocation table in native lib");
+ return res;
+ }
+
+ // Make the copied and patched code visible to instruction fetch.
+ __asm__ __volatile__("memw\n\tisync\n\t" ::: "memory");
+
+ base_addr = (uint32_t)data_ram;
+ entry_addr = (uint32_t)code_ram + entry_offset;
+ ram_alloc = code_ram;
+ ram_data = data_ram;
+#else
+ lbm_set_error_reason(
+ "Relocatable libs are only supported on the ESP32-S3");
+ return res;
+#endif
+ } else {
+ // XIP: runs in place from flash. Entry is after the header:
+ // magic(4) + prog_addr(4) = 8 bytes.
+ base_addr = irom_base;
+ entry_addr = irom_base + 8;
+ }
+
+ loaded_libs[slot].base_addr = base_addr;
+ lib_flash_addr[slot] = irom_base;
+ lib_ram_alloc[slot] = ram_alloc;
+ lib_ram_data[slot] = ram_data;
+
+ bool ok = ((bool (*)(lib_info *))entry_addr)(&loaded_libs[slot]);
+
+ if (ok && loaded_libs[slot].stop_fun != NULL) {
+ void *stop_fun_irom = utils_drom_to_irom(loaded_libs[slot].stop_fun);
+ if (lib_is_func_valid(stop_fun_irom)) {
+ loaded_libs[slot].stop_fun = stop_fun_irom;
+ return lbm_enc_sym(SYM_TRUE);
+ }
+ lbm_set_error_reason("Invalid stop function. Must be static.");
+ } else if (ok) {
+ lbm_set_error_reason("Library init failed - no stop function set");
+ } else {
+ lbm_set_error_reason("Library init failed");
+ }
+
+ // Rollback
+ loaded_libs[slot].stop_fun = NULL;
+ loaded_libs[slot].base_addr = 0;
+ loaded_libs[slot].arg = NULL;
+ lib_flash_addr[slot] = 0;
+ if (lib_ram_alloc[slot]) {
+ heap_caps_free(lib_ram_alloc[slot]);
+ lib_ram_alloc[slot] = NULL;
+ }
+ if (lib_ram_data[slot]) {
+ heap_caps_free(lib_ram_data[slot]);
+ lib_ram_data[slot] = NULL;
+ }
+
+ return res;
+}
+
+lbm_value ext_unload_native_lib(lbm_value *args, lbm_uint argn) {
+ lbm_value res = lbm_enc_sym(SYM_EERROR);
+
+ if (argn != 1 || !lbm_is_number(args[0])) {
+ return res;
+ }
+
+ uint32_t irom_base = lbm_dec_as_u32(args[0]);
+
+ for (int i = 0; i < LIB_NUM_MAX; i++) {
+ if (loaded_libs[i].stop_fun != NULL
+ && lib_flash_addr[i] == irom_base) {
+ // The stop function must stop everything the lib started,
+ // including its threads, before returning.
+ if (lib_is_func_valid(loaded_libs[i].stop_fun)) {
+ loaded_libs[i].stop_fun(loaded_libs[i].arg);
+ }
+ loaded_libs[i].stop_fun = NULL;
+ loaded_libs[i].base_addr = 0;
+ loaded_libs[i].arg = NULL;
+ lib_flash_addr[i] = 0;
+ if (lib_ram_alloc[i]) {
+ heap_caps_free(lib_ram_alloc[i]);
+ lib_ram_alloc[i] = NULL;
+ }
+ if (lib_ram_data[i]) {
+ heap_caps_free(lib_ram_data[i]);
+ lib_ram_data[i] = NULL;
+ }
+
+ return lbm_enc_sym(SYM_TRUE);
+ }
+ }
+
+ lbm_set_error_reason("Library not loaded");
+ return res;
+}
diff --git a/main/lispif_vesc_extensions.c b/main/lispif_vesc_extensions.c
index dd612a1a..99f0cb90 100644
--- a/main/lispif_vesc_extensions.c
+++ b/main/lispif_vesc_extensions.c
@@ -111,6 +111,10 @@
#error "Unsupported target"
#endif
+// Declare native lib extension
+lbm_value ext_load_native_lib(lbm_value *args, lbm_uint argn);
+lbm_value ext_unload_native_lib(lbm_value *args, lbm_uint argn);
+
typedef struct {
// BMS
lbm_uint v_tot;
@@ -158,6 +162,7 @@ typedef struct {
lbm_uint fw_ver;
lbm_uint uuid;
lbm_uint hw_type;
+ lbm_uint hw_target;
lbm_uint part_running;
lbm_uint git_branch;
lbm_uint git_hash;
@@ -294,6 +299,8 @@ static bool compare_symbol(lbm_uint sym, lbm_uint *comp) {
lbm_add_symbol_const("uuid", comp);
} else if (comp == &syms_vesc.hw_type) {
lbm_add_symbol_const("hw-type", comp);
+ } else if (comp == &syms_vesc.hw_target) {
+ lbm_add_symbol_const("hw-target", comp);
} else if (comp == &syms_vesc.part_running) {
lbm_add_symbol_const("part-running", comp);
} else if (comp == &syms_vesc.git_branch) {
@@ -1160,6 +1167,18 @@ static lbm_value ext_sysinfo(lbm_value *args, lbm_uint argn) {
res = lbm_cons(lbm_enc_i(FW_VERSION_MAJOR), res);
} else if (compare_symbol(name, &syms_vesc.hw_type)) {
res = lbm_enc_sym(sym_hw_express);
+ } else if (compare_symbol(name, &syms_vesc.hw_target)) {
+ // Chip this firmware runs on, e.g. "esp32c3". Native libs only run
+ // on the chip they were built for, so multi-target packages use
+ // this to pick the right binary.
+ lbm_value lbm_res;
+ if (lbm_create_array(&lbm_res, strlen(CONFIG_IDF_TARGET) + 1)) {
+ lbm_array_header_t *arr = (lbm_array_header_t*)lbm_car(lbm_res);
+ strcpy((char*)arr->data, CONFIG_IDF_TARGET);
+ res = lbm_res;
+ } else {
+ res = ENC_SYM_MERROR;
+ }
} else if (compare_symbol(name, &syms_vesc.part_running)) {
const esp_partition_t *running = esp_ota_get_running_partition();
if (running != NULL) {
@@ -6753,6 +6772,7 @@ static bool dynamic_loader(const char *str, const char **code) {
}
void lispif_load_vesc_extensions(bool main_found) {
+ lispif_stop_lib();
if (!i2c_mutex_init_done) {
i2c_mutex = xSemaphoreCreateMutex();
i2c_mutex_init_done = true;
@@ -6948,6 +6968,10 @@ void lispif_load_vesc_extensions(bool main_found) {
lbm_add_extension("sleep-config-wakeup-pin", ext_sleep_config_wakeup_pin);
lbm_add_extension("rtc-data", ext_rtc_data);
+ // Native libraries
+ lbm_add_extension("load-native-lib", ext_load_native_lib);
+ lbm_add_extension("unload-native-lib", ext_unload_native_lib);
+
lispif_load_rgbled_extensions();
lispif_load_disp_extensions();
@@ -7098,6 +7122,8 @@ void lispif_disable_all_events(void) {
xSemaphoreGive(rmsg_mutex);
}
+ lispif_stop_lib();
+
event_can_sid_en = false;
event_can_eid_en = false;
event_can2_sid_en = false;
@@ -7147,6 +7173,8 @@ void lispif_disable_all_events(void) {
cmds_running = false;
cmds_state = 0;
+
+ vTaskDelay(pdMS_TO_TICKS(5));
}
void lispif_process_can(uint32_t can_id, uint8_t *data8, int len, bool is_ext) {
diff --git a/main/utils.h b/main/utils.h
index 0ccfa77d..4da882ba 100644
--- a/main/utils.h
+++ b/main/utils.h
@@ -24,6 +24,7 @@
#include
#include
#include
+#include "soc/soc.h"
// Global variables
extern char *string_pin_invalid;
@@ -112,4 +113,28 @@ static inline void utils_norm_angle_rad(float *angle) {
while (*angle >= M_PI) { *angle -= 2.0 * M_PI; }
}
+// Translate between the flash data bus (DROM) and instruction bus (IROM)
+// mappings of the same flash page. The flash MMU maps each page at the same
+// offset on both buses, so this is a constant-offset translation. On chips
+// with a unified bus (C6, P4) both mappings are the same address.
+static inline void* utils_drom_to_irom(void* drom_addr) {
+ uintptr_t addr = (uintptr_t)drom_addr;
+#if SOC_DROM_LOW != SOC_IROM_LOW
+ if (addr >= SOC_DROM_LOW && addr < SOC_DROM_HIGH) {
+ return (void*)(addr - SOC_DROM_LOW + SOC_IROM_LOW);
+ }
+#endif
+ return (void*)addr;
+}
+
+static inline void* utils_irom_to_drom(void* irom_addr) {
+ uintptr_t addr = (uintptr_t)irom_addr;
+#if SOC_DROM_LOW != SOC_IROM_LOW
+ if (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) {
+ return (void*)(addr - SOC_IROM_LOW + SOC_DROM_LOW);
+ }
+#endif
+ return (void*)addr;
+}
+
#endif /* MAIN_UTILS_H_ */
diff --git a/sdkconfig.defaults.esp32s3_fh4 b/sdkconfig.defaults.esp32s3_fh4
index 767e66ce..3b0af0c4 100644
--- a/sdkconfig.defaults.esp32s3_fh4
+++ b/sdkconfig.defaults.esp32s3_fh4
@@ -1454,8 +1454,8 @@ CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=y
#
# Memory protection
#
-CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y
-CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=y
+# Memory protection must stay off so native libs can execute from RAM
+CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=n
# end of Memory protection
CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32
diff --git a/sdkconfig.defaults.esp32s3_fh4r2 b/sdkconfig.defaults.esp32s3_fh4r2
index 4d7435be..ea191548 100644
--- a/sdkconfig.defaults.esp32s3_fh4r2
+++ b/sdkconfig.defaults.esp32s3_fh4r2
@@ -1491,8 +1491,8 @@ CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=y
#
# Memory protection
#
-CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y
-CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=y
+# Memory protection must stay off so native libs can execute from RAM
+CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=n
# end of Memory protection
CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32
diff --git a/sdkconfig.defaults.esp32s3_n16r8 b/sdkconfig.defaults.esp32s3_n16r8
index 9add38dd..6ce256af 100644
--- a/sdkconfig.defaults.esp32s3_n16r8
+++ b/sdkconfig.defaults.esp32s3_n16r8
@@ -1489,8 +1489,8 @@ CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP=y
#
# Memory protection
#
-CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y
-CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=y
+# Memory protection must stay off so native libs can execute from RAM
+CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=n
# end of Memory protection
CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32