astra/pd-power-supply
USB-PD Power Supply with RP2040 controller
- Version
- 1.1.6
- License
- unset
- Stars
- 0
firmware/pd_control.py
"""CH224A/CH224Q PPS control. WCH CH224DS1 v2.1, registers 09/0A/50/53/60..8F.
The IC's I2C voltage register is 100mV/LSB, even though PD PPS itself permits 20mV.
No AVS/EPR requests: this PCB's output range is capped at 20V.
"""
MIN_MV, MAX_MV, STEP_MV = 3300, 20000, 100
FIXED_CODES = {5000: 0, 9000: 1, 12000: 2, 15000: 3, 20000: 4}
def knob_voltage(coarse, fine):
coarse = max(0, min(65535, coarse))
fine = max(0, min(65535, fine))
mv = MIN_MV + (MAX_MV - MIN_MV) * coarse // 65535
mv += fine * 1000 // 65535 - 500
return max(MIN_MV, min(MAX_MV, ((mv + 50) // STEP_MV) * STEP_MV))
class KnobSelector:
def __init__(self):
self.value = 5000
self.pending = self.value
self.samples = 0
def update(self, coarse, fine):
candidate = knob_voltage(coarse, fine)
if candidate != self.pending:
self.pending, self.samples = candidate, 1
else:
self.samples += 1
if self.samples >= 4:
self.value = self.pending
return self.value
def parse_pdos(raw):
"""Decode 7 SPR PDOs. Recognize PD Source_Capabilities header or bare PDOs.
Firmware fails closed on unknown framing. WCH does not document the byte
framing of its SRCCAP register region; confirm the dump on real hardware.
"""
if len(raw) < 4:
raise ValueError('truncated capabilities')
header = int.from_bytes(raw[:2], 'little')
count = (header >> 12) & 7
if header & 31 == 1 and count and not header & 0x8000:
if len(raw) < 2 + count * 4:
raise ValueError('truncated PD message')
data = raw[2:2 + count * 4]
else:
data = raw[:28]
result = []
for offset in range(0, len(data) - 3, 4):
pdo = int.from_bytes(data[offset:offset+4], 'little')
if pdo == 0:
break
kind = pdo >> 30
if kind == 0:
mv, ma = ((pdo >> 10) & 1023) * 50, (pdo & 1023) * 10
if not 3000 <= mv <= 20000 or not 50 <= ma <= 5000:
raise ValueError('invalid fixed PDO')
result.append(('fixed', mv, mv, ma))
elif kind == 3 and (pdo >> 28) & 3 == 0:
lo, hi, ma = ((pdo >> 8) & 255) * 100, ((pdo >> 17) & 255) * 100, (pdo & 127) * 50
if not 3300 <= lo <= hi <= 21000 or not 50 <= ma <= 5000:
raise ValueError('invalid PPS APDO')
result.append(('pps', lo, hi, ma))
elif kind in (1, 2) or kind == 3:
# Battery, variable supply and AVS cannot be used by this controller policy.
result.append(('other', 0, 0, 0))
if not result or result[0][:3] != ('fixed', 5000, 5000):
raise ValueError('capabilities must begin with 5V fixed PDO')
return result
def supports_pps(pdos, mv):
# Reserve >=150mA for controller/display in addition to the 1A output target.
return any(kind == 'pps' and lo <= mv <= min(hi, MAX_MV) and ma >= 1150
for kind, lo, hi, ma in pdos)
class CH224Q:
def __init__(self, bus):
self.bus, self.address, self.pdos = bus, None, []
self.requested_mv, self.mode = 5000, 'fixed'
def read(self, register, count=1):
return self.bus.readfrom_mem(self.address, register, count)
def write(self, register, value):
self.bus.writeto_mem(self.address, register, bytes((value,)))
def connect(self):
found = self.bus.scan()
self.address = next((a for a in (0x22, 0x23) if a in found), None)
if self.address is None:
raise OSError('no PD controller')
# Reset policy explicitly to 5V before any dial request.
self.write(0x0a, 0)
self.requested_mv, self.mode, self.pdos = 5000, 'fixed', []
def capabilities(self):
if not self.read(0x09)[0] & 8:
self.pdos = []
raise ValueError('PD handshake unavailable')
# Read one register at a time: does not assume undocumented burst support.
raw = bytes(self.read(reg)[0] for reg in range(0x60, 0x90))
self.pdos = parse_pdos(raw)
return self.pdos
def request(self, mv):
if mv % STEP_MV or not MIN_MV <= mv <= MAX_MV:
raise ValueError('out of range or off 100mV grid')
if not supports_pps(self.pdos, mv):
# Never silently substitute a different voltage for the displayed target.
self.write(0x0a, 0)
self.requested_mv, self.mode = 5000, 'fixed'
raise ValueError('PPS range/current not supported')
self.write(0x53, mv // STEP_MV)
if self.mode != 'pps':
self.write(0x0a, 6) # Voltage FIRST, then enable PPS.
self.requested_mv, self.mode = mv, 'pps'
return mv