An Electronic Leadscrew Controller using a Pi Pico

I've looked at a lot of ELS examples, and the simple approach seems to be quite adequate. Folks start to theorize about rates and various feedback loops and complicated numerical algorithms but I think that's the wrong approach. If you set up your encoder steps small enough, and your stepper steps small enough you can do the simple integer algorithm and be well within the precision the machine is physically capable of and avoid problems with floating point precision and stability of feedback loops. Essentially we are implementing a digital gear ratio with an adjustable ratio.

In terms of putting it all into the PIO, that would be optimal, but it may not be practical and it certainly isn't necessary. A good fast encoder algorithm in the PIO probably isn't even required. It's been awhile since I did the math but you can dedicate one Pico core to reading the encoder and doing the gearing algorithm. It's read 2 bits, or in the previous position 2 bits, index a 16 lookup table, the result is +1, -1 or zero which gets added to the position. A couple of compares to check for over/underflow, add/subtract if necessary, and put out a pulse or not. I haven't timed this on a Pico in C but it should be able to do this at a rate of many megahertz in a dedicated loop, no interrupts or fancy non-determinstic distractions on that core. The steppers can't keep up with the pulses at the full rate this could operate, so the limiting factor is not the Pico. The other core can handle everything else. The only tricky thing is changing the ratio. Generally this is not done while moving and that makes it simple. If you want to allow changing the gearing ratio while the system is in motion some care would be required. For threads that is probably never needed. For feed rates it might be nice to adjust the feed rate without stopping the spindle, but it can be constrained in ways that make it easier to adjust while running, or some careful code can change the ratio at the moment a motor pulse is being generated, and do some smoothing to account for acceleration/deceleration. But to start with I'd just require the spindle to be stopped, which is pretty much how the mechanical gearboxes work anyway.

Using MicroPython would be easier but will hit limits sooner. If a PIO encoder state machine is used it may still be more than adequate. One question is what max spindle RPM do you end up keeping up with. This depends on the encoder you choose and the gearing between the encoder and the spindle as well as the rate the software can perform. The software should be working at the single encoder count cycle rate, so it only ever sees 1 count at a time for best results. So it boils down to see a count or not, output a pulse or not. Never more than one encoder count or one motor pulse in one cycle. This is simple, tight control. The motor pulse output must be slower than the encoder counts, so the encoder counts per rotation should be high enough that you never have to output more than one motor pulse for one encoder pulse for the simple integer approach. It would be excellent to run MicroPython in one core and a C control algorithm in the other core. But I haven't seen support for that and it is probably beyond what most folks are willing to dig in and do. If you want C level performance in the control core then it is easiest to use C for the other core as well, the way the development environments are generally set up.

If you have a gearbox in your lathe you can use that to help setup optimal pulse ratios. Also the motor microstepping can be optimized, but most ELS and CNC systems use 8-10x microstepping for good overall performance without getting the pulse rates too high. Do some math on your lathe's gearing. The place where there can be trouble is cutting really coarse threads. This requires a lot of motor pulses per spindle encoder pulse.

Start out simple and go from there!
Thanks, lots to think about! I haven't had time to try some new code out, will soon, mostly have been finding potential simple mental pseudocode to try out should one avenue prove unsuccessful.

Yes, I too was thinking about the max RPM I'd need to accommodate. Given 4000 state canges/rev, maybe 3000 RPM max (aka 50hz If I repower my spindle) = 200,000 state changes/sec the pico needs to deal with. 133,000,000hz pico clock/200,000 = 665 clock cycles (assuming the PIO) per potential max tick rate for the spindle. The PIO core seems it should be more than OK to keep up, though your mentioning of gearing is useful re: gearbox or encoder. I do have a quick change gearbox, this is an old 9x20 harbor freight import lathe.

Anyhow, I can see the appeal of tight coupling, e.g., assuming 1/4 ratio as an example: 1 encoder pulse = 3 x-- decrements to waste cycles before finally generating one motor step pulse on the 4th cycle of the core controlling the motor. I did also imagine the steppers would have a finite time to "keep up" and actuate in the real world before needing to be fed more pulses.

In any case, I'll start out playing with micropython and the PIO encoder + stepper output, despite the potential limitations on slowness. I wasn't sure exactly how fast I could imagine the micropython side of things running, but I imagined that it wouldn't be difficult to send multiple steps and eschew perfectly tight coupling in favor of a fairly fast (but still as slow as micropython would limit us) loop that takes PIO derived encoder displacement (more real-time) and sends steps to be taken. I didn't take into account jerkiness or acceleration. It's also a bit unclear as to whether to think about things as velocity, or steps, or I guess steps in time = velocity. I digress.

I guess my three main approach thoughts are now:

-micropython core #1, looping and getting PIO core #1 (encoder) displacement as fast as uPython can, acting as a proportional step pump for steps at desired ratio for PIO core #2 (motor). Arithmetic dead simple to implement. micropython core #2 can supply variables that core #1 uses to derive desired encoder step fraction, or enable/disable actuation. Unknowns: not tightly coupled, may be too slow, unknown jerkiness

- pio core #1 capturing each pulse, pio core #2 immediately acting on pulse to generate a step or wait X # of cycles before pulse to give the proper ratio. Micropython initializing and or refreshing ratio to pio core #2 and starting/stopping actuation.

- playing with a C solution, or mixing as you said.
 
Last edited:
I just threw a quick test loop into micropython on a Pico here. Instead of reading two bits from an encoder it counts modulo 4, combines the prev state bits, does the lookup table, then does the decision / sum / difference and decides to output a pulse or not. It doesn't output pulses. Very basic python code in a tight loop. This runs 1000 times in 40 milliseconds so about 25 khz. So about an order of magnitude slower than your goal. I don't expect C would have any trouble keeping up with 200 khz but I don't have a C setup for Pico to test with. That's with one count per cycle, if you handle multiple counts per cycle then things get more complicated and jittery but it could work as well. I'd rather go faster and avoid all those issues and it appears to be easily done here.

The output pulse to the steppers causes the stepper controller to adjust the currents in the stepper phases to the next microstep value, the available voltage, inductance and the chop frequency of the controller (as well as it's own PID parameters) will determine the lag of the magnetic field and the rotor will follow with it's own mechanical lag as a function of time and torque required, not much we can do about that. The goal of the control system is to make the changes as quickly as practical. Responding to each pulse as it comes is about as good as we can do. The slower the loop runs the more delay, noise and jitter it adds to the process.

The PIO encoder programs I've seen produce a stream of interrupts - one for each encoder state change, so these must be integrated into a sum at this rate. This may be a problem for micropython, not sure what the interrupt rate limit is but doubt it can handle 200k interrupts per second. But it could be used at lower RPM just fine for testing and verification of the techniques and might be a good way to start. This is one program I'd probably move into C sooner rather than later.

Polling the encoder directly will be faster than handling interrupts from the PIO, so perhaps the PIO isn't going to be much help here since we can devote a core totally to the control loop. I wish the PIO state machines had a bit richer instruction set. There may be a clever way to leverage the PIO here but I haven't seen it yet.

Since this is an electronic replacement for a gearbbox and you want precision for cutting threads, implementing it as a ratio of positions is the right approach. If we start with spindle stationary as one would normally do with a gearbox, then the acceleration is dominated by the spindle as long as we have enough capacity in the leadscrew motor.
 
I just threw a quick test loop into micropython on a Pico here. Instead of reading two bits from an encoder it counts modulo 4, combines the prev state bits, does the lookup table, then does the decision / sum / difference and decides to output a pulse or not. It doesn't output pulses. Very basic python code in a tight loop. This runs 1000 times in 40 milliseconds so about 25 khz. So about an order of magnitude slower than your goal. I don't expect C would have any trouble keeping up with 200 khz but I don't have a C setup for Pico to test with. That's with one count per cycle, if you handle multiple counts per cycle then things get more complicated and jittery but it could work as well. I'd rather go faster and avoid all those issues and it appears to be easily done here.

The output pulse to the steppers causes the stepper controller to adjust the currents in the stepper phases to the next microstep value, the available voltage, inductance and the chop frequency of the controller (as well as it's own PID parameters) will determine the lag of the magnetic field and the rotor will follow with it's own mechanical lag as a function of time and torque required, not much we can do about that. The goal of the control system is to make the changes as quickly as practical. Responding to each pulse as it comes is about as good as we can do. The slower the loop runs the more delay, noise and jitter it adds to the process.

The PIO encoder programs I've seen produce a stream of interrupts - one for each encoder state change, so these must be integrated into a sum at this rate. This may be a problem for micropython, not sure what the interrupt rate limit is but doubt it can handle 200k interrupts per second. But it could be used at lower RPM just fine for testing and verification of the techniques and might be a good way to start. This is one program I'd probably move into C sooner rather than later.

Polling the encoder directly will be faster than handling interrupts from the PIO, so perhaps the PIO isn't going to be much help here since we can devote a core totally to the control loop. I wish the PIO state machines had a bit richer instruction set. There may be a clever way to leverage the PIO here but I haven't seen it yet.

Since this is an electronic replacement for a gearbbox and you want precision for cutting threads, implementing it as a ratio of positions is the right approach. If we start with spindle stationary as one would normally do with a gearbox, then the acceleration is dominated by the spindle as long as we have enough capacity in the leadscrew motor.

the jamon lib I mentioned getting running with the encoder doesn't send interrupts for each pulse or anything, it gives an encoder displacement count when polled:



Produces output every second like "0" *turn rotation 3 x within 1s* "3000" etc. Every time it's polled it gives the current displacement value.

Definitely not interested in having uPython handle encoder counting. This is in line with the first avenue I mentioned wanting to explore, polling the PIO Core for current displacement as fast as it can, then sending step pulse count since last displacement proportional to encoder to be taken by a second PIO core. There are a bunch if PIO stepper pulse examples, incl this one in C, with some decent explanations: https://vanhunteradams.com/Pico/Steppers/Lorenz.html , including examples for contingencies that care about position or velocity only . The example is in C, but the assembly is shown. Could also make sense to compile C to *mpy if rly needed for some part of this, but anyway, I'll try to get hacking today.
 
Good find on the encoder PIO program. It's not reading the signals simultaneously or using a lookup table that can detect errors the way I'd like to see it. But the restrictions on the PIOs make it hard to do this.

The Didge article is a good one. I did have that reference buried in my notes, it was discussed about a year ago. That's essentially similar to what I was planning to do though he goes even farther than is probably necessary. I'd be concerned about the very complex use of timers and how that would play out when threading as the velocity changes, stops, and reverses. Any errors in handling all the timer activity, interrupts and DMA could lead to difficult debug problems.

For threading a position feedback loop is needed. Velocity is not constant and position is critical. Emulate those gears precisely. For feeds a velocity feedback would probably be adequate, position is not critical. Velocity is fairly constant.

Being clear on terminology might be helpful. Pico Cores are the full instruction set engines and there are two of those. Cores are separate from PIOs. The two PIOs each have four minimal state machines so 8 total state machines. Quite the interesting little chip, the RP2040.

Good luck with your experiments!
 
Good find on the encoder PIO program. It's not reading the signals simultaneously or using a lookup table that can detect errors the way I'd like to see it. But the restrictions on the PIOs make it hard to do this.

The Didge article is a good one. I did have that reference buried in my notes, it was discussed about a year ago. That's essentially similar to what I was planning to do though he goes even farther than is probably necessary. I'd be concerned about the very complex use of timers and how that would play out when threading as the velocity changes, stops, and reverses. Any errors in handling all the timer activity, interrupts and DMA could lead to difficult debug problems.

For threading a position feedback loop is needed. Velocity is not constant and position is critical. Emulate those gears precisely. For feeds a velocity feedback would probably be adequate, position is not critical. Velocity is fairly constant.

Being clear on terminology might be helpful. Pico Cores are the full instruction set engines and there are two of those. Cores are separate from PIOs. The two PIOs each have four minimal state machines so 8 total state machines. Quite the interesting little chip, the RP2040.

Good luck with your experiments!

I implemented the two timer model for non spindle synchronized stepper acceleration control. It works very well, but i based mine on https://github.com/luni64/TeensyStep. It isn't very difficult to do and not really complicated.

I'd also say that polling isn't likely the right way to go, you should have bresenham or DDS calculate and then evaluate on every encoder interrupt, if it is time to step then you recalculate, this means you only calculate the next or previous step (based on encoder count) and only recalculate that when a step is issued.

Finally, hate to keep saying this, but the esp32 has the peripherals and performance to do this easily along with being able to configure it over wifi/bluetooth and to do it fast in c/c++ and there are working implementations you can use to avoid re-inventing the wheel.
 
My implementation (on a Teensy 4.1) was 100% positional. I measured spindle velocity, simply so I could display it, but never used velocity in the control loop, because it wasn't necessary in practice, at least on my 10 x 22 lathe. For my code, feeding was simply considered as very fine threading (with a blunt cutter). The algorithm was identical for feeding and threading.

I use interrupts on the encoder, position and direction are determined every interrupt of the quadrature encoder. Integer Bresenham is invoked every encoder change, and if required, a stepper pulse of the correct direction is started, and the callback ends. A digital one shot timer is used to time the stepper pulse. At the end of the one shot, the stepper pulse is turned off in a different callback. It's concise, easy to debug, and (so far) has proven to be quite robust.

This isn't a space launch, it is only an ELS. Keep it as simple as possible, it's way easier to understand that way, and makes debug a whole lot easier. I wrote my stuff in C and C++, simply because using Python on micro controllers is rather limited. This is coming from a guy who wrote radar systems simulations in Python, using Numpy and SciPy, because MatLab wanted way too much $$$ for their Toolboxes. I love Python, but find it's not well suited for real time control.
 
Lots of good suggestions and experience here to draw on. Thanks to everyone contributing.

With the Pico, if internet is desired the Pico W has Wifi that works quite well. I've built a number of clocks and other projects with it using MicroPython that self-set from the internet. Very convenient and straightforward. One can also generate a hotspot for local control with a web server on the Pico if you want to do control with a phone or tablet and not be out on the wild internet. One commenter stated he didn't understand why hackers would bother attacking an ELS. The thing is, they probably would have no idea it was an ELS, but if they can gain a foothold on any device they can use it for many purposes they find useful (whether or not you understand their uses or motives), and this is likely to interfere with it's normal use or crash it entirely. An ELS is a little CNC machine and you don't want it distracted when you are running operations.

The ESP32 is older and has more software available, but the documentation on the Pico is better. I've had trouble with the highly variable quality of the ESP based boards. The 32 bit Teensy has some excellent hardware and Paul's libraries are amazing but I find the documentation and complexity level very high compared to traditional microcontrollers. This leads to a less than full understanding of what things are actually doing and that can be problematic, or a really long learning curve that takes more time. People tend to develop favorites and that's fine, but know why you are choosing a micro and what it is costing you in terms of overall time and effort. I've seen many projects fail because some bugs were too hard to eradicate and time and interest just run out.

MicroPython is excellent but this real time application is better served by C/C++. As mentioned before I would like to see a dual setup with Python on one core and C/C++ on the other. Hopefully someone will do that at some point.

KISS is a really good pattern, and the simplest approach in my view is to devote the secondary core to the control and do polled integer gearing. There is no need for interrupts in the control loop when you have a dedicated core, they actually slow the processor down with a lot of bookkeeping activity that has nothing to do with the task at hand. Read the two encoder bits at the same time, mask them off, combine the previously read two bits, index a 16 location lookup table, get an increment, decrement, no change or state skipped error, apply this to the digital sum, check for overflow, if needed subtract and generate the direction and step outputs, and do it again. Very simple loop, and most of the time it gets a zero from the lookup table and has nothing to do but loop again. All the UI can be done in the primary core, and this is where most of the project effort lies. This approach only works in dual core processors, so choose one of those. Both the Pico and Pico W are dual core. PIOs are not even required for this simple approach, and I've reviewed many, many ELS systems that use this technique with success. Most of them use 8 bit processors that are more than an order of magnitude slower than the Pico and they have good results. So any 32 bit processor makes this easier. If it has only one core then you have to get more complicated and probably use encoder hardware and interrupts, but many have successfully done this too. It isn't as simple and has more opportunities for difficult to debug situations, but it can be done. One thing I don't care for is poor documentation for the hardware. Some of these chips have either so many thousands of pages of documentation, or the documentation is so poor or nonexistent that it becomes a grand fishing expedition to find the information you need to get the job done.

That's likely where I will start when I get back to it. Unfortunately the tasks I put my lathe to don't really require an ELS so this project has fallen to the side, but every time I shift the gearbox I think about it. :)

If a PIO was to be used, the first place to use it is in the outputting of direction signal and step pulses to the stepper motor. This is easy to do with the PIO and simpler than the encoder reading and the PIO instruction set makes it very easy to generate the desired timing. Doing the encoder in a PIO doesn't really present an advantage over reading the encoder directly, and it loses the detection of skipped states that you get with a simple table lookup approach. Looking at the PIO encoder code it is probably slower than doing a table lookup as well, though any of these are fast enough by more than an order of magnitude. In any case the encoder reading feeds right into the gearing calculation which the PIO cannot do, so might as well do both in the Pico Core in a really tight efficient loop. Hard get any more KISS than this.

However you choose to make your ELS project, enjoy the journey. :)
 
Tons of conversation, cool, the links are sweet!

I tested driving a spare stepper with an a4988. Worked as expected. Next, tested adding that simple "one step" code to the PIO micropython encoder reading example I got working earlier. Just in a naive 1 pulse = 1 step manner. Worked, but as expected with a 125MHZ clock running on one state machine to monitor the encoder, the encoder could be turned faster than the stepper could keep up with generating pulses 1:1, it seemed.

Again, just to get familiar since this is my first foray with PIO, I separated the encoder reading assembly into its own state machine, running at 125MHZ. I created a second SM at 125KHZ with a wait(1, irq, 1) to test limiting the max step rate of the stepper to something I had previously found reasonable. I substituted the pulse step gen from the encoder detector assembly with a raise IRQ 1, which the second PIO SM picked up on. One direction only. No specific microstepping selected/config'd.

Just for playing around and making sure I could get the requisite hardware doing something somewhat "sane," things seem to work well enough, video:

I had previously tested adding a variable delay to the simple stepper functionality test, to delay X cycles before a step is taken based on a value pushed into the register. This also seemed to work as expected, but I didn't test extensively. Next I'm going to try adding that to this version, to do a simple 1 : X integer ratio of real-time encoder pulses to stepper pulses. Micropython isn't doing anything aside from activating the state machines and setting initial values.

I'm happy to go through experimenting and realizing a few "wrong approaches" as long as I'm tinkering and interested. As to why not just use an esp32, I'm liking the straightforwardness of PIO stuff and just learning while having fun. It's great to know I could use one of the esp32s I have laying around (ESP32-CAMs, actually, which I managed to install an esp32 implementation of wireguard to a while back while retaining webcam server functionality, so it could join an external VPN which I could monitor remotely, but that's another topic)! I could also just as well spend time setting up the linuxcnc installation I have going on a spare computer and do some rudimentary ELS stuff with that, since I found a module there. Tbh it might be a better investment of time, given all the functionality built in. But definitely not the point or my interest this exact second, I'm just exploring and it's really great to see the same thing implemented on many different uCs. It opens things up to people that might only have what's on hand, gets people chatting and thinking, etc. I mean heck, when I saw that MOV EXEC was a function and could read pins or data and exec that from external sources, I was kinda excited that I could potentially use two cheap picos to do things in tandem for fun.

Regarding simplicity, it seems seems inexorable that people have widely different definitions of it. That's kinda fun, too. It definitely seems evident from the discussion of ELS and just thinking about what the implications are for timing and control isn't something that instantly resolves itself in my head. Like, there's an implication that if a x:x ratio of encoder to stepper pulses is performed, just that this loop is running at a given clock dictates a step speed. "Doing things" in time relates to a velocity, and the discussion around velocity/position/precision needed is interesting. I'm ignorant to all but the basics of lathe stuff anyhow, so my mental concept of "good and responsive enough, assuming one only has to compensate for slight spindle bogging down from tool pressure" versus "threads will have this amount of slop acceptable in control scheme until they are out of spec" is kinda nonexistent.

I'm happy to try to re-familiarize myself enough with C to implement things in C if it comes to that point. The jump table encoder implementation with error handling that is in C seems like it wouldn't be difficult to use with my current experiment in micropython, but that jump table version is running short on spare instruction space. Not sure what 1/X variable electronic gear ratio gets me at this point (if anything useful), since I haven't thought about gearing/pitch/etc yet.
 
Last edited:
Back
Top