diff --git a/ports/espressif/Makefile b/ports/espressif/Makefile index b772e384410..c26dfdc5ec8 100644 --- a/ports/espressif/Makefile +++ b/ports/espressif/Makefile @@ -872,7 +872,11 @@ ifneq ($(CIRCUITPY_BLEIO_NATIVE),0) BLE_SDKCONFIG := ;esp-idf-config/sdkconfig-ble.defaults endif -SDKCONFIGS := esp-idf-config/sdkconfig.defaults;$(DEBUG_SDKCONFIG);$(FLASH_SIZE_SDKCONFIG);$(FLASH_MODE_SDKCONFIG);$(FLASH_SPEED_SDKCONFIG);$(PSRAM_SDKCONFIG);$(PSRAM_SIZE_SDKCONFIG);$(PSRAM_MODE_SDKCONFIG);$(PSRAM_SPEED_SDKCONFIG);$(BLE_SDKCONFIG);$(TARGET_SDKCONFIG);boards/$(BOARD)/sdkconfig +ifeq ($(CIRCUITPY_ENABLE_MPY_NATIVE),1) + MPY_NATIVE_SDKCONFIG := ;esp-idf-config/sdkconfig-mpy-native.defaults +endif + +SDKCONFIGS := esp-idf-config/sdkconfig.defaults;$(DEBUG_SDKCONFIG);$(FLASH_SIZE_SDKCONFIG);$(FLASH_MODE_SDKCONFIG);$(FLASH_SPEED_SDKCONFIG);$(PSRAM_SDKCONFIG);$(PSRAM_SIZE_SDKCONFIG);$(PSRAM_MODE_SDKCONFIG);$(PSRAM_SPEED_SDKCONFIG);$(BLE_SDKCONFIG)$(MPY_NATIVE_SDKCONFIG);$(TARGET_SDKCONFIG);boards/$(BOARD)/sdkconfig # create the config headers .PHONY: do-sdkconfig diff --git a/ports/espressif/esp-idf-config/sdkconfig-mpy-native.defaults b/ports/espressif/esp-idf-config/sdkconfig-mpy-native.defaults new file mode 100644 index 00000000000..40e70b6afa9 --- /dev/null +++ b/ports/espressif/esp-idf-config/sdkconfig-mpy-native.defaults @@ -0,0 +1,18 @@ +# +# Espressif IoT Development Framework Configuration +# +# +# Component config +# +# +# ESP System Settings +# +# Memory protection enforces W^X, which makes MALLOC_CAP_EXEC allocations +# impossible. Native .mpy modules have to be copied into executable RAM at +# import time, so it has to be off for CIRCUITPY_ENABLE_MPY_NATIVE builds. +# CONFIG_ESP_SYSTEM_MEMPROT is not set +# end of ESP System Settings + +# end of Component config + +# end of Espressif IoT Development Framework Configuration diff --git a/ports/espressif/mpconfigport.h b/ports/espressif/mpconfigport.h index f0106f26b2c..4f0fe3155d2 100644 --- a/ports/espressif/mpconfigport.h +++ b/ports/espressif/mpconfigport.h @@ -22,6 +22,13 @@ #define MICROPY_NLR_SETJMP (1) #define CIRCUITPY_DEFAULT_STACK_SIZE 0x6000 +#if MICROPY_EMIT_XTENSAWIN +// The GC heap is not instruction-fetchable, so native code has to be moved into +// IRAM before it can be run. +void *port_native_code_commit(void *buf, size_t len, void *reloc); +#define MP_PLAT_COMMIT_EXEC(buf, len, reloc) port_native_code_commit((buf), (len), (reloc)) +#endif + // PSRAM can require more stack space for GC. #define MICROPY_ALLOC_GC_STACK_SIZE (128) diff --git a/ports/espressif/supervisor/port.c b/ports/espressif/supervisor/port.c index 34cfba32b7e..88fa7a90163 100644 --- a/ports/espressif/supervisor/port.c +++ b/ports/espressif/supervisor/port.c @@ -16,6 +16,10 @@ #include "py/mpprint.h" #include "py/runtime.h" +#if MICROPY_EMIT_XTENSAWIN +#include "py/persistentcode.h" +#endif + #include "esp_mac.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -347,6 +351,40 @@ size_t port_heap_get_largest_free_size(void) { return free_size; } +#if MICROPY_EMIT_XTENSAWIN +// Native code loaded from a .mpy is copied into IRAM so that it can be executed, +// and is kept in a list so it can be released when the VM resets. +typedef struct _native_code_node_t { + struct _native_code_node_t *next; + uint32_t data[]; +} native_code_node_t; + +static native_code_node_t *native_code_head; + +void *port_native_code_commit(void *buf, size_t len, void *reloc) { + len = (len + sizeof(uint32_t) - 1) & ~(sizeof(uint32_t) - 1); + native_code_node_t *node = heap_caps_malloc(sizeof(native_code_node_t) + len, MALLOC_CAP_EXEC); + if (node == NULL) { + m_malloc_fail(len); + } + node->next = native_code_head; + native_code_head = node; + if (reloc != NULL) { + mp_native_relocate(reloc, buf, (uintptr_t)node->data); + } + memcpy(node->data, buf, len); + return node->data; +} + +static void native_code_free_all(void) { + while (native_code_head != NULL) { + native_code_node_t *next = native_code_head->next; + heap_caps_free(native_code_head); + native_code_head = next; + } +} +#endif + void reset_port_early(void) { // esp-camera adds an I2C device on the ESP I2C bus, and keeps it there. This // is unlike busio.I2C, which adds and removes the device on each operation. @@ -359,6 +397,10 @@ void reset_port_early(void) { void reset_port(void) { + #if MICROPY_EMIT_XTENSAWIN + native_code_free_all(); + #endif + #if CIRCUITPY_SSL ssl_reset(); #endif diff --git a/ports/mimxrt10xx/mpconfigport.h b/ports/mimxrt10xx/mpconfigport.h index df634dc7328..e12a96423d3 100644 --- a/ports/mimxrt10xx/mpconfigport.h +++ b/ports/mimxrt10xx/mpconfigport.h @@ -29,5 +29,20 @@ extern uint8_t _ld_default_stack_size; #include "py/circuitpy_mpconfig.h" +#if MICROPY_EMIT_THUMB || MICROPY_EMIT_INLINE_THUMB +// The GC heap is in cacheable OCRAM and the M7 runs with both caches enabled, so +// freshly written code has to be pushed out of the data cache before the +// instruction side is allowed to fetch it. +void *port_native_code_commit(void *buf, size_t len, void *reloc); +#define MP_PLAT_COMMIT_EXEC(buf, len, reloc) port_native_code_commit((buf), (len), (reloc)) + +// Defining MP_PLAT_COMMIT_EXEC otherwise tells the core that the port owns the native +// text allocation, which turns off GC tracking for it. This hook only flushes caches and +// hands back the same GC heap pointer, so the text still has to be tracked or a later +// collection reclaims code that is only referenced from inside the block. +#define MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA (1) +#define MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA (0) +#endif + // TODO: // mp_obj_t playing_audio[AUDIO_DMA_CHANNEL_COUNT] as an MP_REGISTER_ROOT_POINTER. diff --git a/ports/mimxrt10xx/supervisor/port.c b/ports/mimxrt10xx/supervisor/port.c index 62d2569cfde..5f04cb47e58 100644 --- a/ports/mimxrt10xx/supervisor/port.c +++ b/ports/mimxrt10xx/supervisor/port.c @@ -14,6 +14,8 @@ #include "supervisor/board.h" #include "supervisor/port.h" +#include "py/persistentcode.h" + #include "fsl_device_registers.h" #if CIRCUITPY_AUDIOBUSIO @@ -49,6 +51,13 @@ #define NO_EXECUTION 1 #define EXECUTION 0 +#if MICROPY_EMIT_THUMB || MICROPY_EMIT_INLINE_THUMB +// Native code from a .mpy is loaded into the GC heap, which lives in OCRAM, and then jumped to. +#define OCRAM_EXECUTION EXECUTION +#else +#define OCRAM_EXECUTION NO_EXECUTION +#endif + // Shareable if the memory system manages coherency. This means shared between memory bus masters, // not just CPUs. #define NOT_SHAREABLE 0 @@ -256,7 +265,7 @@ __attribute__((used, naked, no_instrument_function, optimize("no-tree-loop-distr // cost of 1/4 speed OCRAM accesses. It will leave more room for caching data from the flash // too which might be a net win. MPU->RBAR = ARM_MPU_RBAR(14, ((uint32_t)&_ld_ocram_start)); - MPU->RASR = ARM_MPU_RASR(NO_EXECUTION, ARM_MPU_AP_FULL, NORMAL, NOT_SHAREABLE, CACHEABLE, BUFFERABLE, NO_SUBREGIONS, ARM_MPU_REGION_SIZE_512KB); + MPU->RASR = ARM_MPU_RASR(OCRAM_EXECUTION, ARM_MPU_AP_FULL, NORMAL, NOT_SHAREABLE, CACHEABLE, BUFFERABLE, NO_SUBREGIONS, ARM_MPU_REGION_SIZE_512KB); #if IMXRT10XX // We steal 64k from FlexRAM for ITCM and DTCM so disable those memory regions here. @@ -475,6 +484,25 @@ uint32_t *port_heap_get_top(void) { return &_ld_heap_end; } +#if MICROPY_EMIT_THUMB || MICROPY_EMIT_INLINE_THUMB +// Native code from a .mpy is written into the heap as data, then jumped to. The +// heap is in cacheable OCRAM, so without this the instruction side can fetch +// stale bytes: a hard fault if they decode to nonsense, a wrong answer if they +// do not. Imports are rare, so clean and invalidate the whole of both caches +// rather than worry about the range's alignment. +void *port_native_code_commit(void *buf, size_t len, void *reloc) { + if (reloc != NULL) { + mp_native_relocate(reloc, buf, (uintptr_t)buf); + } + (void)len; + SCB_CleanDCache(); + SCB_InvalidateICache(); + __DSB(); + __ISB(); + return buf; +} +#endif + // Place the word into the low power section of the SNVS. void PLACE_IN_ITCM(port_set_saved_word)(uint32_t value) { SNVS->LPGPR[1] = value; diff --git a/py/asmxtensa.c b/py/asmxtensa.c index 18aea4ea29d..2de21f27356 100644 --- a/py/asmxtensa.c +++ b/py/asmxtensa.c @@ -34,6 +34,12 @@ #include "py/asmxtensa.h" +// N_XTENSAWIN is set by emitnxtensawin.c, but this file is its own translation +// unit so it has to be derived from the configured emitter. +#ifndef N_XTENSAWIN +#define N_XTENSAWIN MICROPY_EMIT_XTENSAWIN +#endif + #if N_XTENSAWIN #define REG_TEMP ASM_XTENSA_REG_TEMPORARY_WIN #else @@ -289,7 +295,7 @@ void asm_xtensa_mov_reg_pcrel(asm_xtensa_t *as, uint reg_dest, uint label) { asm_xtensa_op_add_n(as, reg_dest, reg_dest, ASM_XTENSA_REG_A0); } -void asm_xtensa_l32i_optimised(asm_xtensa_t *as, uint reg_dest, uint reg_base, uint word_offset) { +static void asm_xtensa_l32i_optimised(asm_xtensa_t *as, uint reg_dest, uint reg_base, uint word_offset) { if (word_offset < 16) { asm_xtensa_op_l32i_n(as, reg_dest, reg_base, word_offset); } else if (word_offset < 256) { diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index d91ac8ad23b..1d8a9e7c604 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -1104,6 +1104,9 @@ endif $(patsubst %.c,$(BUILD)/%.o,$(SRC_LIBM)): CFLAGS += -Wno-missing-prototypes endif +# Xtensa requires strict alignment, so the emitter's byte-buffer casts warn. +$(BUILD)/py/asmxtensa.o: CFLAGS += -Wno-cast-align + # Sources used in all ports except unix. SRC_CIRCUITPY_COMMON = \ shared/readline/readline.c \ diff --git a/py/circuitpy_mpconfig.h b/py/circuitpy_mpconfig.h index 53c4b955688..64e2f9bca2a 100644 --- a/py/circuitpy_mpconfig.h +++ b/py/circuitpy_mpconfig.h @@ -70,8 +70,14 @@ extern void common_hal_mcu_enable_interrupts(void); #define MICROPY_COMP_MODULE_CONST (1) #define MICROPY_COMP_TRIPLE_TUPLE_ASSIGN (0) #define MICROPY_DEBUG_PRINTERS (0) +// Enable the native emitter matching the target architecture, so that native +// .mpy modules built for it can be imported. +#if defined(__XTENSA_WINDOWED_ABI__) +#define MICROPY_EMIT_XTENSAWIN (CIRCUITPY_ENABLE_MPY_NATIVE) +#else #define MICROPY_EMIT_INLINE_THUMB (CIRCUITPY_ENABLE_MPY_NATIVE) #define MICROPY_EMIT_THUMB (CIRCUITPY_ENABLE_MPY_NATIVE) +#endif #define MICROPY_EMIT_X64 (0) #define MICROPY_ENABLE_DOC_STRING (0) #define MICROPY_ENABLE_FINALISER (1) diff --git a/tools/mpy-tool.py b/tools/mpy-tool.py index c80344a2b46..24a3f7ce654 100755 --- a/tools/mpy-tool.py +++ b/tools/mpy-tool.py @@ -1795,7 +1795,14 @@ def adjust_bytecode_qstr_obj_indices(bytecode_in, qstr_table_base, obj_table_bas opcodes.append(opcode) ip += sz if fmt == MP_BC_FORMAT_OFFSET: - opcode.arg += ip + # A jump is measured from the byte after its offset, which is the + # end of the instruction for every offset opcode but one. + # MP_BC_UNWIND_JUMP carries a trailing unwind count, and py/vm.c + # adds the offset before stepping over it. Counting that byte here + # puts the destination of every `break` or `continue` out of a + # `try` one past its label, and the lookup below raises KeyError. + # mp_opcode_encode is already right, so only the decode side moves. + opcode.arg += ip - 1 if extra_arg is not None else ip # Link jump opcodes to their destination. for opcode in opcodes: diff --git a/tools/mpy_ld.py b/tools/mpy_ld.py index abb9ab4f4bd..282c61659c5 100755 --- a/tools/mpy_ld.py +++ b/tools/mpy_ld.py @@ -61,6 +61,7 @@ MP_FUN_TABLE_MP_TYPE_TYPE_OFFSET = 74 # ELF constants +R_XTENSA_NONE = 0 R_386_32 = 1 R_RISCV_32 = 1 R_X86_64_64 = 1 @@ -569,6 +570,10 @@ def build_got_xtensa(env): # Look through literal relocations to find any global pointers that should be GOT entries for r in sec.reloc: + # LLVM emits no-op entries with a null symbol at the same offset as + # the real relocation; GAS does not. Nothing to resolve. + if r["r_info_type"] == R_XTENSA_NONE: + continue s = r.sym s_type = s.entry["st_info"]["type"] assert s_type in ("STT_NOTYPE", "STT_FUNC", "STT_OBJECT", "STT_SECTION"), s_type @@ -584,7 +589,7 @@ def build_got_xtensa(env): name = s.name if r["r_addend"] != 0: name = "{}+0x{:x}".format(name, r["r_addend"]) - idx = "{}+0x{:x}".format(sec.filename, r["r_offset"]) + idx = "{}+{}+0x{:x}".format(sec.filename, sec.name, r["r_offset"]) env.xt_literals[idx] = name if name in env.got_entries: # Deduplicate GOT entries @@ -593,7 +598,7 @@ def build_got_xtensa(env): # Go through all literal entries finding those that aren't global pointers so must be actual literals for i in range(0, len(sec.data), env.arch.word_size): - idx = "{}+0x{:x}".format(sec.filename, i) + idx = "{}+{}+0x{:x}".format(sec.filename, sec.name, i) if idx not in env.xt_literals: # This entry is an actual literal value = struct.unpack_from("> 8 - l32r_imm16 = (l32r_imm16 + reloc >> 2) & 0xFFFF - l32r = l32r & 0xFF | l32r_imm16 << 8 + # l32r loads from ((PC + 3) & ~3) + ((0xFFFF0000 | imm16) << 2), so encode the + # final offset directly; the existing imm16 may be a non-zero assembler guess. + l32r_offset = addr - ((r_offset + 3) & ~3) + assert -0x40000 <= l32r_offset <= -4, l32r_offset + l32r = l32r & 0xFF | ((l32r_offset >> 2) & 0xFFFF) << 8 pack_u24le(env.full_text, r_offset, l32r) else: assert 0, reloc_type