- 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>
757 lines
22 KiB
Rust
757 lines
22 KiB
Rust
//! 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)
|
|
}
|
|
}
|