raspberrypi / pico-examples

BSD 3-Clause "New" or "Revised" License
2.62k stars 779 forks source link

Quadrature encoder example: too many ticks? #416

Closed torlarse closed 10 months ago

torlarse commented 10 months ago

I'm getting what I believe is way too many encoder ticks from my DC motors. I'm referring to my RPi forum thread, which understandably has vaned off in attention.

TLDR: I hooked up my DC motor encoders to the Pico, and turned one revolution by hand. It consistently yields 41,500 ticks for one output shaft revolution. Motor data: max 240 RPM, gear ratio 1:34.

https://forums.raspberrypi.com/viewtopic.php?t=355234

lurch commented 10 months ago

ping @pmarques-dev who contributed this code in #126

pmarques-dev commented 10 months ago

Sometimes the encoder counts are specified in terms of "full steps", but the PIO code will count a step on any transition, so it ends up counting 4 steps per count. Note that is doesn't mean that any small noise will produce spurious counts, as a noise in one of the phases will just make the count increment and immediately decrement, as the PIO code implements the proper state machine that can count bidirectionally on every step.

I would leave the max rate parameter at zero, to let the PIO run at full speed to make sure there are no aliasing issues. The only advantage of setting it to a lower value is to save some power, especially on battery powered devices.

From your numbers, 41500 steps per output shaft revolution -> 41500 / 34 = 1220 steps per motor revolution, which would correspond to a 300 counts per turn encoder attached to the motor. If it's a high quality motor, that is not an unusual. The kind of cheap DC motors with gears we use in low cost robotics kits will have 11 counts per turn or similar. Also, at max speed of 240 RPM, that would give around 166.000 steps per second, which the PIO code should be able to handle just fine.

If you still can't get that to work, I would suggest trying a couple of things:

torlarse commented 10 months ago

I found the motor info here. The manufacturer claims that the encoder yields 360 counts per revolution, whilst I measured ~305 CPR by hand. Your PIO code information and estimates check out, thanks!

The deltas do work in my controller. I'm not familiar dealing with two's complement arithmetics, is there any "risk" of dividing the transition values by 4, just for the sake of steps-to-counts conversion?

pmarques-dev commented 10 months ago

As long as you take the full delta and then divide by 4, you should be alright. Just don't divide the output of the PIO code by 4 before taking the delta, as that would mess up the computation.

Note that by doing that you're losing some resolution, which is probably ok in your case since you have plenty of that, although you don't really need to. But in general, especially with low cost encoders, you'll want every step possible, since otherwise at low speeds you'll get a larger quantization error in the speed (or position) estimate. I have even submitted a new pull request with a new version that uses not only the steps but the timing of the transitions to get a good speed estimate even at very low "steps per sample" regimes.

In any case, my advice would be to not bother dividing by 4 and instead compute the floating point constant that converts from steps into whatever unit you're going to work with in the end and just multiply the delta by that. For instance, say you want the output of your system to be in rpm: one turn 360 4 34 = 48960, so if you get a count of 48960 in one minute you have 1 rpm. Since you are measuring once a second, you the count to be 1/60 of that per sample to be at 1 rpm, so 48960 / 60 = 816 steps per sample is 1 rpm.

So if you multiply the delta you get by (1.0f / 816) you get a floating point number in rpm directly from the step delta. The compiler will do that computation at compile time for you. You can even write (1.0f / ((360 4 34) / 60)) if you want to document how you arrived at the number in the code. Something like:

float speed_rpm = delta (1.0f / ((360 4 * 34) / 60));

That is just one floating point multiplication to get you from the PIO output to the unit you want. Note that this will work for any other unit you want: radians per second, meters per second (e.g. this is a wheel of a robot), etc.

torlarse commented 10 months ago

Thanks so much for your advice and effort. I want to compute radians per second, 10 times per second, and chose

double angular_velocity = delta_steps * 2 * M_PI *  (1.0 / ((48960)/ 10));
pmarques-dev commented 10 months ago

I would actually write that expression like this: float angular_velocity = delta_steps * (2 * M_PI * (1.0 / (48960.0 / 10))); i.e., with the constant part written completely inside parenthesis to make sure the compiler is allowed to compute it at compile time. Without the parenthesis, the compiler will have to multiply delta_steps by 2, then by M_PI and then by a constant, leaving 3 multiplications to be done at runtime. Although mathematically they seem the same, in terms of numerical precision they may produce slightly different results and the compiler is not allowed to optimize in that case. Also, the (48960 / 10) part is rather risky, because although in this case the reminder is zero, using floating point here would be more safe in general (i.e., change 48960 to 48960.0 to force the compiler to use floating point). And last, unless you really need the extra precision from the double type, I would stick with "float" type for performance reasons. Note that everything inside the parenthesis will be evaluated at compile time, so it can use doubles to get the best possible constant.

torlarse commented 10 months ago

Thanks much for those additional compile time and precision details I was not aware of, really appreciate it. The only risk I see to this approach, is to change PID loop time interval, and forget to change the denominator :)

pmarques-dev commented 10 months ago

just define it as a constant and use it both to set the timer and in the expression. Something like:

#define PID_FREQUENCY  10    // control frequency in Hz

....
float angular_velocity = delta_steps * (2 * M_PI * (1.0 / (48960.0 / PID_FREQUENCY)));
...

or even:

const int PID_FREQUENCY = 10;

in case you're not a big fan of pre-processor defines. This is still all resolved at compile time anyway.