An Electronic Leadscrew Controller using a Pi Pico

I have a counter on the spindle that I use for winding line on reels or wire on coils so it is easy to do say 100 rotations, and the DRO will read out the carriage motion. So I can make a complete set of measurements in all the gearbox selections to verify the gearing ratios in the machine. I need to swap in the correct gear which finally arrived before starting that project. I have a 91 tooth gear marked 90 that came installed from the factory so I halted the measurement project and they were out of that gear for a couple months. It certainly doesn't matter for feeding, so I haven't bothered. I rarely cut threads on the lathe, but I want it to be accurate of course (I could just compensate with the ELS but would rather get the proper gear installed). There are 15 gearbox settings, and the two different ratios for feeding and threading, plus one more rate for the cross slide. So about 45 different ratios built into the machine, not including change gears. I would want all 45 of those ratios in the UI so I could use any of them. The UI should also calculate and advise the RPM limits and other useful info (perhaps a torque multiplication factor) to help guide the operator in selecting the optimal gearbox ratio for an operation, while setting the ELS ratio appropriately to compensate for the gearbox. For example that would allow any of the 15 gearbox settings to be used for a particular threading operation, however the RPM limits would be different, and the torque multiplication would be different. Some choices would be RPM limited and some would be torque limited, and thus less than ideal gearbox choices. Threading normally is done at low RPM so the gearbox selection can be optimized for torque, which also has the effect of minimizing the effective motor step motion. However when cutting a really coarse thread some gearbox settings may not be feasible at all since the overall ratio needs to reach the thread pitch with fewer motor steps than encoder pulses, at least for the algorithm that I'm planning to start with. If the UI allows the operator to select the thread they want to cut it can then show which gearbox settings are valid for that operation, and the operator can select which they want to use and let the UI know what they have selected so it can set up the appropriate ratio in the encoder to stepper electronic drive subsystem.
Cool!

Yeah, UI design seems fun and there seems to be limitless potential to hone in on making it super handy.

I wanted to share a few other random tidbits, as I hang things up in wait for my stepper to come in on Friday:

1. @AlanB , maybe of interest to you, I found that someone re-implemented the jump table from the gitjer c pio encoder example:


Python:
# Quadrature encoder for RPi 2040 Pio
# Has to be at address 0 of PIO programm space
#
# Original version (c) 2021 pmarques-dev @ github
# (https://github.com/raspberrypi/pico-examples/blob/master/pio/quadrature_encoder/quadrature_encoder.pio)
# Adapted and modified for micropython 2022 by rkompass
#
# SPDX-License-Identifier: BSD-3-Clause
#
# This program was reduced to take 'only' 24 of 32 available PIO instructions.
#
# Quadrature encoding uses a state table in form of a jump table
#   which is fast and has no interrupts.
# The counter x is permanently pushed nonblockingly to the FIFO.
# To read the actual value empty the FIFO then wait for and get the next pushed value.

# The worst case sampling loop takes 14 cycles, so this program is able to read step
#   rates up to sysclk / 14  (e.g., sysclk 125MHz, max step rate = 8.9 Msteps/sec).

from machine import Pin
from rp2 import PIO, StateMachine, asm_pio
from time import sleep_ms

class PIO_QENC:
    def __init__(self, sm_id, pins, freq=10_000_000):
        if not isinstance(pins, (tuple, list)) or len(pins) != 2:
            raise ValueError('2 successive pins required')
        pinA = int(str(pins[0]).split(')')[0].split('(')[1].split(',')[0])
        pinB = int(str(pins[1]).split(')')[0].split('(')[1].split(',')[0])
        if abs(pinA-pinB) != 1:
            raise ValueError('2 successive pins required')
        in_base = pins[0] if pinA < pinB else pins[1]
        self.sm_qenc = StateMachine(sm_id, self.sm_qenc, freq=freq, in_base=in_base, out_base=in_base)
        self.sm_qenc.exec("set(x, 1)")  # we once decrement at the start
        self.sm_qenc.exec("in_(pins, 2)")
        self.sm_qenc.active(1)
   
    @staticmethod
    @rp2.asm_pio(in_shiftdir=PIO.SHIFT_LEFT, out_shiftdir=PIO.SHIFT_RIGHT)
    def sm_qenc():
        jmp("read")        # 0000 : from 00 to 00 = no change
        jmp("decr")        # 0001 : from 00 to 01 = backward
        jmp("incr")        # 0010 : from 00 to 10 = orward
        jmp("read")        # 0011 : from 00 to 11 = error
        jmp("incr")        # 0100 : from 01 to 00 = forward
        jmp("read")        # 0101 : from 01 to 01 = no change
        jmp("read")        # 0110 : from 01 to 10 = error
        jmp("decr")        # 0111 : from 01 to 11 = backward
        jmp("decr")        # 1000 : from 10 to 00 = backward
        jmp("read")        # 1001 : from 10 to 01 = error
        jmp("read")        # 1010 : from 10 to 10 = no change
        jmp("incr")        # 1011 : from 10 to 11 = forward
        jmp("read")        # 1100 : from 11 to 00 = error
        jmp("incr")        # 1101 : from 11 to 01 = forward

        label("decr")
        jmp(x_dec, "read") # 1110 : from 11 to 10 = backward

        label("read")      # 1111 : from 11 to 11 = no change
        mov(osr, isr)      # save last pin input in OSR
        mov(isr, x)
        push(noblock)
        out(isr, 2)        # 2 right bits of OSR into ISR, all other 0
        in_(pins, 2)       # combined with current reading of input pins
        mov(pc, isr)       # jump into jump-table at addr 0

        label("incr")      # increment x by inverting, decrementing and inverting
        mov(x, invert(x))
        jmp(x_dec, "here")
        label("here")
        mov(x, invert(x))
        jmp("read")
       
        nop()
        nop()
        nop()
        nop()
        nop()
        nop()
        nop()

    def read(self):
        for _ in range(self.sm_qenc.rx_fifo()):
            self.sm_qenc.get()
        n = self.sm_qenc.get()
        return n if n < (1<<31) else n - (1<<32)

pinA = Pin(15, Pin.IN, Pin.PULL_UP)
pinB = Pin(16, Pin.IN, Pin.PULL_UP)

qenc = PIO_QENC(0, (pinA, pinB))
print('starting....')
for i in range(120):
    print('x:', qenc.read())
    sleep_ms(500)
qenc.sm_qenc.active(0)
print('stop')

Apparently (and I've tested this and it seems true), python loads the assembly program last to first, so spacing (NOP)s are required to ensure the origin is 0. I was able to test/adapt the first example given and it works as expected with my rotary encoder (except for seeming to generate 4k pulses at incr per rotation, instead of 1k? I haven't compared to the original jump table, I imagined INCR would only get called every 4 physical pulses which equates to one actual pulse. There is a shortened example as well, at the cost of only decrementing twice per rotation.

2. I somehow didn't realize that EACH pio block has 32 word instruction memory, and each of those two 32 word instruction memories are shared between the respective state machines (0-3, 4-7). This is handy: although IRQs are not shared, I should easily be able to raise a pin high to signal to the other PIO block that an encoder step needs to be interpreted and action is ready to be taken. This is cool at least because I can move the entire fractional step handler to another PIO block that's less cramped.

3. I'm looking into DMA as I only have the foggiest understanding of how it's handled or works, but I also was curious about implementing "read gearing ratio from memory" functionality so that it's easy to load new gearing. As shown, I'm currently running the gear ratio with constants and just changing things manually. I know the FIFO/(and ISR?) bits that are removed are when they are pulled and I don't imagine python can fill the FIFOs fast enough to never run out, if the gearing is determined from the ISR.

4. I explored a bit, out of curiosity, what compiling native C code to import as a MPY library in python looks like. I came upon an old example which didn't work, but an example now seems integrated into this doc, so maybe it's easier now: https://docs.micropython.org/en/latest/develop/natmod.html#minimal-example . A caveat is that it only pulls what libraries you explicity include or create, so no division. But that's not difficult to implement. But just more tidbits along the "have C do its thing while python is responsible for config" route.

5. I had another really random and potentially less important thought, in line with the fact that I can run the stepper ratio code on the second PIO block and am not short on memory in that block. It seems that I could pretty easily, in line with my simple numerator/denominator pulse reconfiguration experiment, flip/flop between two gearing ratios to simulate an average intermediate numerator/denominator gearing between n1/m1 and n2/m2. I know maybe it seems silly, but it seems potentially appealing to explore since I could do math with python to figure out initialization values that would potentially get non-integer n/m gearing, on average, by alternating between two gearing states. Initializing with different values isn't too bad, and hey, maybe figuring out what that opens up would be easy to calculate.

Again, my original interest was just hopefully a minimal goal of exploring and potentially getting power feed, so I'm just playing around and hopefully seeing what might be fun to explore. I'll try to give an update if I ran into issues with the most basic setup, once things actually start attaching to the actual lathe.
 
What encoder are you using? I suggest a large number of counts rather than a small number. But there's a trade off for each choice. High counts increase processing load, but allow reasonable integer values for N and D. Lower resolution encoders reduce interrupts, but increase processing because of extra processing with fractional values or floats. Pick your poison ;)
 
Plus it is normal for an encoder and software to produce four events per "slot" in the encoder. So a 1024 encoder will produce 4096 pulses per revolution. x1, x2 or x4 events can be resolved depending on the quadrature detection hardware or software. As I recall the encoders I have most often seen used were producing 1024 type producing 4096 events per rotation in x4 mode. That's what I have for my ELS.

On some projects at work we used ADCs and resolved the analog waveforms from the optical encoders as sine and cosine waves to further resolve the encoder by 8 bits (x256) or so beyond the usual transitions to resolve motion down to the nanometer level. :)
 
Last edited:
I'm looking at the Clough42 ELS for my PM 1236 lathe.

Question about operation: Do the up/down buttons for feed rate require repeated pressing, or can you keep them pressed to operate continuously?

Either way, I'm not a fan of fiddling with the tiny buttons; does anyone think it would be desirable to have potentiometer control instead?

If so, thoughts on how feasible such a modification might be?

I'm thinking that for turning it would be nicer for adjusting feed rate to get the chips to break.
 
I'm looking at the Clough42 ELS for my PM 1236 lathe.

Question about operation: Do the up/down buttons for feed rate require repeated pressing, or can you keep them pressed to operate continuously?

Either way, I'm not a fan of fiddling with the tiny buttons; does anyone think it would be desirable to have potentiometer control instead?

If so, thoughts on how feasible such a modification might be?

I'm thinking that for turning it would be nicer for adjusting feed rate to get the chips to break.
You are asking in the wrong thread... Try a Clough42 ELS thread and you may get more answers. This thread was on a Pi Pico ELS.
To the best of my knowledge, you need to click the buttons as they are digital inputs. If you want to add analog inputs then I suggest you research the Clough42 TI board architecture to see if it is possible. If there's an available ADC, maybe - if you want to write your own code. There's more ways to do this. But they involve getting your hands dirty in hardware or software. I don't see why you can't twiddle the feed rate with the Clough42 set up.
 
Last edited:
Interesting experiment. Programming PIOs is a bit strange, I'm still learning about that. I suppose the question here is if the necessary ratios will be too limiting to the usable encoder rates. But an interesting simple approach!

Awhile back I made some precise measurements of my lathe to map out some of the gearing, and this led to finding that one wrong gear was installed at the factory. It was off by one tooth out of 90, and that's only one in a cascade of gears needed to do the job. Thread cutting requires surprising precision with large ratios, especially imperial/metric work where an imperial lathe is used for metric threading or vice versa.
I'm just starting down this path, and will be going with the pi pico mainly for the PIO. I'm familiar with it from previous projects, and have driven the PIO via DMA where the heavy lifting was done by the processor in loading up the memory.

My initial thinking is to design the hardware so that I never have the case where I need to generate more than one step between encoder pulses. I can do this by limiting the thread to a 5TPI as the coarsest thread. Once I know that I can set the encoder and stepper gearing to guarantee that condition.

The PIO will contain two blocks, one will find the encoder edge(s), and direction. The second (Stepper) block will accept DMA data containing a bitmap where simply put, each bit will indicate whether or not a stepper pulse is needed. That's an over simplified description. Here's an example...
Say the feed rate requires a step every third encoder pulse. The register (DMA'd in) will be setup like this...
001001001001...BitsInReg
When the edge detection block releases the Stepper block, the Stepper block will check the lsb. If it's 1 it will generate a step. If it's 0 it won't. It will then shift the lsb out and if the register is empty it will enable another DMA read.
Notice that All the time critical operations are handled in the PIO.

The processor will do the heavy lifting of filling the memory to be DMA'd with the appropriate set of data once the feed has been changed. The DMA will loop on itself when it's determined that the last step occurred with a minimum error relative to the encoder pulse.

There's a lot of the detail involved with filling the DMA data I haven't covered, but I can elaborate if anyone is interested. It will take a few drawings and some simulation data to show what I'm doing.

My hardware is arriving in a few days and I'll be prototyping after that.
 
Good luck. Looking forward to hearing some results.

I've done 4 TPI with my approach (Teensy4.1) but honestly, it's not something I want to do on a regular basis. I'd need to re-gear/re-ratio things to get more torque. At least for my implementation, the ratio of stepper pulses to encoder pulses was 75/128 for 4 TPI. 100 TPI was 3/128. (On my lathe with a 10TPI lead screw.) I was using a 4096 count per revolution encoder. (1024 pt quadrature encoder). My system was been running well for over a year.
 
Good luck. Looking forward to hearing some results.

I've done 4 TPI with my approach (Teensy4.1) but honestly, it's not something I want to do on a regular basis. I'd need to re-gear/re-ratio things to get more torque. At least for my implementation, the ratio of stepper pulses to encoder pulses was 75/128 for 4 TPI. 100 TPI was 3/128. (On my lathe with a 10TPI lead screw.) I was using a 4096 count per revolution encoder. (1024 pt quadrature encoder). My system was been running well for over a year.
One thing I need to verify is whether or not I need to interpolate my steps. My feedscrew advances .6 thousands per step, and I plan on rounding to the closest encoder pulse when I build my bit map, so my max error will be .3 thousandths. I'm thinking there's no need to interpolate between encoder pulses. Did you interpolate in your design?
 
One thing I need to verify is whether or not I need to interpolate my steps. My feedscrew advances .6 thousands per step, and I plan on rounding to the closest encoder pulse when I build my bit map, so my max error will be .3 thousandths. I'm thinking there's no need to interpolate between encoder pulses. Did you interpolate in your design?
Did no interpolation at all. I did calculate out the instantaneous pitch error at one time and concluded it was small enough that it wouldn't matter. It did concern me a lot initially. In my system, there is 0 average error for all implemented threads and feeds, only a cyclic instantaneous one, that looks something like a small sawtooth. However, that was for my system, you should determine what will work for yours. I did all the math before buying any parts. I even looked at which encoder counts would be optimal for my system. Fortunately, I could get away with a standard 1024 quad encoder.

Effectively, only integer math is used in Bresenham, for deciding to advance the stepper, so there's low computational burden. From a practical perspective, the threads are well cut and exactly the correct pitch. If one is to go through the effort of making one's own ELS, it seems to me that the threads should have no accumulating error. Pitch errors are for gear systems, that get close to the right ratio, but can't quite make it, because they need a fraction of a tooth more. An ELS can change the effective gear ratio to whatever works. We can make any ratio we want with simple integers for both the numerator and denominator. In fact, we don't even need to do computational division using Bresenham. It's quite powerful, yet deceptively simple. The downside is the cyclic instantaneous error, which can be minimized to some extent. The cyclic error is similar to quantization error.
 
Last edited:
Back
Top