Rust UI: host simulator (PNG) + shared pm-ui crate; trim panel init
Answer to 'can you simulate it?': the UI now renders on the host. - pm-ui: shared no_std embedded-graphics drawing (draw_ui), used by BOTH the firmware and the simulator — single source, no divergence. - uisim: host crate that draws pm-ui onto a framebuffer and exports a PNG (pure Rust, no SDL). Confirmed the bring-up pattern renders correctly off-device, so the black screen is a panel/controller issue, not a draw bug. - pm-kit: use pm_ui::draw_ui; trim the ST7796 extension init to just unlock + 0xB6 (the gamma/VCOM sent after DISPON likely blanked it); LED solid during init then slow 1 Hz blink so hung-init / running / reset-loop are distinguishable. Note: the simulator covers WHAT we draw (layout/colour/logic). It does NOT model the ST7796 controller's hardware quirks (0xB6 line count, MADCTL scan, SPI init) — those still need the bench, but that's a one-time bring-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
67182cd74c
commit
0fa32a827f
7 changed files with 149 additions and 56 deletions
|
|
@ -12,6 +12,7 @@ embedded-hal = "1"
|
||||||
embedded-hal-bus = "0.2"
|
embedded-hal-bus = "0.2"
|
||||||
mipidsi = "0.9"
|
mipidsi = "0.9"
|
||||||
embedded-graphics = "0.8"
|
embedded-graphics = "0.8"
|
||||||
|
pm-ui = { path = "../pm-ui" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,6 @@
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use embedded_graphics::{
|
|
||||||
mono_font::{ascii::FONT_10X20, MonoTextStyle},
|
|
||||||
pixelcolor::Rgb565,
|
|
||||||
prelude::*,
|
|
||||||
primitives::{PrimitiveStyle, PrimitiveStyleBuilder, Rectangle},
|
|
||||||
text::Text,
|
|
||||||
};
|
|
||||||
use embedded_hal::delay::DelayNs;
|
use embedded_hal::delay::DelayNs;
|
||||||
use embedded_hal::digital::OutputPin;
|
use embedded_hal::digital::OutputPin;
|
||||||
use embedded_hal_bus::spi::ExclusiveDevice;
|
use embedded_hal_bus::spi::ExclusiveDevice;
|
||||||
|
|
@ -56,6 +49,7 @@ fn main() -> ! {
|
||||||
let pins = hal::gpio::Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
|
let pins = hal::gpio::Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
// --- ST7796 over SPI0 ---
|
// --- ST7796 over SPI0 ---
|
||||||
let sclk = pins.gpio2.into_function::<hal::gpio::FunctionSpi>();
|
let sclk = pins.gpio2.into_function::<hal::gpio::FunctionSpi>();
|
||||||
|
|
@ -92,61 +86,22 @@ fn main() -> ! {
|
||||||
let di = unsafe { display.dcs() };
|
let di = unsafe { display.dcs() };
|
||||||
di.send_command(0xF0, &[0xC3]).unwrap(); // unlock extension command set
|
di.send_command(0xF0, &[0xC3]).unwrap(); // unlock extension command set
|
||||||
di.send_command(0xF0, &[0x96]).unwrap();
|
di.send_command(0xF0, &[0x96]).unwrap();
|
||||||
di.send_command(0xB4, &[0x01]).unwrap(); // 1-dot inversion
|
di.send_command(0xB6, &[0x80, 0x02, 0x3B]).unwrap(); // display function control: 480 lines (the essential one)
|
||||||
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();
|
|
||||||
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);
|
timer.delay_ms(20);
|
||||||
|
|
||||||
let w = WIDTH as i32;
|
// Same UI code the host simulator renders (rust/uisim → PNG). If this is wrong, the sim
|
||||||
let h = HEIGHT as i32;
|
// shows it without the bench; if the sim is right but the panel is wrong, it's a controller
|
||||||
let m = 36; // marker size
|
// issue (init/MADCTL/0xB6), not a draw bug.
|
||||||
|
pm_ui::draw_ui(&mut display).unwrap();
|
||||||
// Full-screen fill via the SAME draw path as the shapes (clear() left snow last time).
|
|
||||||
Rectangle::new(Point::zero(), Size::new(WIDTH as u32, HEIGHT as u32))
|
|
||||||
.into_styled(PrimitiveStyle::with_fill(Rgb565::new(0, 0, 12)))
|
|
||||||
.draw(&mut display)
|
|
||||||
.unwrap();
|
|
||||||
// Red 8px border on all four edges — if any edge is missing, the addressed area != panel.
|
|
||||||
Rectangle::new(Point::zero(), Size::new(WIDTH as u32, HEIGHT as u32))
|
|
||||||
.into_styled(
|
|
||||||
PrimitiveStyleBuilder::new()
|
|
||||||
.stroke_color(Rgb565::RED)
|
|
||||||
.stroke_width(8)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.draw(&mut display)
|
|
||||||
.unwrap();
|
|
||||||
// Distinct corner markers so orientation/mirror is unambiguous.
|
|
||||||
let ms = Size::new(m as u32, m as u32);
|
|
||||||
for (x, y, c) in [
|
|
||||||
(0, 0, Rgb565::GREEN), // top-left
|
|
||||||
(w - m, 0, Rgb565::YELLOW), // top-right
|
|
||||||
(0, h - m, Rgb565::CYAN), // bottom-left
|
|
||||||
(w - m, h - m, Rgb565::MAGENTA), // bottom-right
|
|
||||||
] {
|
|
||||||
Rectangle::new(Point::new(x, y), ms)
|
|
||||||
.into_styled(PrimitiveStyle::with_fill(c))
|
|
||||||
.draw(&mut display)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
// Labels: "TL" near origin, big "PMK" centred.
|
|
||||||
let label = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
|
|
||||||
Text::new("TL", Point::new(44, 28), label).draw(&mut display).unwrap();
|
|
||||||
Text::new("PMK", Point::new(w / 2 - 30, h / 2), label).draw(&mut display).unwrap();
|
|
||||||
|
|
||||||
|
// Reached the loop → display init + draw completed. Slow 1 Hz blink (vs the solid-ON during
|
||||||
|
// init above) so "hung in init" / "running" / "reset loop" are distinguishable on the LED.
|
||||||
loop {
|
loop {
|
||||||
led.set_high().unwrap();
|
led.set_high().unwrap();
|
||||||
timer.delay_ms(250);
|
timer.delay_ms(500);
|
||||||
led.set_low().unwrap();
|
led.set_low().unwrap();
|
||||||
timer.delay_ms(250);
|
timer.delay_ms(500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
8
rust/pm-ui/Cargo.toml
Normal file
8
rust/pm-ui/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "pm-ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Shared PM_K-1 UI rendering (embedded-graphics, no_std). Used by the firmware and the host simulator."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
embedded-graphics = "0.8"
|
||||||
58
rust/pm-ui/src/lib.rs
Normal file
58
rust/pm-ui/src/lib.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
//! Shared PM_K-1 UI rendering — `no_std`, generic over any `embedded-graphics` `DrawTarget`.
|
||||||
|
//!
|
||||||
|
//! The firmware draws this onto the real ST7796; the host simulator (`rust/uisim`) draws the
|
||||||
|
//! SAME code onto a framebuffer and exports a PNG. So UI/layout can be developed and reviewed
|
||||||
|
//! without the device — only true panel/controller quirks need the bench.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
use embedded_graphics::{
|
||||||
|
mono_font::{ascii::FONT_10X20, MonoTextStyle},
|
||||||
|
pixelcolor::Rgb565,
|
||||||
|
prelude::*,
|
||||||
|
primitives::{PrimitiveStyle, PrimitiveStyleBuilder, Rectangle},
|
||||||
|
text::Text,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Current bring-up diagnostic: solid fill + 4-edge border + distinct corner markers + labels.
|
||||||
|
/// (This is where the real metronome UI will grow.)
|
||||||
|
pub fn draw_ui<D>(d: &mut D) -> Result<(), D::Error>
|
||||||
|
where
|
||||||
|
D: DrawTarget<Color = Rgb565>,
|
||||||
|
{
|
||||||
|
let bb = d.bounding_box();
|
||||||
|
let w = bb.size.width as i32;
|
||||||
|
let h = bb.size.height as i32;
|
||||||
|
let m: i32 = 36;
|
||||||
|
|
||||||
|
// Full-screen background.
|
||||||
|
Rectangle::new(Point::zero(), bb.size)
|
||||||
|
.into_styled(PrimitiveStyle::with_fill(Rgb565::new(0, 0, 31)))
|
||||||
|
.draw(d)?;
|
||||||
|
// Red 8px border on all four edges.
|
||||||
|
Rectangle::new(Point::zero(), bb.size)
|
||||||
|
.into_styled(
|
||||||
|
PrimitiveStyleBuilder::new()
|
||||||
|
.stroke_color(Rgb565::RED)
|
||||||
|
.stroke_width(8)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.draw(d)?;
|
||||||
|
// Distinct corner markers: TL green / TR yellow / BL cyan / BR magenta.
|
||||||
|
let ms = Size::new(m as u32, m as u32);
|
||||||
|
for (x, y, c) in [
|
||||||
|
(0, 0, Rgb565::GREEN),
|
||||||
|
(w - m, 0, Rgb565::YELLOW),
|
||||||
|
(0, h - m, Rgb565::CYAN),
|
||||||
|
(w - m, h - m, Rgb565::MAGENTA),
|
||||||
|
] {
|
||||||
|
Rectangle::new(Point::new(x, y), ms)
|
||||||
|
.into_styled(PrimitiveStyle::with_fill(c))
|
||||||
|
.draw(d)?;
|
||||||
|
}
|
||||||
|
// Labels.
|
||||||
|
let label = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
|
||||||
|
Text::new("TL", Point::new(44, 28), label).draw(d)?;
|
||||||
|
Text::new("PMK", Point::new(w / 2 - 30, h / 2), label).draw(d)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
3
rust/uisim/.gitignore
vendored
Normal file
3
rust/uisim/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
target/
|
||||||
|
*.png
|
||||||
|
Cargo.lock
|
||||||
10
rust/uisim/Cargo.toml
Normal file
10
rust/uisim/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "uisim"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Host renderer for pm-ui: draws the firmware UI to a framebuffer and exports a PNG (no hardware)."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pm-ui = { path = "../pm-ui" }
|
||||||
|
embedded-graphics = "0.8"
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||||
58
rust/uisim/src/main.rs
Normal file
58
rust/uisim/src/main.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
//! Render the shared pm-ui onto a 320×480 framebuffer and save it as a PNG — no device, no SDL.
|
||||||
|
//! `cargo run` (host) → pm-kit-ui.png. Lets the UI be developed/reviewed off the bench.
|
||||||
|
|
||||||
|
use embedded_graphics::{pixelcolor::Rgb565, prelude::*};
|
||||||
|
|
||||||
|
const W: u32 = 320;
|
||||||
|
const H: u32 = 480;
|
||||||
|
|
||||||
|
/// A trivial framebuffer DrawTarget.
|
||||||
|
struct Fb {
|
||||||
|
px: Vec<Rgb565>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fb {
|
||||||
|
fn new() -> Self {
|
||||||
|
Fb { px: vec![Rgb565::BLACK; (W * H) as usize] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawTarget for Fb {
|
||||||
|
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 u32) < W && (p.y as u32) < H {
|
||||||
|
self.px[(p.y as u32 * W + p.x as u32) as usize] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OriginDimensions for Fb {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Size::new(W, H)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut fb = Fb::new();
|
||||||
|
pm_ui::draw_ui(&mut fb).unwrap();
|
||||||
|
|
||||||
|
let img = image::RgbImage::from_fn(W, H, |x, y| {
|
||||||
|
let c = fb.px[(y * W + x) as usize];
|
||||||
|
// Rgb565 channels → 8-bit
|
||||||
|
let r = (c.r() << 3) | (c.r() >> 2);
|
||||||
|
let g = (c.g() << 2) | (c.g() >> 4);
|
||||||
|
let b = (c.b() << 3) | (c.b() >> 2);
|
||||||
|
image::Rgb([r, g, b])
|
||||||
|
});
|
||||||
|
let out = std::env::args().nth(1).unwrap_or_else(|| "pm-kit-ui.png".into());
|
||||||
|
img.save(&out).unwrap();
|
||||||
|
println!("wrote {out} ({W}x{H})");
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue