mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-05 11:33:53 +00:00
387 lines
12 KiB
C
387 lines
12 KiB
C
/*
|
||
* ZephCore - Display Abstraction (CFB)
|
||
* Copyright (c) 2025 ZephCore
|
||
* SPDX-License-Identifier: MIT
|
||
*
|
||
* Wraps Zephyr's Character Framebuffer (CFB) subsystem with:
|
||
* - Auto-detection from devicetree (any Zephyr-supported display)
|
||
* - Runtime resolution query (supports any size, not just 128x64)
|
||
* - Auto-off timer via k_work_delayable
|
||
* - Simple text/rect drawing API for UI pages
|
||
*
|
||
* All functions prefixed mc_display_ to avoid collision with
|
||
* Zephyr's display_* namespace in <zephyr/drivers/display.h>.
|
||
*/
|
||
|
||
#ifndef ZEPHCORE_DISPLAY_H
|
||
#define ZEPHCORE_DISPLAY_H
|
||
|
||
#include <stdbool.h>
|
||
#include <stddef.h>
|
||
#include <stdint.h>
|
||
|
||
#include <zephyr/devicetree.h>
|
||
|
||
#ifdef __cplusplus
|
||
extern "C" {
|
||
#endif
|
||
|
||
/**
|
||
* Initialize the display from devicetree.
|
||
* Detects any Zephyr-supported display via:
|
||
* 1. "zephyr,display" chosen node (standard)
|
||
* 2. Legacy nodelabels: sh1106, ssd1306 (backwards compat)
|
||
*
|
||
* Queries actual resolution from driver — no hardcoded dimensions.
|
||
*
|
||
* @return 0 on success, negative errno on failure, -ENODEV if no display
|
||
*/
|
||
int mc_display_init(void);
|
||
|
||
/**
|
||
* Get display width in pixels (queried from hardware at init).
|
||
* Returns 0 if display not initialized.
|
||
*/
|
||
uint16_t mc_display_width(void);
|
||
|
||
/**
|
||
* Get display height in pixels (queried from hardware at init).
|
||
* Returns 0 if display not initialized.
|
||
*/
|
||
uint16_t mc_display_height(void);
|
||
|
||
/**
|
||
* Get active font width in pixels.
|
||
* Returns 0 if display not initialized.
|
||
*/
|
||
uint8_t mc_display_font_width(void);
|
||
|
||
/**
|
||
* Get active font height in pixels.
|
||
* Returns 0 if display not initialized.
|
||
*/
|
||
uint8_t mc_display_font_height(void);
|
||
|
||
/**
|
||
* Turn display on (wake from blanking).
|
||
* Resets the auto-off timer.
|
||
*/
|
||
void mc_display_on(void);
|
||
|
||
/**
|
||
* Turn display off (blanking).
|
||
*/
|
||
void mc_display_off(void);
|
||
|
||
/**
|
||
* @return true if the display is currently on
|
||
*/
|
||
bool mc_display_is_on(void);
|
||
|
||
/**
|
||
* @return true if the display is an e-paper (EPD) type.
|
||
* EPD displays have slow refresh (~2s) and use zero power when static,
|
||
* so callers should use longer update intervals and skip blanking.
|
||
*/
|
||
bool mc_display_is_epd(void);
|
||
|
||
/* Color overlay support is compiled only when the devicetree points at a raw
|
||
* RGB565 TFT (the runtime probe still verifies pixel format and readiness).
|
||
* Boards without one get constant-false / mono-fallback inlines so every
|
||
* color code path — including the ~3.8 KB overlay op queue in display.c —
|
||
* is dropped at compile time.
|
||
*
|
||
* Two ways to name the panel, in priority order:
|
||
*
|
||
* 1. chosen { zephcore,color-tft = <&some_panel>; }
|
||
* 2. the `tft` nodelabel on the panel node
|
||
*
|
||
* (1) exists for boards whose panel node lives in an upstream Zephyr DTS:
|
||
* an overlay cannot add a nodelabel to an existing node, but it can always
|
||
* set a chosen property. (2) is kept because every in-tree board that had
|
||
* color before this indirection names its panel `tft:` — they need no edit.
|
||
*/
|
||
#if DT_HAS_CHOSEN(zephcore_color_tft)
|
||
#define MC_DISPLAY_COLOR_NODE DT_CHOSEN(zephcore_color_tft)
|
||
#elif DT_NODE_EXISTS(DT_NODELABEL(tft))
|
||
#define MC_DISPLAY_COLOR_NODE DT_NODELABEL(tft)
|
||
#else
|
||
#define MC_DISPLAY_COLOR_NODE DT_INVALID_NODE
|
||
#endif
|
||
|
||
#define MC_DISPLAY_COLOR_PANEL DT_NODE_EXISTS(MC_DISPLAY_COLOR_NODE)
|
||
|
||
/*
|
||
* The panel node mc_display_init() will bind to, resolved at compile time in
|
||
* the same priority order the runtime lookup uses. Only needed to ask
|
||
* compile-time questions about the panel; the device handle itself still
|
||
* comes from the runtime lookup.
|
||
*/
|
||
#if DT_HAS_CHOSEN(zephyr_display)
|
||
#define MC_DISPLAY_NODE DT_CHOSEN(zephyr_display)
|
||
#elif DT_NODE_EXISTS(DT_NODELABEL(sh1106))
|
||
#define MC_DISPLAY_NODE DT_NODELABEL(sh1106)
|
||
#elif DT_NODE_EXISTS(DT_NODELABEL(ssd1306))
|
||
#define MC_DISPLAY_NODE DT_NODELABEL(ssd1306)
|
||
#else
|
||
#define MC_DISPLAY_NODE DT_INVALID_NODE
|
||
#endif
|
||
|
||
/*
|
||
* 180-degree rotation support, decided at compile time from the panel.
|
||
*
|
||
* Deliberately an allow-list of the two families where the rotation is a
|
||
* hardware remap the driver actually implements: display_ssd1306.c flips
|
||
* SEGMENT_MAP + COM_OUTPUT_SCAN (two bytes on the wire, framebuffer
|
||
* untouched, no per-frame cost), and that driver backs both solomon,ssd1306
|
||
* and sinowealth,sh1106.
|
||
*
|
||
* Other panel families are excluded on purpose rather than probed:
|
||
* - st7735r/st7789v (our mono-tft boards) return -ENOTSUP upstream for
|
||
* anything but NORMAL, so a probe would just fail;
|
||
* - ssd16xx e-paper *accepts* ROTATED_180 but implements it by flipping
|
||
* the RAM entry mode only, which reverses byte order without reversing
|
||
* bit order inside each byte — the 8 pixels a byte spans stay in their
|
||
* original order. It would report success and render wrong, which is
|
||
* worse than reporting unsupported.
|
||
*
|
||
* The geometry test on top of the family check matters as much as the family
|
||
* check itself. Both remaps reverse the controller's *entire* addressable
|
||
* range, not the part a given panel happens to use, so the flip only lands
|
||
* back on the glass when the visible window is centred in that range:
|
||
*
|
||
* - Vertically, that means the panel uses the full multiplex height from
|
||
* page 0. Boards that window a small panel into a larger controller do
|
||
* not: lilygo_timpulse_plus is a 64x32 glass on a 128x64 SSD1306 driven
|
||
* at page-offset 4 with multiplex-ratio 63, so its content sits on COM
|
||
* 32..63. Reversing the COM scan moves it to COM 31..0 — off the bonded
|
||
* region entirely, i.e. a blank screen. `page-offset == 0` and
|
||
* `height == multiplex-ratio + 1` is exactly the "uses the whole
|
||
* controller" condition, and it excludes that board.
|
||
* - Horizontally the surviving boards are already centred: the SSD1306
|
||
* ones are a full 128 columns at segment-offset 0, and the SH1106 ones
|
||
* are 128 columns at segment-offset 2 in 132 columns of RAM — the
|
||
* standard 2-either-side layout these modules ship with.
|
||
*/
|
||
#if DT_NODE_HAS_COMPAT(MC_DISPLAY_NODE, solomon_ssd1306) || \
|
||
DT_NODE_HAS_COMPAT(MC_DISPLAY_NODE, sinowealth_sh1106)
|
||
#define MC_DISPLAY_ROTATE_SUPPORTED \
|
||
(DT_PROP_OR(MC_DISPLAY_NODE, page_offset, 1) == 0 && \
|
||
DT_PROP_OR(MC_DISPLAY_NODE, height, 0) == \
|
||
DT_PROP_OR(MC_DISPLAY_NODE, multiplex_ratio, 0) + 1)
|
||
#else
|
||
#define MC_DISPLAY_ROTATE_SUPPORTED 0
|
||
#endif
|
||
|
||
/**
|
||
* Rotate the panel 180 degrees, for cases that mount the screen upside down
|
||
* (e.g. the Meshnology N37E kit for the Wio Tracker L1).
|
||
*
|
||
* Takes effect on the next frame; the caller does not need to redraw. On
|
||
* panels outside MC_DISPLAY_ROTATE_SUPPORTED this is a no-op that reports
|
||
* the failure instead of pretending to have rotated.
|
||
*
|
||
* @param rotated true for 180 degrees, false for the panel's native orientation
|
||
* @return 0 on success, -ENOTSUP if the panel cannot rotate, -ENODEV if no
|
||
* display was initialized, or the driver's negative errno
|
||
*/
|
||
int mc_display_set_rotated(bool rotated);
|
||
|
||
/**
|
||
* @return true if the panel is currently rotated 180 degrees.
|
||
*/
|
||
bool mc_display_is_rotated(void);
|
||
|
||
/**
|
||
* @return true when a raw RGB565-capable color panel is available for
|
||
* optional color overlays. Monochrome displays return false.
|
||
*/
|
||
#if MC_DISPLAY_COLOR_PANEL
|
||
bool mc_display_has_color(void);
|
||
#else
|
||
static inline bool mc_display_has_color(void)
|
||
{
|
||
return false;
|
||
}
|
||
#endif
|
||
|
||
/**
|
||
* Glyph cell size of the color overlay renderer in pixels. The 6x8 font
|
||
* is upscaled 1.5x under CONFIG_ZEPHCORE_DISPLAY_LARGE_FONT, so this can
|
||
* differ from the CFB font metrics — layout math for color pages must use
|
||
* these, not mc_display_font_width/height(). On monochrome builds
|
||
* mc_display_color_text() falls back to the CFB path, so these fall back
|
||
* to the CFB metrics too.
|
||
*/
|
||
#if MC_DISPLAY_COLOR_PANEL
|
||
uint8_t mc_display_color_font_width(void);
|
||
uint8_t mc_display_color_font_height(void);
|
||
#else
|
||
static inline uint8_t mc_display_color_font_width(void)
|
||
{
|
||
return mc_display_font_width();
|
||
}
|
||
|
||
static inline uint8_t mc_display_color_font_height(void)
|
||
{
|
||
return mc_display_font_height();
|
||
}
|
||
#endif
|
||
|
||
/**
|
||
* Clear the framebuffer (fill with black).
|
||
* Call before rendering a new frame.
|
||
*/
|
||
void mc_display_clear(void);
|
||
|
||
/**
|
||
* Draw text at position.
|
||
*
|
||
* @param x X position in pixels
|
||
* @param y Y position in pixels
|
||
* @param text Null-terminated string
|
||
* @param invert If true, draw black text on white background
|
||
*/
|
||
void mc_display_text(int x, int y, const char *text, bool invert);
|
||
|
||
/* Common RGB565 colors for optional color-capable pages. */
|
||
#define MC_COLOR_BLACK 0x0000
|
||
#define MC_COLOR_WHITE 0xffff
|
||
#define MC_COLOR_GREEN 0x07e0
|
||
#define MC_COLOR_CYAN 0x07ff
|
||
#define MC_COLOR_YELLOW 0xffe0
|
||
#define MC_COLOR_ORANGE 0xfd20
|
||
#define MC_COLOR_RED 0xf800
|
||
#define MC_COLOR_BLUE 0x001f
|
||
#define MC_COLOR_GRAY 0x8410 /* true mid-gray — small-TFT gamma may crush it near-black */
|
||
#define MC_COLOR_LIGHT_GRAY 0xef7d /* ~93% white — reads as soft white on small TFTs */
|
||
|
||
/**
|
||
* Draw text using RGB565 color when supported. On non-color displays this
|
||
* falls back to mc_display_text(..., invert=false).
|
||
*
|
||
* Color overlays are flushed after the normal CFB frame in mc_display_finalize().
|
||
*/
|
||
#if MC_DISPLAY_COLOR_PANEL
|
||
void mc_display_color_text(int x, int y, const char *text, uint16_t color);
|
||
#else
|
||
static inline void mc_display_color_text(int x, int y, const char *text,
|
||
uint16_t color)
|
||
{
|
||
(void)color;
|
||
mc_display_text(x, y, text, false);
|
||
}
|
||
#endif
|
||
|
||
/**
|
||
* Draw a filled rectangle.
|
||
*
|
||
* @param x Top-left X
|
||
* @param y Top-left Y
|
||
* @param w Width
|
||
* @param h Height
|
||
*/
|
||
void mc_display_fill_rect(int x, int y, int w, int h);
|
||
|
||
/**
|
||
* Draw a filled rectangle using RGB565 color when supported. On non-color
|
||
* displays this falls back to mc_display_fill_rect().
|
||
*/
|
||
#if MC_DISPLAY_COLOR_PANEL
|
||
void mc_display_color_fill_rect(int x, int y, int w, int h, uint16_t color);
|
||
#else
|
||
static inline void mc_display_color_fill_rect(int x, int y, int w, int h,
|
||
uint16_t color)
|
||
{
|
||
(void)color;
|
||
mc_display_fill_rect(x, y, w, h);
|
||
}
|
||
#endif
|
||
|
||
/**
|
||
* Draw a horizontal line.
|
||
*/
|
||
void mc_display_hline(int x, int y, int w);
|
||
|
||
/**
|
||
* Invert a rectangular region of the framebuffer.
|
||
* Pixels that are on (white) become off (black) and vice versa.
|
||
* Used to create clean dark-background modal overlays.
|
||
*/
|
||
void mc_display_invert_rect(int x, int y, int w, int h);
|
||
|
||
/**
|
||
* Draw a monochrome bitmap (Adafruit/Arduino format).
|
||
* MSB first, row-major, 1=foreground.
|
||
* Compatible with Arduino's drawBitmap() and MeshCore icons.h data.
|
||
*
|
||
* @param x Top-left X position
|
||
* @param y Top-left Y position
|
||
* @param data Bitmap data (MSB first, row-major)
|
||
* @param w Width in pixels
|
||
* @param h Height in pixels
|
||
*/
|
||
void mc_display_xbm(int x, int y, const uint8_t *data, int w, int h);
|
||
|
||
/**
|
||
* ZephCore logo bitmap (128 × 13 px, MSB-first, row-major).
|
||
* Shared by both UI variants' splash screens. Defined in ui_common.c.
|
||
*/
|
||
#define ZEPHCORE_LOGO_W 128
|
||
#define ZEPHCORE_LOGO_H 13
|
||
extern const uint8_t zephcore_logo[];
|
||
|
||
/**
|
||
* Flush the framebuffer to the display hardware.
|
||
* Call after all drawing operations for a frame are complete.
|
||
*/
|
||
void mc_display_finalize(void);
|
||
|
||
/**
|
||
* Reset the auto-off timer (called on user interaction).
|
||
*/
|
||
void mc_display_reset_auto_off(void);
|
||
|
||
/**
|
||
* Override the auto-off timeout (0 = revert to Kconfig default).
|
||
* Call from the UI layer when the user changes the screen-off duration
|
||
* so the Kconfig-driven timer and the UI timer stay in sync.
|
||
*/
|
||
void mc_display_set_auto_off_ms(uint32_t ms);
|
||
|
||
/**
|
||
* EPD-only: force a full panel reset cycle before normal page rendering.
|
||
* No-op on non-EPD displays or when display is not initialized.
|
||
*/
|
||
void mc_display_epd_full_reset(void);
|
||
|
||
/**
|
||
* Get the raw display device pointer.
|
||
* Used by easter egg (Doom) to bypass CFB and write directly.
|
||
* Returns NULL if display not initialized.
|
||
*/
|
||
const struct device *mc_display_get_device(void);
|
||
|
||
/**
|
||
* Convert UTF-8 text to the display charset for rendering.
|
||
* Passes ASCII unchanged, converts Latin-1 (U+00A0-U+00FF) to its native
|
||
* code points, maps 32 Latin-2 letters (Hungarian/Czech/Slovak/Polish/...)
|
||
* into font slots 128-159, folds the rest of Latin Extended-A to base ASCII
|
||
* letters, and strips everything else (emojis, CJK). Bytes that are not
|
||
* valid UTF-8 pass through unchanged, so already-converted text survives a
|
||
* second pass.
|
||
*/
|
||
void utf8_to_display(char *dst, const char *src, size_t dst_size);
|
||
|
||
/**
|
||
* utf8_to_display() + leading-space trim (names are sometimes space-padded
|
||
* to game sort order).
|
||
*/
|
||
void utf8_to_latin1(char *dst, const char *src, size_t dst_size);
|
||
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|
||
|
||
#endif /* ZEPHCORE_DISPLAY_H */
|