Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stepper library #6444

Open
ktritz opened this issue May 29, 2022 · 13 comments
Open

Stepper library #6444

ktritz opened this issue May 29, 2022 · 13 comments

Comments

@ktritz
Copy link

ktritz commented May 29, 2022

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:

  • C-level generation of precise pulse train (total steps, frequency)
  • frequencies up to ~2-3 kHz * multistep value (e.g. 32)
  • built in smooth frequency acceleration/deceleration of pulse train
  • capable of asynchronous pulse generation
  • monitor "limit switch" input pins to stop motion
  • STEP/target position mode or RUN/continuous motion mode

Pins:

  • step_pin (digital): each pulse advances the stepper one step
  • dir_pin (digital, optional): pin state controls stepper direction
  • enable_pin (digital, optional): pin state enables stepper driver
  • forward_limit (digital, optional): activating pin stops forward motion
  • reverse_limit (digital, optional): activating pin stops reverse motion

Note: ideally select active HIGH/LOW for each pin

Properties/Functions:

acceleration (property) get/set

  • how fast velocity goes from 0 to velocity_limit
  • how fast velocity goes from velocity_limit to 0
  • MIN < value < MAX
  • scaled by scale_factor

mode (property) get/set

  • STEP: target position mode
  • RUN: async rotatation mode

engaged (property) get/set

  • enable stepper driver operation
  • holding current when stopped

failsafe (property) get/set

  • watchdog timer
  • stops operation if reset_failsafe is not called within time

is_moving (function)

  • returns whether the stepper is currently moving
  • either in RUN mode or async STEP mode

position (property) get

  • get the current position (scaled steps)

scale_factor (property) get/set

  • scale factor to translate steps into physical position
  • also affects velocity and acceleration

multistep (property) get/set

  • steps/velocity/acceleration multiplier
  • can modify driver multistep setting without changing Stepper motion values

reset_failsafe (function)

  • if failsafe is active, reset the timeout

target_position (property) get/set

  • gets or sets the (scaled) step location
  • blocks until motion is finished
  • stops motion if direction limit switch becomes active
  • doesn't move if direction limit switch is active

target_position_async (property) set

  • sets the (scaled) step location
  • immediately returns (non-blocking)
  • stops motion if direction limit switch becomes active
  • doesn't move if direction limit switch is active

velocity (property) get

  • get the current stepper (scaled) velocity

velocity_limit (property) get/set

  • get or set the (scaled) maximum velocity
  • 0 < value < MAX
  • immediately changes the velocity during RUN mode
  • stops motion if direction limit switch becomes active
  • doesn't move if direction limit switch is active

set_limits (function)

  • set digital input pins for motion limits
  • limit for forward and/or reverse directions
  • set input value which triggers limit (False | True)
set_limits(forward_limit=None,
		forward_active=True,
		reverse_limit=None,
		reverse_active=True)

get_limits (function)

  • get status of limit switches
  • e.g. returns forward_limit.value == forward_active
@dhalbert
Copy link
Collaborator

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
Copy link
Author

ktritz commented May 29, 2022

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
Copy link
Collaborator

dhalbert commented May 29, 2022

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
Copy link
Collaborator

dhalbert commented May 29, 2022

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
Copy link
Author

ktritz commented May 29, 2022

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.

@dhalbert dhalbert added this to the Long term milestone May 30, 2022
@alustig3
Copy link

+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.

@ladyada ladyada added the driver label Jun 5, 2022
@ladyada
Copy link
Member

ladyada commented Jun 5, 2022

a user contribution library would be welcome!

@xgpt
Copy link

xgpt commented Jun 13, 2022

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
Copy link

Vexs commented Jun 13, 2022

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
Copy link
Author

ktritz commented Jun 13, 2022 via email

@ktritz
Copy link
Author

ktritz commented Jun 22, 2022

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
Copy link

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants