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/tmc2209.py

"""TMC2209 UART0 adapter, address 0, 0.15 ohm external sense resistors.
Reference code, not tested on hardware. Caller must hold GPIO22 low on setup.
"""
from machine import UART, Pin
from time import sleep_ms, ticks_ms, ticks_diff


def crc8(data):
    crc = 0
    for value in data:
        for _ in range(8):
            crc = ((crc << 1) ^ (7 if (crc >> 7) ^ (value & 1) else 0)) & 255
            value >>= 1
    return crc


class TMC2209:
    def __init__(self, uart=None):
        self.uart = uart or UART(0, baudrate=115200, tx=Pin(20), rx=Pin(21), timeout=10)
        self.configured = False

    def read(self, register):
        self.uart.read()  # flush old local echo/data
        packet = bytes((5, 0, register & 127))
        self.uart.write(packet + bytes((crc8(packet),)))
        started = ticks_ms()
        buf = b''
        while ticks_diff(ticks_ms(), started) < 40:
            buf += self.uart.read() or b''
            # Skip the local transmit echo; device replies have master address FF.
            for i in range(max(0, len(buf) - 7)):
                frame = buf[i:i+8]
                if frame[:3] == bytes((5, 255, register & 127)) and crc8(frame[:7]) == frame[7]:
                    return int.from_bytes(frame[3:7], 'big')
            sleep_ms(1)
        raise OSError('TMC2209 UART timeout or CRC failure')

    def write(self, register, value):
        before = self.read(2) & 255  # IFCNT confirms CRC-valid writes
        packet = bytes((5, 0, register | 128)) + value.to_bytes(4, 'big')
        self.uart.write(packet + bytes((crc8(packet),)))
        sleep_ms(2)
        if self.read(2) & 255 != (before + 1) & 255:
            raise OSError('TMC2209 rejected register write')

    def configure(self):
        self.configured = False
        # Version 0x21 identifies TMC2209. Read also confirms motor-side power.
        if (self.read(6) >> 24) != 0x21:
            raise OSError('Unexpected TMC2209 IOIN version')
        # Digital current scale, external Rsense, UART enabled, register microsteps.
        self.write(0, (1 << 6) | (1 << 7) | (1 << 8))
        self.write(0x10, (6 << 16) | (14 << 8) | 5)  # run ~0.63 A RMS, hold ~0.25 A
        self.write(0x11, 20)
        # 16 microsteps, interpolation, vsense=0, default blanking/hysteresis, TOFF=3.
        self.write(0x6C, 0x14010053)
        self.write(0x22, 0)  # VACTUAL=0: external STEP only
        self.write(1, 7)     # acknowledge startup GSTAT reset/driver/charge-pump flags
        self.configured = True
        self.check()

    def check(self):
        if not self.configured:
            raise OSError('TMC2209 not configured')
        if self.read(0) != ((1 << 6) | (1 << 7) | (1 << 8)) or self.read(1) & 7:
            self.configured = False
            raise OSError('TMC2209 reset or power/driver fault; re-arm to reconfigure')
        # OTPW, OT, short-to-ground and low-side-short flags. Open-load status is
        # unreliable at standstill and deliberately not treated as a hard fault.
        if self.read(0x6F) & 0x3F:
            raise OSError('TMC2209 temperature or short-circuit warning')