astra/pd-power-supply
USB-PD Power Supply with RP2040 controller
- Version
- 1.1.6
- License
- unset
- Stars
- 0
firmware/main.py
"""RP2040 MicroPython: GPIO26 coarse, GPIO27 fine, I2C1 GPIO2/3 CH224Q.
I2C0 GPIO0/1: JST-PH SSD1306 at 0x3c, INA219 at 0x40.
CFG1 hardware strap boots at 9V until firmware explicitly selects 5V.
"""
from machine import Pin, I2C, ADC, WDT
from time import sleep_ms, ticks_ms, ticks_diff
from pd_control import CH224Q, KnobSelector
import framebuf
coarse, fine = ADC(26), ADC(27)
selector = KnobSelector()
bus = I2C(0, sda=Pin(0), scl=Pin(1), freq=100_000)
pd_bus = I2C(1, sda=Pin(2), scl=Pin(3), freq=100_000)
pd = CH224Q(pd_bus)
power_good = Pin(4, Pin.IN)
watchdog = WDT(timeout=4000)
class OLED(framebuf.FrameBuffer):
def __init__(self):
self.buffer = bytearray(128 * 64 // 8)
super().__init__(self.buffer, 128, 64, framebuf.MONO_VLSB)
# SSD1306 internal charge pump, 64 rows, horizontal addressing.
for cmd in (0xae,0x20,0x00,0x40,0xa1,0xc8,0xa8,63,0xd3,0,
0xda,0x12,0xd5,0x80,0xd9,0xf1,0xdb,0x30,
0x81,0x7f,0xa4,0xa6,0x8d,0x14,0xaf):
self.cmd(cmd)
self.fill(0)
self.show()
def cmd(self,c):
bus.writeto(0x3c,bytes((0x80,c)))
def show(self):
for c in (0x21,0,127,0x22,0,7): self.cmd(c)
bus.writeto(0x3c,b'\x40'+self.buffer)
def write_reg(reg,value):
bus.writeto_mem(0x40,reg,bytes((value>>8,value&255)))
def read_reg(reg):
b=bus.readfrom_mem(0x40,reg,2)
return (b[0]<<8)|b[1]
def signed(value):
return value-65536 if value&0x8000 else value
screen = None
sense_ready = False
last_request = last_display = last_caps = ticks_ms() - 3000
changed_at = ticks_ms()
status = 'WAIT FOR PD'
while True:
watchdog.feed()
now = ticks_ms()
target = selector.update(sum(coarse.read_u16() for _ in range(8)) // 8,
sum(fine.read_u16() for _ in range(8)) // 8)
# Re-read capabilities after unplug/replug and on retry. Unknown profiles
# keep the request at 5V and display the error; success requires measurement.
if ticks_diff(now, last_caps) >= 2000:
last_caps = now
try:
if pd.address is None:
pd.connect()
pd.capabilities()
except (OSError, ValueError):
pd.pdos = []
status = 'NO PPS / CAPS'
if ticks_diff(now, last_request) >= 500 and (
target != pd.requested_mv or ticks_diff(now, last_request) >= 2000):
last_request = now
try:
if pd.address is None:
pd.connect()
previous = pd.requested_mv
pd.request(target)
if previous != target:
changed_at = now
status = 'PPS REQUEST'
except OSError:
pd.address, pd.pdos = None, []
status = 'PD I2C ERROR'
except ValueError:
status = 'PPS UNSUPPORTED'
if ticks_diff(now, last_display) >= 200:
last_display = now
voltage = current = None
report = status
try:
if not sense_ready:
write_reg(0x00, 0x319f)
write_reg(0x05, 8192)
sense_ready = True
raw = read_reg(0x02)
voltage = (raw >> 3) * 0.004
current = signed(read_reg(0x04)) * 0.0001
if voltage < 1:
report = 'NO PWR INPUT'
pd.address, pd.pdos = None, []
elif pd.mode == 'pps':
if ticks_diff(now, changed_at) < 1500:
report = 'NEGOTIATING'
elif power_good.value() or abs(voltage - target / 1000) > max(0.2, target / 1000 * 0.03):
report = 'VOLTAGE MISMATCH'
else:
report = 'PPS ACTIVE'
if abs(current) > 1:
report = 'OVER 1A'
if raw & 1:
report = 'SENSE OVERFLOW'
except OSError:
sense_ready = False
report = 'SENSOR ERROR'
try:
if screen is None:
screen = OLED()
screen.fill(0)
screen.text('SET %5.2f V' % (target / 1000), 0, 0)
screen.text('REQ %5.2f V' % (pd.requested_mv / 1000), 0, 12)
if voltage is not None:
screen.text('%5.2fV %5.3fA' % (voltage, current), 0, 30)
screen.text(report, 0, 54)
screen.show()
except OSError:
screen = None
sleep_ms(50)