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_pd_profile.py

import unittest
from pd_profile import PDOS, nvm_9v_image, nvm_is_9v, configure_runtime, verify_runtime
from pd_nvm import NVM

# STUSB4500 factory NVM example published by SparkFun. Reserved/calibration
# fields differ between revisions: the programming code preserves their bytes.
FACTORY = bytes.fromhex(
    '0000b0aa00450000 10409c1cff013cdf 02400f003200fcf1 '
    '001956aff5355f00 004b9021430040fb')

class Clock:
    now = 0
    def ticks_ms(self): return self.now
    def ticks_diff(self, a, b): return a-b
    def sleep_ms(self, delay): self.now += delay

class Device:
    """Bus-level model: sector erase/load/program plus NVM reload on reset."""
    def __init__(self):
        self.image = bytearray(FACTORY)
        self.regs = bytearray(256)
        self.programmed = []
        self.erase_mask = 0
        self.locked = True
        self.stuck = False
        self.corrupt = False
        self.reload()

    def reload(self):
        count = (self.image[26] >> 1) & 3
        volts = (100, (self.image[33] << 2) | (self.image[32] >> 6),
                 ((self.image[35] & 3) << 8) | self.image[34])
        codes = (self.image[26] >> 4, self.image[28] & 15, self.image[29] >> 4)
        self.regs[0x70] = count
        for i, (voltage, code) in enumerate(zip(volts, codes)):
            amps = 25*(code+1) if code <= 11 else 50*code-250
            pdo = (voltage << 10) | amps
            if i == 0 and self.image[26] & 1: pdo |= 1 << 26
            self.regs[0x85+4*i:0x89+4*i] = pdo.to_bytes(4,'little')

    def readfrom_mem(self, address, reg, size):
        return bytes(self.regs[reg:reg+size])

    def writeto_mem(self, address, reg, data):
        self.regs[reg:reg+len(data)] = data
        if reg == 0x95: self.locked = data[0] != 0x47
        if reg == 0x96 and data[0] & 0x10:
            if self.locked: raise OSError('NVM locked')
            op = self.regs[0x97] & 7
            sector = data[0] & 7
            if op == 0:
                self.regs[0x53:0x5B] = self.image[8*sector:8*sector+8]
            elif op == 2: self.erase_mask = self.regs[0x97] >> 3
            elif op == 5:
                for s in range(5):
                    if self.erase_mask & (1 << s): self.image[8*s:8*s+8] = b'\xff'*8
            elif op == 1: self.loaded = bytes(self.regs[0x53:0x5B])
            elif op == 6:
                self.image[8*sector:8*sector+8] = self.loaded
                self.programmed.append(sector)
                if self.corrupt: self.image[8*sector] ^= 1
            if not self.stuck: self.regs[0x96] &= ~0x10

class PDProfileTests(unittest.TestCase):
    def test_factory_to_9v_preserves_unrelated_fields(self):
        desired = nvm_9v_image(FACTORY)
        self.assertFalse(nvm_is_9v(FACTORY))
        self.assertTrue(nvm_is_9v(desired))
        self.assertEqual(desired[:24], FACTORY[:24])
        self.assertEqual(desired[26], 0x15)
        self.assertEqual(desired[36] & 0x60, 0x40)
        self.assertEqual(desired[38] & 0x18, 0x10)
        self.assertEqual(desired[28:30], b'\xf7\x75')
        self.assertEqual(desired[32:36], b'\x00\x2d\xb4\x20')
        self.assertEqual(nvm_9v_image(desired), desired)
        for byte, mask in {26:0xff,28:0x0f,29:0xf0,32:0xc0,33:0xff,34:0xff,35:0x03,36:0x60,38:0x18}.items():
            self.assertEqual((desired[byte] ^ FACTORY[byte]) & ~mask, 0)
        changed = {i for i in range(40) if desired[i] != FACTORY[i]}
        self.assertTrue(changed <= {26,28,29,32,33,34,35,36,38})
        with self.assertRaises(ValueError): nvm_9v_image(b'\x00'*39)

    def test_partial_program_and_cold_start_without_runtime_writes(self):
        bus = Device()
        nvm = NVM(bus, clock=Clock())
        self.assertTrue(nvm.provision_9v())
        self.assertEqual(bus.programmed, [3,4])
        self.assertEqual(bus.erase_mask, 0x18)
        self.assertTrue(bus.locked)
        self.assertEqual(bus.image[:24], FACTORY[:24])
        bus.reload()  # only NVM load: no runtime profile setup to hide bad NVM
        verify_runtime(bus)
        self.assertFalse(nvm.provision_9v())
        self.assertEqual(bus.programmed, [3,4])

    def test_factory_runtime_profiles_and_corruption_rejected(self):
        bus = Device()
        with self.assertRaises(OSError): verify_runtime(bus)
        configure_runtime(bus)
        verify_runtime(bus)
        for pdo in PDOS: self.assertIn(((pdo >> 10) & 1023)*50, (5000,9000))
        for register, voltage in ((0x89, 300), (0x8D, 400)):
            # Reject 15 V active PDO2 and 20 V even in inactive PDO3.
            bus.regs[register:register+4] = ((voltage << 10)|200).to_bytes(4,'little')
            with self.assertRaises(OSError): verify_runtime(bus)
            configure_runtime(bus)
        for count in (0, 1, 3, 4, 6, 7):
            bus.regs[0x70] = count
            with self.assertRaises(OSError): verify_runtime(bus)
        configure_runtime(bus)
        self.assertEqual(bus.regs[0x70] & 7, 2)

    def test_timeout_and_corrupt_write_release_test_mode(self):
        bus = Device(); bus.stuck = True
        clock = Clock()
        with self.assertRaises(OSError): NVM(bus,clock=clock).read()
        self.assertEqual(clock.now,1000)
        self.assertTrue(bus.locked)
        bus = Device(); bus.corrupt = True
        with self.assertRaises(OSError): NVM(bus,clock=Clock()).provision_9v()
        self.assertTrue(bus.locked)

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