0
\$\begingroup\$

I have a motion profile generator which calculates a position 1000/sec. During the decelerating portion of the move, the following calculation is used:

pos = pos - accel_2 + vel; 
vel = vel - accel;

Where all variables are doubles, pos is calculated position, accel is acceleration, vel is speed, and accel_2 is acceleration/2.

The end of the move is when either vel <= 0 or pos >= target position.

I've calculated the point at which deceleration starts correctly, such that by the time velocity is 0, we should be at the target position.

I've tested the profile generator on a desktop computer and can confirm that it works as expected (of course the position may exceed its target by a fraction, but precision is finite, and the error is insignificant).

However, when run on my LPC1768 (using mbed OS), I find that the generated values differ to those generated by the same code on my desktop (i.e. position reaches it's target before deceleration has completed).

When graphed for speed and position, I can see that speed drops off noticeably towards the very end of the move, as position reaches its target. This causes a slight "skew" in the position/time graph as deceleration completes.

I tried changing the profile termination condition such that we only check that vel <=0, therefore allowing speed to to finish decelerating, which results in a more symmetrical and smoother looking position/time graph, although as mentioned above, position overshoots its target.

I've observed that all of this seems to be less of an issue with larger values for accel, speed and position, which leads me to believe that this may be a precision issue with double. Are doubles represented with 64-bits in the LPC1768, and are they implemented in the same way as other platforms?

What else could be causing this?

Below is a position/time graph, where the first profile compares position with target, and the second one doesn't. Notice the difference at the very end.

enter image description here

\$\endgroup\$
12
  • 2
    \$\begingroup\$ At least two things come to mind: (1) Any time you are taking floating point differences, where the values may be close in magnitude, you can lose a great deal of precision. It only takes a moment with paper and pencil to see why. This goes doubly, if it turns out the size of your FP is smaller on the MCU; and, (2) Floating point library code may be notoriously bad, just generally. So you should run some numerical tests. There are free source code choices out there. Run them on both compilers and check the results. \$\endgroup\$
    – jonk
    Commented Sep 17, 2019 at 1:14
  • \$\begingroup\$ Very difficult to tell if nobody can see your implementation. Post the trajectory planner routine, after all this is a place where knowledge sharing is welcome. Many others don't even know where to start when a trajectory is needed, so it could be a big help for them. \$\endgroup\$ Commented Sep 17, 2019 at 6:22
  • \$\begingroup\$ @Jonk, not sure what you mean by floating point library, I'm just using double. In reality, the lack of precision isn't so much an issue as I will be using large values. It's more that I want to understand why there exists a difference between the two platforms and whether this is down to the way floating-point is represented internally. Would the fact that inaccuracies are less as the values increase rally with that? Is it common for platforms to represent doubles differently? \$\endgroup\$
    – 19172281
    Commented Sep 17, 2019 at 7:52
  • 1
    \$\begingroup\$ @19172281 Don't use floating point in embedded applications unless a serious need for extremely wide dynamic range of values (which is what it is designed to handle.) See What Every Computer Scientist Should Know About Floating-Point Arithmetic. When I do choose to use FP, I always verify my compiler performance with it and I always take care in designing my algorithms to account for its weaknesses (differences of similar values.) For example, you should sort values before computing std-dev sums. Read a Numerical Methods book. \$\endgroup\$
    – jonk
    Commented Sep 17, 2019 at 18:58
  • 1
    \$\begingroup\$ @19172281 If your data-in is integer and your data-out is integer, there's no need at all for floating point. I usually look very closely at the ADC values coming in (always integer format), the mathematics involved in-between, and the DAC values going out (always integer, as well.) I then run carefully crafted simulations for the mathematics involved between, while also looking over the dynamic range required. Given that long long int seems to cover your bases, I'd stick with that. So long as you are careful to check the size on various targets, of course. Integer just works right. \$\endgroup\$
    – jonk
    Commented Sep 19, 2019 at 7:29

1 Answer 1

3
\$\begingroup\$

For embedded work, I rarely use floating point variables. There are a lot of good reasons to avoid it. But many years ago, David Goldberg wrote an article that covers many of the reasons why I avoid them: What Every Computer Scientist Should Know About Floating-Point Arithmetic.

In the integer domain, \$a\cdot\left(b+c\right)=a\cdot b+a\cdot c\$. But this isn't true for floating point. (It may be sufficiently close to true for some uses. But in general, you cannot bank on it.)

If you are dealing with a wide dynamic range of values, for example the mass of different particles in the universe which may vary from molecules to super-massive black holes, then by all means use floating point variables. That's what they are designed for and they work as well as is possible for such needs. If you need, and cannot plan to write your own integer versions [I will often write an ln() function designed for integers, preferring that over using a library function requiring floating point] of some complex function that only accepts and generates floating point, then that may be another good reason to use floating point. (Not because it's better, but because you don't have to the time or skills to do otherwise.)

But if your dynamic range can be managed within reasonably sized integers, that's almost always better. Also, keep in mind that in most embedded cases, you are getting analog data from ADCs (which produce integer results) or other digital inputs such as switches and are producing analog DAC outputs (consuming integers) or other digital outputs like an LED. If the data coming in is in integer format and the data going out is also in integer format, why in the world would you then want to introduce floating point during the in-between processing? It would generally be better if you would just stick to one domain, then.

Floating point has a bad problem with differences that can crop up in very unusual ways. For example, let's say you are working with values that possess a relatively wide dynamic range and you need to calculate the standard deviation:

$$\sigma=\sqrt{\frac{\Sigma \:\left(x_i-\overline{x}\right)^2}{N}}$$

The above requires first to compute \$\overline{x}\$, which requires one pass through the data, and then the above calculation requiring a second pass through the data. Often, it's not even possible to do it that way because the data is coming in fast, there's lots of it, and there's only a small bit of RAM for buffering. So one common optimization, found with a very few simple algebraic steps, uses two summations:

$$\sigma=\sqrt{\frac{\Sigma \:x_i^2}{N}-\frac{\left(\Sigma\: x_i\right)^2}{N^2}}$$

(Let's leave aside questions about full and sample populations.)

The above just requires two summations, which can be performed as "running sums" and only one pass through the data so there's no requirement to buffer it. Seems like a good idea?

Well, maybe. Maybe not.

Suppose the situation just happened to be the worst possible case. \$x_0\$ is the largest magnitude value in the set and the remaining values are magically sorted from lowest to largest, so that \$x_1\$ is the smallest value and \$x_{N-1}\$ is the 2nd to the largest value (remember, \$x_0\$ is the largest.) In this odd case, once the largest value (\$x_0\$) is summed, then the following tiny values will "round out" and won't accumulate. They are too small for the format. So the largest value encountered first causes many other values to wash out before they have a chance to accumulate into something that might have affected the lower order bits.

Now look at the above equation. It's the difference of two values. If those two terms (prior to taking the square root) are nearby values, which can be the case, then the subtraction will destroy many of the higher order bits leaving only a very few precision bits near the bottom of the format. (And still worse, should the algorithm be unfortunate enough to have generated a large running sum before encountering small values, still fewer useful precision bits may remain.) The resulting calculation may be very poor and not nearly as good as it might have been had the author been more fully aware of the pitfalls and designed for them.

Had the numbers been pre-sorted, smallest to largest, then the summations would allow the smaller values to accumulate sufficiently to impact the lower order bits of the final sum and preserve more of the precision, than otherwise. But, of course, this now means buffering the values, too.

The point here is that every time you use floating point, you have to engage yourself in analyzing the algorithm details far more than you might have to, with integers. So I generally avoid floating point whenever possible. It's just easier that way. If I use floating point, I will generate exhaustive analysis of the algorithms designed to demonstrate confidence in them across a variety of situations. (Like any engineer should do.)

There also are generic floating point testing suites, some I think available as open source code you can compile yourself, that will help vet library routines and how the compiler processes and optimizes the code. That won't address itself directly to your situation, but it can give you clues about 'gotchas' to consider when thinking through your situation.

Compilers usually first turn expressions into tree structures, and then early-optimized into DAGs (directed acyclic graphs) to include as part of a basic code block. How all these details get optimized both early on, as well as later, can vary quite a bit. And none of that optimization is going to be aware of your specific numerical situation. So the choices made by a compiler are fairly "blind" to your needs and often may not serve your situation well. (Determining that fact is your job, if you choose floating point.) Again, these caveats and/or extra analysis and evaluation time needed become yet another reason to simply avoid floating point in the first place.

So my general recommendation, without knowing specific application details, is "don't use floating point in embedded applications." Of course, granting the exceptions I discussed earlier where floating point may be required. Also, there are those trivial times when it doesn't really matter that much. For example, if you have an integer ADC value that needs to be displayed on some 7-seg LED display as a decimal point value (Celsius, for example?) and a simple expression that uses floating point saves you some "thinking" about the integer and just makes it easier without risking anything important. (I'm still tempted to just work out the integer details and avoid floating point. But time is time and if the customer is paying for it and I know this choice won't harm them, then I very well might use floating point and associated libraries at the "end point" of a chain of computations for display purposes.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.