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).
165 lines
7.8 KiB
Rust
165 lines
7.8 KiB
Rust
//! nRF52840 MCU initialization primitives.
|
|
//!
|
|
//! Provides shared boilerplate for nRF52840 boards: interrupt bindings,
|
|
//! type aliases, StaticCell bus wrapping, USB init, and MAC address reading.
|
|
|
|
use embassy_nrf::bind_interrupts;
|
|
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
|
use static_cell::StaticCell;
|
|
|
|
// ── Interrupts (shared by all nRF52840 boards) ─────────────────────
|
|
|
|
bind_interrupts!(pub struct Irqs {
|
|
USBD => embassy_nrf::usb::InterruptHandler<embassy_nrf::peripherals::USBD>;
|
|
SPIM3 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI3>;
|
|
TWISPI0 => embassy_nrf::twim::InterruptHandler<embassy_nrf::peripherals::TWISPI0>;
|
|
// SPIM2 is the second free SPI peripheral on nRF52840. Only used
|
|
// today by the Heltec T114 (ST7789 TFT panel on a write-only bus
|
|
// that can't share with the SX1262 on SPIM3 because the pins
|
|
// differ). Boards that don't use SPI2 don't construct anything
|
|
// against it and this binding is a no-op for them.
|
|
SPI2 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI2>;
|
|
});
|
|
|
|
// ── MCU-level types ─────────────────────────────────────────────────
|
|
|
|
pub type SpiBus = embassy_nrf::spim::Spim<'static>;
|
|
// I2C is used by some nRF boards (RAK4631 / Wio Tracker L1 OLED) but not
|
|
// others (T114 uses SPI for the TFT). The `#[allow(dead_code)]` keeps
|
|
// the type available for any board that wants it without per-feature
|
|
// gating; rustc otherwise fires a dead-code lint when the active board
|
|
// doesn't construct one.
|
|
#[allow(dead_code)]
|
|
pub type I2cBus = embassy_nrf::twim::Twim<'static>;
|
|
pub type UsbNrfDriver =
|
|
embassy_nrf::usb::Driver<'static, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>;
|
|
|
|
// ── SPI bus sharing ─────────────────────────────────────────────────
|
|
|
|
/// Wrap an initialized SPI peripheral in a shared bus (StaticCell + Mutex).
|
|
pub fn share_spi_bus(spi: SpiBus) -> &'static embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus> {
|
|
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
|
StaticCell::new();
|
|
SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi))
|
|
}
|
|
|
|
// ── I2C DMA buffer ──────────────────────────────────────────────────
|
|
|
|
/// Allocate a DMA-safe buffer for Twim I2C.
|
|
///
|
|
/// Every nRF52840 Twim user needs a `&'static mut [u8]` buffer.
|
|
/// This provides one via StaticCell so board files don't repeat the pattern.
|
|
/// Unused on nRF boards that don't expose I2C (e.g. T114 uses SPI for the TFT).
|
|
#[allow(dead_code)]
|
|
pub fn alloc_i2c_buffer() -> &'static mut [u8; 256] {
|
|
static TWIM_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
|
TWIM_BUF.init([0u8; 256])
|
|
}
|
|
|
|
// ── USB VBUS detection ──────────────────────────────────────────────
|
|
|
|
/// Allocate and initialize software VBUS detection.
|
|
pub fn alloc_vbus_detect(
|
|
vbus_detect: bool,
|
|
self_powered: bool,
|
|
) -> &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect {
|
|
static VBUS: StaticCell<embassy_nrf::usb::vbus_detect::SoftwareVbusDetect> = StaticCell::new();
|
|
VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(
|
|
vbus_detect,
|
|
self_powered,
|
|
))
|
|
}
|
|
|
|
// ── Reset reason ────────────────────────────────────────────────────
|
|
|
|
/// Read the POWER.RESETREAS register, log a human-readable summary,
|
|
/// then clear it so the next boot reflects only the next reset cause.
|
|
///
|
|
/// This is the post-mortem diagnostic for soak runs: if the firmware
|
|
/// crashes and the chip resets, the next boot's RESETREAS tells us
|
|
/// what happened (cortex-m lockup, watchdog, soft reset, debugger,
|
|
/// pin reset, etc.). Without this, a self-rebooting firmware just
|
|
/// looks like log restarts with no signal as to why.
|
|
///
|
|
/// nRF52840 `POWER.RESETREAS` (PS §15.4.1): bits are write-1-to-clear.
|
|
/// The register accumulates causes across resets until cleared, so
|
|
/// always clear after reading.
|
|
pub fn dump_reset_reason() {
|
|
// POWER.RESETREAS lives at 0x40000400 (POWER base 0x40000000 +
|
|
// offset 0x400). Direct memory access avoids requiring embassy_nrf's
|
|
// `unstable-pac` feature. Bit layout per nRF52840 PS §15.4.1:
|
|
// 0 RESETPIN - external NRESET pin asserted
|
|
// 1 DOG - watchdog reset
|
|
// 2 SREQ - software-requested reset (cortex SYSRESETREQ)
|
|
// 3 LOCKUP - cortex-m core lockup
|
|
// 16 OFF - wake from System OFF via DETECT
|
|
// 17 LPCOMP - wake from System OFF via ANADETECT
|
|
// 18 DIF - wake from System OFF via debug interface
|
|
// 24 NFC - wake from System OFF via NFC field
|
|
const RESETREAS_ADDR: *mut u32 = 0x4000_0400 as *mut u32;
|
|
// SAFETY: 0x40000400 is the POWER.RESETREAS register on nRF52840;
|
|
// read/write_volatile is correct for memory-mapped hardware.
|
|
let raw = unsafe { core::ptr::read_volatile(RESETREAS_ADDR) };
|
|
let dog = (raw & (1 << 1)) != 0;
|
|
let lockup = (raw & (1 << 3)) != 0;
|
|
if raw == 0 {
|
|
defmt::info!("boot: cold POR (RESETREAS=0)");
|
|
} else if dog || lockup {
|
|
// Real problems: watchdog timeout (hung task) OR CPU lockup
|
|
// (HardFault → escalated). Worth a WARN — these are bugs.
|
|
defmt::warn!(
|
|
"boot: ABNORMAL reset RESETREAS=0x{=u32:08x} resetpin={=bool} dog={=bool} sreq={=bool} lockup={=bool} off={=bool} lpcomp={=bool} dif={=bool} nfc={=bool}",
|
|
raw,
|
|
(raw & (1 << 0)) != 0,
|
|
dog,
|
|
(raw & (1 << 2)) != 0,
|
|
lockup,
|
|
(raw & (1 << 16)) != 0,
|
|
(raw & (1 << 17)) != 0,
|
|
(raw & (1 << 18)) != 0,
|
|
(raw & (1 << 24)) != 0,
|
|
);
|
|
} else {
|
|
// Normal reset paths: RESETPIN (UF2 bootloader pulses NRESET on
|
|
// app start; user reset button), SREQ (deliberate sys_reset),
|
|
// OFF/LPCOMP/DIF/NFC (wake from System OFF). All expected
|
|
// operational reasons; INFO not WARN.
|
|
defmt::info!(
|
|
"boot: RESETREAS=0x{=u32:08x} resetpin={=bool} sreq={=bool} off={=bool} lpcomp={=bool} dif={=bool} nfc={=bool}",
|
|
raw,
|
|
(raw & (1 << 0)) != 0,
|
|
(raw & (1 << 2)) != 0,
|
|
(raw & (1 << 16)) != 0,
|
|
(raw & (1 << 17)) != 0,
|
|
(raw & (1 << 18)) != 0,
|
|
(raw & (1 << 24)) != 0,
|
|
);
|
|
}
|
|
// Clear all bits — write-1-to-clear per nRF52840 PS §15.4.1.
|
|
// SAFETY: same justification as the read above.
|
|
unsafe { core::ptr::write_volatile(RESETREAS_ADDR, 0xFFFF_FFFF) };
|
|
}
|
|
|
|
// ── MAC address ─────────────────────────────────────────────────────
|
|
|
|
/// Read the factory-programmed device address from FICR registers.
|
|
pub fn mac_address() -> [u8; 6] {
|
|
// SAFETY: 0x10000000 is the nRF52840 FICR (Factory Information Configuration
|
|
// Registers) base address. Offsets 0x0A4 and 0x0A8 are the DEVICEADDR[0] and
|
|
// DEVICEADDR[1] registers containing the factory-programmed device address.
|
|
// read_volatile is correct for memory-mapped hardware registers.
|
|
unsafe {
|
|
let ficr = 0x10000000 as *const u32;
|
|
let addr0 = core::ptr::read_volatile(ficr.byte_add(0x0A4));
|
|
let addr1 = core::ptr::read_volatile(ficr.byte_add(0x0A8));
|
|
[
|
|
addr0 as u8,
|
|
(addr0 >> 8) as u8,
|
|
(addr0 >> 16) as u8,
|
|
(addr0 >> 24) as u8,
|
|
addr1 as u8,
|
|
(addr1 >> 8) as u8,
|
|
]
|
|
}
|
|
}
|