//! 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 { I2c(E), ProgramTooLong, } impl From for Error { fn from(e: E) -> Self { Error::I2c(e) } } // --------------------------------------------------------------------------- // Driver struct // --------------------------------------------------------------------------- /// LP5562 four-channel RGBW LED driver. pub struct Lp5562 { i2c: I2C, addr: u8, } impl Lp5562 where I2C: I2c, { // ---- 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 { let mut buf = [0u8]; self.i2c.write_read(self.addr, &[register], &mut buf)?; Ok(buf[0]) } fn modify_register(&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 { 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 { 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 { 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 { 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> { 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> { 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 { let val = self.read_register(reg::STATUS)?; Ok(Status::from_reg(val)) } pub fn read_status_raw(&mut self) -> Result { 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> { 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) } }