M2 complete: LP5562 engine patterns running autonomously
Add src/pattern/ module with 3 engine patterns: - Breathe: smooth ramp up/down (~2.5s cycle, 1 engine) - Heartbeat: double-pulse with long rest (~1.6s cycle, 1 engine) - RGB cycle: phase-offset breathing via trigger sync (3 engines) Patterns support both RGBW and 3-channel monochrome LED modes via LED_MAP configuration. Main loop cycles through all 3 patterns (8s each) while LP5562 runs them autonomously — MCU just idles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# xblink Project Status
|
# xblink Project Status
|
||||||
|
|
||||||
**Current Milestone**: M2 — Engine Patterns
|
**Current Milestone**: M3 — Hardcoded Pattern Library
|
||||||
**Last Updated**: 2026-03-05
|
**Last Updated**: 2026-03-05
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -28,11 +28,11 @@
|
|||||||
|
|
||||||
### M2: Engine Patterns
|
### M2: Engine Patterns
|
||||||
|
|
||||||
- [ ] Build breathing pattern (1 engine, ramp up/down, branch loop)
|
- [x] Build breathing pattern (1 engine, ramp up/down, branch loop)
|
||||||
- [ ] Build heartbeat pattern (1 engine, fast ramp, slow decay)
|
- [x] Build heartbeat pattern (1 engine, fast ramp, slow decay)
|
||||||
- [ ] Build RGB color cycle (3 engines, trigger sync)
|
- [x] Build RGB cycle (3 engines, trigger-synced phase offset)
|
||||||
- [ ] Verify LP5562 runs patterns autonomously (MCU idle loop)
|
- [x] Verify LP5562 runs patterns autonomously (MCU idle loop)
|
||||||
- [ ] Verify MCU can stop and switch patterns
|
- [x] Verify MCU can stop and switch patterns (8s cycle between all 3)
|
||||||
|
|
||||||
### M3: Hardcoded Pattern Library
|
### M3: Hardcoded Pattern Library
|
||||||
|
|
||||||
|
|||||||
43
src/main.rs
43
src/main.rs
@@ -15,7 +15,9 @@ use hal::prelude::*;
|
|||||||
use pac::{CorePeripherals, Peripherals};
|
use pac::{CorePeripherals, Peripherals};
|
||||||
|
|
||||||
mod led;
|
mod led;
|
||||||
|
mod pattern;
|
||||||
use led::lp5562::{ClockSource, Lp5562, DEFAULT_ADDRESS};
|
use led::lp5562::{ClockSource, Lp5562, DEFAULT_ADDRESS};
|
||||||
|
use pattern::LedMode;
|
||||||
|
|
||||||
/// Blink the on-board LED n times (active-low: low=on, high=off)
|
/// Blink the on-board LED n times (active-low: low=on, high=off)
|
||||||
fn blink<D: embedded_hal::delay::DelayNs>(led: &mut bsp::Led0, delay: &mut D, times: u8, ms: u16) {
|
fn blink<D: embedded_hal::delay::DelayNs>(led: &mut bsp::Led0, delay: &mut D, times: u8, ms: u16) {
|
||||||
@@ -80,25 +82,34 @@ fn main() -> ! {
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
// === DIAGNOSTIC: solid LED on = LP5562 init OK ===
|
// === DIAGNOSTIC: solid LED on = LP5562 init OK ===
|
||||||
led.set_low().unwrap();
|
led.set_low().unwrap();
|
||||||
delay.delay_ms(1000u32);
|
delay.delay_ms(500u32);
|
||||||
|
led.set_high().unwrap();
|
||||||
|
|
||||||
// M1 smoke test: cycle through RGBW channels
|
// LED mode: Rgbw for EVM RGB LED, Mono3 for 3 separate LEDs
|
||||||
let on: u8 = 255;
|
let mode = LedMode::Rgbw;
|
||||||
let step: u16 = 500;
|
|
||||||
|
|
||||||
|
// Cycle through patterns, 8 seconds each
|
||||||
|
let patterns = [
|
||||||
|
pattern::breathe(mode),
|
||||||
|
pattern::heartbeat(mode),
|
||||||
|
pattern::rgb_cycle(mode),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut idx = 0;
|
||||||
loop {
|
loop {
|
||||||
lp.set_all_pwm(on, 0, 0, 0).ok(); // Blue
|
// Blink pattern index (1, 2, or 3) to indicate which pattern
|
||||||
delay.delay_ms(step as u32);
|
blink(&mut led, &mut delay, (idx + 1) as u8, 150);
|
||||||
lp.set_all_pwm(0, on, 0, 0).ok(); // Green
|
|
||||||
delay.delay_ms(step as u32);
|
// Load pattern — LP5562 runs autonomously
|
||||||
lp.set_all_pwm(0, 0, on, 0).ok(); // Red
|
if pattern::load_pattern(&mut lp, &patterns[idx], &mut delay).is_err() {
|
||||||
delay.delay_ms(step as u32);
|
// I2C error loading pattern — fast blink
|
||||||
lp.set_all_pwm(0, 0, 0, on).ok(); // White
|
blink(&mut led, &mut delay, 10, 50);
|
||||||
delay.delay_ms(step as u32);
|
}
|
||||||
lp.set_all_pwm(on, on, on, on).ok(); // All on
|
|
||||||
delay.delay_ms(step as u32);
|
// MCU idles while LP5562 runs the pattern
|
||||||
let _ = lp.all_off();
|
delay.delay_ms(8000u32);
|
||||||
delay.delay_ms(step as u32);
|
|
||||||
|
idx = (idx + 1) % patterns.len();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|||||||
262
src/pattern/mod.rs
Normal file
262
src/pattern/mod.rs
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
//! Predefined LP5562 engine patterns for xblink.
|
||||||
|
//!
|
||||||
|
//! Each pattern is a set of engine programs + LED_MAP configuration.
|
||||||
|
//! The LP5562 runs these autonomously — the MCU can sleep after loading.
|
||||||
|
|
||||||
|
use crate::led::lp5562::{
|
||||||
|
Channel, EngineCommand, EngineId, EngineProgram, LedMapping, Lp5562, Prescale, RampDirection,
|
||||||
|
};
|
||||||
|
use embedded_hal::i2c::I2c;
|
||||||
|
|
||||||
|
/// LED hardware configuration.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum LedMode {
|
||||||
|
/// Single RGBW LED (e.g., LP5562EVM D1). Engines map to R, G, B; W is I2C-direct.
|
||||||
|
Rgbw,
|
||||||
|
/// 3 independent monochrome LEDs. Engines map to B, G, R channels (one each).
|
||||||
|
Mono3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A complete pattern: up to 3 engine programs + LED mapping.
|
||||||
|
pub struct Pattern {
|
||||||
|
pub engine1: Option<EngineProgram>,
|
||||||
|
pub engine2: Option<EngineProgram>,
|
||||||
|
pub engine3: Option<EngineProgram>,
|
||||||
|
/// LED_MAP: which engine (or I2C direct) drives each channel.
|
||||||
|
/// Index: [B, G, R, W] → LedMapping value.
|
||||||
|
pub map_b: LedMapping,
|
||||||
|
pub map_g: LedMapping,
|
||||||
|
pub map_r: LedMapping,
|
||||||
|
pub map_w: LedMapping,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Breathing: smooth ramp up/down, ~2.5s cycle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Breathing pattern — one engine, smooth sine-like ramp.
|
||||||
|
///
|
||||||
|
/// Slow prescale (15.6ms/step):
|
||||||
|
/// Ramp up: step_time=1, increment=4 → 64 steps × 15.6ms ≈ 1.0s (0→252)
|
||||||
|
/// Ramp down: step_time=1, increment=4 → 64 steps × 15.6ms ≈ 1.0s (252→0)
|
||||||
|
/// Wait: step_time=32 → 32 × 15.6ms ≈ 0.5s pause at bottom
|
||||||
|
/// Total: ~2.5s per cycle
|
||||||
|
fn breathe_program() -> EngineProgram {
|
||||||
|
EngineProgram::from_commands(&[
|
||||||
|
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 0→252, ~1s
|
||||||
|
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 252→0, ~1s
|
||||||
|
EngineCommand::wait(Prescale::Slow, 32), // ~0.5s pause
|
||||||
|
EngineCommand::branch(0, 0), // loop forever
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load breathing pattern. All active channels breathe in sync.
|
||||||
|
pub fn breathe(mode: LedMode) -> Pattern {
|
||||||
|
let prog = breathe_program();
|
||||||
|
match mode {
|
||||||
|
LedMode::Rgbw => Pattern {
|
||||||
|
engine1: Some(prog),
|
||||||
|
engine2: None,
|
||||||
|
engine3: None,
|
||||||
|
// Engine1 drives R, G, B; W = I2C direct (off or static)
|
||||||
|
map_b: LedMapping::Engine1,
|
||||||
|
map_g: LedMapping::Engine1,
|
||||||
|
map_r: LedMapping::Engine1,
|
||||||
|
map_w: LedMapping::I2c,
|
||||||
|
},
|
||||||
|
LedMode::Mono3 => Pattern {
|
||||||
|
engine1: Some(prog),
|
||||||
|
engine2: None,
|
||||||
|
engine3: None,
|
||||||
|
// Engine1 drives all 3 mono LEDs in sync
|
||||||
|
map_b: LedMapping::Engine1,
|
||||||
|
map_g: LedMapping::Engine1,
|
||||||
|
map_r: LedMapping::Engine1,
|
||||||
|
map_w: LedMapping::I2c,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Heartbeat: double-pulse with long pause, ~1.6s cycle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Heartbeat pattern — fast double-pulse, long rest.
|
||||||
|
///
|
||||||
|
/// Fast prescale (0.49ms/step):
|
||||||
|
/// set_pwm 255 → snap on
|
||||||
|
/// wait fast, st=20 → 20 × 0.49ms ≈ 10ms hold
|
||||||
|
/// set_pwm 0 → snap off
|
||||||
|
/// wait fast, st=40 → 40 × 0.49ms ≈ 20ms gap
|
||||||
|
/// set_pwm 255 → second beat
|
||||||
|
/// wait fast, st=20 → 10ms hold
|
||||||
|
/// set_pwm 0 → snap off
|
||||||
|
/// Slow prescale for the long rest:
|
||||||
|
/// wait slow, st=63 → 63 × 15.6ms ≈ 1.0s
|
||||||
|
/// wait slow, st=32 → 32 × 15.6ms ≈ 0.5s (total rest ~1.5s)
|
||||||
|
/// branch 0 → loop
|
||||||
|
fn heartbeat_program() -> EngineProgram {
|
||||||
|
EngineProgram::from_commands(&[
|
||||||
|
EngineCommand::set_pwm(255), // 0: first beat ON
|
||||||
|
EngineCommand::wait(Prescale::Fast, 20), // 1: hold ~10ms
|
||||||
|
EngineCommand::set_pwm(0), // 2: first beat OFF
|
||||||
|
EngineCommand::wait(Prescale::Fast, 40), // 3: gap ~20ms
|
||||||
|
EngineCommand::set_pwm(255), // 4: second beat ON
|
||||||
|
EngineCommand::wait(Prescale::Fast, 20), // 5: hold ~10ms
|
||||||
|
EngineCommand::set_pwm(0), // 6: second beat OFF
|
||||||
|
EngineCommand::wait(Prescale::Slow, 63), // 7: rest ~1.0s
|
||||||
|
EngineCommand::wait(Prescale::Slow, 32), // 8: rest ~0.5s
|
||||||
|
EngineCommand::branch(0, 0), // 9: loop forever
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load heartbeat pattern.
|
||||||
|
pub fn heartbeat(mode: LedMode) -> Pattern {
|
||||||
|
let prog = heartbeat_program();
|
||||||
|
match mode {
|
||||||
|
LedMode::Rgbw => Pattern {
|
||||||
|
engine1: Some(prog),
|
||||||
|
engine2: None,
|
||||||
|
engine3: None,
|
||||||
|
map_b: LedMapping::Engine1,
|
||||||
|
map_g: LedMapping::Engine1,
|
||||||
|
map_r: LedMapping::Engine1,
|
||||||
|
map_w: LedMapping::I2c,
|
||||||
|
},
|
||||||
|
LedMode::Mono3 => Pattern {
|
||||||
|
engine1: Some(prog),
|
||||||
|
engine2: None,
|
||||||
|
engine3: None,
|
||||||
|
map_b: LedMapping::Engine1,
|
||||||
|
map_g: LedMapping::Engine1,
|
||||||
|
map_r: LedMapping::Engine1,
|
||||||
|
map_w: LedMapping::I2c,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// RGB cycle / staggered chase: 3 engines, trigger-synced phase offset
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Phase-offset breathing using triggers for synchronization.
|
||||||
|
///
|
||||||
|
/// Engine 1 (leader): breathe up, send trigger, breathe down, wait, send trigger, loop
|
||||||
|
/// Engine 2 (follower): wait for trigger, breathe up, breathe down, wait, wait for trigger, loop
|
||||||
|
/// Engine 3 (follower): wait for trigger from E2, breathe up, breathe down, wait, loop
|
||||||
|
///
|
||||||
|
/// The trigger chain creates a 3-phase offset: E1 starts, signals E2, E2 signals E3.
|
||||||
|
fn rgb_cycle_engine1() -> EngineProgram {
|
||||||
|
EngineProgram::from_commands(&[
|
||||||
|
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 0: ramp up ~1s
|
||||||
|
EngineCommand::trigger(0, 0b010), // 1: send to E2
|
||||||
|
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 2: ramp down ~1s
|
||||||
|
EngineCommand::wait(Prescale::Slow, 32), // 3: pause ~0.5s
|
||||||
|
EngineCommand::wait(Prescale::Slow, 32), // 4: pause ~0.5s
|
||||||
|
EngineCommand::branch(0, 0), // 5: loop forever
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb_cycle_engine2() -> EngineProgram {
|
||||||
|
EngineProgram::from_commands(&[
|
||||||
|
EngineCommand::trigger(0b001, 0), // 0: wait for E1
|
||||||
|
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 1: ramp up ~1s
|
||||||
|
EngineCommand::trigger(0, 0b100), // 2: send to E3
|
||||||
|
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 3: ramp down ~1s
|
||||||
|
EngineCommand::wait(Prescale::Slow, 32), // 4: pause ~0.5s
|
||||||
|
EngineCommand::branch(0, 0), // 5: loop forever
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb_cycle_engine3() -> EngineProgram {
|
||||||
|
EngineProgram::from_commands(&[
|
||||||
|
EngineCommand::trigger(0b010, 0), // 0: wait for E2
|
||||||
|
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Up, 4), // 1: ramp up ~1s
|
||||||
|
EngineCommand::ramp_wait(Prescale::Slow, 1, RampDirection::Down, 4), // 2: ramp down ~1s
|
||||||
|
EngineCommand::wait(Prescale::Slow, 32), // 3: pause ~0.5s
|
||||||
|
EngineCommand::branch(0, 0), // 4: loop forever
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load RGB cycle (RGBW mode) or staggered chase (mono mode).
|
||||||
|
pub fn rgb_cycle(mode: LedMode) -> Pattern {
|
||||||
|
match mode {
|
||||||
|
LedMode::Rgbw => Pattern {
|
||||||
|
engine1: Some(rgb_cycle_engine1()),
|
||||||
|
engine2: Some(rgb_cycle_engine2()),
|
||||||
|
engine3: Some(rgb_cycle_engine3()),
|
||||||
|
// Each engine drives one color channel
|
||||||
|
map_b: LedMapping::Engine1,
|
||||||
|
map_g: LedMapping::Engine2,
|
||||||
|
map_r: LedMapping::Engine3,
|
||||||
|
map_w: LedMapping::I2c,
|
||||||
|
},
|
||||||
|
LedMode::Mono3 => Pattern {
|
||||||
|
engine1: Some(rgb_cycle_engine1()),
|
||||||
|
engine2: Some(rgb_cycle_engine2()),
|
||||||
|
engine3: Some(rgb_cycle_engine3()),
|
||||||
|
// Each engine drives one physical LED
|
||||||
|
map_b: LedMapping::Engine1,
|
||||||
|
map_g: LedMapping::Engine2,
|
||||||
|
map_r: LedMapping::Engine3,
|
||||||
|
map_w: LedMapping::I2c,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Staggered breathe: 3 engines, same breathe program, phase-offset
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Load staggered breathing — all 3 channels breathe independently with phase offset.
|
||||||
|
pub fn staggered_breathe(mode: LedMode) -> Pattern {
|
||||||
|
// Same trigger-chain structure as rgb_cycle but using the same
|
||||||
|
// visual effect (breathe) on each channel. In RGBW mode this creates
|
||||||
|
// a color-shifting breathe; in mono mode it's a traveling wave.
|
||||||
|
rgb_cycle(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pattern loader
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Load a pattern into the LP5562 and start engines running.
|
||||||
|
///
|
||||||
|
/// Caller must have already called `lp.enable()` and `lp.init_direct_control()`.
|
||||||
|
/// This function handles the full sequence: stop engines → set LED map → load programs → run.
|
||||||
|
pub fn load_pattern<I2C, E>(
|
||||||
|
lp: &mut Lp5562<I2C>,
|
||||||
|
pattern: &Pattern,
|
||||||
|
delay: &mut impl embedded_hal::delay::DelayNs,
|
||||||
|
) -> Result<(), crate::led::lp5562::Error<E>>
|
||||||
|
where
|
||||||
|
I2C: I2c<Error = E>,
|
||||||
|
{
|
||||||
|
// Stop all engines first
|
||||||
|
lp.stop_engine(EngineId::Engine1).map_err(crate::led::lp5562::Error::I2c)?;
|
||||||
|
lp.stop_engine(EngineId::Engine2).map_err(crate::led::lp5562::Error::I2c)?;
|
||||||
|
lp.stop_engine(EngineId::Engine3).map_err(crate::led::lp5562::Error::I2c)?;
|
||||||
|
delay.delay_us(200); // >153us between OP_MODE writes
|
||||||
|
|
||||||
|
// Set LED mapping
|
||||||
|
lp.set_led_mapping(Channel::Blue, pattern.map_b).map_err(crate::led::lp5562::Error::I2c)?;
|
||||||
|
lp.set_led_mapping(Channel::Green, pattern.map_g).map_err(crate::led::lp5562::Error::I2c)?;
|
||||||
|
lp.set_led_mapping(Channel::Red, pattern.map_r).map_err(crate::led::lp5562::Error::I2c)?;
|
||||||
|
lp.set_led_mapping(Channel::White, pattern.map_w).map_err(crate::led::lp5562::Error::I2c)?;
|
||||||
|
|
||||||
|
// Load and run each engine that has a program
|
||||||
|
if let Some(ref prog) = pattern.engine1 {
|
||||||
|
lp.run_engine(EngineId::Engine1, prog)?;
|
||||||
|
delay.delay_us(200);
|
||||||
|
}
|
||||||
|
if let Some(ref prog) = pattern.engine2 {
|
||||||
|
lp.run_engine(EngineId::Engine2, prog)?;
|
||||||
|
delay.delay_us(200);
|
||||||
|
}
|
||||||
|
if let Some(ref prog) = pattern.engine3 {
|
||||||
|
lp.run_engine(EngineId::Engine3, prog)?;
|
||||||
|
delay.delay_us(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user