Another Electronic Lead Screw using the Pi Pico

What are the threads per inch limits in the case of 16 TPR Leadscrew,1200 PPR encoder and 200 PPR stepper? Is it ~2.666 to ~69 TPI or can the control word be all zeros and the next "1" wrap to the next control word? Not that it is a practical limitation, just trying to wrap my head around it. Very cool logic!
 
Yes, you’ve got it… The maximum pitch/feed supported by the current configuration (1200/200) results in 2.66TPI.

in the following discussion, let wrap be the point at which I determine I can loop back to the beginning of the control words.

My first iteration supported as few as one control word, and yes, the design supports a single bit in one control word, that can wrap back on itself.

I decided to require a minimum number of control words before a wrap to limit any accumulated error. The final bit in the set of control words is determined by testing the step at that position vs the calculated position. If the error is less than .1% of the encoder pulse spacing, I call it done, and loop back to the beginning. Since I’m looping over the same control words, that error will accumulate over time. I can mitigate the affect of that error by moving the wrap point further out.

Fortunately, in the testing I have done, the the error at the wrap point tends to be much smaller than my arbitrary test limit of .1%.
 
Here’s a quick video I made with the control box and the back of the lathe in view. I use a wide angle setting so things are pretty distorted, but you’ll get the idea.
I stumbled all over myself trying to explain how I can use Idle mode to realign the threading tool with an existing thread. I need to prepare a script next time. What I was trying to say was this… it’s possible to align the threading tool with the Following process.
With the lathe stopped…
1) move the tool close to the thread.
2) turn the chuck in feed mode until the thread indicator is at the appropriate point.
3) engage the half nut
4) select idle mode, turn the chuck until the threading tool is lined up with the root of the thread.
5) select ANSI or Metric mode, and the correct thread size

Thats it, back the tool out, move the carriage over, set the depth of cut and go.
Heres the video…
 
Here's my source code for State machines...
Well, there are four state machines, els, stepPulse, edge and count
Somehow I managed to get all this in PIO0, using 31 of the possible 32 instructions!

Definitions...
OSR = output shift register. This is where data comes into the state machine. It's unfortunate that sometimes commands are referenced from the processors perspective and others from the state machine. You might think the out instruction is for working with data leaving the state machine, but it's referencing data output from the processor

.program els is the routine that handles the control words.

readControlWord:
This is where we start handing a new control word. The out x,5 instruction shifts the 5LSB's from the OSR into the x register. We've now got the bit count-1 in the x register. and proceed to waitForIRQ:

waitForIRQ:
The edge state machine generates IRQ 5 on the rising and falling edges of the encoder phaseA. That provides 600x2 IRQ's per revolution of the spindle. Once IRQ5 is set, we proceed to testForStep:

testForStep:
out y,1 shifts the next control bit out of the OSR.
The jump instruction tests for zero and if it is zero, jumps to checkEndOfData:
If the control bit is 1, it fall through the jump an sets IRQ4 which will trigger the stepPulse machine to generate a stepper pulse.

checkEndOfData:
jmp x--, waitForIRQ is a post decrement test. In other words, the jump is determined by the state of X prior to the decrement. This is the reason why I save the number of bits -1 in the bit count.
if x is non zero, there are more bits in this word, so jump back to waitForIRQ.
if x is zero, the program wraps back to the beginning. I put a commented out jump instruction there to remind me where it's going next.

The inline code below the program is used to setup some of the commands.
.program els
; each control word consists of 26 control bits and a 5 bit count (msb-lsb = ControlBits-5BitCount).
; The 5BitCount indicates how many control bits are in the word -1 (-1 due to the post decrement test in jmp)
; Since we force empty the osr (out x,27) to support variable length control bit words,
; don't use 27 control bits (32-5).
; As a result of this limitation, use a maximum of 26 bits (25 in 5bit count)

readControlWord:
;read in the next control word bitCount
out x, 5 ;x contains the number of bits in the control word
waitForIRQ:
wait 1 irq 5 ;wait for the go signal
testForStep:
out y, 1 ; get the next control bit from the shift register
jmp !y, checkEndOfData ; if control bit is 0, no step, test for end of data
irq nowait 4 ; if falling through, control bit is high so make a step
checkEndOfData:
jmp x--, waitForIRQ
out x,27 ; empty the TxFifo (input to the state machine)
;jmp readControlWord ;Not needed since wrap does the same thing

% c-sdk {
static inline void els_program_init(PIO pio, uint sm, uint offset,
uint pulsePinA) {
pio_sm_config c = els_program_get_default_config(offset);

int pulsePinB = pulsePinA + 1;
sm_config_set_jmp_pin (&c, pulsePinB);
pio_sm_set_consecutive_pindirs(pio, sm, pulsePinA, 1, false);
sm_config_set_out_shift(&c, true, true, 32);
sm_config_set_in_pins(&c, pulsePinA);

sm_config_set_clkdiv(&c, 1);
// Load our configuration, and jump to the start of the program
pio_sm_init(pio, sm, offset, &c);
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}
%}

.program stepPulse
Simple program that waits for IRQ4 (set in els) and when it's set, waits 16 clocks to satisfy the setup time requirement between the direction pin and the stepper pulse (5us). After the 16 clocks, it generates a 5us pulse. The stepper driver pulse requirement is 2.5us.
The inline code sets up the state machine clock frequency so that clock period .5us.
.program stepPulse
;Creates a pulse with a user defined pulse width for the stepper motor
;setting clkDiv to 133 will result in a 1us clock.
;going to setup clkDiv to 133/2 (.5us)
;the 8us setup time is provided to meet the stepper driver direction pin to step setup time of 5us

loop:
wait 1 irq 4 [16] // should provide 8us setup time with clock set to .5us
set pins 1 [10] // provides 5us pulse, driver requirement is >2.5us
set pins 0

% c-sdk {
static inline void stepPulse_program_init(PIO pio, uint sm, uint offset, uint pin, uint clkDiv) {
pio_sm_config c = stepPulse_program_get_default_config(offset);
// Map the state machine's OUT pin group to one pin, namely the `pin`
// parameter to this function.
sm_config_set_set_pins(&c, pin, 1);
// Set this pin's GPIO function (connect PIO to the pad)
pio_gpio_init(pio, pin);
// Set the pin direction to output at the PIO
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
// Set clock div
sm_config_set_clkdiv_int_frac(&c,clkDiv,1);
// Load our configuration, and jump to the start of the program
pio_sm_init(pio, sm, offset, &c);
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}
%}

.program edge
This routine waits for high and low going pulses on the specified pin (pulsePinA in the edge_program_init program), and when either is found, generates IRQ5. This triggers els to start its processing.
I also use this routine to set the direction pin. I've defined phaseA and phaseB pins to be consecutive pins where phaseB is phaseA+1.
on the rising edge of phaseA, I check phaseB, if it's zero, I clear the direction pin. If it's high, I set it.
.program edge
;sets irq5 on any edge

loop:
wait 0 pin 0 ;wait for falling edge
irq nowait 5 ;set irq 5
wait 1 pin 0 ;wait for a rising edge
irq nowait 5 ;set irq 5
jmp pin forward ;if pulse pin is high we're going forward
set pins 0 ;not high, so set direction low
jmp loop ;start over
forward:
set pins 1 ;set direction high

% c-sdk {
static inline void edge_program_init(PIO pio, uint sm, uint offset, uint pulsePinA, uint directionPin) {
pio_sm_config c = edge_program_get_default_config(offset);

// setup the output pins
// Set this pin's GPIO function (connect PIO to the pad)
pio_gpio_init(pio, directionPin);
// Set the pin direction to output at the PIO
pio_sm_set_consecutive_pindirs(pio, sm, directionPin, 1, true);
// Map the state machine's OUT pin group to one pin, namely the `directionPin`
sm_config_set_set_pins(&c, directionPin, 1);

// setup the input pins
// set the jmp pin
uint pulsePinB = pulsePinA + 1;
pio_sm_set_consecutive_pindirs(pio, sm, pulsePinA, 2, false);
sm_config_set_in_pins(&c, pulsePinA);
sm_config_set_jmp_pin (&c, pulsePinB);

sm_config_set_out_shift(&c, true, true, 32);

// Load our configuration, and jump to the start of the program
pio_sm_init(pio, sm, offset, &c);
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}
%}

.program count
Counts down from a preset value between rising edges. of the pulsePin.
When I setup this state machine, I programmatically load the OSR with a start count.

oneTime:
stores that count in the y register. Only executed the first time for the life of this machine.
The y register will be used to set the x register at the start of every cycle.

setup:
jump here on a rising edge.
Set activity high
Write the count to the input shift register (one of those weirdly named commands) input shift register is the output of the state machine.
load x with the value saved in y and decrement until the next rising edge is detected.
Be aware of this, since I need to be counting, I can't wait for the edges. I need to be polling for them while counting. It takes two instruction to perform the decrement and check for the pulse level while the pulse is high and while it's low.

decrement1:
I stay here while the pulse is high, constantly decrementing and testing for the pulse. As long as x isn't zero, the loop consists of the two jump commands. If x decrements to 0, I set activity to 0. When the pulsePin goes low, I fall through to decrement0:

decrement0:
the pulse is now low, we still decrement the count, and test for the pulse going high. Like decrement1, the loop is consists of two commands, jmp pin setup and and jmp x-- decrement0. If x decrements to 0, activity is set to 0. If a high going edge is detected, I jump to setup, reload x with y and start over. We're running!

For both the high pulse and low pulse sections, if x decrements to 0, I just remain in those sections. Decrementing x when It's zero is basically a NOP. The machine is stopped. when it starts again, the first rising edge will kick start the process.
.program count
;Used to calculate RPM of the spindle
;Counts down from a programmed value between rising edges of pulsePin
;The count is pushed into the rxFifo (output of the state machine)
;Any rising edge of pulsePin will set activity to 1
;if count reaches zero, activity is set to 0
oneTime: ;only executed one time
pull ;load the txfifo into the OSR
out y 32 ;store the initial count value

setup: ;come here on rising edge
set pins 1 ;activity high
mov isr x ;write count to RX shift register
push ;push it to the rx fifo
mov x y ;resture the initial count value

decrement1: ;count while pin is high
jmp x-- continueHigh ;decrement count
set pins 0 ;x=0 means no activity
continueHigh:
jmp pin decrement1 ;if pin is still high, go back

decrement0: ;pin is low, count until high
jmp pin setup ;found rising edge, done with this cycle
jmp x-- decrement0 ;decrement count
noActivity:
set pins 0 ;x=0 means no activity
jmp decrement0

% c-sdk {
static inline void count_program_init(PIO pio, uint sm, uint offset, uint pulsePin, uint activityPin) {
pio_sm_config c = count_program_get_default_config(offset);
// setup the output pins
// Set this pin's GPIO function (connect PIO to the pad)
pio_gpio_init(pio, activityPin);
// Set the pin direction to output at the PIO
pio_sm_set_consecutive_pindirs(pio, sm, activityPin, 1, true);
// Map the state machine's OUT pin group to one pin
sm_config_set_set_pins(&c, activityPin, 1);

//pio_sm_set_consecutive_pindirs(pio, sm, pulsePin, 1, false);
//sm_config_set_in_pins(&c, pulsePin);
sm_config_set_jmp_pin (&c, pulsePin);
// Load our configuration, and jump to the start of the program
pio_sm_init(pio, sm, offset, &c);
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}
%}
 
I documented the timing of a few signals. This is encoder phaseA to stepper pulse for 16TPI. The els state machine operates on rising and falling edges. Here you see six edges between successive pulses.
IMG_7192.jpeg

This is phaseA to direction pin going low. You can see how fast the the state machine operates!
IMG_7195.jpeg


phase A to direction going high.
IMG_7194.jpeg

Direction pin to stepper pulse. Note the setup time of 8us and pulse width of 5us.
IMG_7193.jpeg

The beauty of the state machine implementation is the hardware like speed you can get out of it!
 
I added another state machine to support the other phase of the rotary encoder. The system is now running with a 600PR rotary encoder X 4 or 2400 pulses per revolution.
Here's a scope grab of phaseA and the stepper pulse with a simulated spindle speed of 4000RPM and TPI set to 16... A totally unrealistic speed for the stepper, but I wanted to see how it would handle it. Gotta love those state machines!

2000RpmEncoder2400.JPG

I was able to use two PWM's on the pico to simulate the quadrature phase output of the rotary encoder. Here's a scope grab of the two phases running at 80Khz...

IMG_7201.JPG
 
Last edited:
A few pics of some test threads...

12 threads per inch thread on a piece of 1 1/4 aluminum.
Had a hard time with aluminum until I switched to HSS and ground my own tool.
That's dirt on the back edge of the tool. :(
IMG_7221.JPG

Here's 16TPI, stainless outside and aluminum inside thread...
IMG_7222.JPG

Here's the "bolt" just starting...
IMG_7223.JPG

Bottomed out.
IMG_7224.JPG
 
Here's a demo of lead screw tracking with feed set to 16TPI.
It's a somewhat crude method of determining tracking, but short of connecting another rotary encoder to the lead screw, it served its purpose.
I actually found a bug when jogging the spindle back and forth a few days ago. I was setting the direction on encoder phaseA/phaseB rising edges. When I changed to x4, where I'm now using rising and falling edges, I would occasionally step the wrong direction for one pulse when reversing direction.
As a reminder, my current configuration is a 600PR encoder times 4 or 2400 pulses per spindle rev, and a 200 step per rev (full step) stepper geared 2:1 so 400steps per feed screw rev.
 
I'm more than happy with the pico state machine driving the stepper...

Next, I'm going to look at building a gear hobbing attachment for the milling machine with a stepper driving my rotary table. I'll start a new thread for that build, but first I need to look into hobbing... :)
 
Well… I might stick with this project a bit longer. I just realized that if I add a DRO feature to the controller, I could do away with the thread gauge on the lead screw, and greatly simplify cutting metric threads with my 16TPI lead screw. Here’s how it would work. The Controller would start all thread operations with the leadscrew stopped. You Move the carriage to the starting position, and engage the half nut. Then press a button on the controller to start driving the carriage. The first pass, the carriage would start immediately. You disengage the half nut at the end of the thread, back the tool out, move the carriage back to a starting position and set the next cut depth. Set the controller in a “ready“ state where the lead screw is locked, engage the half nut, then press a button to enable threading. This time the controller waits until the tool is lined up with the thread, when it is, it starts feeding the carriage.

This will eliminate the need to leave the half nut engaged when cutting metric threads with an imperial lead screw, and likewise imperial threads with a metric lead screw. You could probably add the ability to learn the end of thread, so it wouldn’t be necessary to disengage the half nut at the end of the thread.

Sounds like an interesting addition to the project.
 
Back
Top