Add T114 screen and screenless

This commit is contained in:
Steve Calvário
2026-05-16 22:43:36 +01:00
parent f2cdab84c4
commit d65703d82b
9 changed files with 348 additions and 5 deletions
+6
View File
@@ -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
+11
View File
@@ -580,6 +580,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
@@ -6,7 +6,7 @@
# - 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
# - ST7789 1.14" 240×135 TFT
# - ST7789V 1.14" 240×135 TFT (SPI3, MIPI-DBI, mono-tft wrapper)
# - LED: P1.03 (active-LOW, LoRa TX)
# - Button: P1.10 (active-LOW)
@@ -18,3 +18,14 @@ 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
@@ -5,10 +5,10 @@
* SPDX-License-Identifier: Apache-2.0
*
* Pin mappings from MeshCore variants/heltec_t114 (variant.cpp, platformio.ini):
* - SPI2 (SX1262): SCK=P0.19, MOSI=P0.22, MISO=P0.23
* - 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
* - SPI3 (ST7789 display): SCK=P1.08, MOSI=P1.09 (no MISO needed)
*/
&pinctrl {
@@ -29,6 +29,21 @@
};
};
spi3_default: spi3_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 8)>,
<NRF_PSEL(SPIM_MOSI, 1, 9)>;
};
};
spi3_sleep: spi3_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 8)>,
<NRF_PSEL(SPIM_MOSI, 1, 9)>;
low-power-enable;
};
};
i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 0, 7)>,
@@ -1,5 +1,5 @@
/*
* Heltec T114 — nRF52840 + SX1262
* Heltec T114 — nRF52840 + SX1262 + ST7789 1.14" 240×135 TFT + ATGM336H GPS
* Copyright (c) 2026 ZephCore
*
* SPDX-License-Identifier: Apache-2.0
@@ -28,6 +28,7 @@
zephyr,code-partition = &code_partition;
zephyr,console = &cdc_acm_uart;
zephyr,shell-uart = &cdc_acm_uart;
zephyr,display = &mono_tft;
};
leds {
@@ -61,7 +62,7 @@
};
/* Input filter: user button multi-tap detection
* 1 tap (400ms wait) → KEY_1 = page next on OLED
* 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
@@ -105,6 +106,82 @@
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>;
/*
@@ -187,6 +264,7 @@
};
};
/* SPI2 - SX1262 LoRa radio */
&spi2 {
compatible = "nordic,nrf-spim";
status = "okay";
@@ -209,6 +287,16 @@
};
};
/* 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.
*/
@@ -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
@@ -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.
+21
View File
@@ -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);
+149
View File
@@ -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 <zephyr/drivers/display.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/byteorder.h>
#include <string.h>
#define LOG_LEVEL CONFIG_DISPLAY_LOG_LEVEL
#include <zephyr/logging/log.h>
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
);