diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index f94de73..85c5661 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -222,7 +222,7 @@ set(ZEPHCORE_COMMON_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/zephcore_com # Detect platform from board name and add platform-specific common config # For boards with qualifiers like "wio_tracker_l1/nrf52840", check both the full BOARD # string and the BOARD_QUALIFIERS which contains just the qualifier (e.g., "nrf52840") -if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "promicro_sx1262" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840" OR BOARD MATCHES "lilygo_techo") +if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "promicro_sx1262" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840" OR BOARD MATCHES "lilygo_techo" OR BOARD MATCHES "heltec_t114") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*") set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf") @@ -540,6 +540,12 @@ if(CONFIG_ZEPHCORE_UI_DISPLAY) ) endif() +if(CONFIG_ZEPHCORE_DISPLAY_MONO_TFT) + target_sources(app PRIVATE + helpers/ui/display_mono_tft.c + ) +endif() + if(CONFIG_ZEPHCORE_UI_BUTTONS OR CONFIG_ZEPHCORE_UI_BUZZER OR CONFIG_ZEPHCORE_UI_DISPLAY) target_sources(app PRIVATE helpers/ui/ui_task.c diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 137515b..b0e3b39 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -583,6 +583,17 @@ config ZEPHCORE_DISPLAY_LARGE_FONT no per-page changes are needed. Requires Zephyr's default CFB fonts to be compiled in (CONFIG_CHARACTER_FRAMEBUFFER_USE_DEFAULT_FONTS). +config ZEPHCORE_DISPLAY_MONO_TFT + bool "Monochrome TFT wrapper (MONO01 => RGB565)" + default y if DT_HAS_ZEPHCORE_MONO_TFT_ENABLED + depends on DT_HAS_ZEPHCORE_MONO_TFT_ENABLED + help + Wraps an RGB TFT (e.g. ST7789V) as a PIXEL_FORMAT_MONO01 display. + CFB allocates x*y/8 bytes (~4 KB for 240×135) instead of ~64 KB RGB565, + making a colour TFT viable on RAM-constrained devices like the nRF52840. + On display_write(), converts 1bpp row-major MONO01 to RGB565 row by row. + Auto-enabled when a "zephcore,mono-tft" device exists in the devicetree. + config ZEPHCORE_EASTER_EGG_DOOM bool "Enable Doom raycaster easter egg" default n diff --git a/zephcore/boards/nrf52840/heltec_t114/Kconfig.heltec_t114 b/zephcore/boards/nrf52840/heltec_t114/Kconfig.heltec_t114 new file mode 100644 index 0000000..c139e57 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/Kconfig.heltec_t114 @@ -0,0 +1,6 @@ +# Copyright (c) 2026 ZephCore +# +# SPDX-License-Identifier: Apache-2.0 + +config BOARD_HELTEC_T114 + select SOC_NRF52840_QIAA diff --git a/zephcore/boards/nrf52840/heltec_t114/board.cmake b/zephcore/boards/nrf52840/heltec_t114/board.cmake new file mode 100644 index 0000000..81f03cd --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/board.cmake @@ -0,0 +1,14 @@ +# Copyright (c) 2026 ZephCore +# +# SPDX-License-Identifier: Apache-2.0 + +board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000") +board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000") + +set(OPENOCD_NRF5_SUBFAMILY "nrf52") + +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) +include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake) +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake) diff --git a/zephcore/boards/nrf52840/heltec_t114/board.conf b/zephcore/boards/nrf52840/heltec_t114/board.conf new file mode 100644 index 0000000..718f379 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/board.conf @@ -0,0 +1,31 @@ +# Heltec T114 — nRF52840 + SX1262 +# Board-specific configuration +# +# Hardware: +# - SX1262 LoRa (22 dBm, DIO2 RF switch, TCXO 1.8V, RX boosted) +# - ATGM336H GPS on UART0 (9600 baud, enable P0.21, reset P1.06 active-LOW) +# - Battery AIN2 (P0.04), 4.9:1 voltage divider, enable P0.06 +# - I2C0 (SDA=P0.07, SCL=P0.08) for sensors +# - ST7789V 1.14" 240×135 TFT (SPI3, MIPI-DBI, mono-tft wrapper) +# - LED: P1.03 (active-LOW, LoRa TX) +# - Button: P1.10 (active-LOW) + +# Board identification (matches Arduino MeshCore variant name) +CONFIG_ZEPHCORE_BOARD_NAME="Heltec T114" + +# Device Information Service model name +CONFIG_BT_DIS_MODEL_NUMBER_STR="Heltec T114" + +# SoftDevice firmware ID (s140 v6.1.1, same as RAK4631) +CONFIG_ZEPHCORE_SD_FWID=0x00B6 + +# ========== RAM budget ========== +# CFB mono-tft framebuffer = 240×135/8 = 4050 bytes, allocated from k_malloc. +# Default heap (2048) is sized for SSD1306 128×64 (1024 bytes). +# Screenless builds: pass no_display.conf to restore the 2048 default. +CONFIG_HEAP_MEM_POOL_SIZE=6144 + +# ========== UI / display ========== +# 1.14" 240×135 TFT - the default 6×8 font is illegible at this pixel density. +# Switch to the smallest Zephyr built-in CFB font >= 16 px tall (typically 10×16). +CONFIG_ZEPHCORE_DISPLAY_LARGE_FONT=y diff --git a/zephcore/boards/nrf52840/heltec_t114/board.yml b/zephcore/boards/nrf52840/heltec_t114/board.yml new file mode 100644 index 0000000..4a322a3 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/board.yml @@ -0,0 +1,10 @@ +# Copyright (c) 2026 ZephCore +# +# SPDX-License-Identifier: Apache-2.0 + +board: + name: heltec_t114 + full_name: Heltec T114 + vendor: heltec + socs: + - name: nrf52840 diff --git a/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840-pinctrl.dtsi b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840-pinctrl.dtsi new file mode 100644 index 0000000..d8a864b --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840-pinctrl.dtsi @@ -0,0 +1,77 @@ +/* + * Heltec T114 pin control definitions + * Copyright (c) 2026 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + * Pin mappings from MeshCore variants/heltec_t114 (variant.cpp, platformio.ini): + * - SPI2 (SX1262 LoRa): SCK=P0.19, MOSI=P0.22, MISO=P0.23 + * - SPI3 (ST7789 display): SCK=P1.08 (PIN_TFT_SCL=40), MOSI=P1.09 (PIN_TFT_SDA=41) + * - I2C0 (Wire1, sensors): SDA=P0.07, SCL=P0.08 + * - UART0 (GPS): TX=P1.07, RX=P1.05 + */ + +&pinctrl { + spi2_default: spi2_default { + group1 { + psels = , + , + ; + }; + }; + + spi2_sleep: spi2_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; + + spi3_default: spi3_default { + group1 { + psels = , + ; + }; + }; + + spi3_sleep: spi3_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + i2c0_default: i2c0_default { + group1 { + psels = , + ; + }; + }; + + i2c0_sleep: i2c0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + uart0_default: uart0_default { + group1 { + psels = , + ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; +}; diff --git a/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.dts b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.dts new file mode 100644 index 0000000..0a38dd6 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.dts @@ -0,0 +1,318 @@ +/* + * Heltec T114 — nRF52840 + SX1262 + ST7789 1.14" 240×135 TFT + ATGM336H GPS + * Copyright (c) 2026 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +/dts-v1/; +#include +#include +#include +#include +#include +#include "heltec_t114_nrf52840-pinctrl.dtsi" + +/* Delete default partitions from nrf52840_partition.dtsi */ +/delete-node/ &boot_partition; +/delete-node/ &slot0_partition; +/delete-node/ &slot1_partition; +/delete-node/ &storage_partition; + +/ { + model = "Heltec T114"; + compatible = "heltec,t114"; + + chosen { + zephyr,code-partition = &code_partition; + zephyr,console = &cdc_acm_uart; + zephyr,shell-uart = &cdc_acm_uart; + zephyr,display = &mono_tft; + }; + + leds { + compatible = "gpio-leds"; + + /* LED_BUILTIN on P1.03, active-LOW (LED_STATE_ON = LOW) */ + tx_led: led_0 { + gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; + label = "LoRa TX LED"; + }; + }; + + buttons: buttons { + compatible = "gpio-keys"; + + user_button: button_0 { + gpios = <&gpio1 10 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + zephyr,code = ; + label = "User Button"; + }; + }; + + /* Input filter: user button long-press detection (1000ms) */ + user_btn_longpress { + compatible = "zephyr,input-longpress"; + input = <&buttons>; + input-codes = ; + short-codes = ; + long-codes = ; + long-delay-ms = <1000>; + }; + + /* Input filter: user button multi-tap detection + * 1 tap (400ms wait) → KEY_1 = page next on TFT + * 2 taps (400ms wait) → KEY_B = flood advert + * 3 taps (400ms wait) → KEY_D = buzzer mute toggle + * 4 taps (400ms wait) → KEY_C = GPS on/off + * 5 taps (immediate) → KEY_E = LED heartbeat toggle */ + user_btn_multitap { + compatible = "zephcore,input-multi-tap"; + /* No 'input' phandle — listen to ALL devices. + * KEY_A is emitted by the longpress pseudo-device, + * not by &buttons, so we must not filter by device. */ + input-codes = ; + tap-codes = ; + tap-delay-ms = <400>; + }; + + /* GPS power enable — P0.21, HIGH = GPS powered on */ + gps_en: gps-enable { + compatible = "gpio-leds"; + gps_enable_pin: gps_enable { + gpios = <&gpio0 21 GPIO_ACTIVE_HIGH>; + label = "GPS Enable"; + }; + }; + + /* GPS hardware reset — P1.06, active-LOW (PIN_GPS_RESET_ACTIVE=LOW) */ + gps_rst: gps-reset { + compatible = "gpio-leds"; + gps_reset_pin: gps_reset { + gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; + label = "GPS Reset"; + }; + }; + + /* + * Battery ADC enable — P0.06, HIGH = enable divider for measurement. + * ZephyrBoard::getBattMilliVolts() calls regulator_{enable,disable} + * around each reading to save power. + */ + vbat_enable: vbat-enable { + compatible = "regulator-fixed"; + regulator-name = "vbat-enable"; + enable-gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>; + }; + + /* + * TFT power rail — P0.03, active-LOW (PIN_TFT_VDD_CTL LOW = display on). + * Owned by display.c: enabled before CFB init, never disabled. + * Screenless builds (no_display.conf) never call display.c so this + * pin stays HIGH and the panel VDD rail is never powered. + */ + tft_pwr_enable: tft-pwr-enable { + compatible = "regulator-fixed"; + regulator-name = "tft-pwr-enable"; + enable-gpios = <&gpio0 3 GPIO_ACTIVE_LOW>; + }; + + /* + * TFT backlight — P0.15, active-LOW (PIN_TFT_LEDA_CTL LOW = backlight on). + * Named disp_pwr_enable so display.c auto-gates it with the display on/off. + */ + disp_pwr_enable: disp-pwr-enable { + compatible = "regulator-fixed"; + regulator-name = "disp-pwr-enable"; + enable-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>; + }; + + /* + * Monochrome wrapper around the ST7789V. + * Reports PIXEL_FORMAT_MONO01 to CFB → ~4 KB framebuffer instead of ~64 KB. + * display.c picks this up via the zephyr,display chosen node. + */ + mono_tft: mono-tft { + compatible = "zephcore,mono-tft"; + tft-dev = <&tft>; + width = <240>; + height = <135>; + }; + + /* + * MIPI-DBI SPI bus for ST7789V. + * DC=P0.12 (PIN_TFT_DC=12), RST=P0.02 (PIN_TFT_RST=2, active-LOW). + * write-only: no MISO on display SPI. + */ + mipi_dbi { + compatible = "zephyr,mipi-dbi-spi"; + spi-dev = <&spi3>; + dc-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; + reset-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>; + write-only; + #address-cells = <1>; + #size-cells = <0>; + + tft: st7789v@0 { + compatible = "sitronix,st7789v"; + mipi-max-frequency = <20000000>; + reg = <0>; + width = <240>; + height = <135>; + x-offset = <40>; + y-offset = <53>; + /* MADCTL=0x60: MX|MV, RGB order (landscape, derived from MeshCore landscapeScreen()) */ + mdac = <0x60>; + colmod = <0x55>; + vcom = <0x20>; + gctrl = <0x35>; + vrhs = <0x0b>; + vdvs = <0x20>; + gamma = <0x01>; + lcm = <0x2c>; + porch-param = [0c 0c 00 33 33]; + cmd2en-param = [5a 69 02 01]; + pwctrl1-param = [a4 a1]; + pvgam-param = [70 2c 2e 15 10 09 48 33 53 0b 19 18 20 25]; + nvgam-param = [70 2c 2e 15 10 09 48 33 53 0b 19 18 20 25]; + ram-param = [00 f0]; + rgb-param = [40 02 14]; + mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE"; + }; + }; + + zephyr,user { + io-channels = <&adc 2>; + /* + * Full-scale = 3600 mV (ADC_GAIN_1_6 + ADC_REF_INTERNAL), + * voltage divider ratio = 4.9 (ADC_MULTIPLIER from variant.h). + * multiplier = 3600 * 4.9 = 17640 + */ + vbat-mv-multiplier = <17640>; + }; + + aliases { + led0 = &tx_led; + lora-tx-led = &tx_led; + sw0 = &user_button; + lora0 = &lora; + watchdog0 = &wdt0; + gps-enable = &gps_enable_pin; + gps-reset = &gps_reset_pin; + }; +}; + +®0 { + status = "okay"; +}; + +/* DC-DC converter (NRF52BoardDCDC) */ +®1 { + regulator-initial-mode = ; +}; + +&adc { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + + /* Battery voltage on P0.04 (AIN2) — 4.9:1 divider, 3.0V ref in Arduino */ + channel@2 { + reg = <2>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&i2c0 { + compatible = "nordic,nrf-twim"; + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c0_default>; + pinctrl-1 = <&i2c0_sleep>; + pinctrl-names = "default", "sleep"; + + #include "../../common/sensors-i2c.dtsi" +}; + +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <9600>; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; + + gnss: gnss { + compatible = "gnss-nmea-generic"; + }; +}; + +/* SPI2 - SX1262 LoRa radio */ +&spi2 { + compatible = "nordic,nrf-spim"; + status = "okay"; + cs-gpios = <&gpio0 24 GPIO_ACTIVE_LOW>; + pinctrl-0 = <&spi2_default>; + pinctrl-1 = <&spi2_sleep>; + pinctrl-names = "default", "sleep"; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + reset-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio0 17 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio0 20 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; + dio2-tx-enable; + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <5>; + spi-max-frequency = <8000000>; + rx-boosted; + }; +}; + +/* SPI3 - ST7789 1.14" 240×135 TFT display */ +&spi3 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi3_default>; + pinctrl-1 = <&spi3_sleep>; + pinctrl-names = "default", "sleep"; + cs-gpios = <&gpio0 11 GPIO_ACTIVE_LOW>; +}; + +/* + * QSPI Flash (MX25R1635F 2MB) — DISABLED. + */ +&qspi { + status = "disabled"; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; + + cdc_acm_uart: cdc_acm_uart { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +/* SoftDevice s140 v6.1.1 flash layout (app@0x26000, bootloader@0xF4000) */ +#include "../../common/nrf52_partitions_sdv6.dtsi" +#include "../../common/nrf52_wakeup.dtsi" diff --git a/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.yaml b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.yaml new file mode 100644 index 0000000..56f46e6 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.yaml @@ -0,0 +1,20 @@ +identifier: heltec_t114/nrf52840 +name: Heltec T114 +type: mcu +arch: arm +toolchain: + - zephyr + - gnuarmemb + - xtools +ram: 256 +flash: 1024 +supported: + - adc + - ble + - gpio + - i2c + - spi + - usb_device + - lora + - gnss +vendor: heltec diff --git a/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840_defconfig b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840_defconfig new file mode 100644 index 0000000..d06a2a1 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840_defconfig @@ -0,0 +1,21 @@ +# Copyright (c) 2026 ZephCore +# SPDX-License-Identifier: Apache-2.0 + +# Enable MPU +CONFIG_ARM_MPU=y + +# Enable hardware stack protection +CONFIG_HW_STACK_PROTECTION=y + +# Enable GPIO +CONFIG_GPIO=y + +# Enable UART driver +CONFIG_SERIAL=y + +# Enable console +CONFIG_CONSOLE=y + +# Build UF2 by default +CONFIG_BUILD_OUTPUT_UF2=y +CONFIG_USE_DT_CODE_PARTITION=y diff --git a/zephcore/boards/nrf52840/heltec_t114/no_display.conf b/zephcore/boards/nrf52840/heltec_t114/no_display.conf new file mode 100644 index 0000000..7584a55 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/no_display.conf @@ -0,0 +1,13 @@ +# Heltec T114 - nRF52840 + SX1262 (screenless) build overlay +# +# Pass via -DEXTRA_CONF_FILE when building for units without the TFT module. +# TFT panel VDD (P0.03) stays off for the entire session. +# +# west build -b heltec_t114 zephcore --pristine -- \ +# -DEXTRA_CONF_FILE="boards/common/prod.conf;boards/nrf52840/heltec_t114/no_display.conf" + +CONFIG_ZEPHCORE_UI_DISPLAY=n +CONFIG_ZEPHCORE_DISPLAY_MONO_TFT=n + +# Restore default heap => No framebuffer to allocate without the display. +CONFIG_HEAP_MEM_POOL_SIZE=2048 diff --git a/zephcore/dts/bindings/display/zephcore,mono-tft.yaml b/zephcore/dts/bindings/display/zephcore,mono-tft.yaml new file mode 100644 index 0000000..8103e98 --- /dev/null +++ b/zephcore/dts/bindings/display/zephcore,mono-tft.yaml @@ -0,0 +1,29 @@ +# Copyright (c) 2026 ZephCore +# +# SPDX-License-Identifier: Apache-2.0 + +description: | + Monochrome wrapper for RGB TFT displays (ST7789V, etc.) + + Reports PIXEL_FORMAT_MONO01 to CFB, so the framebuffer is ~4 KB + (1 bit per pixel) instead of the ~64 KB RGB565 buffer that CFB + would otherwise allocate. On flush, converts row-major 1bpp to + RGB565 and pushes to the underlying display row by row. + +compatible: "zephcore,mono-tft" + +properties: + tft-dev: + type: phandle + required: true + description: The underlying RGB TFT display device (e.g. ST7789V. etc.). + + width: + type: int + required: true + description: Display width in pixels. + + height: + type: int + required: true + description: Display height in pixels. diff --git a/zephcore/helpers/ui/display.c b/zephcore/helpers/ui/display.c index 807a8c2..763e410 100644 --- a/zephcore/helpers/ui/display.c +++ b/zephcore/helpers/ui/display.c @@ -59,6 +59,16 @@ static const struct device *backlight_reg; static bool backlight_on; +/* Optional TFT panel VDD regulator (e.g. T114 P0.03). + * Enabled once before CFB init and never disabled => panel VDD must stay on. + * Screenless builds omit display.c entirely so this is never called. */ +#if DT_NODE_EXISTS(DT_NODELABEL(tft_pwr_enable)) +static const struct device *panel_vdd_reg = + DEVICE_DT_GET_OR_NULL(DT_NODELABEL(tft_pwr_enable)); +#else +static const struct device *panel_vdd_reg; +#endif + /* EPD frame change detection (Arduino-style): * hash draw calls across a frame and skip hardware flush if unchanged. */ static uint32_t epd_frame_hash; @@ -96,6 +106,13 @@ static inline void backlight_set(bool on) } } +static inline void panel_vdd_enable(void) +{ + if (panel_vdd_reg && device_is_ready(panel_vdd_reg)) { + regulator_enable(panel_vdd_reg); + } +} + /* Auto-off work */ static struct k_work_delayable auto_off_work; @@ -202,6 +219,10 @@ int mc_display_init(void) display_blanking_on(disp_dev); } + /* Enable panel VDD before CFB init so the controller is powered + * when the init sequence is sent over SPI. */ + panel_vdd_enable(); + /* Initialize CFB */ int ret = cfb_framebuffer_init(disp_dev); diff --git a/zephcore/helpers/ui/display_mono_tft.c b/zephcore/helpers/ui/display_mono_tft.c new file mode 100644 index 0000000..81f8e93 --- /dev/null +++ b/zephcore/helpers/ui/display_mono_tft.c @@ -0,0 +1,149 @@ +/* + * ZephCore - Monochrome wrapper for RGB TFT displays + * Copyright (c) 2026 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + * Reports PIXEL_FORMAT_MONO01 to CFB so the framebuffer is ~4 KB + * (240×135/8 = 4050 bytes) instead of the ~64 KB RGB565 buffer that + * CFB would otherwise allocate for an ST7789V. + * + * On display_write(), converts row major 1bpp MONO01 (LSB first) to + * RGB565 one row at a time and forwards each row to the underlying TFT. + */ + +#define DT_DRV_COMPAT zephcore_mono_tft + +#include +#include +#include +#include + +#define LOG_LEVEL CONFIG_DISPLAY_LOG_LEVEL +#include +LOG_MODULE_REGISTER(display_mono_tft); + +struct mono_tft_config { + const struct device *tft; + uint16_t width; + uint16_t height; +}; + +/* Hardcoded to instance 0. Only one display wrapper per board is supported. */ +#define MONO_TFT_MAX_WIDTH DT_INST_PROP(0, width) + +struct mono_tft_data { + /* Static allocation avoids ~480 bytes of stack pressure per display_write() call. */ + uint16_t line_buf[MONO_TFT_MAX_WIDTH]; +}; + +static int mono_tft_blanking_on(const struct device *dev) +{ + const struct mono_tft_config *cfg = dev->config; + + return display_blanking_on(cfg->tft); +} + +static int mono_tft_blanking_off(const struct device *dev) +{ + const struct mono_tft_config *cfg = dev->config; + + return display_blanking_off(cfg->tft); +} + +static int mono_tft_write( + const struct device *dev, + const uint16_t x_start, const uint16_t y_start, + const struct display_buffer_descriptor *desc, + const void *buf +){ + const struct mono_tft_config *cfg = dev->config; + struct mono_tft_data *data = dev->data; + const uint8_t *mono_buf = buf; + /* Pitch is in pixels. Convert to bytes by rounding up to the nearest boundary. */ + uint16_t pitch_bytes = (desc->pitch + 7U) / 8U; + + const struct display_buffer_descriptor line_desc = { + .buf_size = (uint32_t)desc->width * 2U, + .width = desc->width, + .height = 1U, + .pitch = desc->width, + }; + + int err = 0; + + for (uint16_t row = 0; row < desc->height; row++) { + const uint8_t *mono_row = mono_buf + (size_t)row * pitch_bytes; + + for (uint16_t col = 0; col < desc->width; col++) { + /* CFB MONO01 is row major and LSB first. Pixel at col uses bit (col%8) of byte (col/8). */ + bool pixel_on = (mono_row[col / 8U] >> (col & 7U)) & 1U; + /* ST7789V expects RGB565 in big endian byte order over SPI. */ + data->line_buf[col] = pixel_on ? sys_cpu_to_be16(0xFFFFU) : 0x0000U; + } + + err = display_write(cfg->tft, x_start, y_start + row, &line_desc, data->line_buf); + if (err) { + return err; + } + } + return 0; +} + +static void mono_tft_get_capabilities( + const struct device *dev, + struct display_capabilities *caps +){ + const struct mono_tft_config *cfg = dev->config; + + memset(caps, 0, sizeof(*caps)); + caps->x_resolution = cfg->width; + caps->y_resolution = cfg->height; + caps->supported_pixel_formats = PIXEL_FORMAT_MONO01; + caps->current_pixel_format = PIXEL_FORMAT_MONO01; +} + +static int mono_tft_set_pixel_format( + const struct device *dev, + const enum display_pixel_format pixel_format +){ + ARG_UNUSED(dev); + return (pixel_format == PIXEL_FORMAT_MONO01) ? 0 : -ENOTSUP; +} + +static int mono_tft_init(const struct device *dev) +{ + const struct mono_tft_config *cfg = dev->config; + + if (!device_is_ready(cfg->tft)) { + LOG_ERR("underlying TFT device not ready"); + return -ENODEV; + } + return 0; +} + +static const struct display_driver_api mono_tft_api = { + .blanking_on = mono_tft_blanking_on, + .blanking_off = mono_tft_blanking_off, + .write = mono_tft_write, + .get_capabilities = mono_tft_get_capabilities, + .set_pixel_format = mono_tft_set_pixel_format, +}; + +static struct mono_tft_data mono_tft_data; +static const struct mono_tft_config mono_tft_config = { + .tft = DEVICE_DT_GET(DT_INST_PHANDLE(0, tft_dev)), + .width = DT_INST_PROP(0, width), + .height = DT_INST_PROP(0, height), +}; + +DEVICE_DT_INST_DEFINE( + 0, + mono_tft_init, + NULL, + &mono_tft_data, + &mono_tft_config, + POST_KERNEL, + CONFIG_DISPLAY_INIT_PRIORITY, + &mono_tft_api +);