shibosoftwaredev/f1c100s-linux-nema8-stepper-controller

Restores component part numbers and CAD models, removes test-point 3D bodies, and validates that solder paste exists only on top-side SMT pads without altering copper or DRC.

Version
0.3.5
License
unset
Stars
0

work/publish/firmware/motor-control.py

#!/usr/bin/env python3
"""Conservative DRV8825 step/dir control through Linux libgpiod 2.x.

This is an integration and bring-up utility, not a real-time motion planner.
Linux userspace scheduling adds pulse jitter; use a timer/PWM-capable kernel
driver or a dedicated motion coprocessor when precise or high-rate motion is
required. The board is fixed at 1/16 microstepping and about 0.5 A peak/phase.
"""

import argparse
import pathlib
import time


STEP = "PE2"
DIR = "PE3"
ENABLE = "PE4"       # high releases both nSLEEP and nRESET
FAULT = "PE5"        # active low
TEMP_ALERT = "PE6"   # active low


def find_tmp102_input():
    for name_path in pathlib.Path("/sys/class/hwmon").glob("hwmon*/name"):
        try:
            if name_path.read_text().strip() == "tmp102":
                candidate = name_path.parent / "temp1_input"
                if candidate.exists():
                    return candidate
        except OSError:
            pass
    return None


def read_temperature_c(path):
    if path is None:
        return None
    return int(path.read_text().strip()) / 1000.0


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--chip", default="/dev/gpiochip0", help="PIO GPIO chip from gpioinfo")
    parser.add_argument("--steps", type=int, default=3200, help="step pulses; 3200 is one revolution for a 200-step motor")
    parser.add_argument("--hz", type=float, default=200.0, help="step frequency (userspace; keep conservative)")
    parser.add_argument("--direction", choices=("forward", "reverse"), default="forward")
    parser.add_argument("--hold-seconds", type=float, default=0.0, help="hold torque before disabling")
    parser.add_argument("--max-temperature", type=float, default=80.0, help="abort at this TMP102 temperature in degC")
    parser.add_argument("--temp-path", type=pathlib.Path, help="override TMP102 hwmon temp1_input path")
    parser.add_argument("--dry-run", action="store_true", help="validate arguments and print the intended motion")
    args = parser.parse_args()

    if args.steps < 0:
        parser.error("--steps must be nonnegative")
    if not 0.5 <= args.hz <= 2000:
        parser.error("--hz must be between 0.5 and 2000 for this userspace utility")
    if args.hold_seconds < 0:
        parser.error("--hold-seconds must be nonnegative")
    temp_path = args.temp_path or find_tmp102_input()
    if args.dry_run:
        print(f"{args.steps} pulses at {args.hz:g} Hz, direction={args.direction}, "
              f"estimated duration={args.steps / args.hz:.3f} s")
        print(f"TMP102 path: {temp_path or 'not found (GPIO ALERT remains active)'}")
        return

    import gpiod
    from gpiod.line import Direction, Value

    low, high = Value.INACTIVE, Value.ACTIVE
    output = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=low)
    input_line = gpiod.LineSettings(direction=Direction.INPUT)
    config = {(STEP, DIR, ENABLE): output, (FAULT, TEMP_ALERT): input_line}
    half_period = 0.5 / args.hz

    with gpiod.request_lines(args.chip, consumer="f1c100s-drv8825", config=config) as request:
        def shutdown():
            request.set_value(STEP, low)
            request.set_value(ENABLE, low)

        def safety_check():
            if request.get_value(FAULT) == low:
                raise RuntimeError("DRV8825 nFAULT asserted; motor disabled")
            temperature = read_temperature_c(temp_path)
            if request.get_value(TEMP_ALERT) == low:
                raise RuntimeError("TMP102 ALERT asserted; motor disabled")
            if temperature is not None and temperature >= args.max_temperature:
                raise RuntimeError(f"TMP102 temperature {temperature:.1f} degC exceeds limit")

        try:
            shutdown()
            request.set_value(DIR, high if args.direction == "reverse" else low)
            time.sleep(0.001)
            safety_check()
            request.set_value(ENABLE, high)
            time.sleep(0.002)
            for index in range(args.steps):
                if index % 32 == 0:
                    safety_check()
                request.set_value(STEP, high)
                time.sleep(half_period)
                request.set_value(STEP, low)
                time.sleep(half_period)
            if args.hold_seconds:
                deadline = time.monotonic() + args.hold_seconds
                while time.monotonic() < deadline:
                    safety_check()
                    time.sleep(min(0.1, max(0.0, deadline - time.monotonic())))
        finally:
            shutdown()


if __name__ == "__main__":
    main()