feat(firmware): drive GDEY0579T93 EPD via dual-SSD1683 driver

Add a thin dual-SSD1683 driver (src/epd.rs, ported from GxEPD2's
GxEPD2_579_GDEY0579T93) for the 792x272 e-paper panel: reset/init,
RAM addressing, the split-and-mirror full-frame blit across the
master/slave seam at x=396, and full-refresh waveform. Frame
implements embedded-graphics' DrawTarget.

Replace the Spike 1 blink harness in main.rs with the Spike 2 test:
SPI at 4 MHz over SCK 12 / DIN 11 / CS 7 / DC 6 / RST 5 / BUSY 4,
alternating normal/inverted frames with a seam-straddling circle,
"Typoena", and the build tag. Verified on hardware 2026-07-04.
This commit is contained in:
Julien Calixte
2026-07-04 18:58:42 +02:00
parent d971f36f10
commit 6145d27405
3 changed files with 402 additions and 56 deletions

View File

@@ -1,14 +1,23 @@
use std::time::Duration;
mod epd;
use embedded_graphics::mono_font::ascii::FONT_10X20;
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::pixelcolor::BinaryColor;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{Circle, PrimitiveStyle};
use embedded_graphics::text::{Alignment, Text};
use esp_idf_svc::hal::delay::FreeRtos;
use esp_idf_svc::hal::gpio::PinDriver;
use esp_idf_svc::hal::gpio::{AnyIOPin, PinDriver, Pull};
use esp_idf_svc::hal::peripherals::Peripherals;
use esp_idf_svc::hal::rmt::config::{TransmitConfig, TxChannelConfig};
use esp_idf_svc::hal::rmt::encoder::CopyEncoder;
use esp_idf_svc::hal::rmt::{PinState, Symbol, TxChannelDriver};
use esp_idf_svc::hal::units::Hertz;
use esp_idf_svc::hal::spi::config::{Config, DriverConfig};
use esp_idf_svc::hal::spi::{Dma, SpiBusDriver, SpiDriver};
use esp_idf_svc::hal::units::FromValueType;
const WS2812_RESOLUTION: Hertz = Hertz(10_000_000);
use epd::Epd;
/// Injected by build.rs so serial output and the panel itself identify the
/// exact build being diagnosed.
const BUILD_TAG: &str = concat!("build ", env!("BUILD_TIME"), " @", env!("BUILD_GIT"));
fn main() -> anyhow::Result<()> {
// Required once before any esp-idf-svc call; some runtime patches
@@ -17,61 +26,76 @@ fn main() -> anyhow::Result<()> {
esp_idf_svc::log::EspLogger::initialize_default();
let peripherals = Peripherals::take()?;
let mut led = PinDriver::output(peripherals.pins.gpio2)?;
let pins = peripherals.pins;
// On-board addressable LED (WS2812) — GPIO 48 on the DevKitC-1 v1.0;
// v1.1 boards moved it to GPIO 38.
let mut rgb = TxChannelDriver::new(
peripherals.pins.gpio48,
&TxChannelConfig {
resolution: WS2812_RESOLUTION,
..Default::default()
},
// GDEY0579T93 wiring on S3-safe GPIOs (clear of flash 2632, octal PSRAM
// 3337, strapping 0/3/45/46, USB 19/20, RGB LED 38/48). See
// docs/v0.1-mvp-technical.md (Spike 2):
// SCK 12 · DIN/MOSI 11 · CS 7 · DC 6 · RST 5 · BUSY 4
let spi = SpiDriver::new(
peripherals.spi2,
pins.gpio12, // SCK
pins.gpio11, // SDO / MOSI (DIN)
None::<AnyIOPin>, // SDI / MISO — unused (write-only panel)
&DriverConfig::new().dma(Dma::Auto(4096)),
)?;
// 4 MHz — GxEPD2's default for this controller. Verified clean on the
// breadboard rig; a loose CS jumper (not clock speed) was behind the
// early bring-up noise.
let bus = SpiBusDriver::new(spi, &Config::new().baudrate(4.MHz().into()))?;
log::info!("Typoena Spike 1 — Blink on GPIO 2 + on-board WS2812 (GPIO 48)");
let cs = PinDriver::output(pins.gpio7)?;
let dc = PinDriver::output(pins.gpio6)?;
let rst = PinDriver::output(pins.gpio5)?;
let busy = PinDriver::input(pins.gpio4, Pull::Down)?;
let mut n: u32 = 0;
let mut epd = Epd::new(bus, dc, rst, cs, busy);
log::info!("Typoena Spike 2b — GDEY0579T93 text test, {BUILD_TAG}");
log::info!("hardware reset…");
epd.reset()?;
log::info!("init…");
epd.init()?;
epd.clear_screen(0xFF)?; // initial clean slate, per GxEPD2
// Alternate normal and inverted frames: circle on the master/slave seam
// (proves the split-and-mirror blit), "Typoena" inside it, and the build
// tag at the bottom so the panel identifies the running build.
let frames = [make_frame(false), make_frame(true)];
let mut i = 0;
loop {
led.set_high()?;
ws2812_set(&mut rgb, 4, 0, 24)?;
log::info!("blink {n}");
n = n.wrapping_add(1);
FreeRtos::delay_ms(500);
led.set_low()?;
ws2812_set(&mut rgb, 0, 0, 0)?;
FreeRtos::delay_ms(500);
log::info!("frame → {}", if i == 0 { "black on WHITE" } else { "white on BLACK" });
epd.display_frame(frames[i].bytes())?;
log::info!("refresh done; holding 3 s");
FreeRtos::delay_ms(3000);
i = 1 - i;
}
}
/// Send one WS2812 GRB frame. The ≥50 µs low reset the LED needs between
/// frames is covered by the 500 ms idle gap between calls.
fn ws2812_set(tx: &mut TxChannelDriver, r: u8, g: u8, b: u8) -> anyhow::Result<()> {
// Bit timings from the WS2812 datasheet.
let zero = Symbol::new_with(
WS2812_RESOLUTION,
PinState::High,
Duration::from_nanos(350),
PinState::Low,
Duration::from_nanos(800),
)?;
let one = Symbol::new_with(
WS2812_RESOLUTION,
PinState::High,
Duration::from_nanos(700),
PinState::Low,
Duration::from_nanos(600),
)?;
let mut symbols = [zero; 24];
for (i, byte) in [g, r, b].into_iter().enumerate() {
for bit in 0..8 {
if byte & (0x80 >> bit) != 0 {
symbols[i * 8 + bit] = one;
}
}
}
tx.send_and_wait(CopyEncoder::new()?, &symbols, &TransmitConfig::default())?;
Ok(())
/// Circle centered on the controller seam, "Typoena" inside, build tag at
/// the bottom edge. `inverted` swaps ink and paper.
fn make_frame(inverted: bool) -> epd::Frame {
let (mut frame, ink) = if inverted {
(epd::Frame::new_black(), BinaryColor::Off)
} else {
(epd::Frame::new_white(), BinaryColor::On)
};
let center = Point::new(epd::WIDTH as i32 / 2, epd::HEIGHT as i32 / 2);
let style = MonoTextStyle::new(&FONT_10X20, ink);
Circle::with_center(center, 200)
.into_styled(PrimitiveStyle::with_stroke(ink, 6))
.draw(&mut frame)
.unwrap();
Text::with_alignment("Typoena", center + Point::new(0, 7), style, Alignment::Center)
.draw(&mut frame)
.unwrap();
Text::with_alignment(
BUILD_TAG,
Point::new(center.x, epd::HEIGHT as i32 - 10),
style,
Alignment::Center,
)
.draw(&mut frame)
.unwrap();
frame
}