seveibar/nema17-stepper-motor-driver-tmc2209-usb-pd

RP2040-based USB-C PD stepper-motor controller with TMC2209 drive, AS5600 position feedback, TMP102 thermal shutdown, current/voltage telemetry, RGB status LED, and protected buzzer alarm circuitry.

Version
0.2.0
License
unset
Stars
0

firmware/thermal_guard.py

"""MicroPython RP2040 adapter. No automatic start or automatic fault restart."""
from machine import Pin, I2C, disable_irq, enable_irq
from time import sleep_ms, ticks_ms
from tmp102 import TMP102, WARN_C
from thermal_policy import ThermalPolicy
from tmc2209 import TMC2209
from status_led import StatusLED
from power_telemetry import PowerTelemetry


class ThermalGuard:
    def __init__(self):
        self.enable = Pin(22, Pin.OUT, value=0)
        self.motor_inputs = [Pin(n, Pin.OUT, value=0) for n in (18, 19)]
        self.driver = TMC2209()
        self.alert = Pin(24, Pin.IN)
        self.driver_fault = Pin(23, Pin.IN)
        self.led = StatusLED()
        self.power = PowerTelemetry()
        self.policy = ThermalPolicy()
        self.irq_trip = 0
        self.configured = False
        self.sensor = TMP102(I2C(1, sda=Pin(26), scl=Pin(27), freq=100_000))
        self.alert.irq(handler=self._temperature_irq, trigger=Pin.IRQ_FALLING, hard=True)
        try:
            self.sensor.configure()
            # Exceed the 125 ms conversion interval plus conversion time.
            sleep_ms(300)
            self.configured = True
        except (OSError, ValueError):
            self.policy.trip("temperature sensor initialization failed")
        self.poll()

    def _temperature_irq(self, _pin):
        # Hardware NAND gate already hold ENN high; capture it until manual rearm.
        self.enable.value(0)
        self.irq_trip = 1

    def stop(self, reason="user stop"):
        self.enable.value(0)
        self.policy.trip(reason)
        for pin in self.motor_inputs:
            pin.value(0)

    def poll(self):
        try:
            if not self.configured:
                raise OSError("Sensor not configured; reboot to retry")
            temp = self.sensor.temperature()
            if not self.power.sample():
                self.policy.trip("PD voltage or motor-supply current outside limits")
            if self.policy.enabled:
                self.driver.check()
            self.policy.sample(temp, bool(self.alert.value()),
                               driver_fault=self.policy.enabled and bool(self.driver_fault.value()))
        except (OSError, ValueError):
            self.policy.sample(None, sensor_ok=False)
        if self.irq_trip:
            self.policy.trip("hardware overtemperature alert")
        if not self.policy.enabled:
            self.enable.value(0)
            for pin in self.motor_inputs:
                pin.value(0)
        self.led.update(self.policy.latched,
                        self.policy.temperature_c is not None and self.policy.temperature_c >= WARN_C,
                        ticks_ms())
        return self.policy.status()

    def arm(self):
        # Always take a fresh verified reading. A stale last sample cannot arm.
        self.poll()
        self.enable.value(0)
        try:
            self.driver.configure()
        except (OSError, ValueError):
            self.stop("TMC2209 initialization failed")
            return False
        self.poll()  # Refresh temperature after UART setup while still disabled.
        # Avoid an I2C transaction with interrupts disabled. The critical section
        # protects the final check and enable write against a concurrent alert.
        irq_state = disable_irq()
        try:
            if not self.alert.value() or not self.power.ok or not self.policy.arm():
                return False
            self.irq_trip = 0
            self.enable.value(1)
        finally:
            enable_irq(irq_state)
        sleep_ms(3)  # Settling interval before checking DIAG and driver registers.
        self.poll()
        return self.policy.enabled