MustafaMulla29/rp2040-bldc-motor-controller-new

A USB‑C PD–powered RP2040 BLDC motor controller PCB with 3.3 V logic, MCT8329A sensorless commutation, three MOSFET half-bridges, protected power switching, USB data, controls, and motor connectors.

Version
0.3.82
License
unset
Stars
0

firmware/test_voltage_selection.py

import unittest
from test_pd_profile import Device, Clock
from pd_profile import configure_runtime, verify_runtime, profiles
from power_policy import (valid_motor_contract, motor_permitted, VOLTAGES_MV,
                          INPUT_LIMIT_MA, MOTOR_MIN_MV, adc_millivolts)
from select_pd_voltage import VoltageSelector

class SelectionTests(unittest.TestCase):
    rdo = (3 << 28) | (200 << 10) | 300

    def test_pdo_wire_bytes_against_st_um2650_field_definitions(self):
        # Independent literals from UM2650 Rev 3 Table 2: 50 mV / 10 mA
        # fields. Do not derive expected bytes with the production encoder.
        for mv,wire in [(5000,'c8900100'),(9000,'c8d00200'),
                        (15000,'c8b00400'),(20000,'c8400600')]:
            bus=Device()
            configure_runtime(bus,selected_mv=mv)
            self.assertEqual(bytes(bus.regs[0x89:0x8d]),bytes.fromhex(wire))
            self.assertEqual(bytes(bus.regs[0x85:0x89]),bytes.fromhex('32900104'))

    def test_profile_count_preserves_reserved_bits_and_reset_is_last(self):
        bus=Device();bus.regs[0x70]=0xa2;writes=[];write=bus.writeto_mem
        def record(address,reg,data):
            writes.append((reg,bytes(data)));write(address,reg,data)
        bus.writeto_mem=record
        configure_runtime(bus,selected_mv=5000)
        self.assertEqual(writes[0],(0x70,b'\xa1'))
        self.assertEqual(writes[-3:],[(0x70,b'\xa2'),(0x51,b'\x0d'),(0x1a,b'\x26')])

    def test_failure_at_each_profile_write_keeps_selector_disarmed(self):
        for failed_write in range(1,8):
            sel,bus,clock,events,state=self.selector(20000)
            count=[0];write=bus.writeto_mem
            def failing(address,reg,data):
                count[0]+=1
                if count[0]==failed_write:raise OSError('injected transaction failure')
                write(address,reg,data)
            bus.writeto_mem=failing
            with self.assertRaises(OSError):sel.select(20000)
            self.assertFalse(sel.ready)
            self.assertEqual(set(events),{'disarm'})

    def test_corrupt_profile_readback_never_sends_soft_reset(self):
        sel,bus,clock,events,state=self.selector(20000)
        write=bus.writeto_mem;commands=[]
        def corrupt(address,reg,data):
            commands.append(reg);write(address,reg,data)
            if reg==0x89:bus.regs[reg]^=1
        bus.writeto_mem=corrupt
        with self.assertRaises(OSError):sel.select(20000)
        self.assertNotIn(0x1a,commands)
        self.assertFalse(sel.ready)
        self.assertEqual(set(events),{'disarm'})

    def test_all_profiles_read_back_and_leave_nvm_unchanged(self):
        bus = Device(); original = bytes(bus.image)
        for mv in VOLTAGES_MV:
            configure_runtime(bus, selected_mv=mv)
            verify_runtime(bus, selected_mv=mv)
            self.assertEqual((int.from_bytes(bus.regs[0x89:0x8d], 'little') >> 10) & 1023, mv//50)
            self.assertEqual(bytes(bus.image), original)
            self.assertEqual(bus.regs[0x51], 0x0d)
            self.assertEqual(bus.regs[0x1a], 0x26)
            with self.assertRaises(OSError): verify_runtime(bus, selected_mv=15000 if mv!=15000 else 9000)
        before = bytes(bus.regs)
        with self.assertRaises(ValueError): configure_runtime(bus, selected_mv=12000)
        self.assertEqual(bytes(bus.regs), before)

    def test_source_3a_capability_accepts_2a_request_at_each_voltage(self):
        for mv in VOLTAGES_MV:
            self.assertTrue(valid_motor_contract(mv,self.rdo,0x18,True,mv))
            self.assertFalse(valid_motor_contract(mv,self.rdo,0x18,False,mv))
            self.assertFalse(valid_motor_contract(mv,self.rdo,0x17,True,mv))
            self.assertFalse(valid_motor_contract(mv,self.rdo|(1<<26),0x18,True,mv))
            self.assertFalse(valid_motor_contract(mv,self.rdo|(1<<27),0x18,True,mv))
            self.assertFalse(valid_motor_contract(mv,(1<<28)|(100<<10)|300,0x18,True,mv))
            self.assertFalse(valid_motor_contract(mv,(1<<28)|(200<<10)|100,0x18,True,mv))
            self.assertFalse(valid_motor_contract(mv,self.rdo,0x18,True,12000))

    def test_low_voltage_and_current_stop_motor_not_restart(self):
        for mv in VOLTAGES_MV:
            args=[mv,mv,INPUT_LIMIT_MA[mv]-1,self.rdo,0x18,True,True,True,mv]
            self.assertTrue(motor_permitted(*args))
            for i,value in [(1,MOTOR_MIN_MV[mv]-1),(2,INPUT_LIMIT_MA[mv]),(6,False),(7,False)]:
                bad=args.copy();bad[i]=value
                self.assertFalse(motor_permitted(*bad))
        self.assertFalse(motor_permitted(4600,4450,300,self.rdo,0x18,True,True,True,5000))

    def selector(self, voltage=9000, motor=0, ok=True):
        bus,clock=Device(),Clock();events=[]
        bus.regs[0x91:0x95]=self.rdo.to_bytes(4,'little');bus.regs[0x29]=0x18
        state=dict(vbus_mv=voltage,vmotor_mv=motor,pd_ok2_low=ok)
        sel=VoltageSelector(bus,lambda:events.append('disarm'),lambda:state,clock)
        return sel,bus,clock,events,state

    def test_successful_20_to_5_selection_never_arms(self):
        sel,bus,clock,events,state=self.selector(20000)
        self.assertEqual(sel.select(20000),20000)
        state['vbus_mv']=5000
        self.assertEqual(sel.select(5000),5000)
        self.assertEqual(set(events),{'disarm'})
        self.assertGreaterEqual(clock.now,500)
        self.assertTrue(sel.ready)

    def test_no_writes_if_motor_bus_energized_or_setting_invalid(self):
        sel,bus,clock,events,state=self.selector(motor=9000)
        before=bytes(bus.regs)
        with self.assertRaises(OSError):sel.select(20000)
        self.assertEqual(bytes(bus.regs),before)
        with self.assertRaises(ValueError):sel.select(12000)
        self.assertEqual(bytes(bus.regs),before)
        self.assertFalse(sel.ready)
        self.assertEqual(events,['disarm','disarm'])

    def test_absent_profile_fallback_and_stale_contract_time_out(self):
        for voltage,ok in [(5000,False),(9000,True),(20000,False)]:
            sel,bus,clock,events,state=self.selector(voltage,ok=ok)
            with self.assertRaises(OSError):sel.select(20000,timeout_ms=500)
            self.assertFalse(sel.ready)
            self.assertEqual(sel.selected_mv,9000)
            self.assertEqual(clock.now,500)

    def test_i2c_failure_leaves_disarmed(self):
        sel,bus,clock,events,state=self.selector(20000)
        def broken(*args):raise OSError('I2C lost')
        bus.writeto_mem=broken
        with self.assertRaises(OSError):sel.select(20000)
        self.assertEqual(events,['disarm']);self.assertFalse(sel.ready)

    def test_motor_bus_becoming_live_aborts_negotiation(self):
        sel,bus,clock,events,state=self.selector(20000)
        def sample():
            return dict(state,vmotor_mv=2000 if clock.now >= 75 else 0)
        sel.sample=sample
        with self.assertRaises(OSError):sel.select(20000)
        self.assertFalse(sel.ready)
        self.assertEqual(set(events),{'disarm'})

    def test_unstable_contract_restarts_stability_timer(self):
        sel,bus,clock,events,state=self.selector(20000)
        sel.sample=lambda:dict(state,pd_ok2_low=not 100 <= clock.now < 150)
        self.assertEqual(sel.select(20000),20000)
        self.assertGreaterEqual(clock.now,400)
        state['vmotor_mv']=20000
        with self.assertRaises(OSError):sel.select(5000)
        self.assertFalse(sel.ready)
        self.assertEqual(set(events),{'disarm'})

    def test_adc_dividers_have_20v_headroom(self):
        for mv in (5000,9000,15000,20000,35500):
            raw=round(mv*65535*15/(3300*235))
            self.assertTrue(0<raw<65535)
            self.assertLessEqual(abs(adc_millivolts(raw)-mv),1)

if __name__=='__main__':unittest.main()