Linux driver for Texas Instruments LP55231, a 9 channel RGB/White LED controller with internal program memory and integrated charge Pump.
Features:
- Full implementation of I2C control interface in datasheet
- Ergonomic API to leverage the programming engine
- Easy to debug with optional features:
- read-after-write checks to validate register writes
- debug output to see the value in a register before and after a write
This example covers a typical initialization of the driver, preparing three LEDs (R, G, B) for animated effects.
use ti_lp55231::{
Channel,
ChargePumpMode,
ClockSelection,
Direction,
Engine,
EngineExec,
EngineMode,
Instruction,
LP55231,
PreScale,
}
// Create the driver
let path = "/dev/i2c-2";
let i2c_addr = 0x32;
let ic = LP55231::create(path, i2c_addr)?;
// Power and configure the driver.
ic.set_enabled(true)?;
ic.set_misc_settings(Misc {
auto_increment_enabled: true,
powersave_enabled: true,
charge_pump_mode: ChargePumpMode::Auto,
pwm_powersave_enabled: true,
clock_selection: ClockSelection::ForceInternal,
})?;
// Channel assignment.
let (r, g, b) = (Channel::D7, Channel::D1, Channel::D2);
// Enable logarithmic brightness for a smoother ramp up effect.
ic.set_log_brightness(r, true)?;
ic.set_log_brightness(g, true)?;
ic.set_log_brightness(b, true)?;
// Enable ratiometric dimming to preserve the ratio between the
// RGB components of all mapped channels during animations
ic.set_ratiometric_dimming(r, true)?;
ic.set_ratiometric_dimming(g, true)?;
ic.set_ratiometric_dimming(b, true)?;
// Set color to orange
ic.set_channel_pwm(r, 255)?;
ic.set_channel_pwm(g, 128)?;
ic.set_channel_pwm(b, 0)?;
// Program the IC (see other example for implementations of `create_program`)
let instructions = create_program(&[r, g, b])?;
ic.load_program(&instructions)?;
// Wait for the ENGINE_BUSY bit to clear,
// indicating that all instructions have been loaded.
ic.wait_while_engine_busy(Duration::from_millis(10))?;
// Set up one of the programming engines to Halt & Hold (ready to execute).
let engine = Engine::E1;
ic.set_engine_exec(engine, EngineExec::Hold)?;
ic.set_engine_mode(engine, EngineMode::Halt)?;
// Run the effect
ic.set_engine_exec(engine, EngineExec::Free)?;
ic.set_engine_mode(engine, EngineMode::RunProgram)?;
This example is an implementation of create_program
that prepares a blinking
effect to run in an endless loop.
fn create_program(channels_to_control: &[Channel]) -> [Instruction; 8] {
[
// ----- LED-to-Engine mapping table
// 00. Map all target output channels to the programming engine for control.
Instruction::map_channels(channels_to_control),
// ----- blink effect start
// 01-02. Set LED mapping table start/end index + activation.
Instruction::mux_map_start(0),
Instruction::mux_ld_end(0),
// 03. Power all mapped LEDs off.
Instruction::set_pwm(0),
// 04. Wait ~0.5 seconds (15.625ms * 30).
Instruction::wait(PreScale::CT15_625, 30),
// 05. Set all LEDs to max brightness.
Instruction::set_pwm(255),
// 06. Wait ~0.5 seconds (15.625ms * 30).
Instruction::wait(PreScale::CT15_625, 30),
// 07. Loop back to beginning of blink effect index.
Instruction::branch(1, 0),
]
}
fn create_program(channels_to_control: &[Channel]) -> [Instruction; 9] {
[
// ----- LED-to-Engine mapping table
// 00. Map all target output channels to the programming engine for control.
Instruction::map_channels(channels_to_control),
// ----- glow effect start
// 01-02. Set LED mapping table start/end index + activation.
Instruction::mux_map_start(0),
Instruction::mux_ld_end(0),
// 03. Quickly ramp up to max brightness.
Instruction::ramp(PreScale::CT0_488, 4, Direction::Up, 255),
// 04. Wait ~0.5 seconds (15.625ms * 30 = 468.75ms).
Instruction::wait(PreScale::CT15_625, 30),
// 05. Begin ramping brightness down to half (255 - 127 = 128).
Instruction::ramp(PreScale::CT15_625, 4, Direction::Down, 127),
// 06. Wait ~0.5 seconds (15.625ms * 30 = 468.75ms).
Instruction::wait(PreScale::CT15_625, 30),
// 07. Begin ramping brightness up to max (128 + 127 = 255).
Instruction::ramp(PreScale::CT15_625, 4, Direction::Up, 127),
// 08. Loop back to first step of effect.
Instruction::branch(1, 0),
]
}
The programming engine supports up to 96 instructions, which gives you plenty of room to set up multiple effects. To switch between effects:
- pause the programming engine
- update the program counter to first index of next effect
- unpause the programming engine.
Example:
// Pause engine execution.
ic.set_engine_exec(Engine::E1, EngineExec::Hold)?;
ic.wait_while_engine_busy(Duration::from_millis(1))?;
// Update the program counter to the starting instruction of the desired effect
// This example assumes we're jumping to instruction 42, of the possible 96
// programming memory addresses.
ic.set_engine_program_counter(Engine::E1, 42)?;
// Unpause the engine and begin the new animation.
ic.set_engine_exec(Engine::E1, EngineExec::Free)?;
ic.set_engine_mode(Engine::E1, EngineMode::RunProgram)?;
Read-after-write checks can be enabled with:
let ic = LP55231::create(...)?;
ic.verify_writes = true;
This will cause the driver to perform a read after every I2C write instruction to compare the value in the register. It will throw an exception if the read value does not match the written value.
Note
This is useful during development, especially around using the programming engines which must be in the correct internal state in order to allow changes.
When enabled via debug_enabled
property, the driver will emit useful (but
rather verbose) output to help you understand the state of registers with every
read and write operation. Example:
let ic = LP55231::create(...)?;
ic.debug_enabled = true;
ic.set_enabled(true)?;
Will produce output:
set_enabled(true) {
00000000 << 0x00 ENABLE_ENGINE_CNTRL1
00100000 >> 0x00 ENABLE_ENGINE_CNTRL1
}
Scope for multiple debug calls can be combined with the debug::scope!
macro:
fn multiple_i2c_calls(
ic: &mut LP55231,
value: bool,
) -> Result<(), LinuxI2CError> {
debug::scope!(ic, "example({})", value);
ic.set_enabled(value)?;
ic.set_enabled(!value)?;
Ok(())
}
multiple_i2_calls(true)?;
Would result in the following output:
example(true) {
set_enabled(true) {
00000000 << 0x00 ENABLE_ENGINE_CNTRL1
00100000 >> 0x00 ENABLE_ENGINE_CNTRL1
}
set_enabled(false) {
00100000 << 0x00 ENABLE_ENGINE_CNTRL1
00000000 >> 0x00 ENABLE_ENGINE_CNTRL1
}
}
See debug.rs docs for more details.
- Clone the project and open the folder in VS Code
- Accept plugin suggestions (dev container required in non-linux envs)
- Re-open in dev container
Note
This project uses hermit to manage the Rust toolchain for this project. No prior installation of Rust required.
- Read/write pages in blocks (
at_once
param inread/write_program_page
)