adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.01k stars 1.19k forks source link

Stepper library #6444

Open ktritz opened 2 years ago

ktritz commented 2 years ago

Using CircuitPython to control stepper motors is currently pretty ugly. At the moment, I use PWMOut, precalculate the frequency steps needed to provide pseudo acceleration/deceleration, compute the time necessary for the total steps in each PWM window, sit in a timing loop while monitoring a digital input 'limit switch' and end up sorta close to the total number of steps I wanted. I realize that I may get better precision with PulseIO or AudioPWM, but they have their own issues. It would be pretty nifty if there was a C-level bit banging library that implemented a Stepper library with something like the following capability:

Stepper module:

Pins:

Note: ideally select active HIGH/LOW for each pin

Properties/Functions:

acceleration (property) get/set

mode (property) get/set

engaged (property) get/set

failsafe (property) get/set

is_moving (function)

position (property) get

scale_factor (property) get/set

multistep (property) get/set

reset_failsafe (function)

target_position (property) get/set

target_position_async (property) set

velocity (property) get

velocity_limit (property) get/set

set_limits (function)

set_limits(forward_limit=None,
        forward_active=True,
        reverse_limit=None,
        reverse_active=True)

get_limits (function)

dhalbert commented 2 years ago

Thanks for the API writeup. We generally recommend using a PCA9685 or similar to drive steppers, since it takes care of doing this without any timing difficulties. You need motor driver power components anyway, so the PCA9685 is the least of it. What are your reasons for preferring to do it directly?

ktritz commented 2 years ago

As far as I can tell, the PCA9585 CP library doesn't really handle any of the specific things I'm looking for, such as generating a precise # of pulses, smooth frequency acceleration/deceleration, and monitoring of limit IO to stop motion. Is there something I'm missing? If I need to separately modify and time the PWM frequency for acceleration, and separately monitor the limit IO, and then communicate over I2C to the PCA9585, that makes my problem even worse with regard to precision stepping.

Typically, we're using standalone stepper drivers which take digital IO for the step/dir/enable control (but don't have limit IO). We're also using a range of steppers from smaller NEMA17 to massive 220V 7A NEMA52s, so a generic stepper motor hat/shield won't quite cut it.

This might be something the seesaw could do well with the proper code.

dhalbert commented 2 years ago

I understand. Yes, an enhanced seesaw could do something like this. Have you looked at the Tic stepper controllers from Pololu? https://www.pololu.com/category/212/tic-stepper-motor-controllers https://www.pololu.com/docs/0J71

Perhaps a CircuitPython library that talks to them would be useful.

dhalbert commented 2 years ago

Another interesting possibility would be to use the RP2040 PIO capability to write the short control programs you would need. You can create and run PIO programs from CircuitPython.

ktritz commented 2 years ago

Yeah, the Pololu controllers would work, though they are considerably more expensive than the seesaw. They could replace our external stepper drivers for some of our steppers, and we could use the step/dir outputs for the NEMA52 drivers. The Status: Rationed and low stock is a bit concerning :) Not sure of their long term reliability.

I have thought about the RP2040 PIO, and it's a possibility I think, though it's a bit tricky to get the right math for smooth acceleration/deceleration. If JMP on PIN works, that at least could monitor limit switch IO. I've done simple PIO stuff, but I'm not sure if there are enough registers to handle all of the functionality. Come to think of it, JMP on PIN could allow two state machines to talk to each other, no? One could be in charge of counting the steps, and one could be in charge of the time period between steps to handle the acceleration and velocity.

alustig3 commented 2 years ago

+1 vote for wanting a circuitpython stepper library

I don't know if this is helpful but https://www.airspayce.com/mikem/arduino/AccelStepper/ and https://github.com/luni64/TeensyStep are two libraries that I have used in the past that work well for Arduino.

dhalbert commented 2 years ago

Interesting PIO stepper links: https://forums.raspberrypi.com/viewtopic.php?t=300874 https://www.youtube.com/watch?v=UJ4JjeCLuaI https://vanhunteradams.com/Pico/Steppers/Lorenz.html https://github.com/DasenB/PicoStepper http://people.ece.cornell.edu/land/courses/ece4760/RP2040/index_rp2040_C.html https://forum.micropython.org/viewtopic.php?t=10199&p=56656

ladyada commented 2 years ago

a user contribution library would be welcome!

xgpt commented 2 years ago

Just curious, would any of this lead to being able to use the incredibly common "StepStick" drivers that are frequently sold/utilized with 3D Printers?!

Vexs commented 2 years ago

I started on a similar path of discovery last night- pwmio + timing resulted in a significant deviation over time, as expected. PulseIO was... better, but the library's API is kind of awkward for this usage and you still missed/over steps.

Fortunately, I can just use digitalIO to toggle a pin on the rp2040 at 500hz which is sufficiently fast for my applications, but not ideal- particularly as I want to do sensor measurement along the movement of the stepper, and throwing blocking i2c reads into the loop is going to be painful. To that end, an async interface is badly needed- particularly given circuitpython's no-threading. A callback that could be scheduled to happen every say, N rotations would be welcome.

I've started playing with PIO for this personally, in part because it looks like it allows me to escape the single-thread issue.

Also, IRT external stepper controllers like the pololu boards- while these would appear to work, a 50$ premium for a stepper controller when I have a microcontroller that should be quite happily capable of doing the operation itself is more than a little frustrating- and if I wanted to use existing common 3d printer hardware is a bit of a no-op. Observationally, there don't appear to be many stepper-controller-controller boards out there either that break out things like the TMC4361 for an affordable price.

ktritz commented 2 years ago

PIO would probably be the easiest solution, but of course not generalizable to other boards. A C library like pulseio, but specifically written for stepper control would be the most useful and should be able to handle async calls with limit switch I/O as well.

On Mon, Jun 13, 2022 at 12:05 PM Vexs @.***> wrote:

I started on a similar path of discovery last night- pwmio + timing resulted in a significant deviation over time, as expected. PulseIO was... better, but the library's API is kind of awkward for this usage and you still missed/over steps.

Fortunately, I can just use digitalIO to toggle a pin on the rp2040 at 500hz which is sufficiently fast for my applications, but not ideal- particularly as I want to do sensor measurement along the movement of the stepper, and throwing blocking i2c reads into the loop is going to be painful. To that end, an async interface is badly needed- particularly given circuitpython's no-threading. A callback that could be scheduled to happen every say, N rotations would be welcome.

I've started playing with PIO for this personally, in part because it looks like it allows me to escape the single-thread issue.

— Reply to this email directly, view it on GitHub https://github.com/adafruit/circuitpython/issues/6444#issuecomment-1154106638, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABIR6YKZQ6BQ7AEXJC6JIM3VO5L4NANCNFSM5XHOYXSA . You are receiving this because you authored the thread.Message ID: @.***>

ktritz commented 2 years ago

Ok, I have some alpha code that does step accurate stepper control using the RP2040 PIO. Right now the state machine outputs steps to a stepper driver with the option of DIR control using either the state machine or manually with digitalIO. There is an option to enable a counter by tying the output to another pin, which can independently verify step counts. Finally, a jmp_pin can be configured to act as a limit switch which will immediately halt the step output (and require a state machine reset).

The files in the lib directory are what goes on the MCU. You can combine the files in the pc and lib directory on a PC to test and plot the step generation. Right now there are two acceleration curves available, a linear ramp and a cos-based S-curve for lower jerk acceleration. The S-curve has more overhead, so there's a small cache to speed up step generation when parameters aren't changing. Feedback is welcome, and I'm going to keep tweaking it.

https://github.com/ktritz/stepper_pio

crbyxwpzfl commented 11 months ago

hi I had the same issue and thought I leave my rough step dir pio here.

this pio is able to vary step frequency and endposition while stepping. So one does not have to wait for it to reach its target.

the current setup is a 32bit input with a delay in the 21msbs and an endposition in the 11lsbs. Like eg. Delay of 100 and endpos of 100 (1100100 bin) 000000000000001100100 00001100100

suboptimal limits and quirks; for once I tried to keep the step period constant, with label compensate, this requires endpositions to have a 1 lsb, alias only odd endpositions. second the delay and endpos share a 32bit limit. at least this can be partitioned relativ freely by changing instruction 2,4.

furthermore the code is not too nice. Im happy for any Tipps or suggestions.

stepdir = adafruit_pioasm.assemble( """
.program stepdir
.side_set 1 opt

loop:                ; requires 21delaybits 11endbits in this order
    pull block         ; 1 pull waits on fresh tx fifo, also here init populates inital y/curpos
    out x 21           ; 2 afterwards osr/11endbits-21bufferzeros, x/11bufferzeros-21delaybits

pause:               ; pause for bussy cycles of delay count                  
    jmp x-- pause  [7]  ; 3 x/delay non zero stay in pause and decr x/delay otherwise continue

prepfinddir:         ; prep x/endpos for comparison, y/curpos already preped since init or later since finddir
    out x 11           ; 4 afterwards osr/32bufferzeros, x/21bufferzeros-11endposbits

    mov osr x          ; 5 afterwards osr/endpos, also no race condition here since no auto pull
    mov isr y  side 0  ; 6 afterwards isr/curpos so y/curpos stays persistant while finddir, also side/setppin alias enter lowtime for >100ns

    jmp x!=y finddir   ; 7 y/curpos not x/endpos finddir otherwise nofinddir alias re loop
    jmp preploop       ; 8 

finddir:
    mov x osr      ; 1 cycle osr/endpos isr/curpos with x/interim save
    mov osr isr    ; 2 shift msb bit of here osr/curpos out
    mov isr x      ; 3 either write carrypin 1 for curpos 0 alias perhaps posdir
    out x 1        ; 4     or write carrypin 0 for curpos 1 alias perhaps negdir
    mov pins ~x    ; 5

    mov x osr      ; 6 cycle x/osr/isr
    mov osr isr    ; 7 shift msb bit of here osr/endpos out
    mov isr x      ; 8 and verifiy possible dir
    out x 1        ; 9

    jmp pin pssblposdir    ; 10 curpos1 alias verify possible negdir
        jmp x-- finddir    ; 11 but endpos1 re finddir    !!! x0 -> xfffff , x1 -> x0    

        jmp y-- goon  [1]  ; 12 but endpos0 found dir so decrement y/peristant curpos       !!! if scratch Y non-zero, prior to decrement
        goon:
        set pins 0         ; 13 setpin/dirpin to negdir, also dirpin to steppin time > 20ns tmc2209 13.1
        jmp compensate     ; 14 nops for same time as posdir case

    pssblposdir:           ; curpos0 alias verify possible posdir   
        jmp !x finddir     ; 15 but endpos0 re findir

        mov y ~y           ; 16 but endpos1 found dir so increment y/persitant curpos
        jmp y-- proc       ; 17                                                         !!! if scratch Y non-zero, prior to decrement
        proc:
        mov y ~y           ; 18 
        set pins 1         ; 19 setpin/dirpin to posdir, also dirpin to steppin time > 20ns tmc2209 13.1

compensate:             ; per leftover bit in osr/endpos do nothing for 11instr, alos only odd endposses valid
    out x 1          [4]  ; 20 deplete osr/endpos bit after bit
    mov x osr        [4]  ; 21 until osr/endpos zero alias depleted last odd bit then proceede
    jmp x-- compensate    ; 22 else x non zero stay in compensate

    in y 32       side 1  ; 23 this stalls on full rx fifo, also y persists, also enter side/setppin alias enter hightime

preploop:
    push noblock  ; 9 explicit push solves auto push stall aboveus, also clears isr

""" )

carrypin = board.LED
setppin = board.SCK
dirpin = board.D24

#enablepin = board.D4

smout = rp2pio.StateMachine(
    program         = stepdir,
    frequency       = 0,
    out_shift_right = False,  # out shift left for finddir
    in_shift_right  = False,  # in shift left for priming/init y/curpos, i32

    first_in_pin              = carrypin ,  # inpin is carrypin for i5, i10 of finddir
    in_pin_count              = 1,

    jmp_pin                   = carrypin,  # jmppin is carrypin for i10 of finddir

    first_out_pin             = carrypin,  # outpin is carrypin for i5 of finddir
    out_pin_count             = 1,
    initial_out_pin_state     = 0,

    first_set_pin             = dirpin,  # setpin is dirpin for i12, i16 of finddir
    set_pin_count             = 1,  
    initial_set_pin_state     = 0,

    first_sideset_pin         = setppin,  # sidepin is setppin
    sideset_pin_count         = 1,
    initial_sideset_pin_state = 0,
    sideset_enable            = True,
)

smout.run(adafruit_pioasm.assemble("mov x null"))
smout.run(adafruit_pioasm.assemble("mov y null"))
smout.run(adafruit_pioasm.assemble("mov osr null"))
smout.stop_background_write()

time.sleep(1)

initpos = 10  # initialise/populate y/curpos with a value 

for bit in range(initpos.bit_length() - 1, -1, -1):
    print(f"{bit}: {(initpos >> bit) & 1}")
    smout.run(adafruit_pioasm.assemble(f"set y {(initpos >> bit) & 1}"))  # form msb to lsb shift bit per bit of initpos into isr
    smout.run(adafruit_pioasm.assemble("in y 1"))   # requires left in shift

smout.run(adafruit_pioasm.assemble("mov y isr"))  # init y/curpos with isr

to send the stepper to an endposition

intdelay = 524636  # must fit in 21 bits with current setup
intendpos = 11    # must fit in 11 bits with current setup and has to be odd for compensation to work
buf = array.array( 'L', [((intdelay<<11)+intendpos)] )
sm.background_write(loop=buf)