ILI9341 and encoders in an Electronic Lead Screw Application

The Error_Callback is a left over from development and it not working anymore. I'll remove the leftover code in the next update. The error callback was invoked whenever the internal state machine detects an A/B pattern which is not allowed. E.g. 1/1 following a 0/0. Such patterns will simply be ignored and shouldn't crash the processor.

Does your Encoder library allow abs(delta)>1?
Currently delta can only be +/- 1 or 0. Later versions might support acceleration, then it can be larger.

Have a look at your callbacks. In case you do something taking too long you might run into problems at high count rates. At 200kHz you only have 5µs between the encoder edges / interrupts. Again, it might be much easier if you show us your code.
 
Thanks, I'll have a closer look later. So, I'll disable the touchdisplay stuff, attach EncSim to pin 0/1 and see if it crashes at about 200kHz right?
 
Think it will crash before then, but yes. Also, I see that EncSim is outputting a couple of random counts in the beginning on start up. Not always, but occasionally. Might be the outputs changing state, but it is interpreted by the other Teensy as a count.
 
Got a crash at Count = 99591/100000 counts. freq = 100000. My instance of spindleEncoder did not print out the count of 99592, but did for 99591. It printed out "-", then restarted and printed the setup information. No SPI or display writes. Only Serial print. Speaking of which, in Arduino, should I select the Teensy ports or the Serial ports? Seems if I use Serial ports, I don't lose as many counts...
 
That was easy ;-)
You print the current position in the encoder callback. At high encoder speeds the printing is not yet done when it tries to invoke the callback again. This will basically lock the system.

I moved the printing to loop which fixes it. Here the recording of a 3'000'000 step move with 500kHz count rate. Works perfectly, no counts lost.

(left->EncSim output, right -> output of your code)
Screenshot 2022-05-17 192813.jpg

Here the changed code:
Code:
#include "stepper_n_encoder_v2.h"

void doincrement(bool adir);

//// imperial 32 TPI
// int N = 293;
// int D = 2000;

//// imperial 8 TPI
// int N = 293;
// int D = 500;

//// imperial 6 TPI
// int N = 781;
// int D = 1000;

// Note: 17 May 2022, N & D were calculated off line.  The value of microsteps is embedded in the ratio.  To
// add one's own feeds or threads, we need to calculate using the required # of microsteps!  This functionality
// is not in the code base yet.

#ifdef imperial
uint16_t myidx               = 1;
float TPI                    = impthread[myidx].tpi;
int N                        = impthread[myidx].N;
int D                        = impthread[myidx].D;
int ustep                    = impthread[myidx].ustep; // added for future capability
float tenpitch_error_percent = impthread[myidx].error;
#endif

OneShotTimer t1(PIT); // for stepper pulse
OneShotTimer t2(PIT); // for measuring speed.

// XPT2046_Touchscreen ts(TOUCH_CS);
// ILI9341_t3n tft = ILI9341_t3n(TFT_CS, TFT_DC, TFT_RST, TFT_MOSI, TFT_SCK, TFT_MISO);

void onEncoderChanged(int value, int delta)
{
    // volatile int mydelta = -delta;
    if (delta > 0)
    {
        if (delta > 1)
        {
            Serial.printf("fault! delta = %i\n", delta);
            errorcount += 1;
        }
        else
        {
            // delta = 1
            myacc = myacc + delta * N;
            if (myacc >= D) // works for positive accumulation only
            {
                myacc = myacc - D;
                mydir = CCW;
                doincrement(mydir);
            }
        }
    }
    if (delta < 0)
    {
        if (delta < -1)
        {
            Serial.printf("fault! delta = %i\n", delta);
            errorcount += 1;
        }
        else
        {
            // delta = -1
            myacc = myacc + delta * N;
            if (myacc <= D)
            {
                myacc = myacc + D;
                mydir = CW;
                doincrement(mydir);
            }
        }
    }
    val1 = value;
    //Serial.printf("Count = %i\n", value);
}

// void onEncoderError() don't know how it is used, nor what to look for!

void pulseme() // turns off stepper pulse after time out
{
    digitalWriteFast(PUL, LOW);
    // digitalWriteFast(LED_BUILTIN, LOW);
}

void doincrement(bool adir) // sets direction and turns on stepper pulse
{
    digitalWriteFast(DIR, adir); // set direction
    delayNanoseconds(100);       // can improve stepper speed by doing this, but is dangerous...
    digitalWriteFast(PUL, HIGH);
    t1.trigger(steplen); // switch off after steplen us (2us)
}

void myms() // xxms timer callback for estimating spindle speed and step rate
{
    val2 = (uint32_t)SpindleEnc.getValue(); // read encoder for current position
    // we know the time was 20 ms

    cps = 50.0f * ((int)val2 - valstart) / (float)encoder_resolution;
    ;                                                                // rev per second  50 = 1/0.02
    sps     = cps * (float)encoder_resolution * (float)N / (float)D; // steps/second
    myrpm   = -cps * 60.0f;
    timeout = true;
}

void readconsole()
{
    if (Serial.available())
    {
        int incomingByte = Serial.read(); // read the incoming byte
        char c           = (char)incomingByte;
        // Parse the command.
        if ((c != '\n') & (c != '\r'))
        {
            Serial.println();
            Serial.print(F("key pressed: "));
            if (c == ' ')
                Serial.println(F("[space]"));
            else
                Serial.println(c);
            if (c == 'r') // reset the counter
            {
                SpindleEnc.setValue(0); // wait til we get the right syntax!
                Serial.printf("Reset spindle counter to 0\n");
            } // add more commands as you see fit
        }
    }
}

void setup()
{
    while (!Serial && millis() < 4000)
        ;

    if (CrashReport)
        Serial.println(CrashReport);

    for (int i = 5; i < 8; i++) pinMode(i, OUTPUT); // init these pins to outputs

    t1.begin(pulseme);          // set up t1 callback function
    t2.begin(myms);             // set up t2 callback function
    digitalWriteFast(DIR, CW);  // sets direction of rotation  LOW =CW, HIGH=CCW
    digitalWriteFast(ENA, LOW); // active low signal, enables the stepper controller

    // Use luni's SW encoder
    SpindleEnc.begin(0, 1, CountMode::full); // encoder on pins 0,1 count all e3dges of the quadrature signal
    SpindleEnc.attachCallback(onEncoderChanged);

    myacc   = 0; // Bresenham accumulator
    oldrpm  = 0.0f;
    timeout = true; // so we start measuring speed right away

    // if (0){ // prevent all tft writess for debug!!!
    //   if (!initMyDisplay(3))
    //   {
    //     Serial.println("Display failed to initialize");
    //     while(1);
    //   }
    //   if (!mainscreen())
    //   {
    //     Serial.println("Failed to send first message to display");
    //     while(1);
    //   }
    // }
    errorcount   = 0;
    displaycount = 0;
    first        = true;

    // Eventually all this will be selected via the touch panel
    Serial.printf("TPI = %3.1f, usteps/step = %i\n", TPI, ustep);
    Serial.printf("N = %i, D = %i\n", N, D);
    Serial.printf("Ten pitch error as per cent of pitch = %4.2f\n", tenpitch_error_percent);
}

void loop()
{
    if (timeout) // if no timeout, skip this
    {
        timeout = false;  // determine how many counts we get in 20ms, this is the base for a speed estimate
        t2.trigger(20ms); // set timer to go off in 20ms
        valstart = val1;  // save current position
    }
    if ((myrpm == 0.0f) && (first))
    {
        Serial.printf("%7.1f%\tRPM\n", -myrpm); // there's a zero bug of some sort
        // displayRPM(-myrpm);
        first = false;
    }
    if (myrpm != 0)
    {
        first = true;
        Serial.printf("pos: %d, RPM: %7.1f\n", SpindleEnc.getValue(), myrpm);
        // displayRPM(myrpm);
    }
    delay(200);
    // readconsole();
}
 
Think I found part of the problem! Was calling Serial print way too often. I updated the code to only print out the RPM once every 100ms, it seems it was trying to output every time it went around the loop. That plus the count being printed at 100KHz put me over the edge. Strange that calling serial print too often can cause a processor reset, I wouldn't think that was possible!

I can now run at freq = 200KHz, and update the display. But I cannot output the count by calling Serial.printf at 200KHz, even at 230400 baud. Eventually the serial buffers overrun and the somehow, the program is reset - still not sure if TyCommander is doing the reset or the Teensy. Disabling the print statement I used in the onEncoderChanged callback, fixed the basic problem.
 
Yes, you found the problem as well! Thank you very much for looking. It is very appreciated.

Still have a problem with resetting the counter value to zero for testing. I get some strange output, but that is just an ordinary bug... I have lots more to do for this project...

Edit: fixed that bug. Just needed to reinitialize a couple of values. Now I can reset the count, if I need to in my programs, which is handy.
 
Last edited:
@luni, Making some good progress. Have two display screens and a way to get from one to the other. Working on selecting the pitch value and loading the associated values for N and D to the main program.

As I am getting closer to a real setup, some interesting questions come up. Obviously, one doesn't want the system to run until it is initialized :rolleyes: And obviously, if the configuration changes, ie. the operator selects a new thread pitch, we don't want to change values on the fly. Instead we want to re-initialize some values, and re-enable things in an orderly fashion. Where we proceed through a state machine. I think I have most of that figured out.

For my code right now, everything is gated on the spindle encoder. For the first time through, I can wait to attach the callback onEncoderChanged. Without the callback, there is no lead screw movement.

What would be a good strategy for a change in setup, ie, pitch change? Is there a way to detach the callback? Or should I just gate the stepper, until I reinitialize N, D and probably the encoder count?
 
Are you sure that you want to change the pitch while the spindle rotates? Thinking of the noise the breaking thread tool makes gives me goose bumps :))

Seriously;
  • In your controlling application I'd require that the spindle is stopped before allowing any change of the pitch.
  • If you really want to change the leadscrew pitch while the spindle rotates you could simply change the bresenham parameters on the fly. While this might not be good for threading it might come in handy for changing the feed in longitudinal turning.
  • If you really need to stop the leadscrew while the spindle moves you can simply attach an empty function to the encoder. I.e., void dummy(int, int){}
  • Alternatively, I could add a detachCallback to the library on the weekend :)
 
No, I would never change thread pitch while cutting threads with the spindle is turning! My state machine (if I write it correctly) would prevent that from happening. Although it might be interesting to see what such a screw would look like!

However, I could foresee threading to a stop someday. I do have a DRO on my lathe. As you know, they are just linear quadrature encoders. That would give me carriage position. If I set the carriage stop, to the position of the thread relief, then the spindle could continue to rotate and the leadscrew would stop rotating when the carriage reached the thread relief. If doing this, I could record the position of the spindle and the leadscrew count and the DRO. I think one could use this to resynchronize the thread for another pass. That might be fun to try once I get the basic unit to run.

A more immediate use, that doesn't require crashing or threading, is a programmable feed rate for the lead screw, when one is simply turning for finish quality. In this case, it would be ok to change the feed rate. Feed rate is just a pitch that is very very small. I do think changing the Bresenham parameters would work for that case.

A detachCallback would be very nice (I think). :)
 
A detachCallback would be very nice (I think)

I just had a look at the library code. You can simply attach a nullptr to remove the callback.
I.e.:

Code:
SpindleEnc.attachCallback(onEncoderChanged); // attaches onEncoderChanged to SpindleEnc
SpindleEnc.attachCallback(nullptr);          // removes the callback
 
@luni, very sorry, not to have acknowledged. Thank you for looking into this. I have updated my code accordingly. Now spending a bit of time on the UI along with the system level state machine. There's probably four times as much code there, than in the motion control, which I understand is typical.

Have ordered a 4 Nm NEMA-24 closed loop stepper motor and driver. The fun part will be figuring out how to mount and package the motor and encoder on the lathe. Fortunately, I have both a milling machine and lathe, so I will be able to machine the parts, once I figure out what's needed.
 
Alas, no significant progress. My laptop has some sort of problem where it just freezes and turns off the power. This typically happens just after flashing a Teensy. Don't know if it is HW, or SW. Been attempting to port nearly everything over to my RPI4-8GB 64bit OS on SSD. It's tolerable, but still quite slow. Been a slow slog getting enough things running ok, email, browsers, files, etc. to carry on. Can't find any HW, or SW problem, nothing shows up in the logs that would point me to a clue. Just rsync'd everything and I am going to do a new install, wiping everything. Really dislike doing this, as it takes a long time to recover, but I've run out of options. After that, it is return to manufacturer for further analysis.

But nearly everything is on the RPI4 now, including Teensyduino and Ty Commander. @luni, thanks for letting me know about TyCommander. Had to use a 64bit OS on the RPI so I could get a modern version of FreeCAD on it. All my mechanical design is using FreeCAD, which is pretty important when working on an Electronic Lead Screw.

Even though the laptop died, I did hook up my NEMA-24 motor to the Teensy and it ran it with no problems. Glad I used EncoderSim for the initial testing.
 
Wow, running Freecad on the RPI! Didn't know that the RPI is that fast by now... I use onshape for mech design. It runs in the browser (no installation required) and is quite state of the art. As long as you can live with a public design it is free.

Looking forward to that video showing the first thread cutting success :)
 
The RPI4B isn't that fast... But it is tolerable, especially compared to doing it all again by hand!
FreeCAD is free. You can have your own designs and they can be proprietary, if you want. Not going to say it isn't clunky at times, but it does work. If you use the spreadsheet mode it is easy to parameterize your design, which is a great help. I have used that mode quite frequently.

I'm quite a ways from first threading!

Ty Commander or something else is causing hard Linux crashes after Teensy flashing. Trying to debug this, and it has been really tough to diagnose. Nothing insightful in system logs. Don't know what the root cause is yet, except is always happens after programming the Teensy and always turns my Linux computer off...
 
Watching this with interest as I've just added a rotary encoder to the spindle of my lathe with the intention of adding an ELS.
 
Seems to be TyCommander that is doing the dirty deed. Why, I don't know. If I use the Teensyloader, it is fine. So for now, that's what I will use.

On a side note, I had ordered a TI Launchpad board, in February, (for Clough42's ELS), which claimed a July 7 delivery date. Just got a notice that the date has been pushed to November 7. Think I am going to cancel that order.

Yeah, good thing I decided to make my own ELS with a Teensy. For what it is worth, the hard part is always the user interface. The basic ELS stuff is pretty easy.
 
Yeah, good thing I decided to make my own ELS with a Teensy. For what it is worth, the hard part is always the user interface. The basic ELS stuff is pretty easy.
Have a look at this. It is an example of how the HMI design can be much easier.
 
Have a look at this. It is an example of how the HMI design can be much easier.

Interesting idea on using Powerpoint to make up the initial storyboards. I just did that part with a couple of pencil sketches. Hadn't thought about making a bit map from it. That bit was clever.

At the moment, I need to do this on the cheap side and wasn't considering such large screens. Would be nice to have one of those nice screens, though! Problem with the UI design for me is, that I need to know exactly what I need to do - and I haven't invented or thought of it yet! I know the basics, but haven't diagrammed it all out yet. Probably should be the next step? :D
 
Some more progress on menus and things. All of the threading section is complete. Most of feeds are done. Have lockouts on the stepper driver until explicitly initialized and started. Still need to clean up the state machine to make it easier to navigate between states.

Somewhat toying with adding my DRO (digital read out for position) outputs to the Teensy. I need a level translator, since these are ancient 5V devices, but that's not hard.

@luni Can I presume that a quadrature linear encoder can be read by Encoder Tool? I will have to do some scope work on the outputs. Are there any digital input pin restrictions? I have two DRO's, the carriage position and the cross-slide. They are magnetic pickups. I am hoping the TTL level signals are available at the interface connector. Will find out tonight.
 
@luni Can I presume that a quadrature linear encoder can be read by Encoder Tool? I will have to do some scope work on the outputs. Are there any digital input pin restrictions? I have two DRO's, the carriage position and the cross-slide. They are magnetic pickups. I am hoping the TTL level signals are available at the interface connector. Will find out tonight.
Of course you can. No restrictions on pins.
 
Making some progress - Electronic Lead Screw

An update. Checked out the DRO read head outputs and they appear to be standard 5V TTL levels. This is good, since I can use the spare pins on my 74LVC245 level translator. I am already using three of the pins for the rotary encoder and have five pins left. I need four signals, so this will work. Have to wire in the circuit to the connector blocks. Will do this today.

Wrote some test code with a calibration factor for the DRO, seems to work ok, at least with a rotary encoder as the input. Haven't integrated all three Encoder instances, but that looks straight forward. Plan to add independent units (Metric and Imperial) and clear ability for each axis. Have to design the human interface for that, but it is just 2 or 3 buttons per axis. The DRO display will always be active in the main page. Think the DRO display will replace the picture of my lathe. I am running out of screen real estate! A 320x240 display isn't all that big...

Are all the Teensy input pins on the same interrupt level? So the interrupt handler has to figure out the source pin? Are there any pins that should be avoided or favored?

Done a bit of machining. (My other hobby:D) Made a stepper motor bracket that houses the motor inside the lathe ways casting. The NEMA-24 motor just fits! Procured the timing belt pulleys and timing belts. Also fabricated a bracket to hold the rotary encoder. I will need to do some creative work to line up the encoder pulley to the spindle gear assembly. I did not account for the full width of the timing pulley so I need to move something about 2mm. Might have to machine a gear shaft, but it looks like there are several good alternatives to try that may be even simpler. The 60T timing pulley (shown in the chip pan) mounts in place of the small metal gear, on top of the M1.5 60T plastic gear. The 40T timing pulley is attached to the rotary encoder. You can see the encoder bracket on the chip pan. The bracket and encoder mount to the right side of the 60T plastic gear. So far there are no hard problems to fix! Thank goodness for that.

Need to add Reverse and Left Hand threading to the electronic lead screw code. Seems I forgot about that! Should not be difficult to add, but I have to work through the details and make sure there are no hidden issues.
 

Attachments

  • PXL_20220816_165130604_1920.jpg
    PXL_20220816_165130604_1920.jpg
    225.2 KB · Views: 26
  • PXL_20220816_164125007_1920.jpg
    PXL_20220816_164125007_1920.jpg
    129.1 KB · Views: 23
  • PXL_20220817_000438378_1920.jpg
    PXL_20220817_000438378_1920.jpg
    110.1 KB · Views: 23
Finally got the stepper motor properly mounted. I had mis-drilled the holes in the lathe casting. Took quite a while to recover from that. Had to make drill bushings and weld them to the mounting plate, then drill through the bushings to make sure the holes went in the right place. Then I had to remove the bushings and machine then flat. Took a while. However, it only took about 10 minutes to get the rotary encoder installed. It's now time to hook up the encoders to the Teensy! First will be the rotary encoder, as that will be easy. Then I need to tap into my DROs and attach them to my level translator. Going to get a lot more fun... First thing to check for is the sense of rotation - clockwise or counter clockwise!
PXL_20220909_210239636_1920.jpg
 
Wired everything up to the Teensy. After some playing around with the stepper driver settings and the gear box, can now cut 10 TPI threads with a 10 TPI setting. Lots more work to do though. Need to reverse the stepper rotation, ahem, got it wrong. It is cutting left hand threads, rather than right hand threads. But the encoder rotation is correct. Very encouraged. I need a filter on the RPM, it is too jittery, with a 10 Hz update. Yeah, and a way to change from left handed and right handed threading. Time on project is about 4 months.
 
Back
Top