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>
This commit is contained in:
280
tools/lp5562_evm.py
Normal file
280
tools/lp5562_evm.py
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user