From cb2b54a924d125bdcf4d65c064d208917d83c003 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 11 Sep 2026 20:42:46 -0500 Subject: [PATCH 1/2] lib/aeabi_div: Add compact integer division helpers. Cortex-M0 has no divide instruction, so every 32-bit integer division calls a libgcc helper. Since GCC 11, the v6-m libgcc only ships speed-optimized helpers: __udivsi3 is fully unrolled and __divsi3 contains its own second copy of the unsigned core, together around 730 bytes. Provide small loop-based replacements that produce identical results, including on division by zero, and enable them on the Move Hub and City Hub. This saves around 640 bytes on each, at the cost of roughly 3 to 5 times slower division, which stays well below 1% of the CPU for the heaviest user (motor control). The new file is compiled without LTO because the compiler generates references to these helpers during LTO code generation, after LTO symbol resolution, so definitions inside an LTO object fail to link. --- .github/workflows/format.yml | 2 + bricks/_common/common.mk | 9 +++- bricks/_common/sources.mk | 6 ++- bricks/cityhub/Makefile | 1 + bricks/movehub/Makefile | 1 + lib/aeabi_div/aeabi_div.c | 80 ++++++++++++++++++++++++++++++++++++ tools/codeformat.py | 1 + 7 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 lib/aeabi_div/aeabi_div.c diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 05e060997..6def683dc 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -7,6 +7,7 @@ on: paths: - ".github/workflows/format.yml" - "bricks/**/*.[ch]" + - "lib/aeabi_div/**/*.[ch]" - "lib/pbio/**/*.[ch]" - "py/*.[ch]" - "pybricks/**/*.[ch]" @@ -17,6 +18,7 @@ on: paths: - ".github/workflows/format.yml" - "bricks/**/*.[ch]" + - "lib/aeabi_div/**/*.[ch]" - "lib/pbio/**/*.[ch]" - "py/*.[ch]" - "pybricks/**/*.[ch]" diff --git a/bricks/_common/common.mk b/bricks/_common/common.mk index 95b46faf5..a13c0b375 100644 --- a/bricks/_common/common.mk +++ b/bricks/_common/common.mk @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2013, 2014 Damien P. George -# Copyright (c) 2019-2023 The Pybricks Authors +# Copyright (c) 2019-2026 The Pybricks Authors # This file is shared by all bare-metal Arm Pybricks ports. @@ -666,6 +666,13 @@ OBJ += $(addprefix $(BUILD)/, $(PBIO_SRC_C:.c=.o)) OBJ += $(addprefix $(BUILD)/, $(LEGO_SPEC_SRC_C:.c=.o)) OBJ += $(addprefix $(BUILD)/, $(SRC_LIBM:.c=.o)) +ifeq ($(PB_LIB_AEABI_DIV),1) +OBJ += $(addprefix $(BUILD)/, $(AEABI_DIV_SRC_C:.c=.o)) +# Overriding the libgcc division helpers only works when they are not +# compiled with LTO. See lib/aeabi_div/aeabi_div.c. +$(BUILD)/lib/aeabi_div/%.o: CFLAGS += -fno-lto +endif + ifeq ($(PB_LIB_BLUENRG),1) OBJ += $(addprefix $(BUILD)/, $(BLUENRG_SRC_C:.c=.o)) endif diff --git a/bricks/_common/sources.mk b/bricks/_common/sources.mk index 1f1215901..db2b53fe8 100644 --- a/bricks/_common/sources.mk +++ b/bricks/_common/sources.mk @@ -1,8 +1,12 @@ # SPDX-License-Identifier: MIT -# Copyright (c) 2019-2023 The Pybricks Authors +# Copyright (c) 2019-2026 The Pybricks Authors # This file contains the sources common to all Pybricks MicroPython ports. +# Compact AEABI division helpers + +AEABI_DIV_SRC_C = lib/aeabi_div/aeabi_div.c + # Ring buffer LWRB_SRC_C = lib/lwrb/src/lwrb/lwrb.c diff --git a/bricks/cityhub/Makefile b/bricks/cityhub/Makefile index feb1254cd..6a0283ef7 100644 --- a/bricks/cityhub/Makefile +++ b/bricks/cityhub/Makefile @@ -6,6 +6,7 @@ PB_MCU_FAMILY = STM32 PB_MCU_SERIES = F0 PB_CMSIS_MCU = STM32F030xC PB_MCU_EXT_OSC_HZ = 0 # uses internal oscillator +PB_LIB_AEABI_DIV = 1 PB_LIB_STM32_HAL = 1 PB_LIB_BLE5STACK = 1 PB_FROZEN_MODULES = 1 diff --git a/bricks/movehub/Makefile b/bricks/movehub/Makefile index 6f7df12cf..110dec053 100644 --- a/bricks/movehub/Makefile +++ b/bricks/movehub/Makefile @@ -6,6 +6,7 @@ PB_MCU_FAMILY = STM32 PB_MCU_SERIES = F0 PB_CMSIS_MCU = STM32F070xB PB_MCU_EXT_OSC_HZ = 0 # uses internal oscillator +PB_LIB_AEABI_DIV = 1 PB_LIB_STM32_HAL = 1 PB_LIB_BLUENRG = 1 PB_FROZEN_MODULES = 0 diff --git a/lib/aeabi_div/aeabi_div.c b/lib/aeabi_div/aeabi_div.c new file mode 100644 index 000000000..f340dc131 --- /dev/null +++ b/lib/aeabi_div/aeabi_div.c @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 The Pybricks Authors + +// Compact replacements for the AEABI integer division helpers. +// +// Cortex-M0 has no divide instruction, so the compiler turns every 32-bit +// integer division into a call to one of the helpers below. Since GCC 11, the +// v6-m libgcc only ships speed-optimized versions: __udivsi3 is fully +// unrolled and __divsi3 contains its own second copy of the unsigned core +// instead of sharing it, which together take around 730 bytes. The loop-based +// versions here produce identical results in around 110 bytes. +// +// The cost is speed: roughly 3 to 5 times slower per division. The heaviest +// user is motor control with around 10 divisions per 5 ms loop, which stays +// well below 1% of the CPU either way, in line with these hubs already +// trading speed for size elsewhere (CSUPEROPT = -Os). +// +// Defining these symbols in an object file makes the linker prefer them over +// the libgcc archive members. This file must be compiled without LTO: the +// compiler generates references to these helpers during LTO code generation, +// after LTO symbol resolution, so definitions inside an LTO object fail to +// link. +// +// Division by zero returns a quotient of 0 and a remainder equal to the +// dividend, the same as libgcc's default __aeabi_idiv0 behavior. + +#include + +// The *divmod helpers return the quotient in r0 and the remainder in r1, +// which matches how a uint64_t is returned (low word in r0, high in r1). + +uint64_t __aeabi_uidivmod(uint32_t n, uint32_t d) { + if (d == 0) { + return (uint64_t)n << 32; + } + + // Scale the divisor up to the dividend, then do schoolbook long division + // in base 2, one quotient bit per iteration. + uint32_t q = 0; + uint32_t bit = 1; + while (d < n && !(d & UINT32_C(0x80000000))) { + d <<= 1; + bit <<= 1; + } + while (bit) { + if (n >= d) { + n -= d; + q |= bit; + } + d >>= 1; + bit >>= 1; + } + return ((uint64_t)n << 32) | q; +} + +uint32_t __aeabi_uidiv(uint32_t n, uint32_t d) { + return (uint32_t)__aeabi_uidivmod(n, d); +} +uint32_t __udivsi3(uint32_t n, uint32_t d) __attribute__((alias("__aeabi_uidiv"))); + +uint64_t __aeabi_idivmod(int32_t n, int32_t d) { + uint64_t res = __aeabi_uidivmod(n < 0 ? -(uint32_t)n : (uint32_t)n, d < 0 ? -(uint32_t)d : (uint32_t)d); + uint32_t q = (uint32_t)res; + uint32_t r = (uint32_t)(res >> 32); + + // C99 division truncates toward zero: the quotient is negative when the + // signs differ and the remainder takes the sign of the dividend. + if ((n ^ d) < 0) { + q = -q; + } + if (n < 0) { + r = -r; + } + return ((uint64_t)r << 32) | q; +} + +int32_t __aeabi_idiv(int32_t n, int32_t d) { + return (int32_t)__aeabi_idivmod(n, d); +} +int32_t __divsi3(int32_t n, int32_t d) __attribute__((alias("__aeabi_idiv"))); diff --git a/tools/codeformat.py b/tools/codeformat.py index 3f508b130..ffa1b00ae 100755 --- a/tools/codeformat.py +++ b/tools/codeformat.py @@ -18,6 +18,7 @@ codeformat.PATHS = [ "bricks/**/*.[ch]", + "lib/aeabi_div/**/*.[ch]", "lib/pbio/**/*.[ch]", "lib/lego/**/*.[ch]", "py/*.[ch]", From 3847bd7d276346941e4e774dea6ccdb41586fe5c Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 11 Sep 2026 20:50:54 -0500 Subject: [PATCH 2/2] .github/workflows: Add missing lib/lego path to format triggers. The path filters in format.yml are supposed to match the paths in tools/codeformat.py, but lib/lego was missing, so a change touching only lib/lego headers did not trigger the format check. --- .github/workflows/format.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 6def683dc..e559a7237 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -8,6 +8,7 @@ on: - ".github/workflows/format.yml" - "bricks/**/*.[ch]" - "lib/aeabi_div/**/*.[ch]" + - "lib/lego/**/*.[ch]" - "lib/pbio/**/*.[ch]" - "py/*.[ch]" - "pybricks/**/*.[ch]" @@ -19,6 +20,7 @@ on: - ".github/workflows/format.yml" - "bricks/**/*.[ch]" - "lib/aeabi_div/**/*.[ch]" + - "lib/lego/**/*.[ch]" - "lib/pbio/**/*.[ch]" - "py/*.[ch]" - "pybricks/**/*.[ch]"