Does TyCommander handle FTDI? Or am I doing something wrong?

@clinker8, are you using the EncoderTool error callback? It looks as if that triggers if a single edge interrupt has been missed, resulting in an apparent simultaneous change on both A and B signals. I could be reading the code wrong, though.
I wasn't using the error callback but could. I'll read about that.
 
Final thoughts before going offline for the night - these may or may not be corroborated by a detailed log of every encoder edge. Based only on reading the encoder state machine code, I think a single lost edge may result in the count ending up 4 behind where it should be: one lost edge, one ignored because the inputs are in an unexpected state (this triggers the error callback), and one edge counts in the wrong direction. So instead of n+3 you're at n-1...

It would be good to log a timestamp for every edge (ARM_DWT_CYCCNT is guaranteed to increment at 600MHz) - you'd soon spot missing edges, and also whether they're significantly different from their nominal positions. You're never going to see exactly 36.6µs = 21,972.7 clocks between edges, but it would be useful to know if the margin you think you have is adequate at all points around the encoder.
 
@clinker8, don't know if you saw my comment about about the condition in your while loop. If dtheta = 360/4096, then it's the delta associated with one encoder edge, so the only way your while condition can be false is if the error is exactly 0. Is that what you intended?

Your change of dtheta/2 to dtheta/4 should make no difference, unless you are keeping track of position with floating point variables, such as:

Code:
float dtheta = 360/4096.0;
float position;
float old_position;
int delta_count;
position = old_position + (delta_count * dtheta); // where delta_count can be +/-

If you're doing something like that, your position variable will accumulate error over time due to lack of precision of the floating point variables. It will be okay for a while, but with each edge, you'll accumulate a tiny bit more error in the position, and eventually you'll be off by more than dtheta (1 count).
 
@clinker8, don't know if you saw my comment about about the condition in your while loop. If dtheta = 360/4096, then it's the delta associated with one encoder edge, so the only way your while condition can be false is if the error is exactly 0. Is that what you intended?
Yes. I want to wait until the angle error is zero, (achieved sync) then pulse the stepper. It's the only place I hang the system like this, waiting for an event.
Your change of dtheta/2 to dtheta/4 should make no difference, unless you are keeping track of position with floating point variables, such as:

Code:
float dtheta = 360/4096.0;
float position;
float old_position;
int delta_count;
position = old_position + (delta_count * dtheta); // where delta_count can be +/-

If you're doing something like that, your position variable will accumulate error over time due to lack of precision of the floating point variables. It will be okay for a while, but with each edge, you'll accumulate a tiny bit more error in the position, and eventually you'll be off by more than dtheta (1 count).
For angle I use integer count, modulo 4096, times 360/4096. I don't accumulate the floating point values because of error accumulation.
 
Luni removed them from the code base.
I’ve fixed the error callback in EncoderTool - you can find it here if you want to use it. Note that when an error occurs, the normal callback is not executed (in either my fix or the original code).

I’ve been playing with comparing EncoderTool and QuadEncoder output, using a Teensy 3.2 to generate quadrature signals and feeding those to a Teensy 4.1. As expected, it’s entirely possible to get missed edges using EncoderTool, though only at ridiculously high RPM rates given my minimal callback functions.

About the only vaguely interesting insight is that the T3.2 occasionally delays a signal edge (due to its own interrupts delaying the IntervalTimer callback), and if that coincides with a delayed EncoderTool interrupt then counts go missing. Otherwise all is well.

The real-life equivalent is that the spindle encoder won’t be perfect, with exact 360°/4096 spacing between all its edges. You might expect its spacing inaccuracies to repeat, so knowing the worst-case spacing might help decide what the maximum allowed callback duration might be. You also need to know the longest ISR latency: that depends on your system, my simple setup is probably only about 2.5us - more instrumentation needed here to capture the rare events.
 
Here's a screen capture of an error event - edges are nominally spaced by 3µs, giving a 4883rpm speed
1754209589827.png

QA is delayed by about 1.6µs, and the EncoderTool ISR is also delayed by about 1.3µs, so the QB edge has already occurred by the time it executes, resulting in an apparent error. Interestingly, we do get two error callbacks, so for this setup (using pins 14 and 15 for EncoderTool) we don't actually miss any edges, though we're unsure of the real timings of the errored ones.

Another thought occurred. Presumably it's entirely deterministic, before a thread cut starts, which spindle edges will produce a stepper output change. So the callback only needs to check a flag to see if a change is required, do the change, and indicate "flag used". The (possibly long-winded) computations of edge to change values could be done outside the callback, and queued ready for use - probably not all in one go, but in large enough batches that the callback is never starved of data. If the error callback is interpreted as a normal one, the above capture suggests edges won't be lost, though the stepper might occasionally be a few microseconds late in stepping.

Maybe you do this already...
 
I’ve fixed the error callback in EncoderTool - you can find it here if you want to use it. Note that when an error occurs, the normal callback is not executed (in either my fix or the original code).

I’ve been playing with comparing EncoderTool and QuadEncoder output, using a Teensy 3.2 to generate quadrature signals and feeding those to a Teensy 4.1. As expected, it’s entirely possible to get missed edges using EncoderTool, though only at ridiculously high RPM rates given my minimal callback functions.

About the only vaguely interesting insight is that the T3.2 occasionally delays a signal edge (due to its own interrupts delaying the IntervalTimer callback), and if that coincides with a delayed EncoderTool interrupt then counts go missing. Otherwise all is well.

The real-life equivalent is that the spindle encoder won’t be perfect, with exact 360°/4096 spacing between all its edges. You might expect its spacing inaccuracies to repeat, so knowing the worst-case spacing might help decide what the maximum allowed callback duration might be. You also need to know the longest ISR latency: that depends on your system, my simple setup is probably only about 2.5us - more instrumentation needed here to capture the rare events.
Wow, thanks for doing that! I will try it out.

I ran my initial tests are similar speeds, and thought with real world hardware, (much lower RPM) I'd be relatively safe from the error condition. Perhaps not.
 
Here's a screen capture of an error event - edges are nominally spaced by 3µs, giving a 4883rpm speed
View attachment 37936
QA is delayed by about 1.6µs, and the EncoderTool ISR is also delayed by about 1.3µs, so the QB edge has already occurred by the time it executes, resulting in an apparent error. Interestingly, we do get two error callbacks, so for this setup (using pins 14 and 15 for EncoderTool) we don't actually miss any edges, though we're unsure of the real timings of the errored ones.

Another thought occurred. Presumably it's entirely deterministic, before a thread cut starts, which spindle edges will produce a stepper output change. So the callback only needs to check a flag to see if a change is required, do the change, and indicate "flag used". The (possibly long-winded) computations of edge to change values could be done outside the callback, and queued ready for use - probably not all in one go, but in large enough batches that the callback is never starved of data. If the error callback is interpreted as a normal one, the above capture suggests edges won't be lost, though the stepper might occasionally be a few microseconds late in stepping.

Maybe you do this already...
Interesting. Don't have a logic analyzer, which complicates things (a lot). Being a few usec off in stepping shouldn't be an issue, since it's a small error compared to the rotational time. But the step must occur on the correct edge. A step could occur on either A or B edges, depending on the thread parameters. Sometimes the thread parameters are odd (as opposed to even numbers).

Not sure how to recover from the 2 Error callbacks. Have any suggestions?
 
Interesting. Don't have a logic analyzer, which complicates things (a lot). Being a few usec off in stepping shouldn't be an issue, since it's a small error compared to the rotational time. But the step must occur on the correct edge. A step could occur on either A or B edges, depending on the thread parameters. Sometimes the thread parameters are odd (as opposed to even numbers).

Not sure how to recover from the 2 Error callbacks. Have any suggestions?
You could consider one of these. They can be bought at many places i.e.
I bought one from the first location and have found it most useful.
 
Turned on LED upon entry to encoder ISR and turned it off at exit. Normal operation, encoder ISR executes in 711ns. During thread to stop, it doubles to 1.42us. Put a scope on the pin, ran statistics on the triggers. 5000 trials. Also had persistence on to verify the long pulses. This sort of makes sense as some of the time, I have to pulse the stepper. This means I call a function from within the ISR to 1) change the digital output and 2) start a one shot timer to turn off the pulse. The turn off (for the stepper pulse) is after the encoder ISR exits. So far, nothing surprising, at least if I just look at a single pulse on the scope. 3 pulse view seems the same. 711ns --> 1.42us during thread to stop.
 
Not sure how to recover from the 2 Error callbacks. Have any suggestions?
It rather depends on whether you experience bounce on the spindle encoder.

If there's no bounce, or the duration is short enough, you could probably regard the error callbacks as wrongly-placed normal callbacks (i.e. do any stepper pulses as usual), but adjust the count within the callback "knowing" the spindle only ever goes in one direction at a time. Or of course use QuadEncoder to correct the count, rather than more-or-less guessing.

The other option is to adopt the suggestion made by @joepasquariello, for example in post #129, and drop EncoderTool altogether, poll QuadEncoder using a very fast IntervalTimer, and call your existing callback whenever the QuadEncoder reading changes in the expected direction, maybe with a sanity check to weed out encoder bounces (e.g. backwards one step 1µs after the previous step gets ignored).
 
1.42µs doesn't sound ruinous, at normal RPM. That suggests there is encoder bounce, or that your system has longer interrupt blocking than observed on a simple test system, or that some encoder edges are perilously close together compared to the nominal 360°/4096 value.
 
1.42µs doesn't sound ruinous, at normal RPM. That suggests there is encoder bounce, or that your system has longer interrupt blocking than observed on a simple test system, or that some encoder edges are perilously close together compared to the nominal 360°/4096 value.
1.42us is about what I expect, since I call a function to do start the step and to trigger a one shot timer, I then return to the encoder ISR and finish up. The one shot timer priority is lower than the encoder.

My lathe can only go 2200 RPM. That would be 6.6us/interrupt, assuming nominal conditions. I'd never consider trying to thread to stop at those rates, it would be terrifying. I'd have to have HW interlocks to prevent damage. I'd consider 400 RPM to be my fastest I'd consider thread to stop. I can certainly prevent operation - and alert the operator that operation is blocked above some RPM. As the developer - I don't have to obey my rules...

In any case, the one shot timer is for about 4us, and all the timer ISR callback does is turn off the stepper pulse. The encoder should return long before the timeout. Even if it doesn't, the stepper driver doesn't care, as the step is initiated on the start of the pulse, not the end.

EncoderTool supposedly uses Bounce2, but I can't follow Luni's code - I don't understand where he invokes it.

It could be something else is blocking - all I have to do is find it.:) Haven't found it in many months of searching, but someday I'll find it. Probably slap myself when I find it. Undoubtedly some dumb thing I did.

My simulator is limited. At 400 RPM AND requesting 13 TPI threads, the 3D printer stepper motor on my desk cannot start instantaneously at the requested (and required) step rate. It would need accel on start up, which is really tough to work out AND be synchronous with the spindle. But 20 TPI threads do work. The stepper on the lathe is quite a bit more capable and can do it. It will do 10 TPI threads at 400 RPM.
 
Thanks, I'll get one of these soon. Looks handy for checking some of these kinds of issues.
You can get the same thing on Amazon for £10 ($13.50) in the UK. I guess the speed of delivery outshines the saving of a few £ or $.
 
Laughing, and crying at the same time. Turning on logging really messes things up! See the count slipping, a lot. Going to have to give up on EncoderTool, over the course of a log, saw a slips over 90 degrees. Even if I reduce the logging overhead, it's still showing up.

I record the count and the modulo count to PSRAM every ISR, if the index = 0. (Have an inverted input.). At startup I wait for the index=0 and set the spindle count = 0. I may not sync it perfectly, but it's moving around way too much.

It's strange to see it messing up like this, from the timing, it seems like it would be fine. But the count is the count. And the index ought to show up at the same count, every time. The persistence on the scope shows an occasional long ISR's as well. (With logging.). Now logging can mess up ISRs. PSRAM is relatively slow, but I'm only writing an integer to it during the ISR.

Think this is enough of a sign for me.

About the only other thing it could be some (unknown) ill behaved library (or that library uses something ill behaved). Think I'm convinced enough to move onto a HW approach for the encoder. Think I've beat the dead horse long enough.
 
It's really simple to determine your worst case ISR execution time. Just add a few variables and a bit of code on entry/exit of your handler. I haven't used EncoderTool, but I assume its signal debouncing and quadrature counting before it calls your handler, and if so your measurement won't include the time for that stuff. You could do this measurement with/without logging, and I think it will show that your worst case time is much worse than you think, and you'll know your starting point if you move on to QuadEncoder.

Code:
// macro to convert ARM cycles to microseconds
#define CYC_TO_US(cyc) ((cyc)*(1e6/F_CPU_ACTUAL))

// variables to track min/max time
volatile uint32_t cyc_min, cyc_max;

// init min/max before test run
cyc_min = 999999;
cyc_max = 0;

// track min/max handler execution time
void handler(void) {
  uint32_t handler_start = ARM_DWT_CYCCNT;
  // your existing code
  uint32_t cyc = ARM_DWT_CYCCNT - handler_start;
  if (cyc < cyc_min)
    cyc_min = cyc;
  if (cyc > cyc_max)
    cyc_max = cyc;
}

// print min/max after test run
Serial.printf( "min = %1.3lf  max = %1.3lf\n", CYC_TO_US(cyc_min), CYC_TO_US(cyc_max) );
 
It's really simple to determine your worst case ISR execution time. Just add a few variables and a bit of code on entry/exit of your handler. I haven't used EncoderTool, but I assume its signal debouncing and quadrature counting before it calls your handler, and if so your measurement won't include the time for that stuff. You could do this measurement with/without logging, and I think it will show that your worst case time is much worse than you think, and you'll know your starting point if you move on to QuadEncoder.

Code:
// macro to convert ARM cycles to microseconds
#define CYC_TO_US(cyc) ((cyc)*(1e6/F_CPU_ACTUAL))

// variables to track min/max time
volatile uint32_t cyc_min, cyc_max;

// init min/max before test run
cyc_min = 999999;
cyc_max = 0;

// track min/max handler execution time
void handler(void) {
  uint32_t handler_start = ARM_DWT_CYCCNT;
  // your existing code
  uint32_t cyc = ARM_DWT_CYCCNT - handler_start;
  if (cyc < cyc_min)
    cyc_min = cyc;
  if (cyc > cyc_max)
    cyc_max = cyc;
}

// print min/max after test run
Serial.printf( "min = %1.3lf  max = %1.3lf\n", CYC_TO_US(cyc_min), CYC_TO_US(cyc_max) );
Thanks. I'm moving on to QuadEncoder, or some routine like that. Saw enough variation in EncoderTool counts to convince me there's a problem with it. It's weird, it seemed awesome in the beginning, but there's apparently some kind of issue since the count is varying for something that should be invariant. The index pulse hole in the etched stainless steel wheel just isn't moving relative to the other etched holes, but EncoderTool seems to think so on my platform. Still could be my bug, but I've put in way too much effort and I'm not getting anywhere. Time for a change.
 
EncoderTool supposedly uses Bounce2, but I can't follow Luni's code - I don't understand where he invokes it.
It only uses Bounce2 for the encoder button (the library appears to be intended for UI controls, not really for machinery).

Agree, it looks like fighting EncoderTool is only ever going to be a losing battle...
 
Just had another thought. In EncoderTool, does the on change callback inherit the priority of the pin interrupt? If it's only priority 128, then it gets interrupted by everything else. Every once in a while I see a long duration ISR and I don't know why. I set the pin interrupts to be 32.

I didn't uncover how EncoderTool sets the ISR priority. Grepping the library shows the attachInterrupt, but I don't see setting NVIC priority anywhere. If the ISR priority is 128, well it will get interrupted by darn near everything, which is the opposite to what I want. I do see occassional long ISR's and I can't explain it unless the priority is much lower than expected.
 
Last edited:
I do see occasional long ISR's and I can't explain it unless the priority is much lower than expected.

Curious what you measure for a long ISR? Maybe it's a clue?

Curious also what is the range of ratios of encoder edges to stepper micro-steps for common threads? If it's at least 2, I'm wondering if the QuadEncoder interrupt on position match could be used. On each interrupt you would do a step, use bresenham to compute the number of encoder edges until the next step, and set the new match value.

I've been looking at EncoderTool, and I could be wrong, but it seems to do the pin setup at a low level, rather than call pinMode, because it supports multiple architectures. Perhaps also supporting interrupt priorities for those architectures was too much, so he left that out. My guess is if you set an interrupt priority after calling EncoderTool::begin(), it will work as expected. Thinking of how to test that.
 
Last edited:
Thinking of how to test that.
I tried last night to read the current interrupt priority level from inside the callback, but was only getting nonsensical answers. I think it should be right to get the currently-executing vector from ICSR or IPSR, then look up its priority in the NVIC table.
 
Curious what you measure for a long ISR? Maybe it's a clue?
Sometimes 8us, have captured over 33us on my scope using persistence.
Curious also what is the range of ratios of encoder edges to stepper micro-steps for common threads? If it's at least 2, I'm wondering if the QuadEncoder interrupt on position match could be used. On each interrupt you would do a step, use bresenham to compute the number of encoder edges until the next step, and set the new match value.
It varies from ~2:1 to 50:1 ball park figures. I need to look it up. I thought about using position match, seemed like a bit of overhead, but it seems like it could be used.
I've been looking at EncoderTool, and I could be wrong, but it seems to do the pin setup at a low level, rather than call pinMode, because it supports multiple architectures. Perhaps also supporting interrupt priorities for those architectures was too much, so he left that out. My guess is if you set an interrupt priority after calling EncoderTool::begin(), it will work as expected. Thinking of how to test that.
I'll check where I set nvic priority, don't remember if before or after the begin statement.
 
Back
Top