pm-kit: working display — ported ST7796 driver + double-buffer + dirty row-bands
Replace mipidsi with a direct port of pico/main.py's ST7796 driver (per-command CS, MADCTL 0x48, INVON, no offset, big-endian RGB565) — fixes the geometry/colour mangling. Render the UI into a 300KB RAM framebuffer (embedded-graphics), then push only the full-width row bands that changed vs the last frame (FNV row-diff) — flicker-free and ~144/480 rows per playhead step. Full-width windows only: the panel's MX bit mirror-maps sub-width windows wrongly (that was the black screen). SPI 62.5 MHz per pico-cp to shrink the tearing window (no TE pin is routed on this board, so hardware vsync isn't available). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
33dc5ff5cb
commit
bd3629ba4a
1 changed files with 239 additions and 90 deletions
|
|
@ -18,17 +18,16 @@ use embedded_hal::delay::DelayNs;
|
||||||
use embedded_hal::digital::{InputPin, OutputPin};
|
use embedded_hal::digital::{InputPin, OutputPin};
|
||||||
use embedded_hal::pwm::SetDutyCycle;
|
use embedded_hal::pwm::SetDutyCycle;
|
||||||
use embedded_hal_0_2::adc::OneShot;
|
use embedded_hal_0_2::adc::OneShot;
|
||||||
use embedded_hal_bus::spi::ExclusiveDevice;
|
use embedded_hal::spi::SpiBus;
|
||||||
use mipidsi::interface::{Interface, SpiInterface};
|
|
||||||
use mipidsi::models::ST7796;
|
|
||||||
use mipidsi::options::{ColorInversion, ColorOrder, Orientation};
|
|
||||||
use mipidsi::Builder;
|
|
||||||
use defmt::info;
|
use defmt::info;
|
||||||
use defmt_rtt as _; // global defmt logger over RTT (read by `probe-rs run`)
|
use defmt_rtt as _; // global defmt logger over RTT (read by `probe-rs run`)
|
||||||
use panic_probe as _; // panic handler that prints the message + location over defmt, then halts
|
use panic_probe as _; // panic handler that prints the message + location over defmt, then halts
|
||||||
use rp235x_hal as hal;
|
use rp235x_hal as hal;
|
||||||
use hal::fugit::RateExtU32;
|
use hal::fugit::RateExtU32;
|
||||||
use hal::Clock;
|
use hal::Clock;
|
||||||
|
use embedded_graphics::pixelcolor::Rgb565;
|
||||||
|
use embedded_graphics::prelude::*;
|
||||||
|
use embedded_graphics::primitives::Rectangle;
|
||||||
|
|
||||||
/// Image definition block — the RP2350 bootrom looks for this to boot the image.
|
/// Image definition block — the RP2350 bootrom looks for this to boot the image.
|
||||||
#[link_section = ".start_block"]
|
#[link_section = ".start_block"]
|
||||||
|
|
@ -39,27 +38,162 @@ const XTAL_FREQ_HZ: u32 = 12_000_000;
|
||||||
const WIDTH: u16 = 320;
|
const WIDTH: u16 = 320;
|
||||||
const HEIGHT: u16 = 480;
|
const HEIGHT: u16 = 480;
|
||||||
|
|
||||||
/// No-op chip-select for the SPI wrapper. The real CS (GP5) is held low for the whole session,
|
/// Direct ST7796 driver — a faithful port of the proven MicroPython driver in `pico/main.py`,
|
||||||
/// because mipidsi's SpiInterface sends a command and its parameters as SEPARATE SpiDevice
|
/// which runs this exact panel on this exact board. Per-command CS framing (CS low → command →
|
||||||
/// transactions — a normal SpiDevice would toggle CS between them, and the ST7796 needs CS
|
/// data → CS high), MADCTL 0x48, INVON, NO row/column offset, big-endian RGB565. This replaces
|
||||||
/// continuous across command+parameters (so MADCTL/COLMOD/B6 args load). DC selects cmd vs data.
|
/// mipidsi, whose split-transaction CS and orientation/offset math were mangling the geometry.
|
||||||
struct NoCs;
|
/// We render into the RAM `FrameBuf` (embedded-graphics), then `blit` the whole frame in one
|
||||||
impl embedded_hal::digital::ErrorType for NoCs {
|
/// windowed RAMWR stream — exactly like main.py's `_window` + `spi.write`.
|
||||||
type Error = core::convert::Infallible;
|
struct St7796<SPI, DC, CS, RST> {
|
||||||
|
spi: SPI,
|
||||||
|
dc: DC,
|
||||||
|
cs: CS,
|
||||||
|
rst: RST,
|
||||||
}
|
}
|
||||||
impl OutputPin for NoCs {
|
impl<SPI, DC, CS, RST> St7796<SPI, DC, CS, RST>
|
||||||
fn set_low(&mut self) -> Result<(), core::convert::Infallible> {
|
where
|
||||||
|
SPI: SpiBus,
|
||||||
|
DC: OutputPin,
|
||||||
|
CS: OutputPin,
|
||||||
|
RST: OutputPin,
|
||||||
|
{
|
||||||
|
fn cmd(&mut self, c: u8, data: &[u8]) {
|
||||||
|
let _ = self.cs.set_low();
|
||||||
|
let _ = self.dc.set_low();
|
||||||
|
let _ = self.spi.write(&[c]);
|
||||||
|
if !data.is_empty() {
|
||||||
|
let _ = self.dc.set_high();
|
||||||
|
let _ = self.spi.write(data);
|
||||||
|
}
|
||||||
|
let _ = self.cs.set_high();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// main.py reset() + init() verbatim.
|
||||||
|
fn init(&mut self, delay: &mut impl DelayNs) {
|
||||||
|
let _ = self.cs.set_high();
|
||||||
|
let _ = self.dc.set_low();
|
||||||
|
let _ = self.rst.set_high();
|
||||||
|
delay.delay_ms(20);
|
||||||
|
let _ = self.rst.set_low();
|
||||||
|
delay.delay_ms(40);
|
||||||
|
let _ = self.rst.set_high();
|
||||||
|
delay.delay_ms(150);
|
||||||
|
|
||||||
|
self.cmd(0x01, &[]); // SWRESET
|
||||||
|
delay.delay_ms(120);
|
||||||
|
self.cmd(0x11, &[]); // SLPOUT
|
||||||
|
delay.delay_ms(120);
|
||||||
|
self.cmd(0xF0, &[0xC3]); // unlock extension command set
|
||||||
|
self.cmd(0xF0, &[0x96]);
|
||||||
|
self.cmd(0x36, &[0x48]); // MADCTL = MX | BGR
|
||||||
|
self.cmd(0x3A, &[0x55]); // COLMOD 16bpp
|
||||||
|
self.cmd(0xB4, &[0x01]);
|
||||||
|
self.cmd(0xB6, &[0x80, 0x02, 0x3B]);
|
||||||
|
self.cmd(0xE8, &[0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33]);
|
||||||
|
self.cmd(0xC1, &[0x06]);
|
||||||
|
self.cmd(0xC2, &[0xA7]);
|
||||||
|
self.cmd(0xC5, &[0x18]);
|
||||||
|
delay.delay_ms(120);
|
||||||
|
self.cmd(0xE0, &[0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B]);
|
||||||
|
self.cmd(0xE1, &[0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B]);
|
||||||
|
self.cmd(0xF0, &[0x3C]); // lock
|
||||||
|
self.cmd(0xF0, &[0x69]);
|
||||||
|
delay.delay_ms(120);
|
||||||
|
self.cmd(0x21, &[]); // INVON (INVERT_COLORS = true)
|
||||||
|
self.cmd(0x29, &[]); // DISPON
|
||||||
|
delay.delay_ms(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blit a full-width band of rows `ylo..ylo+h`. CASET is the full width (0..=319, exactly like
|
||||||
|
/// the full-screen blit), only RASET is partial — and because a full-width band is *contiguous*
|
||||||
|
/// in the row-major framebuffer, `px` is just that contiguous slice, streamed with the identical
|
||||||
|
/// loop the full blit uses. The full-screen blit is `blit_band(&px[..], 0, HEIGHT)`.
|
||||||
|
fn blit_band(&mut self, px: &[Rgb565], ylo: u16, h: u16) {
|
||||||
|
let y1 = ylo + h - 1;
|
||||||
|
self.cmd(0x2A, &[0x00, 0x00, 0x01, 0x3F]); // CASET 0..=319 (full width)
|
||||||
|
self.cmd(0x2B, &[(ylo >> 8) as u8, (ylo & 0xFF) as u8, (y1 >> 8) as u8, (y1 & 0xFF) as u8]);
|
||||||
|
let _ = self.cs.set_low();
|
||||||
|
let _ = self.dc.set_low();
|
||||||
|
let _ = self.spi.write(&[0x2C]); // RAMWR
|
||||||
|
let _ = self.dc.set_high();
|
||||||
|
let mut buf = [0u8; 1024];
|
||||||
|
let mut n = 0;
|
||||||
|
for &c in px {
|
||||||
|
let raw = c.into_storage(); // u16 RGB565, big-endian like main.py
|
||||||
|
buf[n] = (raw >> 8) as u8;
|
||||||
|
buf[n + 1] = (raw & 0xFF) as u8;
|
||||||
|
n += 2;
|
||||||
|
if n == buf.len() {
|
||||||
|
let _ = self.spi.write(&buf);
|
||||||
|
n = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
let _ = self.spi.write(&buf[..n]);
|
||||||
|
}
|
||||||
|
let _ = self.cs.set_high();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full-screen blit — the known-good path. Just a full-height band.
|
||||||
|
fn blit(&mut self, px: &[Rgb565]) {
|
||||||
|
self.blit_band(px, 0, HEIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Off-screen framebuffer: a full 320×480 Rgb565 image in RAM (300 KB). The pm-ui draw functions
|
||||||
|
/// render into THIS (instant, no SPI), then we push the whole thing to the panel in one
|
||||||
|
/// `fill_contiguous` sweep — so the panel never shows a clear-then-redraw transition → no flicker.
|
||||||
|
const FB_LEN: usize = WIDTH as usize * HEIGHT as usize;
|
||||||
|
static mut FRAMEBUF: [Rgb565; FB_LEN] = [Rgb565::new(0, 0, 0); FB_LEN];
|
||||||
|
|
||||||
|
struct FrameBuf {
|
||||||
|
px: &'static mut [Rgb565],
|
||||||
|
}
|
||||||
|
impl DrawTarget for FrameBuf {
|
||||||
|
type Color = Rgb565;
|
||||||
|
type Error = core::convert::Infallible;
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Pixel<Self::Color>>,
|
||||||
|
{
|
||||||
|
for Pixel(p, c) in pixels {
|
||||||
|
if p.x >= 0 && p.y >= 0 && (p.x as u16) < WIDTH && (p.y as u16) < HEIGHT {
|
||||||
|
self.px[p.y as usize * WIDTH as usize + p.x as usize] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn set_high(&mut self) -> Result<(), core::convert::Infallible> {
|
fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Self::Color>,
|
||||||
|
{
|
||||||
|
// fast path used by clear()/fill — write straight into the backing slice row by row
|
||||||
|
let mut it = colors.into_iter();
|
||||||
|
let x0 = area.top_left.x.max(0) as u16;
|
||||||
|
let y0 = area.top_left.y.max(0) as u16;
|
||||||
|
let x1 = (area.top_left.x + area.size.width as i32).min(WIDTH as i32) as u16;
|
||||||
|
let y1 = (area.top_left.y + area.size.height as i32).min(HEIGHT as i32) as u16;
|
||||||
|
for y in y0..y1 {
|
||||||
|
for x in x0..x1 {
|
||||||
|
if let Some(c) = it.next() {
|
||||||
|
self.px[y as usize * WIDTH as usize + x as usize] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl OriginDimensions for FrameBuf {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Size::new(WIDTH as u32, HEIGHT as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[hal::entry]
|
#[hal::entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
// heap for track parsing (track-format uses alloc). RP2350 has 520 KB SRAM.
|
// heap for track parsing (track-format uses alloc). RP2350 has 520 KB SRAM.
|
||||||
const HEAP_SIZE: usize = 96 * 1024;
|
const HEAP_SIZE: usize = 16 * 1024;
|
||||||
{
|
{
|
||||||
use core::mem::MaybeUninit;
|
use core::mem::MaybeUninit;
|
||||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||||
|
|
@ -88,71 +222,24 @@ fn main() -> ! {
|
||||||
let mut led = pins.gpio25.into_push_pull_output();
|
let mut led = pins.gpio25.into_push_pull_output();
|
||||||
led.set_high().unwrap(); // solid ON during init: if it stays solid → hung in init; slow blink → reached the loop
|
led.set_high().unwrap(); // solid ON during init: if it stays solid → hung in init; slow blink → reached the loop
|
||||||
|
|
||||||
// --- ST7796 over SPI0 ---
|
// --- ST7796 over SPI0 (direct driver, ported from pico/main.py) ---
|
||||||
let sclk = pins.gpio2.into_function::<hal::gpio::FunctionSpi>();
|
let sclk = pins.gpio2.into_function::<hal::gpio::FunctionSpi>();
|
||||||
let mosi = pins.gpio3.into_function::<hal::gpio::FunctionSpi>();
|
let mosi = pins.gpio3.into_function::<hal::gpio::FunctionSpi>();
|
||||||
let dc = pins.gpio6.into_push_pull_output();
|
let dc = pins.gpio6.into_push_pull_output();
|
||||||
let mut rst = pins.gpio7.into_push_pull_output();
|
let rst = pins.gpio7.into_push_pull_output();
|
||||||
let mut cs = pins.gpio5.into_push_pull_output();
|
let cs = pins.gpio5.into_push_pull_output();
|
||||||
cs.set_low().unwrap(); // hold CS low for the whole session (see NoCs) — the fix for the 1/4 panel
|
|
||||||
|
|
||||||
let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (mosi, sclk));
|
let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (mosi, sclk));
|
||||||
let spi = spi.init(
|
let spi = spi.init(
|
||||||
&mut pac.RESETS,
|
&mut pac.RESETS,
|
||||||
clocks.peripheral_clock.freq(),
|
clocks.peripheral_clock.freq(),
|
||||||
16.MHz(),
|
62500.kHz(), // pico-cp SPI_BAUD: 62.5 MHz — "faster SPI = smaller tearing window" (drop to 40 MHz if unstable)
|
||||||
embedded_hal::spi::MODE_0,
|
embedded_hal::spi::MODE_0,
|
||||||
);
|
);
|
||||||
let spi_device = ExclusiveDevice::new_no_delay(spi, NoCs).unwrap();
|
|
||||||
|
|
||||||
let mut buffer = [0u8; 512];
|
let mut st = St7796 { spi, dc, cs, rst };
|
||||||
let mut di = SpiInterface::new(spi_device, dc, &mut buffer);
|
st.init(&mut timer);
|
||||||
|
info!("display init ok (ported st7796 driver), {} free", HEAP.free());
|
||||||
// Hardware reset, then the FULL known-good CircuitPython st7796_init as the PRIMARY bring-up
|
|
||||||
// (right after reset, the only time the panel accepts the extension setup cleanly). Confirmed
|
|
||||||
// necessary: mipidsi's address window + MADCTL already match CircuitPython (host initdump), so
|
|
||||||
// the only remaining difference was this extension init not running as the primary init.
|
|
||||||
rst.set_high().unwrap();
|
|
||||||
timer.delay_ms(5);
|
|
||||||
rst.set_low().unwrap();
|
|
||||||
timer.delay_ms(20);
|
|
||||||
rst.set_high().unwrap();
|
|
||||||
timer.delay_ms(150);
|
|
||||||
|
|
||||||
di.send_command(0x01, &[]).unwrap(); // SWRESET
|
|
||||||
timer.delay_ms(120);
|
|
||||||
di.send_command(0x11, &[]).unwrap(); // SLPOUT
|
|
||||||
timer.delay_ms(120);
|
|
||||||
di.send_command(0xF0, &[0xC3]).unwrap(); // unlock extension command set
|
|
||||||
di.send_command(0xF0, &[0x96]).unwrap();
|
|
||||||
di.send_command(0x36, &[0x48]).unwrap(); // MADCTL
|
|
||||||
di.send_command(0x3A, &[0x55]).unwrap(); // COLMOD 16bpp
|
|
||||||
di.send_command(0xB4, &[0x01]).unwrap();
|
|
||||||
di.send_command(0xB6, &[0x80, 0x02, 0x3B]).unwrap(); // display function control: 480 lines
|
|
||||||
di.send_command(0xE8, &[0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33]).unwrap();
|
|
||||||
di.send_command(0xC1, &[0x06]).unwrap();
|
|
||||||
di.send_command(0xC2, &[0xA7]).unwrap();
|
|
||||||
di.send_command(0xC5, &[0x18]).unwrap();
|
|
||||||
timer.delay_ms(120);
|
|
||||||
di.send_command(0xE0, &[0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B]).unwrap();
|
|
||||||
di.send_command(0xE1, &[0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B]).unwrap();
|
|
||||||
di.send_command(0xF0, &[0x3C]).unwrap(); // lock
|
|
||||||
di.send_command(0xF0, &[0x69]).unwrap();
|
|
||||||
timer.delay_ms(120);
|
|
||||||
di.send_command(0x21, &[]).unwrap(); // INVON (INVERT_COLORS = true)
|
|
||||||
di.send_command(0x29, &[]).unwrap(); // DISPON
|
|
||||||
timer.delay_ms(50);
|
|
||||||
|
|
||||||
// Build mipidsi for DRAWING only — NO reset_pin (already reset), so it just re-asserts the
|
|
||||||
// basics (SLPOUT/MADCTL=0x48/INVON/COLMOD/NORON/DISPON) without touching the extension setup.
|
|
||||||
let mut display = Builder::new(ST7796, di)
|
|
||||||
.display_size(WIDTH, HEIGHT)
|
|
||||||
.color_order(ColorOrder::Bgr)
|
|
||||||
.invert_colors(ColorInversion::Inverted)
|
|
||||||
.orientation(Orientation::new().flip_horizontal())
|
|
||||||
.init(&mut timer)
|
|
||||||
.unwrap();
|
|
||||||
info!("display init ok, {} free", HEAP.free());
|
|
||||||
|
|
||||||
// ---- inputs + speaker ----
|
// ---- inputs + speaker ----
|
||||||
let mut btn_a = pins.gpio15.into_pull_up_input(); // A = play/stop
|
let mut btn_a = pins.gpio15.into_pull_up_input(); // A = play/stop
|
||||||
|
|
@ -166,6 +253,7 @@ fn main() -> ! {
|
||||||
spk.set_top(600); // ~2 kHz click
|
spk.set_top(600); // ~2 kHz click
|
||||||
spk.enable();
|
spk.enable();
|
||||||
spk.channel_b.output_to(pins.gpio13);
|
spk.channel_b.output_to(pins.gpio13);
|
||||||
|
info!("scaffolding (buttons/adc/pwm) up, {} free", HEAP.free());
|
||||||
|
|
||||||
// ---- built-in grooves ----
|
// ---- built-in grooves ----
|
||||||
const GROOVES: [&str; 4] = [
|
const GROOVES: [&str; 4] = [
|
||||||
|
|
@ -183,18 +271,24 @@ fn main() -> ! {
|
||||||
let mut playing = true;
|
let mut playing = true;
|
||||||
let mut notation = false;
|
let mut notation = false;
|
||||||
|
|
||||||
|
// double-buffer: draw into the RAM framebuffer, then push only the full-width ROW BANDS that
|
||||||
|
// changed vs the last frame. Full-width windows (CASET 0..319) behave exactly like the proven
|
||||||
|
// full-screen blit — unlike sub-width tiles, which the panel's MX bit mirror-maps wrongly.
|
||||||
|
let mut fb = FrameBuf { px: unsafe { &mut *core::ptr::addr_of_mut!(FRAMEBUF) } };
|
||||||
|
let mut row_hash = [0u64; HEIGHT as usize]; // FNV-1a of each row at its last blit (0 → force first paint)
|
||||||
|
|
||||||
let mut bar_start = timer.get_counter().ticks();
|
let mut bar_start = timer.get_counter().ticks();
|
||||||
let mut last_step = usize::MAX;
|
let mut last_step = usize::MAX; // audio: click on a new step
|
||||||
|
let mut drawn_step = usize::MAX; // draw: advance the playhead
|
||||||
let mut click_off_us: u64 = 0;
|
let mut click_off_us: u64 = 0;
|
||||||
let (mut pa, mut pb) = (false, false);
|
let (mut pa, mut pb) = (false, false);
|
||||||
let mut joy_zone = 0i8;
|
let mut joy_zone = 0i8;
|
||||||
let mut full_redraw = true;
|
let mut dirty = true; // force the first frame
|
||||||
let mut last_draw_us = 0u64;
|
let mut force_full = true; // first paint + big changes use the known-good full blit
|
||||||
let mut hb = false;
|
let mut hb = false;
|
||||||
let mut hb_us = 0u64;
|
let mut hb_us = 0u64;
|
||||||
let mut last_draw_ok = false;
|
|
||||||
led.set_low().unwrap();
|
led.set_low().unwrap();
|
||||||
info!("entering main loop");
|
info!("entering metronome loop (double-buffered)");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let now = timer.get_counter().ticks();
|
let now = timer.get_counter().ticks();
|
||||||
|
|
@ -208,11 +302,12 @@ fn main() -> ! {
|
||||||
bar_start = now;
|
bar_start = now;
|
||||||
last_step = usize::MAX;
|
last_step = usize::MAX;
|
||||||
}
|
}
|
||||||
full_redraw = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
if b && !pb {
|
if b && !pb {
|
||||||
notation = !notation;
|
notation = !notation;
|
||||||
full_redraw = true;
|
dirty = true;
|
||||||
|
force_full = true; // whole screen changes — use the clean full blit
|
||||||
}
|
}
|
||||||
pa = a;
|
pa = a;
|
||||||
pb = b;
|
pb = b;
|
||||||
|
|
@ -243,7 +338,10 @@ fn main() -> ! {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
full_redraw = true;
|
dirty = true;
|
||||||
|
if zone == 3 || zone == 4 {
|
||||||
|
force_full = true; // groove change → new lanes, use the clean full blit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
joy_zone = zone;
|
joy_zone = zone;
|
||||||
|
|
||||||
|
|
@ -270,9 +368,13 @@ fn main() -> ! {
|
||||||
let _ = spk.channel_b.set_duty_cycle(0);
|
let _ = spk.channel_b.set_duty_cycle(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- draw: on change, and periodically (so a draw lost right after init reappears, and
|
// redraw when the playhead steps to a new cell (animation), or on any state change
|
||||||
// the playhead advances). ~7 fps; partial/playhead-only redraw is the next optimization. ----
|
if playing && cur_step != drawn_step {
|
||||||
if full_redraw || now.wrapping_sub(last_draw_us) > 140_000 {
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- draw into the framebuffer, then blit ONCE (flicker-free) ----
|
||||||
|
if dirty {
|
||||||
let lanes: Vec<pm_ui::LaneView> = track
|
let lanes: Vec<pm_ui::LaneView> = track
|
||||||
.lanes
|
.lanes
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -285,23 +387,70 @@ fn main() -> ! {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let screen = pm_ui::Screen { name: NAMES[idx], bpm: tempo, playing, phase, lanes: &lanes };
|
let screen = pm_ui::Screen { name: NAMES[idx], bpm: tempo, playing, phase, lanes: &lanes };
|
||||||
last_draw_ok = if notation {
|
if notation {
|
||||||
pm_ui::draw_notation(&mut display, &screen).is_ok()
|
pm_ui::draw_notation(&mut fb, &screen).ok();
|
||||||
} else {
|
} else {
|
||||||
pm_ui::draw_metronome(&mut display, &screen).is_ok()
|
pm_ui::draw_metronome(&mut fb, &screen).ok();
|
||||||
};
|
}
|
||||||
full_redraw = false;
|
#[inline(always)]
|
||||||
last_draw_us = now;
|
fn row_fnv(row: &[Rgb565]) -> u64 {
|
||||||
|
let mut h = 0xcbf29ce484222325u64; // FNV-1a offset basis
|
||||||
|
for &p in row {
|
||||||
|
h ^= p.into_storage() as u64;
|
||||||
|
h = h.wrapping_mul(0x100000001b3);
|
||||||
|
}
|
||||||
|
h
|
||||||
|
}
|
||||||
|
let w = WIDTH as usize;
|
||||||
|
if force_full {
|
||||||
|
// known-good full-screen blit; seed the row hashes so increments diff against it
|
||||||
|
st.blit(&fb.px[..]);
|
||||||
|
for yy in 0..HEIGHT as usize {
|
||||||
|
row_hash[yy] = row_fnv(&fb.px[yy * w..yy * w + w]);
|
||||||
|
}
|
||||||
|
force_full = false;
|
||||||
|
info!("FULL blit (seeded {} rows)", HEIGHT as u32);
|
||||||
|
} else {
|
||||||
|
// ---- dirty row-band blit: contiguous runs of changed rows, full-width ----
|
||||||
|
let mut pushed_rows = 0u32;
|
||||||
|
let mut y = 0usize;
|
||||||
|
while y < HEIGHT as usize {
|
||||||
|
let h = row_fnv(&fb.px[y * w..y * w + w]);
|
||||||
|
if h != row_hash[y] {
|
||||||
|
let ylo = y;
|
||||||
|
row_hash[y] = h;
|
||||||
|
y += 1;
|
||||||
|
while y < HEIGHT as usize {
|
||||||
|
let hh = row_fnv(&fb.px[y * w..y * w + w]);
|
||||||
|
if hh == row_hash[y] {
|
||||||
|
break; // an unchanged row ends the dirty run
|
||||||
|
}
|
||||||
|
row_hash[y] = hh;
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
let band = (y - ylo) as u16;
|
||||||
|
st.blit_band(&fb.px[ylo * w..y * w], ylo as u16, band);
|
||||||
|
pushed_rows += band as u32;
|
||||||
|
} else {
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pushed_rows > 0 {
|
||||||
|
info!("blit {} / {} rows (bands)", pushed_rows, HEIGHT as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawn_step = cur_step;
|
||||||
|
dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// heartbeat LED + log (~2 Hz) — confirms the loop is alive and the last draw returned Ok
|
// heartbeat LED + log (~2 Hz)
|
||||||
if now.wrapping_sub(hb_us) > 500_000 {
|
if now.wrapping_sub(hb_us) > 500_000 {
|
||||||
hb = !hb;
|
hb = !hb;
|
||||||
let _ = if hb { led.set_high() } else { led.set_low() };
|
let _ = if hb { led.set_high() } else { led.set_low() };
|
||||||
info!("alive: idx={} step={} playing={} draw_ok={} free={}", idx, cur_step, playing, last_draw_ok, HEAP.free());
|
info!("alive: idx={} step={} playing={} free={}", idx, cur_step, playing, HEAP.free());
|
||||||
hb_us = now;
|
hb_us = now;
|
||||||
}
|
}
|
||||||
timer.delay_ms(8);
|
timer.delay_ms(4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue