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)