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::pwm::SetDutyCycle;
|
||||
use embedded_hal_0_2::adc::OneShot;
|
||||
use embedded_hal_bus::spi::ExclusiveDevice;
|
||||
use mipidsi::interface::{Interface, SpiInterface};
|
||||
use mipidsi::models::ST7796;
|
||||
use mipidsi::options::{ColorInversion, ColorOrder, Orientation};
|
||||
use mipidsi::Builder;
|
||||
use embedded_hal::spi::SpiBus;
|
||||
use defmt::info;
|
||||
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 rp235x_hal as hal;
|
||||
use hal::fugit::RateExtU32;
|
||||
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.
|
||||
#[link_section = ".start_block"]
|
||||
|
|
@ -39,27 +38,162 @@ const XTAL_FREQ_HZ: u32 = 12_000_000;
|
|||
const WIDTH: u16 = 320;
|
||||
const HEIGHT: u16 = 480;
|
||||
|
||||
/// No-op chip-select for the SPI wrapper. The real CS (GP5) is held low for the whole session,
|
||||
/// because mipidsi's SpiInterface sends a command and its parameters as SEPARATE SpiDevice
|
||||
/// transactions — a normal SpiDevice would toggle CS between them, and the ST7796 needs CS
|
||||
/// continuous across command+parameters (so MADCTL/COLMOD/B6 args load). DC selects cmd vs data.
|
||||
struct NoCs;
|
||||
impl embedded_hal::digital::ErrorType for NoCs {
|
||||
type Error = core::convert::Infallible;
|
||||
/// Direct ST7796 driver — a faithful port of the proven MicroPython driver in `pico/main.py`,
|
||||
/// which runs this exact panel on this exact board. Per-command CS framing (CS low → command →
|
||||
/// data → CS high), MADCTL 0x48, INVON, NO row/column offset, big-endian RGB565. This replaces
|
||||
/// mipidsi, whose split-transaction CS and orientation/offset math were mangling the geometry.
|
||||
/// We render into the RAM `FrameBuf` (embedded-graphics), then `blit` the whole frame in one
|
||||
/// windowed RAMWR stream — exactly like main.py's `_window` + `spi.write`.
|
||||
struct St7796<SPI, DC, CS, RST> {
|
||||
spi: SPI,
|
||||
dc: DC,
|
||||
cs: CS,
|
||||
rst: RST,
|
||||
}
|
||||
impl OutputPin for NoCs {
|
||||
fn set_low(&mut self) -> Result<(), core::convert::Infallible> {
|
||||
impl<SPI, DC, CS, RST> St7796<SPI, DC, CS, RST>
|
||||
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(())
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
impl OriginDimensions for FrameBuf {
|
||||
fn size(&self) -> Size {
|
||||
Size::new(WIDTH as u32, HEIGHT as u32)
|
||||
}
|
||||
}
|
||||
|
||||
#[hal::entry]
|
||||
fn main() -> ! {
|
||||
// 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;
|
||||
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();
|
||||
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 mosi = pins.gpio3.into_function::<hal::gpio::FunctionSpi>();
|
||||
let dc = pins.gpio6.into_push_pull_output();
|
||||
let mut rst = pins.gpio7.into_push_pull_output();
|
||||
let mut 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 rst = pins.gpio7.into_push_pull_output();
|
||||
let cs = pins.gpio5.into_push_pull_output();
|
||||
|
||||
let spi = hal::spi::Spi::<_, _, _, 8>::new(pac.SPI0, (mosi, sclk));
|
||||
let spi = spi.init(
|
||||
&mut pac.RESETS,
|
||||
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,
|
||||
);
|
||||
let spi_device = ExclusiveDevice::new_no_delay(spi, NoCs).unwrap();
|
||||
|
||||
let mut buffer = [0u8; 512];
|
||||
let mut di = SpiInterface::new(spi_device, dc, &mut buffer);
|
||||
|
||||
// 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());
|
||||
let mut st = St7796 { spi, dc, cs, rst };
|
||||
st.init(&mut timer);
|
||||
info!("display init ok (ported st7796 driver), {} free", HEAP.free());
|
||||
|
||||
// ---- inputs + speaker ----
|
||||
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.enable();
|
||||
spk.channel_b.output_to(pins.gpio13);
|
||||
info!("scaffolding (buttons/adc/pwm) up, {} free", HEAP.free());
|
||||
|
||||
// ---- built-in grooves ----
|
||||
const GROOVES: [&str; 4] = [
|
||||
|
|
@ -183,18 +271,24 @@ fn main() -> ! {
|
|||
let mut playing = true;
|
||||
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 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 pa, mut pb) = (false, false);
|
||||
let mut joy_zone = 0i8;
|
||||
let mut full_redraw = true;
|
||||
let mut last_draw_us = 0u64;
|
||||
let mut dirty = true; // force the first frame
|
||||
let mut force_full = true; // first paint + big changes use the known-good full blit
|
||||
let mut hb = false;
|
||||
let mut hb_us = 0u64;
|
||||
let mut last_draw_ok = false;
|
||||
led.set_low().unwrap();
|
||||
info!("entering main loop");
|
||||
info!("entering metronome loop (double-buffered)");
|
||||
|
||||
loop {
|
||||
let now = timer.get_counter().ticks();
|
||||
|
|
@ -208,11 +302,12 @@ fn main() -> ! {
|
|||
bar_start = now;
|
||||
last_step = usize::MAX;
|
||||
}
|
||||
full_redraw = true;
|
||||
dirty = true;
|
||||
}
|
||||
if b && !pb {
|
||||
notation = !notation;
|
||||
full_redraw = true;
|
||||
dirty = true;
|
||||
force_full = true; // whole screen changes — use the clean full blit
|
||||
}
|
||||
pa = a;
|
||||
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;
|
||||
|
||||
|
|
@ -270,9 +368,13 @@ fn main() -> ! {
|
|||
let _ = spk.channel_b.set_duty_cycle(0);
|
||||
}
|
||||
|
||||
// ---- draw: on change, and periodically (so a draw lost right after init reappears, and
|
||||
// the playhead advances). ~7 fps; partial/playhead-only redraw is the next optimization. ----
|
||||
if full_redraw || now.wrapping_sub(last_draw_us) > 140_000 {
|
||||
// redraw when the playhead steps to a new cell (animation), or on any state change
|
||||
if playing && cur_step != drawn_step {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// ---- draw into the framebuffer, then blit ONCE (flicker-free) ----
|
||||
if dirty {
|
||||
let lanes: Vec<pm_ui::LaneView> = track
|
||||
.lanes
|
||||
.iter()
|
||||
|
|
@ -285,23 +387,70 @@ fn main() -> ! {
|
|||
})
|
||||
.collect();
|
||||
let screen = pm_ui::Screen { name: NAMES[idx], bpm: tempo, playing, phase, lanes: &lanes };
|
||||
last_draw_ok = if notation {
|
||||
pm_ui::draw_notation(&mut display, &screen).is_ok()
|
||||
if notation {
|
||||
pm_ui::draw_notation(&mut fb, &screen).ok();
|
||||
} else {
|
||||
pm_ui::draw_metronome(&mut display, &screen).is_ok()
|
||||
};
|
||||
full_redraw = false;
|
||||
last_draw_us = now;
|
||||
pm_ui::draw_metronome(&mut fb, &screen).ok();
|
||||
}
|
||||
#[inline(always)]
|
||||
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 {
|
||||
hb = !hb;
|
||||
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;
|
||||
}
|
||||
timer.delay_ms(8);
|
||||
timer.delay_ms(4);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue