py: Add AArch64 native emitter and inline assembler. by StrideZhou · Pull Request #19697 · micropython/micropython · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ports_qemu.yml
21 changes: 21 additions & 0 deletions .github/workflows/ports_unix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,27 @@ jobs:
if: failure()
run: tests/run-tests.py --print-failures

qemu_aarch64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
# Python 3.12 is the default for ubuntu-24.04, but that has compatibility issues with settrace tests.
# Can remove this step when ubuntu-latest uses a more recent Python 3.x as the default.
with:
python-version: '3.11'
- name: Install packages
run: tools/ci.sh unix_qemu_aarch64_setup
- name: Build
run: tools/ci.sh unix_qemu_aarch64_build
- name: Run main test suite
run: tools/ci.sh unix_qemu_aarch64_run_tests
- name: Run gcov coverage analysis
run: tools/ci.sh unix_qemu_aarch64_run_coverage
- name: Print failures
if: failure()
run: tests/run-tests.py --print-failures

sanitize_address:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ primarily for maintenance, development and testing:
to another microcontroller.

- The [qemu](ports/qemu) port is a QEMU-based emulated target for Cortex-A,
Cortex-M, RISC-V 32-bit, RISC-V 64-bit, and PowerPC 64-bit architectures.
Cortex-M, RISC-V 32-bit, RISC-V 64-bit, PowerPC 64-bit and AArch64 architectures.

The MicroPython cross-compiler, mpy-cross
-----------------------------------------
Expand Down
171 changes: 171 additions & 0 deletions docs/reference/asm_aarch64.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
.. _asm_aarch64:

Inline assembler for AArch64
============================

This document assumes some familiarity with assembly language programming
and with the ARM 64-bit (AArch64/ARMv8-A) instruction set. For a detailed
description of the instruction set consult the *Arm Architecture Reference
Manual for A-profile architecture*.

The inline assembler is enabled on AArch64 targets (for example the unix
port running on a 64-bit ARM system, or the ``VIRT_AARCH64`` boards of the
``ports/qemu`` port) and is used via the ``@micropython.asm_aarch64``
decorator::

@micropython.asm_aarch64
def add1(x0) -> int:
add(x0, x0, 1)

print(add1(1)) # prints 2

Instructions are written as Python function calls; operands are passed as
arguments. The syntax tries to be as close as possible to that defined in
the ARM manual, converted to Python function calls. Because ``and`` is a
Python keyword, the AND instruction is spelled ``and_``.

Registers and calling convention
--------------------------------

All 31 general purpose registers are available as ``x0``-``x30``, plus
``sp`` and ``lr`` (an alias for ``x30``). All instructions operate on 64-bit
values (the ``x`` registers); the 32-bit ``w`` registers are not accessible
from the inline assembler.

A function may take up to four parameters, which must be named ``x0``,
``x1``, ``x2``, ``x3`` in sequence; the arguments passed by the caller are
available in those registers. Unless another return type is annotated, the
value in ``x0`` when control reaches the end of the function body is treated
as a signed integer result.

``sp`` is only accepted as an operand of ``push`` and ``pop``; every other
instruction expects ``x0``-``x30`` (or ``lr``). Adjust the stack with those
two operations rather than with ``add``/``sub``.

.. warning::

Do not use ``ret()`` to return from the assembly function itself. Leave
the value in ``x0`` and let control fall off the end of the body, so that
the automatically generated epilogue runs. A ``ret()`` at the top level
skips that epilogue, which leaves the stack pointer 96 bytes too low and
``x19``-``x28`` unrestored, corrupting the caller. ``ret()`` is only for
returning from a subroutine reached with ``bl()``, as in the example
below.

The function wrapper saves and restores all callee-saved registers
(``x19``-``x28``) as well as the frame pointer and link register, so user
assembly code may freely use any register. Note that the compiler may use
``x9``/``x10`` as scratch registers when materialising a large immediate
for ``and_``/``eor``/``orr`` that cannot be encoded directly (see below);
this never affects the operands of the instruction being assembled.

Instructions
------------

The following subset of the AArch64 instruction set is supported.

Arithmetic and logical
~~~~~~~~~~~~~~~~~~~~~~

* ``add(Rd, Rn, Rm)`` / ``add(Rd, Rn, imm)`` — ``Rd = Rn + Rm`` or ``Rd = Rn + imm``
* ``sub(Rd, Rn, Rm)`` / ``sub(Rd, Rn, imm)`` — ``Rd = Rn - Rm`` or ``Rd = Rn - imm``
* ``mul(Rd, Rn, Rm)`` — ``Rd = Rn * Rm`` (signed and unsigned, low 64 bits)
* ``and_(Rd, Rn, Rm)`` / ``and_(Rd, Rn, imm)`` — bitwise AND
* ``orr(Rd, Rn, Rm)`` / ``orr(Rd, Rn, imm)`` — bitwise OR
* ``eor(Rd, Rn, Rm)`` / ``eor(Rd, Rn, imm)`` — bitwise exclusive OR
* ``mvn(Rd, Rm)`` — bitwise NOT
* ``cmp(Rn, Rm)`` / ``cmp(Rn, imm)`` — compare (sets the condition flags)
* ``mov(Rd, Rn)`` / ``mov(Rd, imm)`` — move register or 64-bit immediate

For ``add``/``sub``/``cmp`` the immediate must be a 12-bit value
(``0``-``0xfff``), optionally shifted left by 12 bits (i.e. a multiple of
4096 up to ``0xfff000``).

For ``and_``/``eor``/``orr`` any 64-bit immediate is accepted. Immediates
that match the ARM "bitmask immediate" encoding (a rotated run of one bits
replicated across the register, e.g. ``0xff``, ``0xf0``, ``0xffffffff``,
``~0xf``) are emitted as a single instruction; other values are
materialised in a scratch register first.

For ``mov`` any 64-bit immediate is accepted; the shortest ``MOVZ``/``MOVN``/
``MOVK`` sequence is generated.

Shifts
~~~~~~

* ``lsl(Rd, Rn, Rm)`` / ``lsl(Rd, Rn, imm)`` — logical shift left
* ``lsr(Rd, Rn, Rm)`` / ``lsr(Rd, Rn, imm)`` — logical shift right
* ``asr(Rd, Rn, Rm)`` / ``asr(Rd, Rn, imm)`` — arithmetic shift right

The immediate shift amount must be in the range 0-63.

Load and store
~~~~~~~~~~~~~~

* ``ldr(Rt, [Rn, off])`` — load 64-bit word; ``off`` must be a multiple of 8, in 0-32760
* ``ldrh(Rt, [Rn, off])`` — load 16-bit halfword (zero extend); ``off`` must be a multiple of 2, in 0-8190
* ``ldrb(Rt, [Rn, off])`` — load byte (zero extend); ``off`` in 0-4095
* ``str(Rt, [Rn, off])`` / ``strh(Rt, [Rn, off])`` / ``strb(Rt, [Rn, off])`` — the store equivalents

The offset is a constant in bytes; unaligned offsets for ``ldr``/``str``/
``ldrh``/``strh`` are a compile-time error.

Branches and labels
~~~~~~~~~~~~~~~~~~~

* ``label(NAME)`` — define a label
* ``b(label)`` — unconditional branch
* ``bl(label)`` — branch with link (call); return with ``ret(lr)``
* ``b<cc>(label)`` — conditional branch, where ``<cc>`` is one of:
``eq``, ``ne``, ``cs``, ``cc``, ``mi``, ``pl``, ``vs``, ``vc``, ``hi``,
``ls``, ``ge``, ``lt``, ``gt``, ``le``. ``bhs`` and ``blo`` are accepted as
aliases for ``bcs`` and ``bcc``. The condition code is written directly
after the ``b`` with no separator, because ``b.eq`` is not a valid Python
identifier.
* ``ret(Rn)`` — return to the address in ``Rn`` (usually ``lr``)
* ``nop()``, ``wfi()``, ``bkpt()`` — no operation, wait for interrupt,
breakpoint (``BRK #0``)

``b`` and ``bl`` have a native range of ±128MB and are always a single
instruction. ``b<cc>`` has a native range of only ±1MB, so a conditional
branch that is forwards, or backwards beyond ±1MB, is relaxed automatically to
a two-instruction sequence (an inverted ``b<cc>`` over an unconditional ``b``),
which also gives it a ±128MB range. Because the relaxation is decided from
the branch direction rather than from whether the label is known yet, the
generated code has the same size in every compiler pass.

Stack operations
~~~~~~~~~~~~~~~~

* ``push({Ra, Rb, ...})`` — store the listed registers on the stack
(decrementing ``sp``; an odd number of registers is padded with a dummy
slot to keep ``sp`` 16-byte aligned)
* ``pop({Ra, Rb, ...})`` — the inverse of ``push``

Example::

@micropython.asm_aarch64
def call_helper(x0) -> int:
push({lr})
bl(double)
pop({lr})
ret(lr)
label(double)
add(x0, x0, x0)
ret(lr)

Limitations
-----------

- Register-offset addressing (e.g. ``ldr(x0, [x1, x2])``) is not
supported; only constant offsets. Pre-indexed (``[x1, #8]!``) and
post-indexed (``[x1], #8``) forms cannot be written in Python at all.
- ``push`` and ``pop`` round an odd number of registers up to an even number of
stack slots, to keep ``sp`` 16-byte aligned. The padding slot is written
with, and read back into, the zero register, so it never disturbs ``sp``.
Matching ``push``/``pop`` lists therefore keep the stack balanced as long as
they round to the same number of slots.
- Floating point (SIMD/FP) registers and instructions are not supported.
- There is currently no ``.mpy`` architecture ID for AArch64, so inline
assembler functions cannot be frozen into ``.mpy`` files or loaded from
them; they are compiled at runtime only.
1 change: 1 addition & 0 deletions docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ implementation and the best practices to use them.
manifest.rst
packages.rst
asm_thumb2_index.rst
asm_aarch64.rst
filesystem.rst
romfs.rst
unicode_support.rst
Expand Down
12 changes: 12 additions & 0 deletions lib/libm/aarch64_sqrtf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// An implementation of sqrtf for AArch64 using hardware fsqrt instruction.

#include <math.h>

float sqrtf(float x) {
float ret;
__asm__ volatile (
"fsqrt %s0, %s1\n"
: "=w" (ret)
: "w" (x));
return ret;
}
10 changes: 10 additions & 0 deletions lib/libm_dbl/aarch64_sqrt.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// An implementation of sqrt for AArch64 using hardware fsqrt instruction.

double sqrt(double x) {
double ret;
__asm__ volatile (
"fsqrt %d0, %d1\n"
: "=w" (ret)
: "w" (x));
return ret;
}
38 changes: 38 additions & 0 deletions ports/qemu/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ MICROPY_HEAP_SIZE ?= 204800
MICROPY_STACK_SIZE ?= 20480
FROZEN_MANIFEST ?= "require('unittest'); freeze('test-frzmpy', ('frozen_const.py',))"
endif
ifeq ($(QEMU_ARCH),aarch64)
MICROPY_HEAP_SIZE ?= 1048576
MICROPY_STACK_SIZE ?= 20480
FROZEN_MANIFEST ?= "require('unittest'); freeze('test-frzmpy', ('frozen_const.py',))"
endif

MICROPY_FLOAT_IMPL ?= float

Expand Down Expand Up @@ -195,6 +200,31 @@ SRC_BOARD_O += mcu/ppc64/head.o

endif

################################################################################
# AArch64 specific settings

ifeq ($(QEMU_ARCH),aarch64)

CROSS_COMPILE ?= aarch64-none-elf-

LDFLAGS += -nostdlib
LIBS = $(shell $(CC) $(CFLAGS) -print-libgcc-file-name)

QEMU_ARGS += -cpu max -semihosting

AFLAGS += -march=armv8-a
CFLAGS += $(AFLAGS) -mstrict-align -fno-stack-protector -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0

SRC_C += \
mcu/aarch64/errorhandler.c \
mcu/aarch64/startup.c \
mcu/aarch64/ticks.c \
shared/runtime/semihosting_aarch64.c \

SRC_BOARD_O += mcu/aarch64/entrypoint.o

endif

################################################################################
# Project specific settings and compiler/linker flags

Expand Down Expand Up @@ -300,15 +330,23 @@ SRC_C += \
ifeq ($(MICROPY_FLOAT_IMPL),double)
LIBM_SRC_C += $(SRC_LIB_LIBM_DBL_C)
ifeq ($(SUPPORTS_HARDWARE_FP_DOUBLE),1)
ifeq ($(QEMU_ARCH),aarch64)
LIBM_SRC_C += lib/libm_dbl/aarch64_sqrt.c
else
LIBM_SRC_C += $(SRC_LIB_LIBM_DBL_SQRT_HW_C)
endif
else
LIBM_SRC_C += $(SRC_LIB_LIBM_DBL_SQRT_SW_C)
endif
else
ifeq ($(MICROPY_FLOAT_IMPL),float)
LIBM_SRC_C += $(SRC_LIB_LIBM_C)
ifeq ($(SUPPORTS_HARDWARE_FP_SINGLE),1)
ifeq ($(QEMU_ARCH),aarch64)
LIBM_SRC_C += lib/libm/aarch64_sqrtf.c
else
LIBM_SRC_C += $(SRC_LIB_LIBM_SQRT_HW_C)
endif
else
LIBM_SRC_C += $(SRC_LIB_LIBM_SQRT_SW_C)
endif
Expand Down
36 changes: 23 additions & 13 deletions ports/qemu/README.md
Loading
Loading