Add LP5562 driver, M1 smoke test, and milestone-based plan
- Integrate LP5562 driver from sibling project (ntag5-samd21-lp562) with full doc comments restored - Update main.rs with complete M1 test: I2C init, GPIO EN, RGBW cycle - Rewrite DEVELOPMENT_PLAN.md from waterfall to milestone-based groups organized by hardware availability (Groups A-E) - Rewrite STATUS.md with milestone checklist tracking - Add LP5562 timing constraints and flash script docs to CLAUDE.md - Add flash_when_ready.sh for auto-flash dev workflow - Add brainstorm design doc for plan redesign rationale Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
756
src/led/lp5562.rs
Normal file
756
src/led/lp5562.rs
Normal file
@@ -0,0 +1,756 @@
|
||||
//! LP5562 4-channel RGBW LED driver (I2C) — TI SNVS820B
|
||||
//!
|
||||
//! Complete `no_std` driver covering all LP5562 features:
|
||||
//! direct PWM control, current setting, engine programming,
|
||||
//! LED mapping, power-save, and clock configuration.
|
||||
//!
|
||||
//! Generic over `embedded_hal::i2c::I2c` (1.0).
|
||||
|
||||
use embedded_hal::i2c::I2c;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// I2C address constants (7-bit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// I2C address when ADDR_SEL1:0 = 00 (both pins low).
|
||||
pub const ADDR_SEL_00: u8 = 0x30;
|
||||
/// I2C address when ADDR_SEL1:0 = 01.
|
||||
pub const ADDR_SEL_01: u8 = 0x31;
|
||||
/// I2C address when ADDR_SEL1:0 = 10.
|
||||
pub const ADDR_SEL_10: u8 = 0x32;
|
||||
/// I2C address when ADDR_SEL1:0 = 11 (both pins high).
|
||||
pub const ADDR_SEL_11: u8 = 0x33;
|
||||
/// Default address (ADDR_SEL pins both low).
|
||||
pub const DEFAULT_ADDRESS: u8 = ADDR_SEL_00;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register addresses
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// LP5562 register addresses (Table 26, datasheet page 30).
|
||||
pub mod reg {
|
||||
pub const ENABLE: u8 = 0x00;
|
||||
pub const OP_MODE: u8 = 0x01;
|
||||
pub const B_PWM: u8 = 0x02;
|
||||
pub const G_PWM: u8 = 0x03;
|
||||
pub const R_PWM: u8 = 0x04;
|
||||
pub const B_CURRENT: u8 = 0x05;
|
||||
pub const G_CURRENT: u8 = 0x06;
|
||||
pub const R_CURRENT: u8 = 0x07;
|
||||
pub const CONFIG: u8 = 0x08;
|
||||
pub const ENG1_PC: u8 = 0x09;
|
||||
pub const ENG2_PC: u8 = 0x0A;
|
||||
pub const ENG3_PC: u8 = 0x0B;
|
||||
pub const STATUS: u8 = 0x0C;
|
||||
pub const RESET: u8 = 0x0D;
|
||||
pub const W_PWM: u8 = 0x0E;
|
||||
pub const W_CURRENT: u8 = 0x0F;
|
||||
pub const LED_MAP: u8 = 0x70;
|
||||
pub const ENG1_PROG_START: u8 = 0x10;
|
||||
pub const ENG2_PROG_START: u8 = 0x30;
|
||||
pub const ENG3_PROG_START: u8 = 0x50;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bit-field constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ENABLE register (0x00)
|
||||
const ENABLE_LOG_EN: u8 = 1 << 7;
|
||||
const ENABLE_CHIP_EN: u8 = 1 << 6;
|
||||
const ENABLE_ENG1_EXEC_SHIFT: u8 = 4;
|
||||
const ENABLE_ENG2_EXEC_SHIFT: u8 = 2;
|
||||
const ENABLE_ENG3_EXEC_SHIFT: u8 = 0;
|
||||
|
||||
// OP_MODE register (0x01)
|
||||
const OP_MODE_ENG1_SHIFT: u8 = 4;
|
||||
const OP_MODE_ENG2_SHIFT: u8 = 2;
|
||||
const OP_MODE_ENG3_SHIFT: u8 = 0;
|
||||
|
||||
// CONFIG register (0x08)
|
||||
const CONFIG_PWM_HF: u8 = 1 << 6;
|
||||
const CONFIG_PS_EN: u8 = 1 << 5;
|
||||
|
||||
// STATUS register (0x0C)
|
||||
const STATUS_EXT_CLK_USED: u8 = 1 << 3;
|
||||
const STATUS_ENG1_INT: u8 = 1 << 2;
|
||||
const STATUS_ENG2_INT: u8 = 1 << 1;
|
||||
const STATUS_ENG3_INT: u8 = 1 << 0;
|
||||
|
||||
// LED_MAP register (0x70)
|
||||
const LED_MAP_W_SHIFT: u8 = 6;
|
||||
const LED_MAP_R_SHIFT: u8 = 4;
|
||||
const LED_MAP_G_SHIFT: u8 = 2;
|
||||
const LED_MAP_B_SHIFT: u8 = 0;
|
||||
|
||||
const RESET_MAGIC: u8 = 0xFF;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enumerations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// LED channel identifier.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Channel {
|
||||
Blue,
|
||||
Green,
|
||||
Red,
|
||||
White,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
const fn pwm_reg(self) -> u8 {
|
||||
match self {
|
||||
Channel::Blue => reg::B_PWM,
|
||||
Channel::Green => reg::G_PWM,
|
||||
Channel::Red => reg::R_PWM,
|
||||
Channel::White => reg::W_PWM,
|
||||
}
|
||||
}
|
||||
|
||||
const fn current_reg(self) -> u8 {
|
||||
match self {
|
||||
Channel::Blue => reg::B_CURRENT,
|
||||
Channel::Green => reg::G_CURRENT,
|
||||
Channel::Red => reg::R_CURRENT,
|
||||
Channel::White => reg::W_CURRENT,
|
||||
}
|
||||
}
|
||||
|
||||
const fn led_map_shift(self) -> u8 {
|
||||
match self {
|
||||
Channel::White => LED_MAP_W_SHIFT,
|
||||
Channel::Red => LED_MAP_R_SHIFT,
|
||||
Channel::Green => LED_MAP_G_SHIFT,
|
||||
Channel::Blue => LED_MAP_B_SHIFT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execution engine identifier.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum EngineId {
|
||||
Engine1,
|
||||
Engine2,
|
||||
Engine3,
|
||||
}
|
||||
|
||||
impl EngineId {
|
||||
const fn exec_shift(self) -> u8 {
|
||||
match self {
|
||||
EngineId::Engine1 => ENABLE_ENG1_EXEC_SHIFT,
|
||||
EngineId::Engine2 => ENABLE_ENG2_EXEC_SHIFT,
|
||||
EngineId::Engine3 => ENABLE_ENG3_EXEC_SHIFT,
|
||||
}
|
||||
}
|
||||
|
||||
const fn mode_shift(self) -> u8 {
|
||||
match self {
|
||||
EngineId::Engine1 => OP_MODE_ENG1_SHIFT,
|
||||
EngineId::Engine2 => OP_MODE_ENG2_SHIFT,
|
||||
EngineId::Engine3 => OP_MODE_ENG3_SHIFT,
|
||||
}
|
||||
}
|
||||
|
||||
const fn pc_reg(self) -> u8 {
|
||||
match self {
|
||||
EngineId::Engine1 => reg::ENG1_PC,
|
||||
EngineId::Engine2 => reg::ENG2_PC,
|
||||
EngineId::Engine3 => reg::ENG3_PC,
|
||||
}
|
||||
}
|
||||
|
||||
const fn prog_start(self) -> u8 {
|
||||
match self {
|
||||
EngineId::Engine1 => reg::ENG1_PROG_START,
|
||||
EngineId::Engine2 => reg::ENG2_PROG_START,
|
||||
EngineId::Engine3 => reg::ENG3_PROG_START,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Engine execution state (ENABLE register, 2-bit field per engine).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum EngineExec {
|
||||
/// Hold: finish current command then stop. PC is R/W in this state.
|
||||
Hold = 0b00,
|
||||
/// Step: execute one instruction, increment PC, return to Hold.
|
||||
Step = 0b01,
|
||||
/// Run: execute from current PC continuously.
|
||||
Run = 0b10,
|
||||
/// Execute current instruction once, then return to Hold.
|
||||
ExecuteOnce = 0b11,
|
||||
}
|
||||
|
||||
/// Engine operation mode (OP_MODE register, 2-bit field per engine).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum EngineMode {
|
||||
/// Disabled: resets PC, mapped LED output = 0.
|
||||
Disabled = 0b00,
|
||||
/// Load: SRAM writable, all engines held.
|
||||
Load = 0b01,
|
||||
/// Run: execution controlled by EXEC bits in ENABLE.
|
||||
Run = 0b10,
|
||||
/// Direct: engine PWM from corresponding I2C PWM register.
|
||||
Direct = 0b11,
|
||||
}
|
||||
|
||||
/// LED-to-engine mapping (LED_MAP register, 2-bit field per channel).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum LedMapping {
|
||||
/// Controlled via I2C PWM register.
|
||||
I2c = 0b00,
|
||||
/// Controlled by Engine 1.
|
||||
Engine1 = 0b01,
|
||||
/// Controlled by Engine 2.
|
||||
Engine2 = 0b10,
|
||||
/// Controlled by Engine 3.
|
||||
Engine3 = 0b11,
|
||||
}
|
||||
|
||||
/// Clock source (CONFIG register bits 1:0).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum ClockSource {
|
||||
/// External 32 kHz clock on CLK_32K pin.
|
||||
External = 0b00,
|
||||
/// Internal oscillator.
|
||||
Internal = 0b01,
|
||||
/// Automatic detection.
|
||||
Auto = 0b10,
|
||||
}
|
||||
|
||||
/// PWM output frequency.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PwmFrequency {
|
||||
/// 256 Hz.
|
||||
Hz256,
|
||||
/// 558 Hz.
|
||||
Hz558,
|
||||
}
|
||||
|
||||
/// Prescaler for ramp/wait engine commands.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Prescale {
|
||||
/// Divide by 16: 0.49 ms per step.
|
||||
Fast = 0,
|
||||
/// Divide by 512: 15.6 ms per step.
|
||||
Slow = 1,
|
||||
}
|
||||
|
||||
/// Ramp direction.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RampDirection {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Decoded STATUS register (0x0C, read-only). Reading clears interrupt bits.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Status {
|
||||
pub ext_clk_used: bool,
|
||||
pub engine1_int: bool,
|
||||
pub engine2_int: bool,
|
||||
pub engine3_int: bool,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
fn from_reg(val: u8) -> Self {
|
||||
Self {
|
||||
ext_clk_used: val & STATUS_EXT_CLK_USED != 0,
|
||||
engine1_int: val & STATUS_ENG1_INT != 0,
|
||||
engine2_int: val & STATUS_ENG2_INT != 0,
|
||||
engine3_int: val & STATUS_ENG3_INT != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Engine command builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Builder for 16-bit engine program commands.
|
||||
pub struct EngineCommand;
|
||||
|
||||
impl EngineCommand {
|
||||
/// Ramp/Wait command.
|
||||
///
|
||||
/// - `prescale`: Fast (0.49ms/step) or Slow (15.6ms/step)
|
||||
/// - `step_time`: 1..=63 (number of prescaled periods per step)
|
||||
/// - `direction`: Up or Down
|
||||
/// - `increment`: 0 = wait only, 1..=127 = number of PWM steps
|
||||
pub const fn ramp_wait(
|
||||
prescale: Prescale,
|
||||
step_time: u8,
|
||||
direction: RampDirection,
|
||||
increment: u8,
|
||||
) -> u16 {
|
||||
let ps = (prescale as u16) << 14;
|
||||
let st = ((step_time & 0x3F) as u16) << 8;
|
||||
let sign = match direction {
|
||||
RampDirection::Down => 1u16 << 7,
|
||||
RampDirection::Up => 0,
|
||||
};
|
||||
let inc = (increment & 0x7F) as u16;
|
||||
ps | st | sign | inc
|
||||
}
|
||||
|
||||
/// Pure wait (ramp with increment = 0).
|
||||
pub const fn wait(prescale: Prescale, step_time: u8) -> u16 {
|
||||
Self::ramp_wait(prescale, step_time, RampDirection::Up, 0)
|
||||
}
|
||||
|
||||
/// Set PWM to an absolute value (0-255).
|
||||
pub const fn set_pwm(value: u8) -> u16 {
|
||||
0x4000 | (value as u16)
|
||||
}
|
||||
|
||||
/// Go to Start: reset PC to 0.
|
||||
pub const fn go_to_start() -> u16 {
|
||||
0x0000
|
||||
}
|
||||
|
||||
/// Branch to `step_number` with `loop_count` (0 = infinite).
|
||||
pub const fn branch(loop_count: u8, step_number: u8) -> u16 {
|
||||
let base: u16 = 0b101 << 13;
|
||||
let lc = ((loop_count & 0x3F) as u16) << 7;
|
||||
let sn = (step_number & 0x0F) as u16;
|
||||
base | lc | sn
|
||||
}
|
||||
|
||||
/// End program. Optionally fire interrupt and/or reset PWM to 0.
|
||||
pub const fn end(interrupt: bool, reset_pwm: bool) -> u16 {
|
||||
let base: u16 = 0b110 << 13;
|
||||
let int_bit = if interrupt { 1u16 << 12 } else { 0 };
|
||||
let rst_bit = if reset_pwm { 1u16 << 11 } else { 0 };
|
||||
base | int_bit | rst_bit
|
||||
}
|
||||
|
||||
/// Trigger for engine synchronization.
|
||||
///
|
||||
/// Bitmask: bit0 = Engine1, bit1 = Engine2, bit2 = Engine3.
|
||||
pub const fn trigger(wait_engines: u8, send_engines: u8) -> u16 {
|
||||
let base: u16 = 0b111 << 13;
|
||||
let wait = ((wait_engines & 0x07) as u16) << 8;
|
||||
let send = ((send_engines & 0x07) as u16) << 1;
|
||||
base | wait | send
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Engine program container
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Maximum commands per engine.
|
||||
pub const MAX_PROGRAM_LEN: usize = 16;
|
||||
|
||||
/// A program to load into engine SRAM (up to 16 commands).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EngineProgram {
|
||||
commands: [u16; MAX_PROGRAM_LEN],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl EngineProgram {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
commands: [0u16; MAX_PROGRAM_LEN],
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from a command slice. Panics if > 16 commands.
|
||||
pub fn from_commands(cmds: &[u16]) -> Self {
|
||||
assert!(cmds.len() <= MAX_PROGRAM_LEN);
|
||||
let mut prog = Self::new();
|
||||
let mut i = 0;
|
||||
while i < cmds.len() {
|
||||
prog.commands[i] = cmds[i];
|
||||
i += 1;
|
||||
}
|
||||
prog.len = cmds.len();
|
||||
prog
|
||||
}
|
||||
|
||||
pub fn push(&mut self, cmd: u16) -> Result<(), ()> {
|
||||
if self.len >= MAX_PROGRAM_LEN {
|
||||
return Err(());
|
||||
}
|
||||
self.commands[self.len] = cmd;
|
||||
self.len += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.len == 0
|
||||
}
|
||||
|
||||
/// Full 32-byte SRAM image (big-endian, unused slots = 0x0000).
|
||||
pub fn as_bytes(&self) -> [u8; 32] {
|
||||
let mut buf = [0u8; 32];
|
||||
let mut i = 0;
|
||||
while i < MAX_PROGRAM_LEN {
|
||||
buf[i * 2] = (self.commands[i] >> 8) as u8;
|
||||
buf[i * 2 + 1] = self.commands[i] as u8;
|
||||
i += 1;
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error<E> {
|
||||
I2c(E),
|
||||
ProgramTooLong,
|
||||
}
|
||||
|
||||
impl<E> From<E> for Error<E> {
|
||||
fn from(e: E) -> Self {
|
||||
Error::I2c(e)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Driver struct
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// LP5562 four-channel RGBW LED driver.
|
||||
pub struct Lp5562<I2C> {
|
||||
i2c: I2C,
|
||||
addr: u8,
|
||||
}
|
||||
|
||||
impl<I2C, E> Lp5562<I2C>
|
||||
where
|
||||
I2C: I2c<Error = E>,
|
||||
{
|
||||
// ---- Construction ----
|
||||
|
||||
pub fn new(i2c: I2C, addr: u8) -> Self {
|
||||
Self { i2c, addr }
|
||||
}
|
||||
|
||||
pub fn new_default(i2c: I2C) -> Self {
|
||||
Self::new(i2c, DEFAULT_ADDRESS)
|
||||
}
|
||||
|
||||
/// Consume the driver and return the I2C bus.
|
||||
pub fn release(self) -> I2C {
|
||||
self.i2c
|
||||
}
|
||||
|
||||
// ---- Low-level register access ----
|
||||
|
||||
pub fn write_register(&mut self, register: u8, value: u8) -> Result<(), E> {
|
||||
self.i2c.write(self.addr, &[register, value])
|
||||
}
|
||||
|
||||
pub fn read_register(&mut self, register: u8) -> Result<u8, E> {
|
||||
let mut buf = [0u8];
|
||||
self.i2c.write_read(self.addr, &[register], &mut buf)?;
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
fn modify_register<F>(&mut self, register: u8, f: F) -> Result<(), E>
|
||||
where
|
||||
F: FnOnce(u8) -> u8,
|
||||
{
|
||||
let val = self.read_register(register)?;
|
||||
self.write_register(register, f(val))
|
||||
}
|
||||
|
||||
// ---- Chip enable / disable / reset ----
|
||||
|
||||
/// Enable the chip (CHIP_EN = 1).
|
||||
///
|
||||
/// Caller MUST wait >= 500 us after this before issuing further commands.
|
||||
pub fn enable(&mut self) -> Result<(), E> {
|
||||
self.modify_register(reg::ENABLE, |v| v | ENABLE_CHIP_EN)
|
||||
}
|
||||
|
||||
pub fn disable(&mut self) -> Result<(), E> {
|
||||
self.modify_register(reg::ENABLE, |v| v & !ENABLE_CHIP_EN)
|
||||
}
|
||||
|
||||
pub fn is_enabled(&mut self) -> Result<bool, E> {
|
||||
let val = self.read_register(reg::ENABLE)?;
|
||||
Ok(val & ENABLE_CHIP_EN != 0)
|
||||
}
|
||||
|
||||
/// Software reset (write 0xFF to RESET register).
|
||||
pub fn reset(&mut self) -> Result<(), E> {
|
||||
self.write_register(reg::RESET, RESET_MAGIC)
|
||||
}
|
||||
|
||||
// ---- PWM control ----
|
||||
|
||||
/// Set PWM duty cycle for a channel (0 = off, 255 = full).
|
||||
pub fn set_pwm(&mut self, channel: Channel, value: u8) -> Result<(), E> {
|
||||
self.write_register(channel.pwm_reg(), value)
|
||||
}
|
||||
|
||||
pub fn get_pwm(&mut self, channel: Channel) -> Result<u8, E> {
|
||||
self.read_register(channel.pwm_reg())
|
||||
}
|
||||
|
||||
/// Set PWM for all channels. B/G/R use auto-increment, W separate.
|
||||
pub fn set_all_pwm(&mut self, blue: u8, green: u8, red: u8, white: u8) -> Result<(), E> {
|
||||
self.i2c.write(self.addr, &[reg::B_PWM, blue, green, red])?;
|
||||
self.write_register(reg::W_PWM, white)
|
||||
}
|
||||
|
||||
pub fn set_rgb(&mut self, red: u8, green: u8, blue: u8) -> Result<(), E> {
|
||||
self.i2c.write(self.addr, &[reg::B_PWM, blue, green, red])
|
||||
}
|
||||
|
||||
pub fn set_rgbw(&mut self, red: u8, green: u8, blue: u8, white: u8) -> Result<(), E> {
|
||||
self.set_all_pwm(blue, green, red, white)
|
||||
}
|
||||
|
||||
pub fn all_off(&mut self) -> Result<(), E> {
|
||||
self.set_all_pwm(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
// ---- Current control ----
|
||||
|
||||
/// Set LED driver current (0-255 = 0.0-25.5 mA in 0.1 mA steps).
|
||||
/// Default after reset: 0xAF = 17.5 mA.
|
||||
pub fn set_current(&mut self, channel: Channel, value: u8) -> Result<(), E> {
|
||||
self.write_register(channel.current_reg(), value)
|
||||
}
|
||||
|
||||
pub fn get_current(&mut self, channel: Channel) -> Result<u8, E> {
|
||||
self.read_register(channel.current_reg())
|
||||
}
|
||||
|
||||
pub fn set_all_current(&mut self, blue: u8, green: u8, red: u8, white: u8) -> Result<(), E> {
|
||||
self.i2c.write(self.addr, &[reg::B_CURRENT, blue, green, red])?;
|
||||
self.write_register(reg::W_CURRENT, white)
|
||||
}
|
||||
|
||||
// ---- Configuration ----
|
||||
|
||||
pub fn set_clock_source(&mut self, source: ClockSource) -> Result<(), E> {
|
||||
self.modify_register(reg::CONFIG, |v| (v & !0x03) | (source as u8))
|
||||
}
|
||||
|
||||
pub fn set_pwm_frequency(&mut self, freq: PwmFrequency) -> Result<(), E> {
|
||||
self.modify_register(reg::CONFIG, |v| match freq {
|
||||
PwmFrequency::Hz256 => v & !CONFIG_PWM_HF,
|
||||
PwmFrequency::Hz558 => v | CONFIG_PWM_HF,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_power_save(&mut self, enabled: bool) -> Result<(), E> {
|
||||
self.modify_register(reg::CONFIG, |v| {
|
||||
if enabled { v | CONFIG_PS_EN } else { v & !CONFIG_PS_EN }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_log_mode(&mut self, enabled: bool) -> Result<(), E> {
|
||||
self.modify_register(reg::ENABLE, |v| {
|
||||
if enabled { v | ENABLE_LOG_EN } else { v & !ENABLE_LOG_EN }
|
||||
})
|
||||
}
|
||||
|
||||
/// Write the entire CONFIG register at once.
|
||||
pub fn write_config(
|
||||
&mut self,
|
||||
clock: ClockSource,
|
||||
pwm_hf: bool,
|
||||
power_save: bool,
|
||||
) -> Result<(), E> {
|
||||
let mut val = clock as u8;
|
||||
if pwm_hf { val |= CONFIG_PWM_HF; }
|
||||
if power_save { val |= CONFIG_PS_EN; }
|
||||
self.write_register(reg::CONFIG, val)
|
||||
}
|
||||
|
||||
// ---- LED mapping ----
|
||||
|
||||
pub fn set_led_mapping(&mut self, channel: Channel, mapping: LedMapping) -> Result<(), E> {
|
||||
let shift = channel.led_map_shift();
|
||||
self.modify_register(reg::LED_MAP, |v| {
|
||||
(v & !(0x03 << shift)) | ((mapping as u8) << shift)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_all_led_mappings(
|
||||
&mut self,
|
||||
blue: LedMapping,
|
||||
green: LedMapping,
|
||||
red: LedMapping,
|
||||
white: LedMapping,
|
||||
) -> Result<(), E> {
|
||||
let val = ((white as u8) << LED_MAP_W_SHIFT)
|
||||
| ((red as u8) << LED_MAP_R_SHIFT)
|
||||
| ((green as u8) << LED_MAP_G_SHIFT)
|
||||
| ((blue as u8) << LED_MAP_B_SHIFT);
|
||||
self.write_register(reg::LED_MAP, val)
|
||||
}
|
||||
|
||||
/// Set all channels to direct I2C control.
|
||||
pub fn set_all_i2c_controlled(&mut self) -> Result<(), E> {
|
||||
self.write_register(reg::LED_MAP, 0x00)
|
||||
}
|
||||
|
||||
// ---- Engine execution state ----
|
||||
|
||||
/// Set execution state for one engine.
|
||||
///
|
||||
/// Wait >= 488 us between consecutive ENABLE register writes.
|
||||
pub fn set_engine_exec(&mut self, engine: EngineId, exec: EngineExec) -> Result<(), E> {
|
||||
let shift = engine.exec_shift();
|
||||
self.modify_register(reg::ENABLE, |v| {
|
||||
(v & !(0x03 << shift)) | ((exec as u8) << shift)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_all_engine_exec(
|
||||
&mut self,
|
||||
eng1: EngineExec,
|
||||
eng2: EngineExec,
|
||||
eng3: EngineExec,
|
||||
) -> Result<(), E> {
|
||||
self.modify_register(reg::ENABLE, |v| {
|
||||
(v & 0xC0)
|
||||
| ((eng1 as u8) << ENABLE_ENG1_EXEC_SHIFT)
|
||||
| ((eng2 as u8) << ENABLE_ENG2_EXEC_SHIFT)
|
||||
| ((eng3 as u8) << ENABLE_ENG3_EXEC_SHIFT)
|
||||
})
|
||||
}
|
||||
|
||||
// ---- Engine operation mode ----
|
||||
|
||||
/// Set operation mode for one engine.
|
||||
///
|
||||
/// Wait >= 153 us between consecutive OP_MODE register writes.
|
||||
/// When transitioning from Run, first set exec to Hold.
|
||||
pub fn set_engine_mode(&mut self, engine: EngineId, mode: EngineMode) -> Result<(), E> {
|
||||
let shift = engine.mode_shift();
|
||||
self.modify_register(reg::OP_MODE, |v| {
|
||||
(v & !(0x03 << shift)) | ((mode as u8) << shift)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_all_engine_modes(
|
||||
&mut self,
|
||||
eng1: EngineMode,
|
||||
eng2: EngineMode,
|
||||
eng3: EngineMode,
|
||||
) -> Result<(), E> {
|
||||
let val = ((eng1 as u8) << OP_MODE_ENG1_SHIFT)
|
||||
| ((eng2 as u8) << OP_MODE_ENG2_SHIFT)
|
||||
| ((eng3 as u8) << OP_MODE_ENG3_SHIFT);
|
||||
self.write_register(reg::OP_MODE, val)
|
||||
}
|
||||
|
||||
// ---- Engine program counter ----
|
||||
|
||||
/// Set PC (0-15). Engine exec must be Hold.
|
||||
pub fn set_engine_pc(&mut self, engine: EngineId, pc: u8) -> Result<(), E> {
|
||||
self.write_register(engine.pc_reg(), pc & 0x0F)
|
||||
}
|
||||
|
||||
pub fn get_engine_pc(&mut self, engine: EngineId) -> Result<u8, E> {
|
||||
let val = self.read_register(engine.pc_reg())?;
|
||||
Ok(val & 0x0F)
|
||||
}
|
||||
|
||||
// ---- Engine program loading ----
|
||||
|
||||
/// Load a program into engine SRAM.
|
||||
///
|
||||
/// Sets engine to Load mode internally. Caller must have set exec
|
||||
/// to Hold first and must wait >= 153 us before the next OP_MODE write.
|
||||
pub fn load_engine_program(
|
||||
&mut self,
|
||||
engine: EngineId,
|
||||
program: &EngineProgram,
|
||||
) -> Result<(), Error<E>> {
|
||||
self.set_engine_mode(engine, EngineMode::Load)
|
||||
.map_err(Error::I2c)?;
|
||||
|
||||
let prog_bytes = program.as_bytes();
|
||||
let start = engine.prog_start();
|
||||
let mut buf = [0u8; 33];
|
||||
buf[0] = start;
|
||||
buf[1..33].copy_from_slice(&prog_bytes);
|
||||
self.i2c.write(self.addr, &buf).map_err(Error::I2c)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load raw u16 commands into engine SRAM.
|
||||
pub fn load_engine_commands(
|
||||
&mut self,
|
||||
engine: EngineId,
|
||||
commands: &[u16],
|
||||
) -> Result<(), Error<E>> {
|
||||
if commands.len() > MAX_PROGRAM_LEN {
|
||||
return Err(Error::ProgramTooLong);
|
||||
}
|
||||
let prog = EngineProgram::from_commands(commands);
|
||||
self.load_engine_program(engine, &prog)
|
||||
}
|
||||
|
||||
// ---- Status ----
|
||||
|
||||
/// Read and decode STATUS register. Reading clears interrupt flags.
|
||||
pub fn read_status(&mut self) -> Result<Status, E> {
|
||||
let val = self.read_register(reg::STATUS)?;
|
||||
Ok(Status::from_reg(val))
|
||||
}
|
||||
|
||||
pub fn read_status_raw(&mut self) -> Result<u8, E> {
|
||||
self.read_register(reg::STATUS)
|
||||
}
|
||||
|
||||
// ---- High-level convenience ----
|
||||
|
||||
/// Initialize for direct I2C PWM control.
|
||||
///
|
||||
/// Call `enable()` first, then wait >= 500 us, then call this.
|
||||
pub fn init_direct_control(&mut self, clock: ClockSource) -> Result<(), E> {
|
||||
self.write_config(clock, false, false)?;
|
||||
self.set_all_i2c_controlled()
|
||||
}
|
||||
|
||||
/// Load a program, set engine to Run mode and Run exec state.
|
||||
///
|
||||
/// Engine exec must be Hold before calling. Caller is responsible
|
||||
/// for timing delays between register writes in production code.
|
||||
pub fn run_engine(
|
||||
&mut self,
|
||||
engine: EngineId,
|
||||
program: &EngineProgram,
|
||||
) -> Result<(), Error<E>> {
|
||||
self.load_engine_program(engine, program)?;
|
||||
self.set_engine_mode(engine, EngineMode::Run)
|
||||
.map_err(Error::I2c)?;
|
||||
self.set_engine_exec(engine, EngineExec::Run)
|
||||
.map_err(Error::I2c)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop an engine: set exec to Hold, then mode to Disabled.
|
||||
pub fn stop_engine(&mut self, engine: EngineId) -> Result<(), E> {
|
||||
self.set_engine_exec(engine, EngineExec::Hold)?;
|
||||
self.set_engine_mode(engine, EngineMode::Disabled)
|
||||
}
|
||||
}
|
||||
1
src/led/mod.rs
Normal file
1
src/led/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod lp5562;
|
||||
Reference in New Issue
Block a user