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()