Files
xblink/tools/lp5562_evm.py
michael 5970a21b54 Add LP5562EVM USB-I2C control tool, document reverse-engineered protocol
Reverse-engineered the MSP430 serial protocol on the LP5562EVM:
- I<addr><reg> reads, O<addr><reg><data> writes, status 00=OK / 02=NAK
- EN pin must be jumpered to VDD (MSP430 doesn't drive it after reset)
- CONFIG (0x0B) is write-only, W_CURRENT (0x09) not writable via bridge
- LED cycling verified: all 4 channels respond to direct PWM control

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:11:35 -08:00

281 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""
LP5562EVM USB-I2C bridge control tool.
Controls the LP5562 LED driver via the EVM's MSP430 USB-to-I2C bridge.
The MSP430 presents as a CDC serial device (/dev/ttyACM0).
Protocol (reverse-engineered):
Read: I<addr2><reg2> → "SS DD OK\n" (SS=00 success, DD=data hex)
Write: O<addr2><reg2><data2> → "SS OK\n" (SS=00 success)
Info: ? → "TI <date>\n"
Prerequisites:
- LP5562 EN pin must be held high (jumper EN to VDD on EVM P1/P6 header).
The MSP430 does NOT drive EN after reset.
- CONFIG register (0x0B) is write-only via this bridge — writes take effect
but reads always return 0x00. Internal oscillator must be explicitly set.
- W_CURRENT (0x09) cannot be written via this bridge (LP5562 quirk or
MSP430 limitation). White channel uses reset default (0xAF = 17.5mA).
Usage:
./lp5562_evm.py dump # Read all registers
./lp5562_evm.py init # Enable chip + internal clock + direct PWM
./lp5562_evm.py color R G B W # Set RGBW PWM values (0-255)
./lp5562_evm.py off # All LEDs off
./lp5562_evm.py cycle # Cycle through RGBW channels
./lp5562_evm.py breathe # Breathing pattern demo
"""
import serial
import sys
import time
PORT = '/dev/ttyACM0'
BAUD = 115200
LP5562_ADDR = 0x30
# LP5562 register map
REG_ENABLE = 0x00
REG_OP_MODE = 0x01
REG_B_PWM = 0x02
REG_G_PWM = 0x03
REG_R_PWM = 0x04
REG_W_PWM = 0x05
REG_B_CURRENT = 0x06
REG_G_CURRENT = 0x07
REG_R_CURRENT = 0x08
REG_W_CURRENT = 0x09
REG_CONFIG = 0x0B
REG_STATUS = 0x0C
REG_RESET = 0x0D
REG_INT_GPO = 0x0E
REG_LED_MAP = 0x70
REG_NAMES = {
0x00: 'ENABLE', 0x01: 'OP_MODE', 0x02: 'B_PWM',
0x03: 'G_PWM', 0x04: 'R_PWM', 0x05: 'W_PWM',
0x06: 'B_CURRENT', 0x07: 'G_CURRENT', 0x08: 'R_CURRENT',
0x09: 'W_CURRENT', 0x0B: 'CONFIG', 0x0C: 'STATUS',
0x0D: 'RESET', 0x0E: 'INT_GPO', 0x70: 'LED_MAP',
}
# ENABLE register bits
CHIP_EN = 0x40
LOG_EN = 0x80
# CONFIG register bits
CLK_INT = 0x01 # Internal oscillator
# OP_MODE: engine modes (2 bits each: [7:6]=E3, [5:4]=E2, [3:2]=E1)
MODE_DISABLED = 0b00
MODE_LOAD = 0b01
MODE_RUN = 0b10
MODE_DIRECT = 0b11
class LP5562EVM:
def __init__(self, port=PORT, baud=BAUD):
self.ser = serial.Serial(port, baud, timeout=0.5)
time.sleep(0.1)
self.ser.reset_input_buffer()
def close(self):
self.ser.close()
def version(self):
self.ser.reset_input_buffer()
self.ser.write(b'?')
time.sleep(0.2)
return self.ser.read(256).decode('ascii', errors='replace').strip()
def read(self, reg):
"""Read LP5562 register. Returns data byte or raises on error."""
self.ser.reset_input_buffer()
self.ser.write(f'I{LP5562_ADDR:02X}{reg:02X}'.encode())
time.sleep(0.15)
resp = self.ser.read(256).decode('ascii', errors='replace').strip()
parts = resp.split()
if len(parts) < 3 or parts[-1] != 'OK':
raise IOError(f"Read reg 0x{reg:02X} failed: {resp}")
status = int(parts[0], 16)
if status != 0:
raise IOError(f"I2C NAK reading reg 0x{reg:02X} (status={status:02X}). Is EN pin high?")
return int(parts[1], 16)
def write(self, reg, val):
"""Write LP5562 register."""
self.ser.reset_input_buffer()
self.ser.write(f'O{LP5562_ADDR:02X}{reg:02X}{val:02X}'.encode())
time.sleep(0.15)
resp = self.ser.read(256).decode('ascii', errors='replace').strip()
parts = resp.split()
if not parts or parts[-1] != 'OK':
raise IOError(f"Write reg 0x{reg:02X}=0x{val:02X} failed: {resp}")
status = int(parts[0], 16)
if status != 0:
raise IOError(f"I2C NAK writing reg 0x{reg:02X} (status={status:02X}). Is EN pin high?")
def dump(self):
"""Read and display all registers."""
print("LP5562 Register Dump")
print("=" * 40)
for reg in sorted(REG_NAMES.keys()):
try:
val = self.read(reg)
name = REG_NAMES[reg]
print(f" 0x{reg:02X} {name:12s} = 0x{val:02X} ({val:3d}) {val:08b}")
except IOError as e:
print(f" 0x{reg:02X} {REG_NAMES[reg]:12s} = ERROR: {e}")
def reset(self):
"""Software reset the LP5562."""
self.write(REG_RESET, 0xFF)
time.sleep(0.5)
def init(self, current_ma=5.0):
"""Initialize LP5562 for direct PWM control."""
# Reset to known state
self.write(REG_RESET, 0xFF)
time.sleep(0.5)
# Enable chip
self.write(REG_ENABLE, CHIP_EN)
time.sleep(0.001) # >500us startup
# Internal oscillator (write-only: reads back as 0x00, but takes effect)
self.write(REG_CONFIG, CLK_INT)
# All LEDs mapped to I2C direct control
self.write(REG_LED_MAP, 0x00)
# Set current for B/G/R (0-255 maps to 0-25.5mA, so 10 = 1mA)
# W_CURRENT (0x09) is not writable via EVM bridge — uses default 0xAF
current_val = min(255, max(0, int(current_ma * 10)))
self.write(REG_B_CURRENT, current_val)
self.write(REG_G_CURRENT, current_val)
self.write(REG_R_CURRENT, current_val)
print(f"LP5562 initialized: chip_en, internal osc, {current_ma:.1f}mA/ch (W=17.5mA default)")
def set_color(self, r=0, g=0, b=0, w=0):
"""Set RGBW PWM values (0-255)."""
self.write(REG_R_PWM, r & 0xFF)
self.write(REG_G_PWM, g & 0xFF)
self.write(REG_B_PWM, b & 0xFF)
self.write(REG_W_PWM, w & 0xFF)
def off(self):
"""All LEDs off."""
self.set_color(0, 0, 0, 0)
def cmd_dump(evm, args):
ver = evm.version()
print(f"EVM firmware: {ver}")
print()
evm.dump()
def cmd_init(evm, args):
current = float(args[0]) if args else 5.0
evm.init(current_ma=current)
evm.dump()
def cmd_color(evm, args):
if len(args) < 3:
print("Usage: color R G B [W]")
return
r, g, b = int(args[0]), int(args[1]), int(args[2])
w = int(args[3]) if len(args) > 3 else 0
evm.set_color(r, g, b, w)
print(f"Set color: R={r} G={g} B={b} W={w}")
def cmd_off(evm, args):
evm.off()
print("All LEDs off")
def cmd_cycle(evm, args):
step = float(args[0]) if args else 0.5
pwm = 255
print(f"Cycling RGBW channels ({step}s per step). Ctrl+C to stop.")
try:
while True:
evm.set_color(0, 0, pwm, 0) # Blue
print(" Blue")
time.sleep(step)
evm.set_color(0, pwm, 0, 0) # Green
print(" Green")
time.sleep(step)
evm.set_color(pwm, 0, 0, 0) # Red
print(" Red")
time.sleep(step)
evm.set_color(0, 0, 0, pwm) # White
print(" White")
time.sleep(step)
evm.set_color(pwm, pwm, pwm, pwm) # All
print(" All on")
time.sleep(step)
evm.off()
print(" Off")
time.sleep(step)
except KeyboardInterrupt:
evm.off()
print("\nStopped.")
def cmd_breathe(evm, args):
"""Software breathing effect (ramp PWM up and down)."""
print("Breathing pattern on blue channel. Ctrl+C to stop.")
try:
while True:
# Ramp up
for i in range(0, 256, 4):
evm.set_color(0, 0, i, 0)
time.sleep(0.02)
# Ramp down
for i in range(255, -1, -4):
evm.set_color(0, 0, max(0, i), 0)
time.sleep(0.02)
time.sleep(0.3)
except KeyboardInterrupt:
evm.off()
print("\nStopped.")
COMMANDS = {
'dump': cmd_dump,
'init': cmd_init,
'color': cmd_color,
'off': cmd_off,
'cycle': cmd_cycle,
'breathe': cmd_breathe,
}
def main():
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
print("Usage: lp5562_evm.py <command> [args...]")
print()
print("Commands:")
print(" dump Read all registers")
print(" init [mA] Enable chip, set current (default 5mA)")
print(" color R G B [W] Set RGBW PWM (0-255)")
print(" off All LEDs off")
print(" cycle [sec] Cycle through RGBW (default 0.5s)")
print(" breathe Software breathing pattern")
sys.exit(1)
evm = LP5562EVM()
try:
COMMANDS[sys.argv[1]](evm, sys.argv[2:])
finally:
evm.close()
if __name__ == '__main__':
main()