astra/nema17-stepper-motor-controller-spring-clamp-rp2040

A 4-layer RP2040 controller board that negotiates USB‑C PD motor power and drives a NEMA17 stepper via a DRV8825, with current sensing, thermal shutdown, RGB status LED, buzzer alarm, optional AS5600 encoder, protection/filtering, and test points.

Version
1.4.0
License
unset
Stars
0

firmware/main.py

"""Reference USB temperature/current monitor. Copy all firmware/*.py onto MicroPython.

Commands: arm, stop, status. No encoder is required. This monitor never generates motion commands.
Use guard.step(direction) for individual STEP pulses while armed.
The old four-input winding/PWM firmware is incompatible with this PCB.
The main loop must call guard.poll() and feed the watchdog at least every 100 ms.
"""
from machine import Pin, WDT, PWM, ADC
from time import ticks_ms, ticks_diff, sleep_ms
import sys
import select
from thermal_guard import ThermalGuard
from audible_alarm import AudibleAlarm
from current_monitor import CurrentMonitor
from status_led import StatusLed

Pin(22, Pin.OUT, value=0)
watchdog = WDT(timeout=1000)
guard = None
alarm = AudibleAlarm(PWM(Pin(16)))
current = CurrentMonitor(ADC(Pin(28)), ADC(Pin(29)))
status_led = StatusLed(PWM(Pin(25)), PWM(Pin(2)), PWM(Pin(3)))
try:
    guard = ThermalGuard()
    console = select.poll()
    console.register(sys.stdin, select.POLLIN)
    command = ""
    last_sample = ticks_ms()
    last_report = last_sample
    while True:
        now = ticks_ms()
        if ticks_diff(now, last_sample) >= 100:
            guard.poll()
            current.sample()
            status_led.update(guard.policy, not current.clipped)
            alarm.update(guard.policy.temperature_c, bool(guard.alert.value()), now)
            last_sample = now
        # Bound the input workload; an incomplete line cannot block monitoring.
        for _ in range(16):
            if not console.poll(0):
                break
            char = sys.stdin.read(1)
            if char in ("\n", "\r"):
                if command == "arm":
                    print("ARMED" if guard.arm() else "ARM DENIED")
                elif command == "stop":
                    guard.stop()
                elif command == "status":
                    print(guard.policy.temperature_c, guard.policy.status(), "winding_signed_mean_A=", current.amperes, "current_clipped=", current.clipped)
                command = ""
            elif len(command) < 32:
                command += char
        if ticks_diff(now, last_report) >= 1000:
            print("temperature_C=", guard.policy.temperature_c, guard.policy.status(), "winding_signed_mean_A=", current.amperes, "current_clipped=", current.clipped)
            last_report = now
        # Feed only in this loop, never in an interrupt or unrelated timer.
        watchdog.feed()
        sleep_ms(10)
finally:
    # Disable the motor before cleaning up any optional indicators.
    Pin(22, Pin.OUT, value=0)
    if guard is not None:
        guard.stop("monitor exited")
    alarm.close()
    status_led.close()