diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index c737ade70b..6d0e22fe26 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -266,10 +266,9 @@ jobs: arch: "armhf" library-arch: arm-linux-gnueabihf + # JIT armv6m build (Thumb-1 only JIT code) - cc: "arm-linux-gnueabihf-gcc" cxx: "arm-linux-gnueabihf-g++" - # -D_FILE_OFFSET_BITS=64 is required for making atomvm:posix_readdir/1 test work - # otherwise readdir will fail due to 64 bits inode numbers with 32 bit ino_t cflags: "-mcpu=cortex-a7 -mfloat-abi=hard -O3 -mthumb -mthumb-interwork -D_FILE_OFFSET_BITS=64" cmake_opts_other: "-DAVM_DISABLE_JIT=OFF -DAVM_JIT_TARGET_ARCH=armv6m -DCMAKE_TOOLCHAIN_FILE=${RUNNER_TEMP}/armhf_toolchain.cmake" compiler_pkgs: "crossbuild-essential-armhf libc6-dbg:armhf zlib1g-dev:armhf libmbedtls-dev:armhf qemu-user qemu-user-binfmt binfmt-support" @@ -277,6 +276,16 @@ jobs: library-arch: arm-linux-gnueabihf jit_target_arch: "armv6m" + # JIT armv6m+thumb2 build (Thumb-2 JIT code) + - cc: "arm-linux-gnueabihf-gcc" + cxx: "arm-linux-gnueabihf-g++" + cflags: "-mcpu=cortex-a7 -mfloat-abi=hard -O3 -mthumb -mthumb-interwork -D_FILE_OFFSET_BITS=64" + cmake_opts_other: "-DAVM_DISABLE_JIT=OFF -DAVM_JIT_TARGET_ARCH=armv6m+thumb2 -DCMAKE_TOOLCHAIN_FILE=${RUNNER_TEMP}/armhf_toolchain.cmake" + compiler_pkgs: "crossbuild-essential-armhf libc6-dbg:armhf zlib1g-dev:armhf libmbedtls-dev:armhf qemu-user qemu-user-binfmt binfmt-support" + arch: "armhf" + library-arch: arm-linux-gnueabihf + jit_target_arch: "armv6m+thumb2" + # JIT ARM32 (ARM mode) build - cc: "arm-linux-gnueabihf-gcc" cxx: "arm-linux-gnueabihf-g++" @@ -301,6 +310,20 @@ jobs: library-arch: arm-linux-gnueabihf jit_target_arch: "armv6m" + # JIT + DWARF build (armv6m+thumb2) + - os: "ubuntu-24.04" + cc: "arm-linux-gnueabihf-gcc" + cxx: "arm-linux-gnueabihf-g++" + cflags: "-mcpu=cortex-a7 -mfloat-abi=hard -O2 -mthumb -mthumb-interwork -D_FILE_OFFSET_BITS=64" + otp: "28" + elixir_version: "1.17" + rebar3_version: "3.24.0" + cmake_opts_other: "-DAVM_DISABLE_JIT=OFF -DAVM_DISABLE_JIT_DWARF=OFF -DAVM_JIT_TARGET_ARCH=armv6m+thumb2 -DCMAKE_TOOLCHAIN_FILE=${RUNNER_TEMP}/armhf_toolchain.cmake" + compiler_pkgs: "crossbuild-essential-armhf libc6-dbg:armhf zlib1g-dev:armhf libmbedtls-dev:armhf qemu-user qemu-user-binfmt binfmt-support" + arch: "armhf" + library-arch: arm-linux-gnueabihf + jit_target_arch: "armv6m+thumb2" + # JIT + DWARF build (arm32) - os: "ubuntu-24.04" cc: "arm-linux-gnueabihf-gcc" diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf733c9c5..597b4628af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added DWARF debug information support for JIT-compiled code - Added I2C and SPI APIs to rp2 platform - Added `code:get_object_code/1` +- Added Thumb-2 support to armv6m JIT backend, optimizing code for ARMv7-M and later cores ### Changed - ~10% binary size reduction by rewriting module loading logic diff --git a/CMakeLists.txt b/CMakeLists.txt index e8623abf6a..3d307381c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ if (NOT AVM_DISABLE_JIT AND NOT DEFINED AVM_JIT_TARGET_ARCH) endif() endif() -set(AVM_PRECOMPILED_TARGETS "x86_64;aarch64;arm32;armv6m;armv6m+float32;riscv32;riscv64" CACHE STRING "Targets to precompile code to if AVM_DISABLE_JIT is OFF or AVM_ENABLE_PRECOMPILED is ON") +set(AVM_PRECOMPILED_TARGETS "x86_64;aarch64;arm32;armv6m;armv6m+float32;armv6m+thumb2;riscv32;riscv64" CACHE STRING "Targets to precompile code to if AVM_DISABLE_JIT is OFF or AVM_ENABLE_PRECOMPILED is ON") if((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") OR (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") OR diff --git a/CMakeModules/BuildErlang.cmake b/CMakeModules/BuildErlang.cmake index ab11c8fc2e..ed207a95e5 100644 --- a/CMakeModules/BuildErlang.cmake +++ b/CMakeModules/BuildErlang.cmake @@ -103,6 +103,11 @@ macro(pack_precompiled_archive avm_name) ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_${jit_target_arch}.beam ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_${jit_target_arch}_asm.beam ) + if("${jit_target_arch_variant}" MATCHES "thumb2") + list(APPEND jit_compiler_modules + ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_armv7m_asm.beam + ) + endif() if (NOT AVM_DISABLE_JIT_DWARF) set(jit_precompile_dwarf_flag "dwarf") diff --git a/doc/src/atomvm-internals.md b/doc/src/atomvm-internals.md index 1f7047fcad..fefaeccb76 100644 --- a/doc/src/atomvm-internals.md +++ b/doc/src/atomvm-internals.md @@ -137,7 +137,7 @@ Following BEAM, there are two flavors of the emulator: jit and emu, but eventual - 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. - Hybrid: the VM can run native code as well as emulated BEAM code and some code is precompiled on the desktop. -JIT is available on some platforms (currently x86_64, aarch64, arm32, armv6m, riscv32 and riscv64) and compiles Erlang bytecode at runtime. Erlang bytecode is never interpreted. EMU is available on all platforms and Erlang bytecode is interpreted. +JIT is available on some platforms (currently x86_64, aarch64, arm32, armv6m, armv6m+thumb2, riscv32 and riscv64) and compiles Erlang bytecode at runtime. Erlang bytecode is never interpreted. EMU is available on all platforms and Erlang bytecode is interpreted. 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. @@ -158,7 +158,7 @@ A backend implementation is required for each architecture. The backend is calle - `jit_x86_64` for System V X86 64 ABI - `jit_aarch64` for AArch64 ABI - `jit_arm32` for ARM32 (AArch32 ARM mode) ABI -- `jit_armv6m` for ARMv6-M (AArch32 Thumb mode) ABI +- `jit_armv6m` for ARMv6-M (AArch32 Thumb mode) ABI, with an ARMv7-M or later variant using Thumb-2 32-bit encodings for Cortex-M3+ targets (Raspberry Pi Pico 2, STM32 with Cortex-M3/M4/M7/M33) - `jit_riscv32` for rv32imc ilp32 ABI - `jit_riscv64` for rv64gc lp64 ABI. @@ -169,7 +169,7 @@ A stream implementation is responsible for streaming the machine code, especiall ### Embedded JIT and Native -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). +On embedded devices, Native mode means the code is precompiled on the desktop and executed natively on the device. This currently works on all ARMv6-M devices (Pico and STM32 with Cortex-M0/M0+) as well as ARMv7-M devices using the Thumb-2 variant (Pico 2 and STM32 with Cortex-M3/M4/M7/M33). 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). diff --git a/doc/src/jit.md b/doc/src/jit.md index ac44a11858..0c1f7b9808 100644 --- a/doc/src/jit.md +++ b/doc/src/jit.md @@ -21,7 +21,8 @@ The JIT compiler supports the following target architectures: * `x86_64` — 64-bit x86 (Linux, macOS, FreeBSD) * `aarch64` — 64-bit ARM (Linux, macOS) * `arm32` — 32-bit ARM (Linux) -* `armv6m` — ARM Cortex-M0+ (Raspberry Pi Pico, STM32) +* `armv6m` — ARM Cortex-M0+ (Raspberry Pi Pico, STM32 with Cortex-M0/M0+) +* `armv6m+thumb2` — ARM Cortex-M3+ with Thumb-2 support, ARMv7-M or later (Raspberry Pi Pico 2, STM32 with Cortex-M3/M4/M7/M33) * `riscv32` — 32-bit RISC-V * `riscv64` — 64-bit RISC-V @@ -176,5 +177,5 @@ $ riscv64-elf-objdump -d module.elf |--------|---------|-------------| | `AVM_DISABLE_JIT` | `ON` | Disable JIT compilation | | `AVM_DISABLE_JIT_DWARF` | `ON` | Disable DWARF debug information in JIT | -| `AVM_JIT_TARGET_ARCH` | auto-detected | Target architecture (`x86_64`, `aarch64`, `arm32`, `armv6m`, `riscv32`, `riscv64`) | +| `AVM_JIT_TARGET_ARCH` | auto-detected | Target architecture (`x86_64`, `aarch64`, `arm32`, `armv6m`, `armv6m+thumb2`, `riscv32`, `riscv64`) | | `AVM_DISABLE_SMP` | `OFF` | Disable SMP support | diff --git a/libs/jit/include/jit.hrl b/libs/jit/include/jit.hrl index 251f8a832e..d4500928b8 100644 --- a/libs/jit/include/jit.hrl +++ b/libs/jit/include/jit.hrl @@ -33,5 +33,6 @@ -define(JIT_VARIANT_PIC, 1). -define(JIT_VARIANT_FLOAT32, 2). +-define(JIT_VARIANT_THUMB2, 4). -define(MAX_REG, 16). diff --git a/libs/jit/src/CMakeLists.txt b/libs/jit/src/CMakeLists.txt index b9744c71ea..c7cbf82b9c 100644 --- a/libs/jit/src/CMakeLists.txt +++ b/libs/jit/src/CMakeLists.txt @@ -37,6 +37,7 @@ set(ERLANG_MODULES jit_arm32_asm jit_armv6m jit_armv6m_asm + jit_armv7m_asm jit_riscv32 jit_riscv32_asm jit_riscv64 diff --git a/libs/jit/src/jit_armv6m.erl b/libs/jit/src/jit_armv6m.erl index f828a8bafe..3743434233 100644 --- a/libs/jit/src/jit_armv6m.erl +++ b/libs/jit/src/jit_armv6m.erl @@ -146,19 +146,22 @@ ). -type stream() :: any(). +-type branch_type() :: + {adr, armv6m_register()} | b_w | {far_branch, non_neg_integer(), armv6m_register()}. -record(state, { stream_module :: module(), stream :: stream(), offset :: non_neg_integer(), - branches :: [{non_neg_integer(), non_neg_integer(), non_neg_integer()}], + branches :: [{non_neg_integer(), non_neg_integer(), branch_type()}], jump_table_start :: non_neg_integer(), available_regs :: non_neg_integer(), used_regs :: non_neg_integer(), labels :: [{integer() | reference(), integer()}], variant :: non_neg_integer(), literal_pool :: [{non_neg_integer(), armv6m_register(), non_neg_integer()}], - regs :: jit_regs:regs() + regs :: jit_regs:regs(), + thumb2 :: boolean() }). -type state() :: #state{}. @@ -203,8 +206,8 @@ -define(MODULE_INDEX(ModuleReg), {ModuleReg, 0}). -define(JUMP_TABLE_ENTRY_SIZE, 12). +-define(JUMP_TABLE_ENTRY_SIZE_THUMB2, 6). -% aarch64 ABI specific %% ARMv6-M register mappings %% IP can be used as an additional scratch register @@ -300,7 +303,8 @@ new(Variant, StreamModule, Stream) -> labels = [], variant = Variant, literal_pool = [], - regs = jit_regs:new() + regs = jit_regs:new(), + thumb2 = (Variant band ?JIT_VARIANT_THUMB2) =/= 0 }. %%----------------------------------------------------------------------------- @@ -415,7 +419,7 @@ assert_all_native_free(State) -> %% 0 (special entry for lines and labels information) to LabelsCount included %% (special entry for OP_INT_CALL_END). %% -%% On this platform, each jump table entry is 12 bytes. +%% On ARMv6-M (Thumb-1), each jump table entry is 12 bytes: %% ``` %% ldr r3, pc+4 %% push {r1, r4, r5, r6, r7, lr} @@ -424,6 +428,12 @@ assert_all_native_free(State) -> %% offset_to_label0 %% ``` %% +%% On ARMv7-M/ARMv8-M (Thumb-2 variant), each jump table entry is 6 bytes: +%% ``` +%% push {r1, r4, r5, r6, r7, lr} +%% b.w offset_to_label0 +%% ``` +%% %% @end %% @param State current backend state %% @param LabelsCount number of labels in the module. @@ -436,12 +446,26 @@ jump_table(#state{stream_module = StreamModule, stream = Stream0} = State, Label jump_table0(State, N, LabelsCount) when N > LabelsCount -> State; +jump_table0( + #state{stream_module = StreamModule, stream = Stream0, thumb2 = true} = State, + N, + LabelsCount +) -> + % Thumb-2 jump table entry: push + b.w (6 bytes) + I1 = jit_armv6m_asm:push([r1, r4, r5, r6, r7, lr]), + % Placeholder b.w - will be patched by update_branches + I2 = <<16#FFFF:16, 16#FFFF:16>>, + + JumpEntry = <>, + Stream1 = StreamModule:append(Stream0, JumpEntry), + + jump_table0(State#state{stream = Stream1}, N + 1, LabelsCount); jump_table0( #state{stream_module = StreamModule, stream = Stream0} = State, N, LabelsCount ) -> - % Create jump table entry with calculated offsets - all at emit time + % ARMv6-M jump table entry: ldr + push + add pc + nop + literal (12 bytes) I1 = jit_armv6m_asm:ldr(r3, {pc, 4}), I2 = jit_armv6m_asm:push([r1, r4, r5, r6, r7, lr]), I3 = jit_armv6m_asm:add(pc, r3), @@ -469,6 +493,8 @@ patch_branch(StreamModule, Stream, Offset, Type, LabelOffset) -> case Type of {adr, Reg} when Rel rem 4 =:= 0 -> jit_armv6m_asm:adr(Reg, Rel); {adr, Reg} when Rel rem 4 =:= 2 -> jit_armv6m_asm:adr(Reg, Rel + 2); + b_w -> + jit_armv7m_asm:b_w(Rel - 4); {far_branch, Size, TempReg} -> % Check if branch can now be optimized to near branch if @@ -917,6 +943,11 @@ jump_to_continuation( State2 = State1#state{stream = Stream2, available_regs = ?AVAILABLE_REGS_MASK, used_regs = 0}, flush_literal_pool(State2). +branch_to_offset_code(#state{thumb2 = true}, Offset, TargetOffset) -> + % Thumb-2: b.w has +-16MB range, always sufficient + % b.w offset is relative to PC (instruction address + 4) + Rel = TargetOffset - (Offset + 4), + jit_armv7m_asm:b_w(Rel); branch_to_offset_code(_State, Offset, TargetOffset) when TargetOffset - Offset =< 2050, TargetOffset - Offset >= -2044 -> @@ -952,6 +983,13 @@ branch_to_offset_code( branch_to_label_code(State, Offset, Label, {Label, LabelOffset}) -> CodeBlock = branch_to_offset_code(State, Offset, LabelOffset), {State, CodeBlock}; +branch_to_label_code( + #state{branches = Branches, thumb2 = true} = State0, Offset, Label, false +) -> + CodeBlock = <<16#FFFF:16, 16#FFFF:16>>, + Reloc = {Label, Offset, b_w}, + State1 = State0#state{branches = [Reloc | Branches]}, + {State1, CodeBlock}; branch_to_label_code( #state{available_regs = Available, branches = Branches} = State0, Offset, Label, false ) when Available =/= 0 -> @@ -3170,7 +3208,12 @@ set_continuation_to_label( Temp1 = first_avail(Avail), Temp2 = first_avail(Avail band (bnot reg_bit(Temp1))), % Calculate jump table entry offset - JumpTableEntryOffset = (Label * ?JUMP_TABLE_ENTRY_SIZE) + JumpTableOffset, + EntrySize = + case State#state.thumb2 of + true -> ?JUMP_TABLE_ENTRY_SIZE_THUMB2; + false -> ?JUMP_TABLE_ENTRY_SIZE + end, + JumpTableEntryOffset = (Label * EntrySize) + JumpTableOffset, AdrOffset = StreamModule:offset(Stream0), % ADR Temp, +.4 means we're storing PC value in Temp1. @@ -3494,6 +3537,26 @@ mov_immediate(#state{stream_module = StreamModule, stream = Stream0} = State, Re I = jit_armv6m_asm:movs(Reg, Val), Stream1 = StreamModule:append(Stream0, I), State#state{stream = Stream1}; +mov_immediate( + #state{stream_module = StreamModule, stream = Stream0, thumb2 = true} = State, Reg, Val +) when + Val > 255 andalso Val =< 65535 +-> + I = jit_armv7m_asm:movw(Reg, Val), + Stream1 = StreamModule:append(Stream0, I), + State#state{stream = Stream1}; +mov_immediate( + #state{stream_module = StreamModule, stream = Stream0, thumb2 = true} = State, Reg, Val +) when + ?IS_SIGNED_OR_UNSIGNED_INT32_T(Val) +-> + UVal = Val band 16#FFFFFFFF, + Lo16 = UVal band 16#FFFF, + Hi16 = (UVal bsr 16) band 16#FFFF, + I1 = jit_armv7m_asm:movw(Reg, Lo16), + I2 = jit_armv7m_asm:movt(Reg, Hi16), + Stream1 = StreamModule:append(Stream0, <>), + State#state{stream = Stream1}; mov_immediate(#state{stream_module = StreamModule, stream = Stream0} = State, Reg, Val) when Val >= -255 andalso Val < 0 -> @@ -4192,30 +4255,43 @@ add_label( stream = Stream0, jump_table_start = JumpTableStart, branches = Branches, - labels = Labels + labels = Labels, + thumb2 = Thumb2 } = State, Label, LabelOffset ) when is_integer(Label) -> - % Patch the jump table entry immediately - % Each jump table entry is 12 bytes: - % - ldr r3, [pc, 4] (2 bytes) at offset 0 - % - push {...} (2 bytes) at offset 2 - % - add pc, r3 (2 bytes) at offset 4 - % - nop (2 bytes) at offset 6 - % - data (4 bytes) at offset 8 - JumpTableEntryStart = JumpTableStart + Label * 12, - DataOffset = JumpTableEntryStart + 8, - AddInstrOffset = JumpTableEntryStart + 4, - - % Calculate offset from 'add pc, r3' instruction to target label - % When 'add pc, r3' executes, PC reads as AddInstrOffset + 4 - % Result goes through BXWritePC, so bit 0 must be 1 for Thumb mode - AddPC = AddInstrOffset + 4, - RelativeOffset = LabelOffset - AddPC + 1, - DataBytes = <>, - - Stream1 = StreamModule:replace(Stream0, DataOffset, DataBytes), + Stream1 = + case Thumb2 of + true -> + % Thumb-2 jump table entry is 6 bytes: + % - push {...} (2 bytes) at offset 0 + % - b.w (4 bytes) at offset 2 + JumpTableEntryStart = JumpTableStart + Label * ?JUMP_TABLE_ENTRY_SIZE_THUMB2, + BranchInstrOffset = JumpTableEntryStart + 2, + % b.w offset is relative to instruction address + 4 + BranchPC = BranchInstrOffset + 4, + RelativeOffset = LabelOffset - BranchPC, + BranchBytes = jit_armv7m_asm:b_w(RelativeOffset), + StreamModule:replace(Stream0, BranchInstrOffset, BranchBytes); + false -> + % ARMv6-M jump table entry is 12 bytes: + % - ldr r3, [pc, 4] (2 bytes) at offset 0 + % - push {...} (2 bytes) at offset 2 + % - add pc, r3 (2 bytes) at offset 4 + % - nop (2 bytes) at offset 6 + % - data (4 bytes) at offset 8 + JumpTableEntryStart = JumpTableStart + Label * ?JUMP_TABLE_ENTRY_SIZE, + DataOffset = JumpTableEntryStart + 8, + AddInstrOffset = JumpTableEntryStart + 4, + % Calculate offset from 'add pc, r3' instruction to target label + % When 'add pc, r3' executes, PC reads as AddInstrOffset + 4 + % Result goes through BXWritePC, so bit 0 must be 1 for Thumb mode + AddPC = AddInstrOffset + 4, + RelativeOffset = LabelOffset - AddPC + 1, + DataBytes = <>, + StreamModule:replace(Stream0, DataOffset, DataBytes) + end, % Eagerly patch any branches targeting this label {Stream2, RemainingBranches} = patch_branches_for_label( diff --git a/libs/jit/src/jit_armv6m_asm.erl b/libs/jit/src/jit_armv6m_asm.erl index 5efabba05d..8ad08f641a 100644 --- a/libs/jit/src/jit_armv6m_asm.erl +++ b/libs/jit/src/jit_armv6m_asm.erl @@ -59,6 +59,7 @@ ]). -export_type([ + arm_gpr_register/0, cc/0 ]). diff --git a/libs/jit/src/jit_armv7m_asm.erl b/libs/jit/src/jit_armv7m_asm.erl new file mode 100644 index 0000000000..adcbef804a --- /dev/null +++ b/libs/jit/src/jit_armv7m_asm.erl @@ -0,0 +1,128 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + +%% @doc Thumb-2 (ARMv7-M / ARMv8-M) instruction assembler. +%% +%% This module encodes Thumb-2 32-bit instructions that are not available +%% in the ARMv6-M (Thumb-1 only) instruction set. It is used as a companion +%% to jit_armv6m_asm when the thumb2 variant is enabled. +%% +%% Reference: ARM Architecture Reference Manual ARMv7-M (DDI 0403E). +%% These encodings are also valid for ARMv8-M Mainline. +%% Thumb-2 instructions are encoded as two 16-bit halfwords, stored +%% little-endian. The first halfword contains the high bits. + +-module(jit_armv7m_asm). + +-export([ + b_w/1, + movw/2, + movt/2 +]). + +-type arm_gpr_register() :: jit_armv6m_asm:arm_gpr_register(). + +%%----------------------------------------------------------------------------- +%% Thumb-2 32-bit branch (B.W) +%% +%% Encoding T4 (ARMv7-M): +%% First halfword: 11110 S imm10[9:0] +%% Second halfword: 10 J1 1 J2 imm11[10:0] +%% +%% Where: +%% I1 = NOT(J1 XOR S) +%% I2 = NOT(J2 XOR S) +%% imm32 = SignExtend(S:I1:I2:imm10:imm11:0, 32) +%% +%% Range: -16777216 to +16777214 (±16 MB), 2-byte aligned +%% +%% The offset is relative to PC (instruction address + 4). +%%----------------------------------------------------------------------------- +-spec b_w(integer()) -> binary(). +b_w(Offset) when + is_integer(Offset), + Offset >= -16777216, + Offset =< 16777214, + (Offset rem 2) =:= 0 +-> + %% imm32 = SignExtend(S:I1:I2:imm10:imm11:0, 32) + %% After dropping the low zero bit we have a 24-bit value: S:I1:I2:imm10:imm11 + Imm24 = (Offset bsr 1) band 16#FFFFFF, + S = (Imm24 bsr 23) band 1, + I1 = (Imm24 bsr 22) band 1, + I2 = (Imm24 bsr 21) band 1, + Imm10 = (Imm24 bsr 11) band 16#3FF, + Imm11 = Imm24 band 16#7FF, + %% J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S) + J1 = (1 - (I1 bxor S)) band 1, + J2 = (1 - (I2 bxor S)) band 1, + HW1 = (2#11110 bsl 11) bor (S bsl 10) bor Imm10, + HW2 = (2#10 bsl 14) bor (J1 bsl 13) bor (1 bsl 12) bor (J2 bsl 11) bor Imm11, + <>; +b_w(Offset) -> + error({unencodable_branch_offset, Offset}). + +%%----------------------------------------------------------------------------- +%% Thumb-2 MOVW (Move Wide) - loads 16-bit immediate into lower half of register +%% +%% Encoding T3: +%% First halfword: 11110 i 10 0 1 0 0 imm4[3:0] +%% Second halfword: 0 imm3[2:0] Rd[3:0] imm8[7:0] +%% +%% Where: imm16 = imm4:i:imm3:imm8 +%% +%% Sets lower 16 bits of Rd to imm16, zeroes upper 16 bits. +%%----------------------------------------------------------------------------- +-spec movw(arm_gpr_register(), non_neg_integer()) -> binary(). +movw(Rd, Imm16) when is_integer(Imm16), Imm16 >= 0, Imm16 =< 65535 -> + encode_mov_imm16(2#100100, Rd, Imm16); +movw(_Rd, Imm) -> + error({unencodable_immediate, Imm}). + +%%----------------------------------------------------------------------------- +%% Thumb-2 MOVT (Move Top) - loads 16-bit immediate into upper half of register +%% +%% Encoding T1: +%% First halfword: 11110 i 10 1 1 0 0 imm4[3:0] +%% Second halfword: 0 imm3[2:0] Rd[3:0] imm8[7:0] +%% +%% Where: imm16 = imm4:i:imm3:imm8 +%% +%% Sets upper 16 bits of Rd to imm16, preserves lower 16 bits. +%%----------------------------------------------------------------------------- +-spec movt(arm_gpr_register(), non_neg_integer()) -> binary(). +movt(Rd, Imm16) when is_integer(Imm16), Imm16 >= 0, Imm16 =< 65535 -> + encode_mov_imm16(2#101100, Rd, Imm16); +movt(_Rd, Imm) -> + error({unencodable_immediate, Imm}). + +%%----------------------------------------------------------------------------- +%% Internal helpers +%%----------------------------------------------------------------------------- + +%% Encode a MOVW or MOVT instruction, differing only in the opcode field. +encode_mov_imm16(Opcode, Rd, Imm16) -> + RdNum = jit_armv6m_asm:reg_to_num(Rd), + Imm4 = (Imm16 bsr 12) band 16#F, + I = (Imm16 bsr 11) band 1, + Imm3 = (Imm16 bsr 8) band 7, + Imm8 = Imm16 band 16#FF, + HW1 = (2#11110 bsl 11) bor (I bsl 10) bor (Opcode bsl 4) bor Imm4, + HW2 = (Imm3 bsl 12) bor (RdNum bsl 8) bor Imm8, + <>. diff --git a/libs/jit/src/jit_dwarf.erl b/libs/jit/src/jit_dwarf.erl index b57774a552..3c32dd8e88 100644 --- a/libs/jit/src/jit_dwarf.erl +++ b/libs/jit/src/jit_dwarf.erl @@ -21,6 +21,7 @@ -module(jit_dwarf). -include("jit_dwarf.hrl"). +-include_lib("jit.hrl"). -record(dwarf, { % Backend module (jit_armv6m, etc.) @@ -40,7 +41,9 @@ reg_locations = [] :: [{non_neg_integer(), [{non_neg_integer(), non_neg_integer()}]}], stream_module :: module(), stream :: any(), - line_resolver :: fun((non_neg_integer()) -> false | {ok, binary(), pos_integer()}) + line_resolver :: fun((non_neg_integer()) -> false | {ok, binary(), pos_integer()}), + % JIT variant flags (for ARM attributes generation) + variant = 0 :: non_neg_integer() }). -type state() :: #dwarf{}. @@ -48,6 +51,7 @@ -export([ new/4, new/5, + new/6, extract_x_reg_locations/2, opcode/2, opcode/3, @@ -74,7 +78,7 @@ %%----------------------------------------------------------------------------- -spec new(module(), module(), module(), pos_integer()) -> state(). new(Backend, ModuleName, StreamModule, MaxSize) -> - new(Backend, ModuleName, StreamModule, MaxSize, fun(_) -> false end). + new(Backend, ModuleName, StreamModule, MaxSize, fun(_) -> false end, 0). %%----------------------------------------------------------------------------- %% @returns A new state @@ -85,6 +89,22 @@ new(Backend, ModuleName, StreamModule, MaxSize) -> (non_neg_integer()) -> false | {ok, binary(), pos_integer()} )) -> state(). new(Backend, ModuleName, StreamModule, MaxSize, LineResolver) -> + new(Backend, ModuleName, StreamModule, MaxSize, LineResolver, 0). + +%%----------------------------------------------------------------------------- +%% @returns A new state +%% @doc Create a new state with the proxied stream and variant. +%% @end +%%----------------------------------------------------------------------------- +-spec new( + module(), + module(), + module(), + pos_integer(), + fun((non_neg_integer()) -> false | {ok, binary(), pos_integer()}), + non_neg_integer() +) -> state(). +new(Backend, ModuleName, StreamModule, MaxSize, LineResolver, Variant) -> Stream = StreamModule:new(MaxSize), #dwarf{ backend = Backend, @@ -92,6 +112,7 @@ new(Backend, ModuleName, StreamModule, MaxSize, LineResolver) -> stream_module = StreamModule, stream = Stream, line_resolver = LineResolver, + variant = Variant, % Add jump table symbol at offset 0, size will be calculated opcodes = [{0, jump_table, 0}] }. @@ -371,7 +392,7 @@ elf(#dwarf{module_name = ModuleName, backend = Backend} = State, NativeCode) -> {Sections, SectionRelocs} = case Backend of jit_armv6m -> - ArmAttributesSection = generate_arm_attributes_section(), + ArmAttributesSection = generate_arm_attributes_section(State#dwarf.variant), { BaseSections ++ [{<<".ARM.attributes">>, ArmAttributesSection}], BaseSectionRelocs ++ [[]] @@ -439,24 +460,37 @@ find_section_index_helper(SectionName, [_ | Rest], Index) -> %% Find .symtab section index in section headers -%% Generate ARM attributes section for ARMv6-M -generate_arm_attributes_section() -> +%% Generate ARM attributes section +%% Variant is used to select between ARMv6-M (Thumb-1) and ARMv7-M (Thumb-2) attributes. +generate_arm_attributes_section(Variant) -> % ARM EABI attributes format according to ARM IHI 0045E + Thumb2 = (Variant band ?JIT_VARIANT_THUMB2) =/= 0, + + % CPU_arch and THUMB_ISA_use depend on the variant + {CpuArch, ThumbIsaUse} = + case Thumb2 of + true -> + % ARMv7 (value 10), Thumb-2 (value 2) + {10, 2}; + false -> + % ARMv6-M (value 11), Thumb-1 only (value 1) + {11, 1} + end, % Build the tag-value pairs for file attributes TagValuePairs = << - % CPU_arch attribute: ARMv6S-M (value 11) + % CPU_arch attribute 6, - 11, + CpuArch, % CPU_arch_profile attribute: 'M' profile (value 77 = 'M') 7, 77, % ARM_ISA_use attribute: No ARM ISA (value 0) 8, 0, - % THUMB_ISA_use attribute: Thumb-1 only (value 1) + % THUMB_ISA_use attribute 9, - 1, + ThumbIsaUse, % FP_arch attribute: No FP (value 0) 10, 0, diff --git a/libs/jit/src/jit_precompile.erl b/libs/jit/src/jit_precompile.erl index 7a373bd553..5207f22e06 100644 --- a/libs/jit/src/jit_precompile.erl +++ b/libs/jit/src/jit_precompile.erl @@ -39,6 +39,7 @@ start() -> %% Examples: %% "armv6m" -> {"armv6m", ?JIT_VARIANT_PIC} %% "armv6m+float32" -> {"armv6m", ?JIT_VARIANT_PIC + ?JIT_VARIANT_FLOAT32} +%% "armv6m+thumb2" -> {"armv6m", ?JIT_VARIANT_PIC + ?JIT_VARIANT_THUMB2} %% "x86_64" -> {"x86_64", ?JIT_VARIANT_PIC} parse_target(Target) -> case string:split(Target, "+", all) of @@ -48,7 +49,9 @@ parse_target(Target) -> RequestedVariant = lists:foldl( fun(Variant, Acc) -> case Variant of - "float32" -> Acc + ?JIT_VARIANT_FLOAT32 + "float32" -> Acc + ?JIT_VARIANT_FLOAT32; + "thumb2" -> Acc + ?JIT_VARIANT_THUMB2; + _ -> error({unsupported_variant, Variant}) end end, ?JIT_VARIANT_PIC, @@ -133,7 +136,9 @@ compile(Target, Dir, Dwarf, Path) -> Stream2 = case Dwarf of true -> - Stream0 = jit_dwarf:new(Backend, Module, jit_stream_binary, 0, LineResolver), + Stream0 = jit_dwarf:new( + Backend, Module, jit_stream_binary, 0, LineResolver, RequestedVariant + ), Backend:new(RequestedVariant, jit_dwarf, Stream0); false -> Backend:new( diff --git a/src/libAtomVM/CMakeLists.txt b/src/libAtomVM/CMakeLists.txt index 8793dc3b30..6f6b34fb0f 100644 --- a/src/libAtomVM/CMakeLists.txt +++ b/src/libAtomVM/CMakeLists.txt @@ -192,6 +192,9 @@ endif() if (AVM_JIT_TARGET_ARCH STREQUAL "arm32") target_compile_definitions(libAtomVM PUBLIC AVM_JIT_ARM32) endif() +if (AVM_JIT_TARGET_ARCH MATCHES "thumb2") + target_compile_definitions(libAtomVM PUBLIC AVM_JIT_THUMB2) +endif() if(HAVE_PLATFORM_SMP_H) target_compile_definitions(libAtomVM PUBLIC HAVE_PLATFORM_SMP_H) diff --git a/src/libAtomVM/jit.h b/src/libAtomVM/jit.h index eb0dcf6b4e..46696368da 100644 --- a/src/libAtomVM/jit.h +++ b/src/libAtomVM/jit.h @@ -190,6 +190,7 @@ enum TrapAndLoadResult #define JIT_VARIANT_PIC 1 #define JIT_VARIANT_FLOAT32 2 +#define JIT_VARIANT_THUMB2 4 #ifndef AVM_NO_JIT @@ -208,8 +209,12 @@ enum TrapAndLoadResult #define JIT_JUMPTABLE_ENTRY_SIZE 8 #elif defined(__arm__) #define JIT_ARCH_TARGET JIT_ARCH_ARMV6M +#ifdef AVM_JIT_THUMB2 +#define JIT_JUMPTABLE_ENTRY_SIZE 6 +#else #define JIT_JUMPTABLE_ENTRY_SIZE 12 #endif +#endif #if defined(__riscv) && (__riscv_xlen == 32) #define JIT_ARCH_TARGET JIT_ARCH_RISCV32 diff --git a/src/libAtomVM/module.c b/src/libAtomVM/module.c index 99ed6b89f2..cb3aba41b9 100644 --- a/src/libAtomVM/module.c +++ b/src/libAtomVM/module.c @@ -1124,11 +1124,12 @@ Module *module_new_from_iff_binary(GlobalContext *global, const void *iff_binary fprintf(stderr, "Unknown native code chunk version (%d)\n", ENDIAN_SWAP_16(native_code->version)); } else { for (int arch_index = 0; arch_index < ENDIAN_SWAP_16(native_code->architectures_count); arch_index++) { - uint16_t runtime_variant; + uint16_t runtime_variant = JIT_VARIANT_PIC; #ifdef AVM_USE_SINGLE_PRECISION - runtime_variant = JIT_VARIANT_FLOAT32 | JIT_VARIANT_PIC; -#else - runtime_variant = JIT_VARIANT_PIC; + runtime_variant |= JIT_VARIANT_FLOAT32; +#endif +#ifdef AVM_JIT_THUMB2 + runtime_variant |= JIT_VARIANT_THUMB2; #endif if (ENDIAN_SWAP_16(native_code->architectures[arch_index].architecture) == JIT_ARCH_TARGET && ENDIAN_SWAP_16(native_code->architectures[arch_index].variant) == runtime_variant) { size_t offset = ENDIAN_SWAP_32(native_code->info_size) + ENDIAN_SWAP_32(native_code->architectures[arch_index].offset) + sizeof(native_code->info_size); diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 6f413d0d9c..9dfc51b0f9 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -6325,11 +6325,14 @@ static term nif_jit_variant(Context *ctx, int argc, term argv[]) UNUSED(argc); UNUSED(argv); + int variant = JIT_VARIANT_PIC; #ifdef AVM_USE_SINGLE_PRECISION - return term_from_int(JIT_VARIANT_FLOAT32 | JIT_VARIANT_PIC); -#else - return term_from_int(JIT_VARIANT_PIC); + variant |= JIT_VARIANT_FLOAT32; +#endif +#ifdef AVM_JIT_THUMB2 + variant |= JIT_VARIANT_THUMB2; #endif + return term_from_int(variant); } #endif diff --git a/src/platforms/rp2/CMakeLists.txt b/src/platforms/rp2/CMakeLists.txt index 7ffe620a57..b2143ab4cd 100644 --- a/src/platforms/rp2/CMakeLists.txt +++ b/src/platforms/rp2/CMakeLists.txt @@ -75,11 +75,16 @@ option(AVM_REBOOT_ON_NOT_OK "Reboot Pico if result is not ok" OFF) option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) option(AVM_DISABLE_JIT "Disable just in time compilation." ON) option(AVM_PRINT_PROCESS_CRASH_DUMPS "Print crash reports when processes die with non-standard reasons" ON) -if(CMAKE_SYSTEM_PROCESSOR MATCHES "^cortex-m.+$") - # We only have armv6m for now, which all cortex-m should support +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^cortex-m0") + # Cortex-M0/M0+ (e.g. RP2040): ARMv6-M, Thumb-1 only if (NOT AVM_DISABLE_JIT) set(AVM_JIT_TARGET_ARCH "armv6m") endif() +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^cortex-m.+$") + # Cortex-M3/M4/M7/M33 (e.g. RP2350): ARMv7-M or ARMv8-M, Thumb-2 support + if (NOT AVM_DISABLE_JIT) + set(AVM_JIT_TARGET_ARCH "armv6m+thumb2") + endif() elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^hazard3$") # Pico2 RISC-V processor (Hazard3) if (NOT AVM_DISABLE_JIT) diff --git a/src/platforms/stm32/CMakeLists.txt b/src/platforms/stm32/CMakeLists.txt index c313411c30..ca5ce979a0 100644 --- a/src/platforms/stm32/CMakeLists.txt +++ b/src/platforms/stm32/CMakeLists.txt @@ -84,9 +84,6 @@ endif () mark_as_advanced(CMAKE_TOOLCHAIN_FILE) option(AVM_DISABLE_JIT "Disable just in time compilation." ON) -if (NOT AVM_DISABLE_JIT) - set(AVM_JIT_TARGET_ARCH "armv6m") -endif() if ((NOT ${CMAKE_C_COMPILER_ID} STREQUAL "GNU") OR (NOT ${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") OR @@ -131,6 +128,16 @@ set(HAVE_RMDIR "" CACHE INTERNAL "Have symbol rmdir" FORCE) # Include device detection (sets STM32_FAMILY, CPU flags, HAL defines, etc.) include(cmake/stm32_device.cmake) +# Set JIT target architecture based on detected CPU (needs STM32_CPU from stm32_device.cmake) +if (NOT AVM_DISABLE_JIT) + # Cortex-M0/M0+ are ARMv6-M (Thumb-1 only), all others support Thumb-2 + if (STM32_CPU MATCHES "cortex-m0") + set(AVM_JIT_TARGET_ARCH "armv6m") + else() + set(AVM_JIT_TARGET_ARCH "armv6m+thumb2") + endif() +endif() + # Include auto-device configuration (escript query for clock, ROM, etc.) include(cmake/atomvm_dev_config.cmake) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e274e4926a..3c7f880c49 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -102,6 +102,9 @@ target_link_libraries(test-enif PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX target_link_libraries(test-heap PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) # test-jit_stream_flash includes jit_stream_flash.c and provides its own mock platform implementation target_compile_definitions(test-jit_stream_flash PRIVATE TEST_JIT_STREAM_FLASH) +if (NOT AVM_DISABLE_JIT) + target_compile_definitions(test-erlang PRIVATE AVM_JIT_TARGET_ARCH_DIR="${AVM_JIT_TARGET_ARCH}") +endif() target_link_libraries(test-jit_stream_flash PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-mailbox PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-structs PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 34d02834f6..462cbff886 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -23,14 +23,21 @@ project (erlang_tests) macro(jit_precompile module_name) if(NOT AVM_DISABLE_JIT) + # Extract base architecture for module dependencies + string(REGEX REPLACE "\\+.*$" "" _jit_base_arch "${AVM_JIT_TARGET_ARCH}") set(jit_compiler_modules ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit.beam ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_precompile.beam ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_stream_binary.beam ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_dwarf.beam - ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_${AVM_JIT_TARGET_ARCH}.beam - ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_${AVM_JIT_TARGET_ARCH}_asm.beam + ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_${_jit_base_arch}.beam + ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_${_jit_base_arch}_asm.beam ) + if("${AVM_JIT_TARGET_ARCH}" MATCHES "thumb2") + list(APPEND jit_compiler_modules + ${CMAKE_BINARY_DIR}/libs/jit/src/beams/jit_armv7m_asm.beam + ) + endif() if (NOT AVM_DISABLE_JIT_DWARF) set(jit_precompile_dwarf_flag "dwarf") else() @@ -63,6 +70,7 @@ function(compile_erlang module_name) COMMAND erlc ${erlc_define} ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}.erl DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}.erl ${COMPILE_ERLANG_DEPENDS} COMMENT "Compiling ${module_name}.erl" + VERBATIM ) jit_precompile(${module_name}) endfunction() diff --git a/tests/libs/jit/CMakeLists.txt b/tests/libs/jit/CMakeLists.txt index 3607a0f65f..8ea06408da 100644 --- a/tests/libs/jit/CMakeLists.txt +++ b/tests/libs/jit/CMakeLists.txt @@ -34,6 +34,8 @@ set(ERLANG_MODULES jit_arm32_asm_tests jit_armv6m_tests jit_armv6m_asm_tests + jit_armv7m_tests + jit_armv7m_asm_tests jit_riscv32_tests jit_riscv32_asm_tests jit_riscv64_tests diff --git a/tests/libs/jit/jit_armv7m_asm_tests.erl b/tests/libs/jit/jit_armv7m_asm_tests.erl new file mode 100644 index 0000000000..c7f3be7a13 --- /dev/null +++ b/tests/libs/jit/jit_armv7m_asm_tests.erl @@ -0,0 +1,189 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(jit_armv7m_asm_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-define(_assertAsmEqual(Bin, Str, Value), + ?_assertEqual(jit_tests_common:asm(arm_thumb2, Bin, Str), Value) +). + +%% B.W (32-bit unconditional branch) tests +b_w_test_() -> + [ + % b.w .+0 (branch to self+4, offset 0) + ?_assertAsmEqual( + <<16#F000:16/little, 16#B800:16/little>>, + "b.w .+4", + jit_armv7m_asm:b_w(0) + ), + % b.w .+4 (forward offset 4) + ?_assertAsmEqual( + <<16#F000:16/little, 16#B802:16/little>>, + "b.w .+8", + jit_armv7m_asm:b_w(4) + ), + % b.w .-4 (backward offset -4) + ?_assertAsmEqual( + <<16#F7FF:16/little, 16#BFFE:16/little>>, + "b.w .", + jit_armv7m_asm:b_w(-4) + ), + % b.w .+256 + ?_assertAsmEqual( + <<16#F000:16/little, 16#B880:16/little>>, + "b.w .+260", + jit_armv7m_asm:b_w(256) + ), + % b.w .-256 + ?_assertAsmEqual( + <<16#F7FF:16/little, 16#BF80:16/little>>, + "b.w .-252", + jit_armv7m_asm:b_w(-256) + ), + % b.w .+4096 + ?_assertAsmEqual( + <<16#F001:16/little, 16#B800:16/little>>, + "b.w .+4100", + jit_armv7m_asm:b_w(4096) + ), + % b.w .-4096 + ?_assertAsmEqual( + <<16#F7FF:16/little, 16#B800:16/little>>, + "b.w .-4092", + jit_armv7m_asm:b_w(-4096) + ), + % b.w maximum positive offset (+16777214) + ?_assertAsmEqual( + <<16#F3FF:16/little, 16#97FF:16/little>>, + "b.w .+16777218", + jit_armv7m_asm:b_w(16777214) + ), + % b.w maximum negative offset (-16777216) + ?_assertAsmEqual( + <<16#F400:16/little, 16#9000:16/little>>, + "b.w .-16777212", + jit_armv7m_asm:b_w(-16777216) + ), + % b.w out of range (positive) + ?_assertError( + {unencodable_branch_offset, 16777216}, + jit_armv7m_asm:b_w(16777216) + ), + % b.w out of range (negative) + ?_assertError( + {unencodable_branch_offset, -16777218}, + jit_armv7m_asm:b_w(-16777218) + ), + % b.w odd offset + ?_assertError( + {unencodable_branch_offset, 3}, + jit_armv7m_asm:b_w(3) + ) + ]. + +%% MOVW (move 16-bit immediate to lower half) tests +movw_test_() -> + [ + % movw r0, #0 + ?_assertAsmEqual( + <<16#F240:16/little, 16#0000:16/little>>, + "movw r0, #0", + jit_armv7m_asm:movw(r0, 0) + ), + % movw r0, #1 + ?_assertAsmEqual( + <<16#F240:16/little, 16#0001:16/little>>, + "movw r0, #1", + jit_armv7m_asm:movw(r0, 1) + ), + % movw r0, #255 + ?_assertAsmEqual( + <<16#F240:16/little, 16#00FF:16/little>>, + "movw r0, #255", + jit_armv7m_asm:movw(r0, 255) + ), + % movw r0, #256 + ?_assertAsmEqual( + <<16#F240:16/little, 16#1000:16/little>>, + "movw r0, #256", + jit_armv7m_asm:movw(r0, 256) + ), + % movw r3, #0x1234 + ?_assertAsmEqual( + <<16#F241:16/little, 16#2334:16/little>>, + "movw r3, #0x1234", + jit_armv7m_asm:movw(r3, 16#1234) + ), + % movw r7, #0xFFFF + ?_assertAsmEqual( + <<16#F64F:16/little, 16#77FF:16/little>>, + "movw r7, #0xFFFF", + jit_armv7m_asm:movw(r7, 16#FFFF) + ), + % movw r12, #0xABCD + ?_assertAsmEqual( + <<16#F64A:16/little, 16#3CCD:16/little>>, + "movw r12, #0xABCD", + jit_armv7m_asm:movw(r12, 16#ABCD) + ), + % movw r1, #0x800 (test i bit) + ?_assertAsmEqual( + <<16#F640:16/little, 16#0100:16/little>>, + "movw r1, #2048", + jit_armv7m_asm:movw(r1, 2048) + ) + ]. + +%% MOVT (move 16-bit immediate to upper half) tests +movt_test_() -> + [ + % movt r0, #0 + ?_assertAsmEqual( + <<16#F2C0:16/little, 16#0000:16/little>>, + "movt r0, #0", + jit_armv7m_asm:movt(r0, 0) + ), + % movt r0, #1 + ?_assertAsmEqual( + <<16#F2C0:16/little, 16#0001:16/little>>, + "movt r0, #1", + jit_armv7m_asm:movt(r0, 1) + ), + % movt r3, #0x1234 + ?_assertAsmEqual( + <<16#F2C1:16/little, 16#2334:16/little>>, + "movt r3, #0x1234", + jit_armv7m_asm:movt(r3, 16#1234) + ), + % movt r7, #0xFFFF + ?_assertAsmEqual( + <<16#F6CF:16/little, 16#77FF:16/little>>, + "movt r7, #0xFFFF", + jit_armv7m_asm:movt(r7, 16#FFFF) + ), + % movt r12, #0xABCD + ?_assertAsmEqual( + <<16#F6CA:16/little, 16#3CCD:16/little>>, + "movt r12, #0xABCD", + jit_armv7m_asm:movt(r12, 16#ABCD) + ) + ]. diff --git a/tests/libs/jit/jit_armv7m_tests.erl b/tests/libs/jit/jit_armv7m_tests.erl new file mode 100644 index 0000000000..8efdcbb238 --- /dev/null +++ b/tests/libs/jit/jit_armv7m_tests.erl @@ -0,0 +1,83 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +%% @doc Backend-level tests for jit_armv6m with the thumb2 variant enabled. +%% These tests exercise Thumb-2 specific code paths in the backend: +%% jump table entries, mov_immediate, and branch generation. + +-module(jit_armv7m_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-include("jit/include/jit.hrl"). +-include("jit/src/term.hrl"). +-include("jit/src/default_atoms.hrl"). +-include("jit/src/primitives.hrl"). +-include("jit_tests_common.hrl"). + +-define(BACKEND, jit_armv6m). +-define(THUMB2_VARIANT, ?JIT_VARIANT_PIC bor ?JIT_VARIANT_THUMB2). + +%% Jump table entries are 6 bytes with Thumb-2 (push + b.w) +%% vs 12 bytes with Thumb-1 (ldr + push + add pc + nop + literal) +jump_table_thumb2_test() -> + State0 = ?BACKEND:new(?THUMB2_VARIANT, jit_stream_binary, jit_stream_binary:new(0)), + State1 = ?BACKEND:jump_table(State0, 512), + Stream = ?BACKEND:stream(State1), + ?assertEqual((512 + 1) * 6, byte_size(Stream)). + +%% mov_immediate with Thumb-2: values 256-65535 use movw (4 bytes) +%% instead of Thumb-1 movs+adds (4 bytes) or ldr from literal pool. +%% The third argument to call_primitive goes into r2. +mov_immediate_thumb2_movw_test() -> + State0 = ?BACKEND:new(?THUMB2_VARIANT, jit_stream_binary, jit_stream_binary:new(0)), + {State1, _ResultReg} = ?BACKEND:call_primitive(State0, 0, [ctx, jit_state, 1000]), + Stream = ?BACKEND:stream(State1), + MovwR2_1000 = jit_armv7m_asm:movw(r2, 1000), + ?assertNotEqual(nomatch, binary:match(Stream, MovwR2_1000)). + +%% mov_immediate with Thumb-2: 32-bit values use movw+movt (8 bytes) +%% instead of ldr from literal pool (2 bytes + 4 bytes data, needs pool flush). +mov_immediate_thumb2_movw_movt_test() -> + State0 = ?BACKEND:new(?THUMB2_VARIANT, jit_stream_binary, jit_stream_binary:new(0)), + {State1, _ResultReg} = ?BACKEND:call_primitive(State0, 0, [ctx, jit_state, 16#12345678]), + Stream = ?BACKEND:stream(State1), + MovwR2_Lo = jit_armv7m_asm:movw(r2, 16#5678), + MovtR2_Hi = jit_armv7m_asm:movt(r2, 16#1234), + MovwMovt = <>, + ?assertNotEqual(nomatch, binary:match(Stream, MovwMovt)). + +%% Verify that call_primitive produces the same instructions as Thumb-1 +%% for small values (movs path is shared) +call_primitive_0_thumb2_test() -> + State0 = ?BACKEND:new(?THUMB2_VARIANT, jit_stream_binary, jit_stream_binary:new(0)), + {State1, ResultReg} = ?BACKEND:call_primitive(State0, 0, [ctx, jit_state]), + ?assertEqual(r7, ResultReg), + Stream = ?BACKEND:stream(State1), + Dump = + << + " 0: 6817 ldr r7, [r2, #0]\n" + " 2: b405 push {r0, r2}\n" + " 4: 9902 ldr r1, [sp, #8]\n" + " 6: 47b8 blx r7\n" + " 8: 4607 mov r7, r0\n" + " a: bc05 pop {r0, r2}" + >>, + ?assertStream(arm_thumb2, Dump, Stream). diff --git a/tests/libs/jit/jit_tests_common.erl b/tests/libs/jit/jit_tests_common.erl index e7a847fbb9..42a7f5e045 100644 --- a/tests/libs/jit/jit_tests_common.erl +++ b/tests/libs/jit/jit_tests_common.erl @@ -88,6 +88,8 @@ find_binutils_beam(Arch) -> case Arch of riscv32 -> Prefixes0 ++ toolchain_prefixes(riscv64); + arm_thumb2 -> + Prefixes0 ++ toolchain_prefixes(arm); _ -> Prefixes0 end, @@ -121,6 +123,8 @@ get_asm_header(arm) -> ".arch armv6-m\n.thumb\n.syntax unified\n"; get_asm_header(arm32) -> ".arch armv6\n.arm\n.syntax unified\n"; +get_asm_header(arm_thumb2) -> + ".arch armv7-m\n.thumb\n.syntax unified\n"; get_asm_header(aarch64) -> ".text\n"; get_asm_header(x86_64) -> @@ -136,6 +140,8 @@ get_as_flags(arm) -> ""; get_as_flags(arm32) -> ""; +get_as_flags(arm_thumb2) -> + ""; get_as_flags(aarch64) -> ""; get_as_flags(x86_64) -> @@ -335,6 +341,8 @@ get_objdump_flags(x86_64) -> "-m i386:x86-64"; get_objdump_flags(riscv32) -> "-m riscv:rv32"; +get_objdump_flags(arm_thumb2) -> + "-marm --disassembler-options=force-thumb"; get_objdump_flags(riscv64) -> "-m riscv:rv64". diff --git a/tests/libs/jit/tests.erl b/tests/libs/jit/tests.erl index b6ae81b633..cc62a1cc28 100644 --- a/tests/libs/jit/tests.erl +++ b/tests/libs/jit/tests.erl @@ -34,6 +34,8 @@ start() -> jit_arm32_asm_tests, jit_armv6m_tests, jit_armv6m_asm_tests, + jit_armv7m_tests, + jit_armv7m_asm_tests, jit_riscv32_tests, jit_riscv32_asm_tests, jit_riscv64_tests, diff --git a/tests/test.c b/tests/test.c index c899fb74ce..3321fdf356 100644 --- a/tests/test.c +++ b/tests/test.c @@ -738,39 +738,10 @@ int test_modules_execution(bool beam, bool skip, int count, char **item) } #ifndef AVM_NO_JIT if (!beam) { -#if JIT_ARCH_TARGET == JIT_ARCH_X86_64 - if (chdir("x86_64") != 0) { - perror("Error: cannot find x86_64 directory"); + if (chdir(AVM_JIT_TARGET_ARCH_DIR) != 0) { + perror("Error: cannot find " AVM_JIT_TARGET_ARCH_DIR " directory"); return EXIT_FAILURE; } -#elif JIT_ARCH_TARGET == JIT_ARCH_AARCH64 - if (chdir("aarch64") != 0) { - perror("Error: cannot find aarch64 directory"); - return EXIT_FAILURE; - } -#elif JIT_ARCH_TARGET == JIT_ARCH_ARMV6M - if (chdir("armv6m") != 0) { - perror("Error: cannot find armv6m directory"); - return EXIT_FAILURE; - } -#elif JIT_ARCH_TARGET == JIT_ARCH_ARM32 - if (chdir("arm32") != 0) { - perror("Error: cannot find arm32 directory"); - return EXIT_FAILURE; - } -#elif JIT_ARCH_TARGET == JIT_ARCH_RISCV32 - if (chdir("riscv32") != 0) { - perror("Error: cannot find riscv32 directory"); - return EXIT_FAILURE; - } -#elif JIT_ARCH_TARGET == JIT_ARCH_RISCV64 - if (chdir("riscv64") != 0) { - perror("Error: cannot find riscv64 directory"); - return EXIT_FAILURE; - } -#else -#error Unknown JIT target -#endif } #endif