Files
firmware/src/driver/st7789.rs
T
swaits aca4c3cffd feat(t114): add Heltec Mesh Node T114 board support
Adds nRF52840 + SX1262 + 1.14" 240×135 ST7789 color TFT support. First
color display board in the project; introduces a BoardDisplay trait so
each board renders to its native pixel format and resolution without
aliasing or color-quantization round-trips.

Architecture:
- New BoardDisplay trait (src/display.rs) bundles render_*, present(),
  set_bright/set_dim. Replaces the old DrawTarget<Color=BinaryColor>
  + DisplayBrightness combo as the LoRaBoard::DisplayDriver bound.
- Mono OLED drivers (Sh1106, Ssd1306Async) get adapter impls that
  delegate to the existing display::render module — no behavior change
  for any existing board.
- T114 ST7789 driver (src/driver/st7789.rs) wraps lcd-async (async fork
  of mipidsi) with a 64,800-byte raw byte framebuffer in .bss. Renders
  via lcd_async::raw_framebuf::RawFrameBuf<Rgb565, _>, presents via a
  single async DMA blit (panel.show_raw_data(...).await) — the await
  yields the executor for the ~65 ms SPI transfer so the radio task
  isn't starved during repaints.
- PWM backlight on PWM0 / P0.15 (active-LOW, inverted polarity) for
  smooth dim/bright transitions instead of GPIO toggle.
- Native 240×135 Rgb565 renderer (src/display/render_color.rs) with
  splash + dashboard + sparkline.
- Shared QR-code blit (src/display/qr.rs), generic over PixelColor,
  re-centers the 64×64 bitmap which has cells in cols 0..50 and ~14 px
  of right-side padding — both mono and color splash now show the QR
  with 7 px quiet zones on all four sides (was visibly left-shifted).
- Shared content types (BoardInfo, DashboardCtx) decouple what to draw
  from how each board renders it.
- create_display() now takes the board's full DisplayParts bundle
  (was just the I2C bus) so non-I2C displays can carry SPI + control
  pins through.

Hardware:
- VEXT_ENABLE on P0.21 driven HIGH at boot — powers the SX1262 + TFT
  rail. Without this, lora-phy init succeeds at the SPI level but
  TX_DONE never fires.
- VTFT_CTRL on P0.03, active LOW; backlight on P0.15, active LOW.
- TFT SPI2 (SCK P1.08 / MOSI P1.09) at 8 MHz over EasyDMA. Async
  ExclusiveDevice from embedded-hal-bus (with the 'async' feature).
- lcd-async builder at display_size(135, 240), display_offset(52, 40),
  Deg90 rotation, ColorInversion::Inverted — standard 1.14" ST7789
  die mapping.
- Linker: shared ld/nrf52840-memory.x (S140 SoftDevice preserved at
  0x01000-0x26000 per project policy).
2026-04-30 03:11:36 -06:00

124 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! ST7789 240×135 IPS TFT driver wrapper for the Heltec Mesh Node T114.
//!
//! Wraps `lcd_async::Display<…, models::ST7789, …>` (async fork of
//! mipidsi) with an in-RAM raw byte framebuffer (240×135 × 2 bytes
//! Rgb565 BE = 64,800 bytes) and an active-LOW PWM backlight on PWM0.
//!
//! Renderers draw into the framebuffer via `lcd_async::raw_framebuf::RawFrameBuf`;
//! `present()` awaits a single async DMA blit of the entire buffer to
//! the panel via `panel.show_raw_data(...).await`. The await lets
//! embassy-nrf's DMA-backed Spim yield the executor for the duration
//! of the ~65 ms SPI transfer, so the radio task can run alongside
//! and the SX1262 IRQ handler isn't starved during repaints.
use embassy_nrf::pwm::{DutyCycle, SimplePwm};
use embedded_graphics::pixelcolor::Rgb565;
use lcd_async::interface::Interface;
use lcd_async::models::ST7789;
use lcd_async::raw_framebuf::RawFrameBuf;
use lcd_async::Display;
use crate::display::{render_color, BoardDisplay, BoardInfo, DashboardCtx};
/// PWM countertop. Drives the backlight at ~1 kHz (well above flicker).
pub const BL_MAX: u16 = 1000;
fn bl_bright() -> DutyCycle {
// Inverted polarity: HIGH for 0 counts → always LOW → full bright.
DutyCycle::inverted(0)
}
fn bl_dim() -> DutyCycle {
// ~80% HIGH → ~20% LOW → 20% brightness.
DutyCycle::inverted((BL_MAX * 4) / 5)
}
#[allow(dead_code)]
fn bl_off() -> DutyCycle {
DutyCycle::inverted(BL_MAX)
}
// ── Framebuffer ───────────────────────────────────────────────────────
pub const FB_W: usize = 240;
pub const FB_H: usize = 135;
/// Bytes for one full Rgb565 frame (2 bytes/pixel, big-endian — matches
/// what ST7789 expects in 16-bit color mode).
pub const FB_BYTES: usize = FB_W * FB_H * 2;
// ── St7789Color ───────────────────────────────────────────────────────
pub struct St7789Color<DI, RST>
where
DI: Interface,
RST: embedded_hal::digital::OutputPin,
{
panel: Display<DI, ST7789, RST>,
backlight: SimplePwm<'static>,
fb: &'static mut [u8; FB_BYTES],
}
impl<DI, RST> St7789Color<DI, RST>
where
DI: Interface,
RST: embedded_hal::digital::OutputPin,
{
pub fn new(
panel: Display<DI, ST7789, RST>,
backlight: SimplePwm<'static>,
fb: &'static mut [u8; FB_BYTES],
) -> Self {
Self { panel, backlight, fb }
}
/// Wrap the raw byte buffer as a `RawFrameBuf<Rgb565, _>` so
/// `embedded-graphics` primitives can draw into it, then run `f`.
/// The borrow ends when this returns, freeing `self.fb` for the
/// subsequent blit in `present()`.
fn with_fb<F: FnOnce(&mut RawFrameBuf<Rgb565, &mut [u8]>)>(&mut self, f: F) {
let mut fbuf = RawFrameBuf::<Rgb565, _>::new(&mut self.fb[..], FB_W, FB_H);
f(&mut fbuf);
}
}
impl<DI, RST> BoardDisplay for St7789Color<DI, RST>
where
DI: Interface<Word = u8>,
RST: embedded_hal::digital::OutputPin,
{
/// Blit the entire framebuffer to the panel via the async SPI
/// interface. embassy-nrf's Spim runs the transfer over EasyDMA;
/// the `.await` yields the executor for the duration so the radio
/// task can process SX1262 IRQs concurrently.
async fn present(&mut self) {
let _ = self
.panel
.show_raw_data(0, 0, FB_W as u16, FB_H as u16, self.fb.as_ref())
.await;
}
async fn set_bright(&mut self) {
self.backlight.set_duty(0, bl_bright());
}
async fn set_dim(&mut self) {
self.backlight.set_duty(0, bl_dim());
}
fn render_splash_learn_more(&mut self) {
self.with_fb(|fb| render_color::splash_learn_more(fb));
}
fn render_splash_info(&mut self, info: &BoardInfo<'_>) {
self.with_fb(|fb| render_color::splash_info(fb, info));
}
fn render_dashboard(&mut self, ctx: &DashboardCtx<'_>) {
self.with_fb(|fb| render_color::dashboard(fb, ctx));
}
fn render_blank(&mut self) {
self.with_fb(|fb| render_color::blank(fb));
}
}