From 99b1e14879273ddc5d406ae35f67ca1f9124cfb3 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Tue, 17 Feb 2026 10:09:52 -0800 Subject: [PATCH] Add support for fixed Zephyr displays This adds zephyr_display. It acts similar to BusDisplay because we write regions to update to Zephyr. --- .../actions/deps/ports/zephyr-cp/action.yml | 6 +- AGENTS.md | 1 + locale/circuitpython.pot | 16 +- ports/zephyr-cp/CLAUDE.md | 1 + ports/zephyr-cp/Makefile | 28 +- ports/zephyr-cp/README.md | 30 ++ .../bindings/zephyr_display/Display.c | 195 ++++++++ .../bindings/zephyr_display/Display.h | 37 ++ .../bindings/zephyr_display/__init__.c | 24 + .../bindings/zephyr_display/__init__.h | 7 + ports/zephyr-cp/boards/board_aliases.cmake | 1 + ports/zephyr-cp/boards/ek_ra8d1.conf | 3 + .../native/native_sim/autogen_board_info.toml | 3 +- .../nrf5340bsim/autogen_board_info.toml | 1 + ports/zephyr-cp/boards/native_sim.conf | 4 + .../nordic/nrf5340dk/autogen_board_info.toml | 1 + .../nordic/nrf54h20dk/autogen_board_info.toml | 1 + .../nordic/nrf54l15dk/autogen_board_info.toml | 1 + .../nordic/nrf7002dk/autogen_board_info.toml | 1 + .../nxp/frdm_mcxn947/autogen_board_info.toml | 1 + .../nxp/frdm_rw612/autogen_board_info.toml | 1 + .../mimxrt1170_evk/autogen_board_info.toml | 1 + .../da14695_dk_usb/autogen_board_info.toml | 1 + .../renesas/ek_ra6m5/autogen_board_info.toml | 1 + .../renesas/ek_ra8d1/autogen_board_info.toml | 3 +- .../renesas/ek_ra8d1/circuitpython.toml | 1 + .../nucleo_n657x0_q/autogen_board_info.toml | 1 + .../nucleo_u575zi_q/autogen_board_info.toml | 1 + .../st/stm32h750b_dk/autogen_board_info.toml | 116 +++++ .../st/stm32h750b_dk/circuitpython.toml | 1 + .../st/stm32h7b3i_dk/autogen_board_info.toml | 5 +- .../stm32wba65i_dk1/autogen_board_info.toml | 1 + ...m32h750b_dk_stm32h750xx_ext_flash_app.conf | 2 + ...h750b_dk_stm32h750xx_ext_flash_app.overlay | 14 + ports/zephyr-cp/boards/stm32h7b3i_dk.conf | 2 + ports/zephyr-cp/boards/stm32h7b3i_dk.overlay | 22 + .../common-hal/zephyr_display/Display.c | 446 ++++++++++++++++++ .../common-hal/zephyr_display/Display.h | 31 ++ ports/zephyr-cp/cptools/board_tools.py | 27 ++ .../zephyr-cp/cptools/build_circuitpython.py | 3 + .../zephyr-cp/cptools/get_west_shield_args.py | 70 +++ .../cptools/pre_zephyr_build_prep.py | 8 +- ports/zephyr-cp/cptools/zephyr2cp.py | 48 ++ ports/zephyr-cp/debug.conf | 15 + ports/zephyr-cp/tests/__init__.py | 10 + ports/zephyr-cp/tests/conftest.py | 153 ++++-- ports/zephyr-cp/tests/perfetto_input_trace.py | 85 ++-- .../zephyr-cp/tests/zephyr_display/README.md | 45 ++ .../golden/color_gradient_320x240.png | Bin 0 -> 12102 bytes .../golden/color_gradient_320x240_AL_88.png | Bin 0 -> 17323 bytes .../color_gradient_320x240_ARGB_8888.png | Bin 0 -> 1024 bytes .../golden/color_gradient_320x240_BGR_565.png | Bin 0 -> 12102 bytes .../golden/color_gradient_320x240_L_8.png | Bin 0 -> 17323 bytes .../golden/color_gradient_320x240_MONO01.png | Bin 0 -> 1516 bytes .../golden/color_gradient_320x240_MONO10.png | Bin 0 -> 1516 bytes .../golden/color_gradient_320x240_RGB_565.png | Bin 0 -> 12102 bytes .../golden/color_gradient_320x240_RGB_888.png | Bin 0 -> 15287 bytes .../terminal_console_output_320x240.mask.png | Bin 0 -> 450 bytes .../terminal_console_output_320x240.png | Bin 0 -> 3930 bytes .../terminal_console_output_320x240_AL_88.png | Bin 0 -> 3900 bytes ...minal_console_output_320x240_ARGB_8888.png | Bin 0 -> 1024 bytes ...erminal_console_output_320x240_BGR_565.png | Bin 0 -> 3930 bytes .../terminal_console_output_320x240_L_8.png | Bin 0 -> 3900 bytes ...terminal_console_output_320x240_MONO01.png | Bin 0 -> 3655 bytes ...onsole_output_320x240_MONO01_no_vtiled.png | Bin 0 -> 3655 bytes ...terminal_console_output_320x240_MONO10.png | Bin 0 -> 3655 bytes ...onsole_output_320x240_MONO10_no_vtiled.png | Bin 0 -> 3655 bytes ...erminal_console_output_320x240_RGB_565.png | Bin 0 -> 3930 bytes ...erminal_console_output_320x240_RGB_888.png | Bin 0 -> 3930 bytes .../zephyr_display/test_zephyr_display.py | 365 ++++++++++++++ ports/zephyr-cp/zephyr-config/west.yml | 2 +- py/circuitpy_mpconfig.mk | 3 + shared-module/displayio/__init__.c | 19 + shared-module/displayio/__init__.h | 6 + 74 files changed, 1797 insertions(+), 74 deletions(-) create mode 100644 ports/zephyr-cp/CLAUDE.md create mode 100644 ports/zephyr-cp/bindings/zephyr_display/Display.c create mode 100644 ports/zephyr-cp/bindings/zephyr_display/Display.h create mode 100644 ports/zephyr-cp/bindings/zephyr_display/__init__.c create mode 100644 ports/zephyr-cp/bindings/zephyr_display/__init__.h create mode 100644 ports/zephyr-cp/boards/ek_ra8d1.conf create mode 100644 ports/zephyr-cp/boards/st/stm32h750b_dk/autogen_board_info.toml create mode 100644 ports/zephyr-cp/boards/st/stm32h750b_dk/circuitpython.toml create mode 100644 ports/zephyr-cp/boards/stm32h750b_dk_stm32h750xx_ext_flash_app.conf create mode 100644 ports/zephyr-cp/boards/stm32h750b_dk_stm32h750xx_ext_flash_app.overlay create mode 100644 ports/zephyr-cp/boards/stm32h7b3i_dk.conf create mode 100644 ports/zephyr-cp/common-hal/zephyr_display/Display.c create mode 100644 ports/zephyr-cp/common-hal/zephyr_display/Display.h create mode 100644 ports/zephyr-cp/cptools/get_west_shield_args.py create mode 100644 ports/zephyr-cp/debug.conf create mode 100644 ports/zephyr-cp/tests/zephyr_display/README.md create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_AL_88.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_ARGB_8888.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_BGR_565.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_L_8.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_MONO01.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_MONO10.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_RGB_565.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_RGB_888.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240.mask.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_AL_88.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_ARGB_8888.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_BGR_565.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_L_8.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO01.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO01_no_vtiled.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO10.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO10_no_vtiled.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_RGB_565.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_RGB_888.png create mode 100644 ports/zephyr-cp/tests/zephyr_display/test_zephyr_display.py diff --git a/.github/actions/deps/ports/zephyr-cp/action.yml b/.github/actions/deps/ports/zephyr-cp/action.yml index 5f52cc7f0c259..8f67998a55499 100644 --- a/.github/actions/deps/ports/zephyr-cp/action.yml +++ b/.github/actions/deps/ports/zephyr-cp/action.yml @@ -3,11 +3,13 @@ name: Fetch Zephyr port deps runs: using: composite steps: - - name: Get libusb and mtools + - name: Get Linux build dependencies if: runner.os == 'Linux' run: | + sudo dpkg --add-architecture i386 + export PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig sudo apt-get update - sudo apt-get install -y libusb-1.0-0-dev libudev-dev mtools + sudo apt-get install -y libusb-1.0-0-dev libudev-dev pkg-config libsdl2-dev:i386 libsdl2-image-dev:i386 mtools shell: bash - name: Setup Zephyr project uses: zephyrproject-rtos/action-zephyr-setup@v1 diff --git a/AGENTS.md b/AGENTS.md index 145e31c127159..e7d2fdbbe8caf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,4 @@ - Capture CircuitPython output by finding the matching device in `/dev/serial/by-id` - You can mount the CIRCUITPY drive by doing `udisksctl mount -b /dev/disk/by-label/CIRCUITPY` and access it via `/run/media//CIRCUITPY`. - `circup` is a command line tool to install libraries and examples to CIRCUITPY. +- When connecting to serial devices on Linux use /dev/serial/by-id. These will be more stable than /dev/ttyACM*. diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index e2eebdea0fe04..2edd3347e280b 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -165,8 +165,8 @@ msgstr "" msgid "%q must be %d" msgstr "" -#: py/argcheck.c shared-bindings/busdisplay/BusDisplay.c -#: shared-bindings/displayio/Bitmap.c +#: ports/zephyr-cp/bindings/zephyr_display/Display.c py/argcheck.c +#: shared-bindings/busdisplay/BusDisplay.c shared-bindings/displayio/Bitmap.c #: shared-bindings/framebufferio/FramebufferDisplay.c #: shared-bindings/is31fl3741/FrameBuffer.c #: shared-bindings/rgbmatrix/RGBMatrix.c @@ -458,6 +458,7 @@ msgstr "" msgid ", in %q\n" msgstr "" +#: ports/zephyr-cp/bindings/zephyr_display/Display.c #: shared-bindings/busdisplay/BusDisplay.c #: shared-bindings/epaperdisplay/EPaperDisplay.c #: shared-bindings/framebufferio/FramebufferDisplay.c @@ -646,6 +647,7 @@ msgstr "" msgid "Baudrate not supported by peripheral" msgstr "" +#: ports/zephyr-cp/common-hal/zephyr_display/Display.c #: shared-module/busdisplay/BusDisplay.c #: shared-module/framebufferio/FramebufferDisplay.c msgid "Below minimum frame rate" @@ -668,6 +670,7 @@ msgstr "" msgid "Both RX and TX required for flow control" msgstr "" +#: ports/zephyr-cp/bindings/zephyr_display/Display.c #: shared-bindings/busdisplay/BusDisplay.c #: shared-bindings/framebufferio/FramebufferDisplay.c msgid "Brightness not adjustable" @@ -941,6 +944,10 @@ msgstr "" msgid "Display must have a 16 bit colorspace." msgstr "" +#: ports/zephyr-cp/common-hal/zephyr_display/Display.c +msgid "Display not ready" +msgstr "" + #: shared-bindings/busdisplay/BusDisplay.c #: shared-bindings/epaperdisplay/EPaperDisplay.c #: shared-bindings/framebufferio/FramebufferDisplay.c @@ -1144,6 +1151,7 @@ msgstr "" msgid "Generic Failure" msgstr "" +#: ports/zephyr-cp/bindings/zephyr_display/Display.c #: shared-bindings/framebufferio/FramebufferDisplay.c #: shared-module/busdisplay/BusDisplay.c #: shared-module/framebufferio/FramebufferDisplay.c @@ -2407,6 +2415,10 @@ msgstr "" msgid "Update failed" msgstr "" +#: ports/zephyr-cp/bindings/zephyr_display/Display.c +msgid "Use board.DISPLAY" +msgstr "" + #: ports/zephyr-cp/common-hal/busio/I2C.c #: ports/zephyr-cp/common-hal/busio/SPI.c #: ports/zephyr-cp/common-hal/busio/UART.c diff --git a/ports/zephyr-cp/CLAUDE.md b/ports/zephyr-cp/CLAUDE.md new file mode 100644 index 0000000000000..43c994c2d3617 --- /dev/null +++ b/ports/zephyr-cp/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/ports/zephyr-cp/Makefile b/ports/zephyr-cp/Makefile index 5e905701668ba..ae1260c0f4dc1 100644 --- a/ports/zephyr-cp/Makefile +++ b/ports/zephyr-cp/Makefile @@ -8,13 +8,24 @@ BUILD ?= build-$(BOARD) TRANSLATION ?= en_US -.DEFAULT_GOAL := $(BUILD)/zephyr-cp/zephyr/zephyr.elf +# Compute shield args once. Command-line SHIELD/SHIELDS values override board defaults from circuitpython.toml. +ifneq ($(strip $(BOARD)),) +WEST_SHIELD_ARGS := $(shell SHIELD_ORIGIN="$(origin SHIELD)" SHIELDS_ORIGIN="$(origin SHIELDS)" SHIELD="$(SHIELD)" SHIELDS="$(SHIELDS)" python cptools/get_west_shield_args.py $(BOARD)) +endif -.PHONY: $(BUILD)/zephyr-cp/zephyr/zephyr.elf flash recover debug run run-sim clean menuconfig all clean-all test fetch-port-submodules +WEST_CMAKE_ARGS := -DZEPHYR_BOARD_ALIASES=$(CURDIR)/boards/board_aliases.cmake -Dzephyr-cp_TRANSLATION=$(TRANSLATION) + +# When DEBUG=1, apply additional Kconfig fragments for debug-friendly settings. +DEBUG_CONF_FILE ?= $(CURDIR)/debug.conf +ifeq ($(DEBUG),1) +WEST_CMAKE_ARGS += -Dzephyr-cp_EXTRA_CONF_FILE=$(DEBUG_CONF_FILE) +endif + +.PHONY: $(BUILD)/zephyr-cp/zephyr/zephyr.elf flash recover debug debug-jlink debugserver attach run run-sim clean menuconfig all clean-all test fetch-port-submodules $(BUILD)/zephyr-cp/zephyr/zephyr.elf: python cptools/pre_zephyr_build_prep.py $(BOARD) - west build -b $(BOARD) -d $(BUILD) --sysbuild -- -DZEPHYR_BOARD_ALIASES=$(CURDIR)/boards/board_aliases.cmake -Dzephyr-cp_TRANSLATION=$(TRANSLATION) + west build -b $(BOARD) -d $(BUILD) $(WEST_SHIELD_ARGS) --sysbuild -- $(WEST_CMAKE_ARGS) $(BUILD)/firmware.elf: $(BUILD)/zephyr-cp/zephyr/zephyr.elf cp $^ $@ @@ -37,6 +48,15 @@ recover: $(BUILD)/zephyr-cp/zephyr/zephyr.elf debug: $(BUILD)/zephyr-cp/zephyr/zephyr.elf west debug -d $(BUILD) +debug-jlink: $(BUILD)/zephyr-cp/zephyr/zephyr.elf + west debug --runner jlink -d $(BUILD) + +debugserver: $(BUILD)/zephyr-cp/zephyr/zephyr.elf + west debugserver -d $(BUILD) + +attach: $(BUILD)/zephyr-cp/zephyr/zephyr.elf + west attach -d $(BUILD) + run: $(BUILD)/firmware.exe $^ @@ -51,7 +71,7 @@ run-sim: build-native_native_sim/firmware.exe --flash=build-native_native_sim/flash.bin --flash_rm -wait_uart -rt menuconfig: - west build --sysbuild -d $(BUILD) -t menuconfig + west build $(WEST_SHIELD_ARGS) --sysbuild -d $(BUILD) -t menuconfig -- $(WEST_CMAKE_ARGS) clean: rm -rf $(BUILD) diff --git a/ports/zephyr-cp/README.md b/ports/zephyr-cp/README.md index f4391fc4cb635..28bbfbf298441 100644 --- a/ports/zephyr-cp/README.md +++ b/ports/zephyr-cp/README.md @@ -42,6 +42,36 @@ If a local `./CIRCUITPY/` folder exists, its files are used as the simulator's C Edit files in `./CIRCUITPY` (for example `code.py`) and rerun `make run-sim` to test changes. +## Shields + +Board defaults can be set in `boards///circuitpython.toml`: + +```toml +SHIELDS = ["shield1", "shield2"] +``` + +For example, `boards/renesas/ek_ra8d1/circuitpython.toml` enables: + +```toml +SHIELDS = ["rtkmipilcdb00000be"] +``` + +You can override shield selection from the command line: + +```sh +# Single shield +make BOARD=renesas_ek_ra8d1 SHIELD=rtkmipilcdb00000be + +# Multiple shields (comma, semicolon, or space separated) +make BOARD=my_vendor_my_board SHIELDS="shield1,shield2" +``` + +Behavior and precedence: + +- If `SHIELD` or `SHIELDS` is explicitly provided, it overrides board defaults. +- If neither is provided, defaults from `circuitpython.toml` are used. +- Use `SHIELD=` (empty) to disable a board default shield for one build. + ## Testing other boards [Any Zephyr board](https://docs.zephyrproject.org/latest/boards/index.html#) can diff --git a/ports/zephyr-cp/bindings/zephyr_display/Display.c b/ports/zephyr-cp/bindings/zephyr_display/Display.c new file mode 100644 index 0000000000000..267e36f3fca79 --- /dev/null +++ b/ports/zephyr-cp/bindings/zephyr_display/Display.c @@ -0,0 +1,195 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "bindings/zephyr_display/Display.h" + +#include "py/objproperty.h" +#include "py/objtype.h" +#include "py/runtime.h" +#include "shared-bindings/displayio/Group.h" +#include "shared-module/displayio/__init__.h" + +static mp_obj_t zephyr_display_display_make_new(const mp_obj_type_t *type, + size_t n_args, + size_t n_kw, + const mp_obj_t *all_args) { + (void)type; + (void)n_args; + (void)n_kw; + (void)all_args; + mp_raise_NotImplementedError(MP_ERROR_TEXT("Use board.DISPLAY")); + return mp_const_none; +} + +static zephyr_display_display_obj_t *native_display(mp_obj_t display_obj) { + mp_obj_t native = mp_obj_cast_to_native_base(display_obj, &zephyr_display_display_type); + mp_obj_assert_native_inited(native); + return MP_OBJ_TO_PTR(native); +} + +static mp_obj_t zephyr_display_display_obj_show(mp_obj_t self_in, mp_obj_t group_in) { + (void)self_in; + (void)group_in; + mp_raise_AttributeError(MP_ERROR_TEXT(".show(x) removed. Use .root_group = x")); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(zephyr_display_display_show_obj, zephyr_display_display_obj_show); + +static mp_obj_t zephyr_display_display_obj_refresh(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { + ARG_target_frames_per_second, + ARG_minimum_frames_per_second, + }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_target_frames_per_second, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, + { MP_QSTR_minimum_frames_per_second, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + zephyr_display_display_obj_t *self = native_display(pos_args[0]); + + uint32_t maximum_ms_per_real_frame = NO_FPS_LIMIT; + mp_int_t minimum_frames_per_second = args[ARG_minimum_frames_per_second].u_int; + if (minimum_frames_per_second > 0) { + maximum_ms_per_real_frame = 1000 / minimum_frames_per_second; + } + + uint32_t target_ms_per_frame; + if (args[ARG_target_frames_per_second].u_obj == mp_const_none) { + target_ms_per_frame = NO_FPS_LIMIT; + } else { + target_ms_per_frame = 1000 / mp_obj_get_int(args[ARG_target_frames_per_second].u_obj); + } + + return mp_obj_new_bool(common_hal_zephyr_display_display_refresh( + self, + target_ms_per_frame, + maximum_ms_per_real_frame)); +} +MP_DEFINE_CONST_FUN_OBJ_KW(zephyr_display_display_refresh_obj, 1, zephyr_display_display_obj_refresh); + +static mp_obj_t zephyr_display_display_obj_get_auto_refresh(mp_obj_t self_in) { + zephyr_display_display_obj_t *self = native_display(self_in); + return mp_obj_new_bool(common_hal_zephyr_display_display_get_auto_refresh(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(zephyr_display_display_get_auto_refresh_obj, zephyr_display_display_obj_get_auto_refresh); + +static mp_obj_t zephyr_display_display_obj_set_auto_refresh(mp_obj_t self_in, mp_obj_t auto_refresh) { + zephyr_display_display_obj_t *self = native_display(self_in); + common_hal_zephyr_display_display_set_auto_refresh(self, mp_obj_is_true(auto_refresh)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(zephyr_display_display_set_auto_refresh_obj, zephyr_display_display_obj_set_auto_refresh); + +MP_PROPERTY_GETSET(zephyr_display_display_auto_refresh_obj, + (mp_obj_t)&zephyr_display_display_get_auto_refresh_obj, + (mp_obj_t)&zephyr_display_display_set_auto_refresh_obj); + +static mp_obj_t zephyr_display_display_obj_get_brightness(mp_obj_t self_in) { + zephyr_display_display_obj_t *self = native_display(self_in); + mp_float_t brightness = common_hal_zephyr_display_display_get_brightness(self); + if (brightness < 0) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Brightness not adjustable")); + } + return mp_obj_new_float(brightness); +} +MP_DEFINE_CONST_FUN_OBJ_1(zephyr_display_display_get_brightness_obj, zephyr_display_display_obj_get_brightness); + +static mp_obj_t zephyr_display_display_obj_set_brightness(mp_obj_t self_in, mp_obj_t brightness_obj) { + zephyr_display_display_obj_t *self = native_display(self_in); + mp_float_t brightness = mp_obj_get_float(brightness_obj); + if (brightness < 0.0f || brightness > 1.0f) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be %d-%d"), MP_QSTR_brightness, 0, 1); + } + bool ok = common_hal_zephyr_display_display_set_brightness(self, brightness); + if (!ok) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Brightness not adjustable")); + } + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(zephyr_display_display_set_brightness_obj, zephyr_display_display_obj_set_brightness); + +MP_PROPERTY_GETSET(zephyr_display_display_brightness_obj, + (mp_obj_t)&zephyr_display_display_get_brightness_obj, + (mp_obj_t)&zephyr_display_display_set_brightness_obj); + +static mp_obj_t zephyr_display_display_obj_get_width(mp_obj_t self_in) { + zephyr_display_display_obj_t *self = native_display(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_zephyr_display_display_get_width(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(zephyr_display_display_get_width_obj, zephyr_display_display_obj_get_width); +MP_PROPERTY_GETTER(zephyr_display_display_width_obj, (mp_obj_t)&zephyr_display_display_get_width_obj); + +static mp_obj_t zephyr_display_display_obj_get_height(mp_obj_t self_in) { + zephyr_display_display_obj_t *self = native_display(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_zephyr_display_display_get_height(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(zephyr_display_display_get_height_obj, zephyr_display_display_obj_get_height); +MP_PROPERTY_GETTER(zephyr_display_display_height_obj, (mp_obj_t)&zephyr_display_display_get_height_obj); + +static mp_obj_t zephyr_display_display_obj_get_rotation(mp_obj_t self_in) { + zephyr_display_display_obj_t *self = native_display(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_zephyr_display_display_get_rotation(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(zephyr_display_display_get_rotation_obj, zephyr_display_display_obj_get_rotation); + +static mp_obj_t zephyr_display_display_obj_set_rotation(mp_obj_t self_in, mp_obj_t value) { + zephyr_display_display_obj_t *self = native_display(self_in); + common_hal_zephyr_display_display_set_rotation(self, mp_obj_get_int(value)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(zephyr_display_display_set_rotation_obj, zephyr_display_display_obj_set_rotation); + +MP_PROPERTY_GETSET(zephyr_display_display_rotation_obj, + (mp_obj_t)&zephyr_display_display_get_rotation_obj, + (mp_obj_t)&zephyr_display_display_set_rotation_obj); + +static mp_obj_t zephyr_display_display_obj_get_root_group(mp_obj_t self_in) { + zephyr_display_display_obj_t *self = native_display(self_in); + return common_hal_zephyr_display_display_get_root_group(self); +} +MP_DEFINE_CONST_FUN_OBJ_1(zephyr_display_display_get_root_group_obj, zephyr_display_display_obj_get_root_group); + +static mp_obj_t zephyr_display_display_obj_set_root_group(mp_obj_t self_in, mp_obj_t group_in) { + zephyr_display_display_obj_t *self = native_display(self_in); + displayio_group_t *group = NULL; + if (group_in != mp_const_none) { + group = MP_OBJ_TO_PTR(native_group(group_in)); + } + + bool ok = common_hal_zephyr_display_display_set_root_group(self, group); + if (!ok) { + mp_raise_ValueError(MP_ERROR_TEXT("Group already used")); + } + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(zephyr_display_display_set_root_group_obj, zephyr_display_display_obj_set_root_group); + +MP_PROPERTY_GETSET(zephyr_display_display_root_group_obj, + (mp_obj_t)&zephyr_display_display_get_root_group_obj, + (mp_obj_t)&zephyr_display_display_set_root_group_obj); + +static const mp_rom_map_elem_t zephyr_display_display_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_show), MP_ROM_PTR(&zephyr_display_display_show_obj) }, + { MP_ROM_QSTR(MP_QSTR_refresh), MP_ROM_PTR(&zephyr_display_display_refresh_obj) }, + + { MP_ROM_QSTR(MP_QSTR_auto_refresh), MP_ROM_PTR(&zephyr_display_display_auto_refresh_obj) }, + { MP_ROM_QSTR(MP_QSTR_brightness), MP_ROM_PTR(&zephyr_display_display_brightness_obj) }, + { MP_ROM_QSTR(MP_QSTR_width), MP_ROM_PTR(&zephyr_display_display_width_obj) }, + { MP_ROM_QSTR(MP_QSTR_height), MP_ROM_PTR(&zephyr_display_display_height_obj) }, + { MP_ROM_QSTR(MP_QSTR_rotation), MP_ROM_PTR(&zephyr_display_display_rotation_obj) }, + { MP_ROM_QSTR(MP_QSTR_root_group), MP_ROM_PTR(&zephyr_display_display_root_group_obj) }, +}; +static MP_DEFINE_CONST_DICT(zephyr_display_display_locals_dict, zephyr_display_display_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + zephyr_display_display_type, + MP_QSTR_Display, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, zephyr_display_display_make_new, + locals_dict, &zephyr_display_display_locals_dict); diff --git a/ports/zephyr-cp/bindings/zephyr_display/Display.h b/ports/zephyr-cp/bindings/zephyr_display/Display.h new file mode 100644 index 0000000000000..a50dda8fe8b69 --- /dev/null +++ b/ports/zephyr-cp/bindings/zephyr_display/Display.h @@ -0,0 +1,37 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/displayio/Group.h" +#include "common-hal/zephyr_display/Display.h" + +extern const mp_obj_type_t zephyr_display_display_type; + +#define NO_FPS_LIMIT 0xffffffff + +void common_hal_zephyr_display_display_construct_from_device(zephyr_display_display_obj_t *self, + const struct device *device, + uint16_t rotation, + bool auto_refresh); + +bool common_hal_zephyr_display_display_refresh(zephyr_display_display_obj_t *self, + uint32_t target_ms_per_frame, + uint32_t maximum_ms_per_real_frame); + +bool common_hal_zephyr_display_display_get_auto_refresh(zephyr_display_display_obj_t *self); +void common_hal_zephyr_display_display_set_auto_refresh(zephyr_display_display_obj_t *self, bool auto_refresh); + +uint16_t common_hal_zephyr_display_display_get_width(zephyr_display_display_obj_t *self); +uint16_t common_hal_zephyr_display_display_get_height(zephyr_display_display_obj_t *self); +uint16_t common_hal_zephyr_display_display_get_rotation(zephyr_display_display_obj_t *self); +void common_hal_zephyr_display_display_set_rotation(zephyr_display_display_obj_t *self, int rotation); + +mp_float_t common_hal_zephyr_display_display_get_brightness(zephyr_display_display_obj_t *self); +bool common_hal_zephyr_display_display_set_brightness(zephyr_display_display_obj_t *self, mp_float_t brightness); + +mp_obj_t common_hal_zephyr_display_display_get_root_group(zephyr_display_display_obj_t *self); +bool common_hal_zephyr_display_display_set_root_group(zephyr_display_display_obj_t *self, displayio_group_t *root_group); diff --git a/ports/zephyr-cp/bindings/zephyr_display/__init__.c b/ports/zephyr-cp/bindings/zephyr_display/__init__.c new file mode 100644 index 0000000000000..eecfeeaec58ae --- /dev/null +++ b/ports/zephyr-cp/bindings/zephyr_display/__init__.c @@ -0,0 +1,24 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "py/obj.h" +#include "py/runtime.h" + +#include "bindings/zephyr_display/Display.h" + +static const mp_rom_map_elem_t zephyr_display_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_zephyr_display) }, + { MP_ROM_QSTR(MP_QSTR_Display), MP_ROM_PTR(&zephyr_display_display_type) }, +}; + +static MP_DEFINE_CONST_DICT(zephyr_display_module_globals, zephyr_display_module_globals_table); + +const mp_obj_module_t zephyr_display_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&zephyr_display_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_zephyr_display, zephyr_display_module); diff --git a/ports/zephyr-cp/bindings/zephyr_display/__init__.h b/ports/zephyr-cp/bindings/zephyr_display/__init__.h new file mode 100644 index 0000000000000..4256bfac2fe16 --- /dev/null +++ b/ports/zephyr-cp/bindings/zephyr_display/__init__.h @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once diff --git a/ports/zephyr-cp/boards/board_aliases.cmake b/ports/zephyr-cp/boards/board_aliases.cmake index dce5b100aa6df..5914ae61f28e6 100644 --- a/ports/zephyr-cp/boards/board_aliases.cmake +++ b/ports/zephyr-cp/boards/board_aliases.cmake @@ -41,6 +41,7 @@ cp_board_alias(nxp_frdm_mcxn947 frdm_mcxn947/mcxn947/cpu0) cp_board_alias(nxp_frdm_rw612 frdm_rw612) cp_board_alias(nxp_mimxrt1170_evk mimxrt1170_evk@A/mimxrt1176/cm7) cp_board_alias(st_stm32h7b3i_dk stm32h7b3i_dk) +cp_board_alias(st_stm32h750b_dk stm32h750b_dk/stm32h750xx/ext_flash_app) cp_board_alias(st_stm32wba65i_dk1 stm32wba65i_dk1) cp_board_alias(st_nucleo_u575zi_q nucleo_u575zi_q/stm32u575xx) cp_board_alias(st_nucleo_n657x0_q nucleo_n657x0_q/stm32n657xx) diff --git a/ports/zephyr-cp/boards/ek_ra8d1.conf b/ports/zephyr-cp/boards/ek_ra8d1.conf new file mode 100644 index 0000000000000..f979d31e751f1 --- /dev/null +++ b/ports/zephyr-cp/boards/ek_ra8d1.conf @@ -0,0 +1,3 @@ + +# Enable Zephyr display subsystem so DT chosen zephyr,display creates a device. +CONFIG_DISPLAY=y diff --git a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml index 6261be1ec7585..587c935f56d18 100644 --- a/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/native/native_sim/autogen_board_info.toml @@ -37,7 +37,7 @@ canio = false codeop = false countio = false digitalio = true -displayio = true # Zephyr board has busio +displayio = true # Zephyr board has displayio dotclockframebuffer = false dualbank = false epaperdisplay = true # Zephyr board has busio @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = true # Zephyr board has zephyr_display zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/native/nrf5340bsim/autogen_board_info.toml b/ports/zephyr-cp/boards/native/nrf5340bsim/autogen_board_info.toml index a512a48088988..1f9e51e9492a4 100644 --- a/ports/zephyr-cp/boards/native/nrf5340bsim/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/native/nrf5340bsim/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/native_sim.conf b/ports/zephyr-cp/boards/native_sim.conf index ddbfef11266d8..739a71eeeb61e 100644 --- a/ports/zephyr-cp/boards/native_sim.conf +++ b/ports/zephyr-cp/boards/native_sim.conf @@ -14,6 +14,10 @@ CONFIG_TRACING_GPIO=y # I2C emulation for testing CONFIG_I2C_EMUL=y +# Display emulation for display/terminal golden tests. +CONFIG_DISPLAY=y +CONFIG_SDL_DISPLAY=y + # EEPROM emulation for testing CONFIG_EEPROM=y CONFIG_EEPROM_AT24=y diff --git a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml index 80cf2e119ed39..a102426717a35 100644 --- a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/nordic/nrf54h20dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf54h20dk/autogen_board_info.toml index b3f72751cbbac..26760f9b6a31a 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54h20dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nordic/nrf54h20dk/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml index f4d2dd478c5f7..275dac5a93ce5 100644 --- a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml index 2557bc74df48e..1eb9dd4410262 100644 --- a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = true # Zephyr board has wifi +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml b/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml index b7cd8eb61104f..82659ca7fdb2b 100644 --- a/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nxp/frdm_mcxn947/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml b/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml index 91a2776ae74f9..ce87ff95daa9e 100644 --- a/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nxp/frdm_rw612/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = true # Zephyr board has wifi +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/nxp/mimxrt1170_evk/autogen_board_info.toml b/ports/zephyr-cp/boards/nxp/mimxrt1170_evk/autogen_board_info.toml index 6f648df82fba7..bebc3937a8bc8 100644 --- a/ports/zephyr-cp/boards/nxp/mimxrt1170_evk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/nxp/mimxrt1170_evk/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/renesas/da14695_dk_usb/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/da14695_dk_usb/autogen_board_info.toml index aa947de521397..b6093b3239dad 100644 --- a/ports/zephyr-cp/boards/renesas/da14695_dk_usb/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/renesas/da14695_dk_usb/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml index f7495b527230d..4197b7be810c3 100644 --- a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml index b65c12b0aa93e..e5c983473061f 100644 --- a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml @@ -37,7 +37,7 @@ canio = false codeop = false countio = false digitalio = true -displayio = true # Zephyr board has busio +displayio = true # Zephyr board has displayio dotclockframebuffer = false dualbank = false epaperdisplay = true # Zephyr board has busio @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = true # Zephyr board has zephyr_display zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/renesas/ek_ra8d1/circuitpython.toml b/ports/zephyr-cp/boards/renesas/ek_ra8d1/circuitpython.toml index 3272dd4c5f319..0e19d8d71574e 100644 --- a/ports/zephyr-cp/boards/renesas/ek_ra8d1/circuitpython.toml +++ b/ports/zephyr-cp/boards/renesas/ek_ra8d1/circuitpython.toml @@ -1 +1,2 @@ CIRCUITPY_BUILD_EXTENSIONS = ["elf"] +SHIELDS = ["rtkmipilcdb00000be"] diff --git a/ports/zephyr-cp/boards/st/nucleo_n657x0_q/autogen_board_info.toml b/ports/zephyr-cp/boards/st/nucleo_n657x0_q/autogen_board_info.toml index 92122525a2933..d3dd9e185e2fb 100644 --- a/ports/zephyr-cp/boards/st/nucleo_n657x0_q/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/nucleo_n657x0_q/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml index e73b1c062a5fd..610a3914a3fbd 100644 --- a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/st/stm32h750b_dk/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32h750b_dk/autogen_board_info.toml new file mode 100644 index 0000000000000..58b09c6d8c742 --- /dev/null +++ b/ports/zephyr-cp/boards/st/stm32h750b_dk/autogen_board_info.toml @@ -0,0 +1,116 @@ +# This file is autogenerated when a board is built. Do not edit. Do commit it to git. Other scripts use its info. +name = "STMicroelectronics STM32H750B Discovery Kit" + +[modules] +__future__ = true +_bleio = false +_eve = false +_pew = false +_pixelmap = false +_stage = false +adafruit_bus_device = false +adafruit_pixelbuf = false +aesio = false +alarm = false +analogbufio = false +analogio = false +atexit = false +audiobusio = false +audiocore = false +audiodelays = false +audiofilters = false +audiofreeverb = false +audioio = false +audiomixer = false +audiomp3 = false +audiopwmio = false +aurora_epaper = false +bitbangio = false +bitmapfilter = true # Zephyr board has busio +bitmaptools = true # Zephyr board has busio +bitops = false +board = false +busdisplay = true # Zephyr board has busio +busio = true # Zephyr board has busio +camera = false +canio = false +codeop = false +countio = false +digitalio = true +displayio = true # Zephyr board has displayio +dotclockframebuffer = false +dualbank = false +epaperdisplay = true # Zephyr board has busio +floppyio = false +fontio = true # Zephyr board has busio +fourwire = true # Zephyr board has busio +framebufferio = true # Zephyr board has busio +frequencyio = false +getpass = false +gifio = false +gnss = false +hashlib = false +i2cdisplaybus = true # Zephyr board has busio +i2cioexpander = false +i2ctarget = false +imagecapture = false +ipaddress = false +is31fl3741 = false +jpegio = false +keypad = false +keypad_demux = false +locale = false +lvfontio = true # Zephyr board has busio +math = false +max3421e = false +mdns = false +memorymap = false +memorymonitor = false +microcontroller = true +mipidsi = false +msgpack = false +neopixel_write = false +nvm = false +onewireio = false +os = true +paralleldisplaybus = false +ps2io = false +pulseio = false +pwmio = false +qrio = false +rainbowio = true +random = true +rclcpy = false +rgbmatrix = false +rotaryio = false +rtc = false +sdcardio = true # Zephyr board has busio +sdioio = false +sharpdisplay = true # Zephyr board has busio +socketpool = false +spitarget = false +ssl = false +storage = true # Zephyr board has flash +struct = true +supervisor = true +synthio = false +terminalio = true # Zephyr board has busio +tilepalettemapper = true # Zephyr board has busio +time = true +touchio = false +traceback = true +uheap = false +usb = false +usb_cdc = false +usb_hid = false +usb_host = false +usb_midi = false +usb_video = false +ustack = false +vectorio = true # Zephyr board has busio +warnings = true +watchdog = false +wifi = false +zephyr_display = true # Zephyr board has zephyr_display +zephyr_kernel = false +zlib = false diff --git a/ports/zephyr-cp/boards/st/stm32h750b_dk/circuitpython.toml b/ports/zephyr-cp/boards/st/stm32h750b_dk/circuitpython.toml new file mode 100644 index 0000000000000..83e6bcd39c4f9 --- /dev/null +++ b/ports/zephyr-cp/boards/st/stm32h750b_dk/circuitpython.toml @@ -0,0 +1 @@ +CIRCUITPY_BUILD_EXTENSIONS = ["hex"] diff --git a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml index 949cb89f0828a..1afd4075c5a20 100644 --- a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml @@ -37,7 +37,7 @@ canio = false codeop = false countio = false digitalio = true -displayio = true # Zephyr board has busio +displayio = true # Zephyr board has displayio dotclockframebuffer = false dualbank = false epaperdisplay = true # Zephyr board has busio @@ -102,7 +102,7 @@ touchio = false traceback = true uheap = false usb = false -usb_cdc = false +usb_cdc = true usb_hid = false usb_host = false usb_midi = false @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = true # Zephyr board has zephyr_display zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/st/stm32wba65i_dk1/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32wba65i_dk1/autogen_board_info.toml index 50c17ef6dba7c..e41a1849f6ca4 100644 --- a/ports/zephyr-cp/boards/st/stm32wba65i_dk1/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/st/stm32wba65i_dk1/autogen_board_info.toml @@ -112,5 +112,6 @@ vectorio = true # Zephyr board has busio warnings = true watchdog = false wifi = false +zephyr_display = false zephyr_kernel = false zlib = false diff --git a/ports/zephyr-cp/boards/stm32h750b_dk_stm32h750xx_ext_flash_app.conf b/ports/zephyr-cp/boards/stm32h750b_dk_stm32h750xx_ext_flash_app.conf new file mode 100644 index 0000000000000..24afffb8e88ee --- /dev/null +++ b/ports/zephyr-cp/boards/stm32h750b_dk_stm32h750xx_ext_flash_app.conf @@ -0,0 +1,2 @@ +# Enable Zephyr display subsystem so the built-in LTDC panel is available. +CONFIG_DISPLAY=y diff --git a/ports/zephyr-cp/boards/stm32h750b_dk_stm32h750xx_ext_flash_app.overlay b/ports/zephyr-cp/boards/stm32h750b_dk_stm32h750xx_ext_flash_app.overlay new file mode 100644 index 0000000000000..fdb4960477b77 --- /dev/null +++ b/ports/zephyr-cp/boards/stm32h750b_dk_stm32h750xx_ext_flash_app.overlay @@ -0,0 +1,14 @@ +&ext_flash { + partitions { + /delete-node/ partition@7800000; + + circuitpy_partition: partition@7800000 { + label = "circuitpy"; + reg = <0x7800000 DT_SIZE_M(8)>; + }; + }; +}; + +&rng { + status = "okay"; +}; diff --git a/ports/zephyr-cp/boards/stm32h7b3i_dk.conf b/ports/zephyr-cp/boards/stm32h7b3i_dk.conf new file mode 100644 index 0000000000000..24afffb8e88ee --- /dev/null +++ b/ports/zephyr-cp/boards/stm32h7b3i_dk.conf @@ -0,0 +1,2 @@ +# Enable Zephyr display subsystem so the built-in LTDC panel is available. +CONFIG_DISPLAY=y diff --git a/ports/zephyr-cp/boards/stm32h7b3i_dk.overlay b/ports/zephyr-cp/boards/stm32h7b3i_dk.overlay index 88ad0415485b8..c2b5f3129c48a 100644 --- a/ports/zephyr-cp/boards/stm32h7b3i_dk.overlay +++ b/ports/zephyr-cp/boards/stm32h7b3i_dk.overlay @@ -2,6 +2,28 @@ /delete-node/ partitions; }; +&sram5 { + status = "disabled"; +}; + &rng { status = "okay"; }; + +&fdcan1 { + status = "disabled"; +}; + +/ { + chosen { + /delete-property/ zephyr,canbus; + }; +}; + +zephyr_udc0: &usbotg_hs { + pinctrl-0 = <&usb_otg_hs_dm_pa11 &usb_otg_hs_dp_pa12>; + pinctrl-names = "default"; + status = "okay"; +}; + +#include "../app.overlay" diff --git a/ports/zephyr-cp/common-hal/zephyr_display/Display.c b/ports/zephyr-cp/common-hal/zephyr_display/Display.c new file mode 100644 index 0000000000000..1a83a1a23a947 --- /dev/null +++ b/ports/zephyr-cp/common-hal/zephyr_display/Display.c @@ -0,0 +1,446 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#include "bindings/zephyr_display/Display.h" + +#include + +#include "py/gc.h" +#include "py/runtime.h" +#include "shared-bindings/time/__init__.h" +#include "shared-module/displayio/__init__.h" +#include "supervisor/shared/display.h" +#include "supervisor/shared/tick.h" + +#if CIRCUITPY_TINYUSB +#include "supervisor/usb.h" +#endif + +static const displayio_area_t *zephyr_display_get_refresh_areas(zephyr_display_display_obj_t *self) { + if (self->core.full_refresh) { + self->core.area.next = NULL; + return &self->core.area; + } else if (self->core.current_group != NULL) { + return displayio_group_get_refresh_areas(self->core.current_group, NULL); + } + return NULL; +} + +static enum display_pixel_format zephyr_display_select_pixel_format(const struct display_capabilities *caps) { + uint32_t formats = caps->supported_pixel_formats; + + if (formats & PIXEL_FORMAT_RGB_565) { + return PIXEL_FORMAT_RGB_565; + } + if (formats & PIXEL_FORMAT_RGB_888) { + return PIXEL_FORMAT_RGB_888; + } + if (formats & PIXEL_FORMAT_ARGB_8888) { + return PIXEL_FORMAT_ARGB_8888; + } + if (formats & PIXEL_FORMAT_RGB_565X) { + return PIXEL_FORMAT_RGB_565X; + } + if (formats & PIXEL_FORMAT_L_8) { + return PIXEL_FORMAT_L_8; + } + if (formats & PIXEL_FORMAT_AL_88) { + return PIXEL_FORMAT_AL_88; + } + if (formats & PIXEL_FORMAT_MONO01) { + return PIXEL_FORMAT_MONO01; + } + if (formats & PIXEL_FORMAT_MONO10) { + return PIXEL_FORMAT_MONO10; + } + return caps->current_pixel_format; +} + +static void zephyr_display_select_colorspace(zephyr_display_display_obj_t *self, + uint16_t *color_depth, + uint8_t *bytes_per_cell, + bool *grayscale, + bool *pixels_in_byte_share_row, + bool *reverse_pixels_in_byte, + bool *reverse_bytes_in_word) { + *color_depth = 16; + *bytes_per_cell = 2; + *grayscale = false; + *pixels_in_byte_share_row = false; + *reverse_pixels_in_byte = false; + *reverse_bytes_in_word = false; + + if (self->pixel_format == PIXEL_FORMAT_RGB_565X) { + // RGB_565X is big-endian RGB_565, so byte-swap from native LE. + *reverse_bytes_in_word = true; + } else if (self->pixel_format == PIXEL_FORMAT_RGB_888) { + *color_depth = 24; + *bytes_per_cell = 3; + *reverse_bytes_in_word = false; + } else if (self->pixel_format == PIXEL_FORMAT_ARGB_8888) { + *color_depth = 32; + *bytes_per_cell = 4; + *reverse_bytes_in_word = false; + } else if (self->pixel_format == PIXEL_FORMAT_L_8 || + self->pixel_format == PIXEL_FORMAT_AL_88) { + *color_depth = 8; + *bytes_per_cell = 1; + *grayscale = true; + *reverse_bytes_in_word = false; + } else if (self->pixel_format == PIXEL_FORMAT_MONO01 || + self->pixel_format == PIXEL_FORMAT_MONO10) { + bool vtiled = self->capabilities.screen_info & SCREEN_INFO_MONO_VTILED; + bool msb_first = self->capabilities.screen_info & SCREEN_INFO_MONO_MSB_FIRST; + *color_depth = 1; + *bytes_per_cell = 1; + *grayscale = true; + *pixels_in_byte_share_row = !vtiled; + *reverse_pixels_in_byte = msb_first; + *reverse_bytes_in_word = false; + } +} + +void common_hal_zephyr_display_display_construct_from_device(zephyr_display_display_obj_t *self, + const struct device *device, + uint16_t rotation, + bool auto_refresh) { + self->auto_refresh = false; + + if (device == NULL || !device_is_ready(device)) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Display not ready")); + } + + self->device = device; + display_get_capabilities(self->device, &self->capabilities); + + self->pixel_format = zephyr_display_select_pixel_format(&self->capabilities); + if (self->pixel_format != self->capabilities.current_pixel_format) { + (void)display_set_pixel_format(self->device, self->pixel_format); + display_get_capabilities(self->device, &self->capabilities); + self->pixel_format = self->capabilities.current_pixel_format; + } + + uint16_t color_depth; + uint8_t bytes_per_cell; + bool grayscale; + bool pixels_in_byte_share_row; + bool reverse_pixels_in_byte; + bool reverse_bytes_in_word; + zephyr_display_select_colorspace(self, &color_depth, &bytes_per_cell, + &grayscale, &pixels_in_byte_share_row, &reverse_pixels_in_byte, + &reverse_bytes_in_word); + + displayio_display_core_construct( + &self->core, + self->capabilities.x_resolution, + self->capabilities.y_resolution, + 0, + color_depth, + grayscale, + pixels_in_byte_share_row, + bytes_per_cell, + reverse_pixels_in_byte, + reverse_bytes_in_word); + + self->native_frames_per_second = 60; + self->native_ms_per_frame = 1000 / self->native_frames_per_second; + self->first_manual_refresh = !auto_refresh; + + if (rotation != 0) { + common_hal_zephyr_display_display_set_rotation(self, rotation); + } + + (void)display_blanking_off(self->device); + + displayio_display_core_set_root_group(&self->core, &circuitpython_splash); + common_hal_zephyr_display_display_set_auto_refresh(self, auto_refresh); +} + +uint16_t common_hal_zephyr_display_display_get_width(zephyr_display_display_obj_t *self) { + return displayio_display_core_get_width(&self->core); +} + +uint16_t common_hal_zephyr_display_display_get_height(zephyr_display_display_obj_t *self) { + return displayio_display_core_get_height(&self->core); +} + +mp_float_t common_hal_zephyr_display_display_get_brightness(zephyr_display_display_obj_t *self) { + (void)self; + return -1; +} + +bool common_hal_zephyr_display_display_set_brightness(zephyr_display_display_obj_t *self, mp_float_t brightness) { + (void)self; + (void)brightness; + return false; +} + +static bool zephyr_display_refresh_area(zephyr_display_display_obj_t *self, const displayio_area_t *area) { + uint16_t buffer_size = CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE / sizeof(uint32_t); + + displayio_area_t clipped; + if (!displayio_display_core_clip_area(&self->core, area, &clipped)) { + return true; + } + + uint16_t rows_per_buffer = displayio_area_height(&clipped); + uint8_t pixels_per_word = (sizeof(uint32_t) * 8) / self->core.colorspace.depth; + // For AL_88, displayio fills at 1 byte/pixel (L_8) but output needs 2 bytes/pixel, + // so halve the effective pixels_per_word for buffer sizing. + uint8_t effective_pixels_per_word = pixels_per_word; + if (self->pixel_format == PIXEL_FORMAT_AL_88) { + effective_pixels_per_word = sizeof(uint32_t) / 2; + } + uint16_t pixels_per_buffer = displayio_area_size(&clipped); + uint16_t subrectangles = 1; + + // When pixels_in_byte_share_row is false (mono vtiled), 8 vertical pixels + // pack into one column byte. The byte layout needs width * ceil(height/8) + // bytes, which can exceed the pixel-count-based buffer size. + bool vtiled = self->core.colorspace.depth < 8 && + !self->core.colorspace.pixels_in_byte_share_row; + + bool needs_subdivision = displayio_area_size(&clipped) > buffer_size * effective_pixels_per_word; + if (vtiled && !needs_subdivision) { + uint16_t width = displayio_area_width(&clipped); + uint16_t height = displayio_area_height(&clipped); + uint32_t vtiled_bytes = (uint32_t)width * ((height + 7) / 8); + needs_subdivision = vtiled_bytes > buffer_size * sizeof(uint32_t); + } + + if (needs_subdivision) { + rows_per_buffer = buffer_size * effective_pixels_per_word / displayio_area_width(&clipped); + if (vtiled) { + rows_per_buffer = (rows_per_buffer / 8) * 8; + if (rows_per_buffer == 0) { + rows_per_buffer = 8; + } + } + if (rows_per_buffer == 0) { + rows_per_buffer = 1; + } + subrectangles = displayio_area_height(&clipped) / rows_per_buffer; + if (displayio_area_height(&clipped) % rows_per_buffer != 0) { + subrectangles++; + } + pixels_per_buffer = rows_per_buffer * displayio_area_width(&clipped); + buffer_size = pixels_per_buffer / pixels_per_word; + if (pixels_per_buffer % pixels_per_word) { + buffer_size += 1; + } + // Ensure buffer is large enough for vtiled packing. + if (vtiled) { + uint16_t width = displayio_area_width(&clipped); + uint16_t vtiled_words = (width * ((rows_per_buffer + 7) / 8) + sizeof(uint32_t) - 1) / sizeof(uint32_t); + if (vtiled_words > buffer_size) { + buffer_size = vtiled_words; + } + } + // Ensure buffer is large enough for AL_88 expansion. + if (self->pixel_format == PIXEL_FORMAT_AL_88) { + uint16_t al88_words = (pixels_per_buffer * 2 + sizeof(uint32_t) - 1) / sizeof(uint32_t); + if (al88_words > buffer_size) { + buffer_size = al88_words; + } + } + } + + uint32_t buffer[buffer_size]; + uint32_t mask_length = (pixels_per_buffer / 32) + 1; + uint32_t mask[mask_length]; + + uint16_t remaining_rows = displayio_area_height(&clipped); + + for (uint16_t j = 0; j < subrectangles; j++) { + displayio_area_t subrectangle = { + .x1 = clipped.x1, + .y1 = clipped.y1 + rows_per_buffer * j, + .x2 = clipped.x2, + .y2 = clipped.y1 + rows_per_buffer * (j + 1), + }; + + if (remaining_rows < rows_per_buffer) { + subrectangle.y2 = subrectangle.y1 + remaining_rows; + } + remaining_rows -= rows_per_buffer; + + memset(mask, 0, mask_length * sizeof(mask[0])); + memset(buffer, 0, buffer_size * sizeof(buffer[0])); + + displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer); + + uint16_t width = displayio_area_width(&subrectangle); + uint16_t height = displayio_area_height(&subrectangle); + size_t pixel_count = (size_t)width * (size_t)height; + + if (self->pixel_format == PIXEL_FORMAT_MONO10) { + uint8_t *bytes = (uint8_t *)buffer; + size_t byte_count = (pixel_count + 7) / 8; + for (size_t i = 0; i < byte_count; i++) { + bytes[i] = ~bytes[i]; + } + } + + if (self->pixel_format == PIXEL_FORMAT_AL_88) { + uint8_t *bytes = (uint8_t *)buffer; + for (size_t i = pixel_count; i > 0; i--) { + bytes[(i - 1) * 2 + 1] = 0xFF; + bytes[(i - 1) * 2] = bytes[i - 1]; + } + } + + // Compute buf_size based on the Zephyr pixel format. + uint32_t buf_size_bytes; + if (self->pixel_format == PIXEL_FORMAT_MONO01 || + self->pixel_format == PIXEL_FORMAT_MONO10) { + buf_size_bytes = (pixel_count + 7) / 8; + } else if (self->pixel_format == PIXEL_FORMAT_AL_88) { + buf_size_bytes = pixel_count * 2; + } else { + buf_size_bytes = pixel_count * (self->core.colorspace.depth / 8); + } + + struct display_buffer_descriptor desc = { + .buf_size = buf_size_bytes, + .width = width, + .height = height, + .pitch = width, + .frame_incomplete = false, + }; + + int err = display_write(self->device, subrectangle.x1, subrectangle.y1, &desc, buffer); + if (err < 0) { + return false; + } + + RUN_BACKGROUND_TASKS; + #if CIRCUITPY_TINYUSB + usb_background(); + #endif + } + + return true; +} + +static void zephyr_display_refresh(zephyr_display_display_obj_t *self) { + if (!displayio_display_core_start_refresh(&self->core)) { + return; + } + + const displayio_area_t *current_area = zephyr_display_get_refresh_areas(self); + while (current_area != NULL) { + if (!zephyr_display_refresh_area(self, current_area)) { + break; + } + current_area = current_area->next; + } + + displayio_display_core_finish_refresh(&self->core); +} + +void common_hal_zephyr_display_display_set_rotation(zephyr_display_display_obj_t *self, int rotation) { + bool transposed = (self->core.rotation == 90 || self->core.rotation == 270); + bool will_transposed = (rotation == 90 || rotation == 270); + if (transposed != will_transposed) { + int tmp = self->core.width; + self->core.width = self->core.height; + self->core.height = tmp; + } + + displayio_display_core_set_rotation(&self->core, rotation); + + if (self == &displays[0].zephyr_display) { + supervisor_stop_terminal(); + supervisor_start_terminal(self->core.width, self->core.height); + } + + if (self->core.current_group != NULL) { + displayio_group_update_transform(self->core.current_group, &self->core.transform); + } +} + +uint16_t common_hal_zephyr_display_display_get_rotation(zephyr_display_display_obj_t *self) { + return self->core.rotation; +} + +bool common_hal_zephyr_display_display_refresh(zephyr_display_display_obj_t *self, + uint32_t target_ms_per_frame, + uint32_t maximum_ms_per_real_frame) { + if (!self->auto_refresh && !self->first_manual_refresh && (target_ms_per_frame != NO_FPS_LIMIT)) { + uint64_t current_time = supervisor_ticks_ms64(); + uint32_t current_ms_since_real_refresh = current_time - self->core.last_refresh; + if (current_ms_since_real_refresh > maximum_ms_per_real_frame) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Below minimum frame rate")); + } + uint32_t current_ms_since_last_call = current_time - self->last_refresh_call; + self->last_refresh_call = current_time; + if (current_ms_since_last_call > target_ms_per_frame) { + return false; + } + uint32_t remaining_time = target_ms_per_frame - (current_ms_since_real_refresh % target_ms_per_frame); + while (supervisor_ticks_ms64() - self->last_refresh_call < remaining_time) { + RUN_BACKGROUND_TASKS; + } + } + self->first_manual_refresh = false; + zephyr_display_refresh(self); + return true; +} + +bool common_hal_zephyr_display_display_get_auto_refresh(zephyr_display_display_obj_t *self) { + return self->auto_refresh; +} + +void common_hal_zephyr_display_display_set_auto_refresh(zephyr_display_display_obj_t *self, bool auto_refresh) { + self->first_manual_refresh = !auto_refresh; + if (auto_refresh != self->auto_refresh) { + if (auto_refresh) { + supervisor_enable_tick(); + } else { + supervisor_disable_tick(); + } + } + self->auto_refresh = auto_refresh; +} + +void zephyr_display_display_background(zephyr_display_display_obj_t *self) { + if (self->auto_refresh && (supervisor_ticks_ms64() - self->core.last_refresh) > self->native_ms_per_frame) { + zephyr_display_refresh(self); + } +} + +void release_zephyr_display(zephyr_display_display_obj_t *self) { + common_hal_zephyr_display_display_set_auto_refresh(self, false); + release_display_core(&self->core); + self->device = NULL; + self->base.type = &mp_type_NoneType; +} + +void zephyr_display_display_collect_ptrs(zephyr_display_display_obj_t *self) { + (void)self; + displayio_display_core_collect_ptrs(&self->core); +} + +void zephyr_display_display_reset(zephyr_display_display_obj_t *self) { + if (self->device != NULL && device_is_ready(self->device)) { + common_hal_zephyr_display_display_set_auto_refresh(self, true); + displayio_display_core_set_root_group(&self->core, &circuitpython_splash); + self->core.full_refresh = true; + } else { + release_zephyr_display(self); + } +} + +mp_obj_t common_hal_zephyr_display_display_get_root_group(zephyr_display_display_obj_t *self) { + if (self->core.current_group == NULL) { + return mp_const_none; + } + return self->core.current_group; +} + +bool common_hal_zephyr_display_display_set_root_group(zephyr_display_display_obj_t *self, displayio_group_t *root_group) { + return displayio_display_core_set_root_group(&self->core, root_group); +} diff --git a/ports/zephyr-cp/common-hal/zephyr_display/Display.h b/ports/zephyr-cp/common-hal/zephyr_display/Display.h new file mode 100644 index 0000000000000..bdec1e96ba5f3 --- /dev/null +++ b/ports/zephyr-cp/common-hal/zephyr_display/Display.h @@ -0,0 +1,31 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Scott Shawcroft for Adafruit Industries +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +#include "py/obj.h" +#include "shared-module/displayio/display_core.h" + +typedef struct { + mp_obj_base_t base; + displayio_display_core_t core; + const struct device *device; + struct display_capabilities capabilities; + enum display_pixel_format pixel_format; + uint64_t last_refresh_call; + uint16_t native_frames_per_second; + uint16_t native_ms_per_frame; + bool auto_refresh; + bool first_manual_refresh; +} zephyr_display_display_obj_t; + +void zephyr_display_display_background(zephyr_display_display_obj_t *self); +void zephyr_display_display_collect_ptrs(zephyr_display_display_obj_t *self); +void zephyr_display_display_reset(zephyr_display_display_obj_t *self); +void release_zephyr_display(zephyr_display_display_obj_t *self); diff --git a/ports/zephyr-cp/cptools/board_tools.py b/ports/zephyr-cp/cptools/board_tools.py index 088b4bb54914b..305abab3f19b5 100644 --- a/ports/zephyr-cp/cptools/board_tools.py +++ b/ports/zephyr-cp/cptools/board_tools.py @@ -1,3 +1,6 @@ +import tomllib + + def find_mpconfigboard(portdir, board_id): next_underscore = board_id.find("_") while next_underscore != -1: @@ -8,3 +11,27 @@ def find_mpconfigboard(portdir, board_id): return p next_underscore = board_id.find("_", next_underscore + 1) return None + + +def load_mpconfigboard(portdir, board_id): + mpconfigboard_path = find_mpconfigboard(portdir, board_id) + if mpconfigboard_path is None or not mpconfigboard_path.exists(): + return None, {} + + with mpconfigboard_path.open("rb") as f: + return mpconfigboard_path, tomllib.load(f) + + +def get_shields(mpconfigboard): + shields = mpconfigboard.get("SHIELDS") + if shields is None: + shields = mpconfigboard.get("SHIELD") + + if shields is None: + return [] + if isinstance(shields, str): + return [shields] + if isinstance(shields, (list, tuple)): + return [str(shield) for shield in shields] + + return [str(shields)] diff --git a/ports/zephyr-cp/cptools/build_circuitpython.py b/ports/zephyr-cp/cptools/build_circuitpython.py index e00cf6cac8dc1..9b561c211311b 100644 --- a/ports/zephyr-cp/cptools/build_circuitpython.py +++ b/ports/zephyr-cp/cptools/build_circuitpython.py @@ -68,6 +68,9 @@ "busio": ["fourwire", "i2cdisplaybus", "sdcardio", "sharpdisplay"], "fourwire": ["displayio", "busdisplay", "epaperdisplay"], "i2cdisplaybus": ["displayio", "busdisplay", "epaperdisplay"], + # Zephyr display backends need displayio and, by extension, terminalio so + # the REPL console appears on the display by default. + "zephyr_display": ["displayio"], "displayio": [ "vectorio", "bitmapfilter", diff --git a/ports/zephyr-cp/cptools/get_west_shield_args.py b/ports/zephyr-cp/cptools/get_west_shield_args.py new file mode 100644 index 0000000000000..deda6bf5f26f8 --- /dev/null +++ b/ports/zephyr-cp/cptools/get_west_shield_args.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Resolve shield arguments for west build. + +Priority: +1. SHIELD / SHIELDS make variables (if explicitly provided) +2. SHIELD / SHIELDS from boards///circuitpython.toml +""" + +import argparse +import os +import pathlib +import re +import shlex + +import board_tools + + +def split_shields(raw): + if not raw: + return [] + + return [shield for shield in re.split(r"[,;\s]+", raw.strip()) if shield] + + +def dedupe(values): + deduped = [] + seen = set() + + for value in values: + if value in seen: + continue + seen.add(value) + deduped.append(value) + + return deduped + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("board") + args = parser.parse_args() + + portdir = pathlib.Path(__file__).resolve().parent.parent + + _, mpconfigboard = board_tools.load_mpconfigboard(portdir, args.board) + + shield_origin = os.environ.get("SHIELD_ORIGIN", "undefined") + shields_origin = os.environ.get("SHIELDS_ORIGIN", "undefined") + + shield_override = os.environ.get("SHIELD", "") + shields_override = os.environ.get("SHIELDS", "") + + override_requested = shield_origin != "undefined" or shields_origin != "undefined" + + if override_requested: + shields = split_shields(shield_override) + split_shields(shields_override) + else: + shields = board_tools.get_shields(mpconfigboard) + + shields = dedupe(shields) + + west_shield_args = [] + for shield in shields: + west_shield_args.extend(("--shield", shield)) + + print(shlex.join(west_shield_args)) + + +if __name__ == "__main__": + main() diff --git a/ports/zephyr-cp/cptools/pre_zephyr_build_prep.py b/ports/zephyr-cp/cptools/pre_zephyr_build_prep.py index acc3ae786196d..f42fc1a3a8547 100644 --- a/ports/zephyr-cp/cptools/pre_zephyr_build_prep.py +++ b/ports/zephyr-cp/cptools/pre_zephyr_build_prep.py @@ -2,7 +2,6 @@ import pathlib import subprocess import sys -import tomllib import board_tools @@ -10,14 +9,11 @@ board = sys.argv[-1] -mpconfigboard = board_tools.find_mpconfigboard(portdir, board) -if mpconfigboard is None: +_, mpconfigboard = board_tools.load_mpconfigboard(portdir, board) +if not mpconfigboard: # Assume it doesn't need any prep. sys.exit(0) -with mpconfigboard.open("rb") as f: - mpconfigboard = tomllib.load(f) - blobs = mpconfigboard.get("BLOBS", []) blob_fetch_args = mpconfigboard.get("blob_fetch_args", {}) for blob in blobs: diff --git a/ports/zephyr-cp/cptools/zephyr2cp.py b/ports/zephyr-cp/cptools/zephyr2cp.py index f7d79517195e0..c123d90ce7816 100644 --- a/ports/zephyr-cp/cptools/zephyr2cp.py +++ b/ports/zephyr-cp/cptools/zephyr2cp.py @@ -488,6 +488,14 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir): # noqa value = device_tree.root.nodes["chosen"].props[k] path2chosen[value.to_path()] = k chosen2path[k] = value.to_path() + + chosen_display = chosen2path.get("zephyr,display") + if chosen_display is not None: + status = chosen_display.props.get("status", None) + if status is None or status.to_string() == "okay": + board_info["zephyr_display"] = True + board_info["displayio"] = True + remaining_nodes = set([device_tree.root]) while remaining_nodes: node = remaining_nodes.pop() @@ -724,6 +732,43 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir): # noqa zephyr_binding_objects = "\n".join(zephyr_binding_objects) zephyr_binding_labels = "\n".join(zephyr_binding_labels) + zephyr_display_header = "" + zephyr_display_object = "" + zephyr_display_board_entry = "" + if board_info.get("zephyr_display", False): + zephyr_display_header = """ +#include +#include +#include "shared-module/displayio/__init__.h" +#include "bindings/zephyr_display/Display.h" + """.strip() + zephyr_display_object = """ +void board_init(void) { +#if CIRCUITPY_ZEPHYR_DISPLAY && DT_HAS_CHOSEN(zephyr_display) + // Always allocate a display slot so board.DISPLAY is at least a valid + // NoneType object even if the underlying Zephyr display is unavailable. + primary_display_t *display_obj = allocate_display(); + if (display_obj == NULL) { + return; + } + + zephyr_display_display_obj_t *display = &display_obj->zephyr_display; + display->base.type = &mp_type_NoneType; + + const struct device *display_dev = device_get_binding(DEVICE_DT_NAME(DT_CHOSEN(zephyr_display))); + if (display_dev == NULL || !device_is_ready(display_dev)) { + return; + } + + display->base.type = &zephyr_display_display_type; + common_hal_zephyr_display_display_construct_from_device(display, display_dev, 0, true); +#endif +} + """.strip() + zephyr_display_board_entry = ( + "{ MP_ROM_QSTR(MP_QSTR_DISPLAY), MP_ROM_PTR(&displays[0].zephyr_display) }," + ) + board_dir.mkdir(exist_ok=True, parents=True) header = board_dir / "mpconfigboard.h" if status_led: @@ -797,6 +842,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir): # noqa #include "py/mphal.h" {zephyr_binding_headers} +{zephyr_display_header} const struct device* const flashes[] = {{ {", ".join(flashes)} }}; const int circuitpy_flash_device_count = {len(flashes)}; @@ -810,6 +856,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir): # noqa {pin_defs} {zephyr_binding_objects} +{zephyr_display_object} static const mp_rom_map_elem_t mcu_pin_globals_table[] = {{ {mcu_pin_mapping} @@ -820,6 +867,7 @@ def zephyr_dts_to_cp_board(board_id, portdir, builddir, zephyrbuilddir): # noqa CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS {hostnetwork_entry} +{zephyr_display_board_entry} {board_pin_mapping} {zephyr_binding_labels} diff --git a/ports/zephyr-cp/debug.conf b/ports/zephyr-cp/debug.conf new file mode 100644 index 0000000000000..90c1f52d4db6b --- /dev/null +++ b/ports/zephyr-cp/debug.conf @@ -0,0 +1,15 @@ +CONFIG_DEBUG=y +CONFIG_DEBUG_OPTIMIZATIONS=y +CONFIG_LOG_MAX_LEVEL=4 + +CONFIG_STACK_SENTINEL=y +CONFIG_DEBUG_THREAD_INFO=y +CONFIG_DEBUG_INFO=y +CONFIG_EXCEPTION_STACK_TRACE=y + +CONFIG_ASSERT=y +CONFIG_LOG_BLOCK_IN_THREAD=y +CONFIG_FRAME_POINTER=y + +CONFIG_FLASH_LOG_LEVEL_DBG=y +CONFIG_LOG_MODE_IMMEDIATE=y diff --git a/ports/zephyr-cp/tests/__init__.py b/ports/zephyr-cp/tests/__init__.py index 18e596e8e7046..8ab7610ce0f53 100644 --- a/ports/zephyr-cp/tests/__init__.py +++ b/ports/zephyr-cp/tests/__init__.py @@ -1,3 +1,5 @@ +from pathlib import Path + import serial import subprocess import threading @@ -144,6 +146,14 @@ def shutdown(self): self.serial.close() self.debug_serial.close() + def display_capture_paths(self) -> list[Path]: + """Return paths to numbered PNG capture files produced by trace-driven capture.""" + pattern = getattr(self, "_capture_png_pattern", None) + count = getattr(self, "_capture_count", 0) + if not pattern or count == 0: + return [] + return [Path(pattern % i) for i in range(count)] + def wait_until_done(self): start_time = time.monotonic() while self._proc.poll() is None and time.monotonic() - start_time < self._timeout: diff --git a/ports/zephyr-cp/tests/conftest.py b/ports/zephyr-cp/tests/conftest.py index 1a364ba2995eb..b0047f0c94763 100644 --- a/ports/zephyr-cp/tests/conftest.py +++ b/ports/zephyr-cp/tests/conftest.py @@ -4,23 +4,31 @@ """Pytest fixtures for CircuitPython native_sim testing.""" import logging -import re -import select +import os import subprocess -import time -from dataclasses import dataclass from pathlib import Path import pytest import serial -from . import NativeSimProcess + from .perfetto_input_trace import write_input_trace from perfetto.trace_processor import TraceProcessor +from . import NativeSimProcess + logger = logging.getLogger(__name__) +def pytest_addoption(parser): + parser.addoption( + "--update-goldens", + action="store_true", + default=False, + help="Overwrite golden images with captured output instead of comparing.", + ) + + def pytest_configure(config): config.addinivalue_line( "markers", "circuitpy_drive(files): run CircuitPython with files in the flash image" @@ -51,6 +59,20 @@ def pytest_configure(config): "markers", "native_sim_rt: run native_sim in realtime mode (-rt instead of -no-rt)", ) + config.addinivalue_line( + "markers", + "display(capture_times_ns=None): run test with SDL display; " + "capture_times_ns is a list of nanosecond timestamps for trace-triggered captures", + ) + config.addinivalue_line( + "markers", + "display_pixel_format(format): override the display pixel format " + "(e.g. 'RGB_565', 'ARGB_8888')", + ) + config.addinivalue_line( + "markers", + "display_mono_vtiled(value): override the mono vtiled screen_info flag (True or False)", + ) ZEPHYR_CP = Path(__file__).parent.parent @@ -136,7 +158,6 @@ def board(request): @pytest.fixture def native_sim_binary(request, board): """Return path to native_sim binary, skip if not built.""" - ZEPHYR_CP = Path(__file__).parent.parent build_dir = ZEPHYR_CP / f"build-{board}" binary = build_dir / "zephyr-cp/zephyr/zephyr.exe" @@ -150,6 +171,26 @@ def native_sim_env() -> dict[str, str]: return {} +PIXEL_FORMAT_BITMASK = { + "RGB_888": 1 << 0, + "MONO01": 1 << 1, + "MONO10": 1 << 2, + "ARGB_8888": 1 << 3, + "RGB_565": 1 << 4, + "BGR_565": 1 << 5, + "L_8": 1 << 6, + "AL_88": 1 << 7, +} + + +@pytest.fixture +def pixel_format(request) -> str: + """Indirect-parametrize fixture: adds display_pixel_format marker.""" + fmt = request.param + request.node.add_marker(pytest.mark.display_pixel_format(fmt)) + return fmt + + @pytest.fixture def sim_id(request) -> str: return request.node.nodeid.replace("/", "_") @@ -175,6 +216,54 @@ def circuitpython(request, board, sim_id, native_sim_binary, native_sim_env, tmp if input_trace_markers and len(input_trace_markers[0][1].args) == 1: input_trace = input_trace_markers[0][1].args[0] + input_trace_file = None + if input_trace is not None: + input_trace_file = tmp_path / "input.perfetto" + write_input_trace(input_trace_file, input_trace) + + marker = request.node.get_closest_marker("duration") + if marker is None: + timeout = 10 + else: + timeout = marker.args[0] + + runs_marker = request.node.get_closest_marker("code_py_runs") + if runs_marker is None: + code_py_runs = 1 + else: + code_py_runs = int(runs_marker.args[0]) + + display_marker = request.node.get_closest_marker("display") + if display_marker is None: + display_marker = request.node.get_closest_marker("display_capture") + + capture_times_ns = None + if display_marker is not None: + capture_times_ns = display_marker.kwargs.get("capture_times_ns", None) + + pixel_format_marker = request.node.get_closest_marker("display_pixel_format") + pixel_format = None + if pixel_format_marker is not None and pixel_format_marker.args: + pixel_format = pixel_format_marker.args[0] + + mono_vtiled_marker = request.node.get_closest_marker("display_mono_vtiled") + mono_vtiled = None + if mono_vtiled_marker is not None and mono_vtiled_marker.args: + mono_vtiled = mono_vtiled_marker.args[0] + + # If capture_times_ns is set, merge display_capture track into input trace. + if capture_times_ns is not None: + if input_trace is None: + input_trace = {} + else: + input_trace = dict(input_trace) + input_trace["display_capture"] = list(capture_times_ns) + if input_trace_file is None: + input_trace_file = tmp_path / "input.perfetto" + write_input_trace(input_trace_file, input_trace) + + use_realtime = request.node.get_closest_marker("native_sim_rt") is not None + procs = [] for i in range(instance_count): flash = tmp_path / f"flash-{i}.bin" @@ -189,30 +278,14 @@ def circuitpython(request, board, sim_id, native_sim_binary, native_sim_env, tmp for name, content in files.items(): src = tmp_drive / name - src.write_text(content) + if isinstance(content, bytes): + src.write_bytes(content) + else: + src.write_text(content) subprocess.run(["mcopy", "-i", str(flash), str(src), f"::{name}"], check=True) trace_file = tmp_path / f"trace-{i}.perfetto" - input_trace_file = None - if input_trace is not None: - input_trace_file = tmp_path / f"input-{i}.perfetto" - write_input_trace(input_trace_file, input_trace) - - marker = request.node.get_closest_marker("duration") - if marker is None: - timeout = 10 - else: - timeout = marker.args[0] - - runs_marker = request.node.get_closest_marker("code_py_runs") - if runs_marker is None: - code_py_runs = 1 - else: - code_py_runs = int(runs_marker.args[0]) - - use_realtime = request.node.get_closest_marker("native_sim_rt") is not None - if "bsim" in board: cmd = [str(native_sim_binary), f"--flash_app={flash}"] if instance_count > 1: @@ -231,7 +304,9 @@ def circuitpython(request, board, sim_id, native_sim_binary, native_sim_env, tmp cmd = [str(native_sim_binary), f"--flash={flash}"] # native_sim vm-runs includes the boot VM setup run. realtime_flag = "-rt" if use_realtime else "-no-rt" - cmd.extend((realtime_flag, "-wait_uart", f"--vm-runs={code_py_runs + 1}")) + cmd.extend( + (realtime_flag, "-display_headless", "-wait_uart", f"--vm-runs={code_py_runs + 1}") + ) if input_trace_file is not None: cmd.append(f"--input-trace={input_trace_file}") @@ -240,9 +315,31 @@ def circuitpython(request, board, sim_id, native_sim_binary, native_sim_env, tmp if marker and len(marker.args) > 0: for device in marker.args: cmd.append(f"--disable-i2c={device}") + + if pixel_format is not None: + cmd.append(f"--display_pixel_format={PIXEL_FORMAT_BITMASK[pixel_format]}") + + if mono_vtiled is not None: + cmd.append(f"--display_mono_vtiled={'true' if mono_vtiled else 'false'}") + + env = os.environ.copy() + env.update(native_sim_env) + + capture_png_pattern = None + if capture_times_ns is not None: + if instance_count == 1: + capture_png_pattern = str(tmp_path / "frame_%d.png") + else: + capture_png_pattern = str(tmp_path / f"frame-{i}_%d.png") + cmd.append(f"--display_capture_png={capture_png_pattern}") + logger.info("Running: %s", " ".join(cmd)) + proc = NativeSimProcess(cmd, timeout, trace_file, env) + proc.display_dump = None + proc._capture_png_pattern = capture_png_pattern + proc._capture_count = len(capture_times_ns) if capture_times_ns is not None else 0 + procs.append(proc) - procs.append(NativeSimProcess(cmd, timeout, trace_file, native_sim_env)) if instance_count == 1: yield procs[0] else: diff --git a/ports/zephyr-cp/tests/perfetto_input_trace.py b/ports/zephyr-cp/tests/perfetto_input_trace.py index d0cde49be087a..494c9cdcadf69 100644 --- a/ports/zephyr-cp/tests/perfetto_input_trace.py +++ b/ports/zephyr-cp/tests/perfetto_input_trace.py @@ -7,11 +7,12 @@ python -m tests.perfetto_input_trace input_trace.json output.perfetto -Input JSON format: +Input JSON format — counter tracks use [timestamp, value] pairs, instant +tracks use bare timestamps: { "gpio_emul.01": [[8000000000, 0], [9000000000, 1], [10000000000, 0]], - "gpio_emul.02": [[8000000000, 0], [9200000000, 1]] + "display_capture": [500000000, 1000000000] } """ @@ -22,7 +23,9 @@ from pathlib import Path from typing import Mapping, Sequence -InputTraceData = Mapping[str, Sequence[tuple[int, int]]] +# Counter tracks: list of (timestamp, value) pairs. +# Instant tracks: list of timestamps (bare ints). +InputTraceData = Mapping[str, Sequence[tuple[int, int] | int]] def _load_perfetto_pb2(): @@ -31,8 +34,15 @@ def _load_perfetto_pb2(): return perfetto_pb2 +def _is_instant_track(events: Sequence) -> bool: + """Return True if *events* is a list of bare timestamps (instant track).""" + if not events: + return False + return isinstance(events[0], int) + + def build_input_trace(trace_data: InputTraceData, *, sequence_id: int = 1): - """Build a Perfetto Trace protobuf for input replay counter tracks.""" + """Build a Perfetto Trace protobuf for input replay counter and instant tracks.""" perfetto_pb2 = _load_perfetto_pb2() trace = perfetto_pb2.Trace() @@ -41,6 +51,7 @@ def build_input_trace(trace_data: InputTraceData, *, sequence_id: int = 1): for idx, (track_name, events) in enumerate(trace_data.items()): track_uuid = 1001 + idx + instant = _is_instant_track(events) desc_packet = trace.packet.add() desc_packet.timestamp = 0 @@ -49,16 +60,28 @@ def build_input_trace(trace_data: InputTraceData, *, sequence_id: int = 1): desc_packet.sequence_flags = seq_incremental_state_cleared desc_packet.track_descriptor.uuid = track_uuid desc_packet.track_descriptor.name = track_name - desc_packet.track_descriptor.counter.unit = perfetto_pb2.CounterDescriptor.Unit.UNIT_COUNT + if not instant: + desc_packet.track_descriptor.counter.unit = ( + perfetto_pb2.CounterDescriptor.Unit.UNIT_COUNT + ) - for ts, value in events: - event_packet = trace.packet.add() - event_packet.timestamp = ts - event_packet.trusted_packet_sequence_id = sequence_id - event_packet.sequence_flags = seq_needs_incremental_state - event_packet.track_event.type = perfetto_pb2.TrackEvent.Type.TYPE_COUNTER - event_packet.track_event.track_uuid = track_uuid - event_packet.track_event.counter_value = value + if instant: + for ts in events: + event_packet = trace.packet.add() + event_packet.timestamp = ts + event_packet.trusted_packet_sequence_id = sequence_id + event_packet.sequence_flags = seq_needs_incremental_state + event_packet.track_event.type = perfetto_pb2.TrackEvent.Type.TYPE_INSTANT + event_packet.track_event.track_uuid = track_uuid + else: + for ts, value in events: + event_packet = trace.packet.add() + event_packet.timestamp = ts + event_packet.trusted_packet_sequence_id = sequence_id + event_packet.sequence_flags = seq_needs_incremental_state + event_packet.track_event.type = perfetto_pb2.TrackEvent.Type.TYPE_COUNTER + event_packet.track_event.track_uuid = track_uuid + event_packet.track_event.counter_value = value return trace @@ -72,27 +95,35 @@ def write_input_trace( trace_file.write_bytes(trace.SerializeToString()) -def _parse_trace_json(data: object) -> dict[str, list[tuple[int, int]]]: +def _parse_trace_json(data: object) -> dict[str, list[tuple[int, int]] | list[int]]: if not isinstance(data, dict): raise ValueError("top-level JSON value must be an object") - parsed: dict[str, list[tuple[int, int]]] = {} + parsed: dict[str, list[tuple[int, int]] | list[int]] = {} for track_name, events in data.items(): if not isinstance(track_name, str): raise ValueError("track names must be strings") if not isinstance(events, list): - raise ValueError( - f"track {track_name!r} must map to a list of [timestamp, value] events" - ) - - parsed_events: list[tuple[int, int]] = [] - for event in events: - if not isinstance(event, (list, tuple)) or len(event) != 2: - raise ValueError(f"track {track_name!r} events must be [timestamp, value] pairs") - timestamp_ns, value = event - parsed_events.append((int(timestamp_ns), int(value))) - - parsed[track_name] = parsed_events + raise ValueError(f"track {track_name!r} must map to a list of events") + + if not events: + parsed[track_name] = [] + continue + + # Distinguish instant (bare ints) vs counter ([ts, value] pairs). + if isinstance(events[0], (int, float)): + parsed[track_name] = [int(ts) for ts in events] + else: + parsed_events: list[tuple[int, int]] = [] + for event in events: + if not isinstance(event, (list, tuple)) or len(event) != 2: + raise ValueError( + f"track {track_name!r} events must be [timestamp, value] pairs " + "or bare timestamps" + ) + timestamp_ns, value = event + parsed_events.append((int(timestamp_ns), int(value))) + parsed[track_name] = parsed_events return parsed diff --git a/ports/zephyr-cp/tests/zephyr_display/README.md b/ports/zephyr-cp/tests/zephyr_display/README.md new file mode 100644 index 0000000000000..6b50202154352 --- /dev/null +++ b/ports/zephyr-cp/tests/zephyr_display/README.md @@ -0,0 +1,45 @@ +# Zephyr Display Golden Tests + +This directory contains native_sim golden-image tests for the Zephyr-specific `zephyr_display` path. + +## What is tested + +- `board.DISPLAY` is present and usable. +- CircuitPython terminal/console tilegrids are attached to the default display root group. +- Deterministic console terminal output matches a checked-in golden image. +- `zephyr_display` pixel format constants are exposed. +- `displayio` rendering produces expected stripe colors at sampled pixel locations. + +## Files + +- `test_zephyr_display.py` – pytest tests. +- `golden/terminal_console_output_320x240.png` – console terminal output golden reference image. + +## How capture works + +These tests use trace-driven SDL display capture triggered by Perfetto instant events: + +- `--input-trace=` provides a Perfetto trace containing a `"display_capture"` track + with instant events at the desired capture timestamps. +- `--display_capture_png=` specifies the output PNG pattern (may contain `%d` for + a sequence number). +- `--display_headless` runs SDL in headless/hidden-window mode (always enabled for native_sim tests). + +The test harness sets these flags automatically when tests use +`@pytest.mark.display(capture_times_ns=[...])`. + +## Regenerating the console golden image + +```bash +rm -rf /tmp/zephyr-display-golden +pytest -q ports/zephyr-cp/tests/zephyr_display/test_zephyr_display.py::test_console_output_golden \ + --basetemp=/tmp/zephyr-display-golden +cp /tmp/zephyr-display-golden/test_console_output_golden0/frame_0.png \ + ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240.png +``` + +## Running the tests + +```bash +pytest -q ports/zephyr-cp/tests/zephyr_display/test_zephyr_display.py +``` diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240.png b/ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240.png new file mode 100644 index 0000000000000000000000000000000000000000..5739b8ee156eb72f2fd419d5de58eb81d6750650 GIT binary patch literal 12102 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 literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_AL_88.png b/ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_AL_88.png new file mode 100644 index 0000000000000000000000000000000000000000..6bdb72b802c97f5981b6e59a0505d9f390d3de74 GIT binary patch literal 17323 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 vpQBuz?7x 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 literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_L_8.png b/ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_L_8.png new file mode 100644 index 0000000000000000000000000000000000000000..6bdb72b802c97f5981b6e59a0505d9f390d3de74 GIT binary patch literal 17323 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 literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_RGB_888.png b/ports/zephyr-cp/tests/zephyr_display/golden/color_gradient_320x240_RGB_888.png new file mode 100644 index 0000000000000000000000000000000000000000..c4301da47c53003aedb988f1c486a3b791f2d9d8 GIT binary patch literal 15287 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�i~jU>H#B-Jo0@FkIX5`)dUo`*lS+Q$eD2lGU3J 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 m!0h;2L6A(6^gI` literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240.mask.png b/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240.mask.png new file mode 100644 index 0000000000000000000000000000000000000000..3ebb967560f0a9d474e71d36edf028375d984561 GIT binary patch literal 450 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoKX3pEh7h+$+kpa|o-U3d6?5L+b>uyuz`$}a z=>NQKwoUvCW(F2SGMmraT(g^fO;tJnj=2pJ@j#G)SilG4$Gglg9IhztVCfl!_(MP~ Xr-j+IP3CSbC>%Xq{an^LB{Ts5OZSCr literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240.png b/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240.png new file mode 100644 index 0000000000000000000000000000000000000000..77577a65601abe1b7eb8d0771e8e1ceb795e8400 GIT binary patch literal 3930 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 literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_ARGB_8888.png b/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_ARGB_8888.png new file mode 100644 index 0000000000000000000000000000000000000000..90b422792db10a0205ab8fdaecbad4e1588d2106 GIT binary patch literal 1024 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 literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO01.png b/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO01.png new file mode 100644 index 0000000000000000000000000000000000000000..59928bcd1070454803935e4a81b825c039f3eac1 GIT binary patch literal 3655 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 J-u2?0e*wX57cBq) literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO01_no_vtiled.png b/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO01_no_vtiled.png new file mode 100644 index 0000000000000000000000000000000000000000..59928bcd1070454803935e4a81b825c039f3eac1 GIT binary patch literal 3655 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 J-u2?0e*wX57cBq) literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO10.png b/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO10.png new file mode 100644 index 0000000000000000000000000000000000000000..59928bcd1070454803935e4a81b825c039f3eac1 GIT binary patch literal 3655 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 J-u2?0e*wX57cBq) literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO10_no_vtiled.png b/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_MONO10_no_vtiled.png new file mode 100644 index 0000000000000000000000000000000000000000..59928bcd1070454803935e4a81b825c039f3eac1 GIT binary patch literal 3655 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 J-u2?0e*wX57cBq) literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_RGB_565.png b/ports/zephyr-cp/tests/zephyr_display/golden/terminal_console_output_320x240_RGB_565.png new file mode 100644 index 0000000000000000000000000000000000000000..77577a65601abe1b7eb8d0771e8e1ceb795e8400 GIT binary patch literal 3930 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 literal 0 HcmV?d00001 diff --git a/ports/zephyr-cp/tests/zephyr_display/test_zephyr_display.py b/ports/zephyr-cp/tests/zephyr_display/test_zephyr_display.py new file mode 100644 index 0000000000000..6700c42ec4a94 --- /dev/null +++ b/ports/zephyr-cp/tests/zephyr_display/test_zephyr_display.py @@ -0,0 +1,365 @@ +# SPDX-FileCopyrightText: 2026 Scott Shawcroft for Adafruit Industries +# SPDX-License-Identifier: MIT + +import colorsys +import shutil +import struct +from pathlib import Path + +import pytest +from PIL import Image + + +def _read_image(path: Path) -> tuple[int, int, bytes]: + """Read an image file and return (width, height, RGB bytes).""" + with Image.open(path) as img: + rgb = img.convert("RGB") + return rgb.width, rgb.height, rgb.tobytes() + + +def _read_mask(golden_path: Path) -> bytes | None: + """Load the companion mask PNG for a golden image, if it exists. + + Non-zero pixels are masked (skipped during comparison). + """ + mask_path = golden_path.with_suffix(".mask.png") + if not mask_path.exists(): + return None + with Image.open(mask_path) as img: + return img.convert("L").tobytes() + + +def _assert_pixels_equal_masked( + golden_pixels: bytes, actual_pixels: bytes, mask: bytes | None = None +): + """Assert pixels match, skipping positions where the mask is non-zero.""" + assert len(golden_pixels) == len(actual_pixels) + for i in range(0, len(golden_pixels), 3): + if mask is not None and mask[i // 3] != 0: + continue + if golden_pixels[i : i + 3] != actual_pixels[i : i + 3]: + pixel = i // 3 + assert False, ( + f"Pixel {pixel} mismatch: " + f"golden={tuple(golden_pixels[i : i + 3])} " + f"actual={tuple(actual_pixels[i : i + 3])}" + ) + + +BOARD_DISPLAY_AVAILABLE_CODE = """\ +import board +print(hasattr(board, 'DISPLAY')) +print(type(board.DISPLAY).__name__) +print(board.DISPLAY.width, board.DISPLAY.height) +print('done') +""" + + +@pytest.mark.circuitpy_drive({"code.py": BOARD_DISPLAY_AVAILABLE_CODE}) +@pytest.mark.display +@pytest.mark.duration(8) +def test_board_display_available(circuitpython): + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "True" in output + assert "Display" in output + assert "320 240" in output + assert "done" in output + + +CONSOLE_TERMINAL_PRESENT_CODE = """\ +import board + +root = board.DISPLAY.root_group +has_terminal_tilegrids = ( + type(root).__name__ == 'Group' and + len(root) >= 2 and + type(root[0]).__name__ == 'TileGrid' and + type(root[-1]).__name__ == 'TileGrid' +) +print('has_terminal_tilegrids:', has_terminal_tilegrids) +print('done') +""" + + +@pytest.mark.circuitpy_drive({"code.py": CONSOLE_TERMINAL_PRESENT_CODE}) +@pytest.mark.display +@pytest.mark.duration(8) +def test_console_terminal_present_by_default(circuitpython): + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "has_terminal_tilegrids: True" in output + assert "done" in output + + +CONSOLE_OUTPUT_GOLDEN_CODE = """\ +import time +time.sleep(0.25) +print('done') +while True: + time.sleep(1) +""" + + +def _golden_compare_or_update(request, captures, golden_path, mask_path=None): + """Compare captured PNG against golden, or update golden if --update-goldens. + + mask_path overrides the default companion mask lookup (golden.mask.png). + """ + if not captures or not captures[0].exists(): + pytest.skip("display capture was not produced") + + if request.config.getoption("--update-goldens"): + golden_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(captures[0], golden_path) + return + + gw, gh, gpx = _read_image(golden_path) + dw, dh, dpx = _read_image(captures[0]) + if mask_path is not None: + mask = _read_mask(mask_path) + else: + mask = _read_mask(golden_path) + + assert (dw, dh) == (gw, gh) + _assert_pixels_equal_masked(gpx, dpx, mask) + + +@pytest.mark.circuitpy_drive({"code.py": CONSOLE_OUTPUT_GOLDEN_CODE}) +@pytest.mark.display(capture_times_ns=[4_000_000_000]) +@pytest.mark.duration(8) +def test_console_output_golden(request, circuitpython): + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "done" in output + + golden = Path(__file__).parent / "golden" / "terminal_console_output_320x240.png" + _golden_compare_or_update(request, circuitpython.display_capture_paths(), golden) + + +PIXEL_FORMATS = ["ARGB_8888", "RGB_888", "RGB_565", "BGR_565", "L_8", "AL_88", "MONO01", "MONO10"] + +# Shared mask: the same screen regions vary regardless of pixel format. +_CONSOLE_GOLDEN_MASK = Path(__file__).parent / "golden" / "terminal_console_output_320x240.png" + + +@pytest.mark.circuitpy_drive({"code.py": CONSOLE_OUTPUT_GOLDEN_CODE}) +@pytest.mark.display(capture_times_ns=[4_000_000_000]) +@pytest.mark.duration(8) +@pytest.mark.parametrize( + "pixel_format", + PIXEL_FORMATS, + indirect=True, +) +def test_console_output_golden_pixel_format(pixel_format, request, circuitpython): + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "done" in output + + golden_name = f"terminal_console_output_320x240_{pixel_format}.png" + golden = Path(__file__).parent / "golden" / golden_name + _golden_compare_or_update( + request, circuitpython.display_capture_paths(), golden, _CONSOLE_GOLDEN_MASK + ) + + +MONO_NO_VTILED_FORMATS = ["MONO01", "MONO10"] + + +@pytest.mark.circuitpy_drive({"code.py": CONSOLE_OUTPUT_GOLDEN_CODE}) +@pytest.mark.display(capture_times_ns=[4_000_000_000]) +@pytest.mark.display_mono_vtiled(False) +@pytest.mark.duration(8) +@pytest.mark.parametrize( + "pixel_format", + MONO_NO_VTILED_FORMATS, + indirect=True, +) +def test_console_output_golden_mono_no_vtiled(pixel_format, request, circuitpython): + circuitpython.wait_until_done() + + output = circuitpython.serial.all_output + assert "done" in output + + golden_name = f"terminal_console_output_320x240_{pixel_format}_no_vtiled.png" + golden = Path(__file__).parent / "golden" / golden_name + _golden_compare_or_update( + request, circuitpython.display_capture_paths(), golden, _CONSOLE_GOLDEN_MASK + ) + + +def _generate_gradient_bmp(width, height): + """Generate a 24-bit BMP with HSL color gradient. + + Hue sweeps left to right, lightness goes from black (bottom) to white (top), + saturation is 1.0. + """ + row_size = width * 3 + row_padding = (4 - (row_size % 4)) % 4 + padded_row_size = row_size + row_padding + pixel_data_size = padded_row_size * height + file_size = 14 + 40 + pixel_data_size + + header = struct.pack( + "<2sIHHI", + b"BM", + file_size, + 0, + 0, + 14 + 40, + ) + info = struct.pack( + "