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).
157 lines
5.8 KiB
Rust
157 lines
5.8 KiB
Rust
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
|
use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull};
|
|
use embassy_nrf::spim::{self, Spim};
|
|
use embassy_nrf::twim;
|
|
use embassy_nrf::usb::Driver;
|
|
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
|
use embassy_time::Delay;
|
|
|
|
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
|
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
|
|
|
use super::traits::{BoardParts, LoRaBoard};
|
|
use crate::hal::nrf52840 as mcu;
|
|
|
|
// ── Concrete peripheral types ───────────────────────────────────────
|
|
|
|
type Nss = Output<'static>;
|
|
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
|
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
|
|
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
|
pub type UsbDriver = mcu::UsbNrfDriver;
|
|
pub type DisplayI2c = mcu::I2cBus;
|
|
|
|
pub type LedDriver = crate::driver::simple_led::SimpleLed<Output<'static>>;
|
|
|
|
pub type DisplayDriver = crate::driver::sh1106::Sh1106<DisplayI2c>;
|
|
|
|
// ── Peripheral bundles ──────────────────────────────────────────────
|
|
|
|
pub struct RadioParts {
|
|
pub driver: RadioDriver,
|
|
pub delay: Delay,
|
|
}
|
|
|
|
pub struct UsbParts {
|
|
pub driver: UsbDriver,
|
|
}
|
|
|
|
pub struct DisplayParts {
|
|
pub i2c: DisplayI2c,
|
|
}
|
|
|
|
// ── Display init ────────────────────────────────────────────────────
|
|
|
|
pub async fn create_display(parts: DisplayParts) -> Option<DisplayDriver> {
|
|
let mut display = crate::driver::sh1106::Sh1106::new(parts.i2c, 0x3D);
|
|
|
|
embassy_time::Timer::after_millis(100).await;
|
|
if display.init().await.is_err() {
|
|
defmt::error!("SH1106 display init failed");
|
|
return None;
|
|
}
|
|
let _ = display.set_brightness(0xFF).await;
|
|
Some(display)
|
|
}
|
|
|
|
// ── Board init ──────────────────────────────────────────────────────
|
|
|
|
pub struct Board {
|
|
p: embassy_nrf::Peripherals,
|
|
}
|
|
|
|
impl LoRaBoard for Board {
|
|
const NAME: &'static str = "Wio Tracker L1";
|
|
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
|
|
|
type RadioParts = RadioParts;
|
|
type CommParts = UsbParts;
|
|
type DisplayParts = DisplayParts;
|
|
type DisplayDriver = DisplayDriver;
|
|
type LedDriver = LedDriver;
|
|
|
|
fn init() -> Self {
|
|
let mut config = embassy_nrf::config::Config::default();
|
|
config.hfclk_source = embassy_nrf::config::HfclkSource::ExternalXtal;
|
|
config.lfclk_source = embassy_nrf::config::LfclkSource::ExternalXtal;
|
|
let p = embassy_nrf::init(config);
|
|
Self { p }
|
|
}
|
|
|
|
fn mac_address() -> [u8; 6] {
|
|
mcu::mac_address()
|
|
}
|
|
|
|
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, LedDriver> {
|
|
let p = self.p;
|
|
|
|
// ── SPI bus for SX1262 ──────────────────────────────────
|
|
let mut spi_cfg = spim::Config::default();
|
|
spi_cfg.frequency = spim::Frequency::M1;
|
|
let spi = Spim::new(p.SPI3, mcu::Irqs, p.P0_30, p.P0_03, p.P0_28, spi_cfg);
|
|
let spi_bus = mcu::share_spi_bus(spi);
|
|
|
|
let nss = Output::new(p.P1_14, Level::High, OutputDrive::Standard);
|
|
let spi_device = SpiDevice::new(spi_bus, nss);
|
|
|
|
let reset = Output::new(p.P1_07, Level::High, OutputDrive::Standard);
|
|
let dio1 = Input::new(p.P0_07, Pull::Down);
|
|
// BUSY has an internal 20 kΩ pull-up on the SX1262 die (datasheet
|
|
// p. 53); an MCU-side Pull::Down fights it and leaves the line at
|
|
// an indeterminate voltage, making wait_on_busy unreliable. See
|
|
// lora-rs/lora-rs#260.
|
|
let busy = Input::new(p.P1_10, Pull::None);
|
|
|
|
// RF switch: RXEN on P1.08 (GPIO, toggled by lora_phy), TX via DIO2 (SX1262 internal).
|
|
let rx_enable = Output::new(p.P1_08, Level::Low, OutputDrive::Standard);
|
|
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, Some(rx_enable), None)
|
|
.expect("SX1262 interface init");
|
|
|
|
let sx_config = sx126x::Config {
|
|
chip: Sx1262,
|
|
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V8),
|
|
use_dcdc: true,
|
|
rx_boost: false,
|
|
};
|
|
|
|
let radio = RadioParts {
|
|
driver: Sx126x::new(spi_device, iv, sx_config),
|
|
delay: Delay,
|
|
};
|
|
|
|
// ── USB ─────────────────────────────────────────────────
|
|
let vbus = mcu::alloc_vbus_detect(true, true);
|
|
let host = UsbParts {
|
|
driver: Driver::new(p.USBD, mcu::Irqs, vbus),
|
|
};
|
|
|
|
// ── BAT_CTL: P0.04, active HIGH — enables power to OLED and battery sense
|
|
let bat_ctl = Output::new(p.P0_04, Level::High, OutputDrive::Standard);
|
|
core::mem::forget(bat_ctl);
|
|
|
|
// ── Display (SH1106 OLED on I2C) ────────────────────────
|
|
let twim_buf = mcu::alloc_i2c_buffer();
|
|
let i2c = embassy_nrf::twim::Twim::new(
|
|
p.TWISPI0,
|
|
mcu::Irqs,
|
|
p.P0_06,
|
|
p.P0_05,
|
|
twim::Config::default(),
|
|
twim_buf,
|
|
);
|
|
let display = Some(DisplayParts { i2c });
|
|
|
|
// Green LED on P1.01
|
|
let led_pin = Output::new(p.P1_01, Level::Low, OutputDrive::Standard);
|
|
let led = crate::driver::simple_led::SimpleLed(led_pin);
|
|
|
|
BoardParts {
|
|
radio,
|
|
host,
|
|
display,
|
|
led,
|
|
mac: Self::mac_address(),
|
|
}
|
|
}
|
|
}
|