While the Arduino library does an excellent job of hiding some of C/C++’s warts, at the end of the day, it is still just C/C++. This fact causes a few non-intuitive issues for inexperienced programmers. When it looks like Arduino math is wrong, it is probably one of these reasons.
When people ask me for help with their programming, I check each of these Arduino math mistakes. If your code seems to be hitting a bug, check to make sure it is not how the compiler handles math.
Before I start, I want to point out that these issues are not “Arduino” issues. These are related to how C compilers handle variables and math. With that said, I am no compiler expert so these are all learned from experience.
Before getting into why Arduino math does not work, I need to cover a brief explanation on variable types. If you know the difference between integer and float, you can skip down to the next section.
Computers have a difficult time with decimal numbers. Binary representations work great for integers like 1,2,3,4,5, etc. Decimal points, however, add complexity to the situation. For that reason, numeric variable types are known as “integer” or “floating point.” Integer types can only handle, well, integers, while floating point can hold decimal numbers.
#1 Long Arduino delay()s don’t work
Skipping right passed delay() vs. millis(), a common action is doing a delay that lasts hours or days. This problem seems to come up with projects like sprinklers or aquariums. Instead of doing the math ahead of time, you might try letting the computer do the work for you.
// 1000 ms, 60 sec, 60 min, 24, hours, 14 days unsigned long wait = 1000 * 60 * 60 * 24 * 14; Serial.print("wait = "); Serial.println(wait); // Prints wait = 2048
The code seems logical and straightforward. It should calculate the number of milliseconds in fourteen days, which is: 1,209,600,000 milliseconds. An unsigned long can hold a value up to 4,294,967,295. So there should be no problem there.
If an unsigned long can hold the value, why did Arduino math calculate 2048? Well, even though you plan to store the result of the math operation in an unsigned long variable type, the compiler does not care. It performs the operation on the right side of the equal sign before storing the value into the desired variable.
The way that C/C++ handles this situation is that the constants, “1000”, “60”, “24”, and “14” will all fit into an integer data type. So it performs the multiplication as “integer math.” Which means storing each intermediate math operation in an integer. Eventually, those results overflow variable as small as an integer.
If we tell the compiler to treat the constants as an unsigned long, instead of a teeny integer, then the intermediate results are stored in a larger variable type. An easy way to promote the operation is to use the modifier “UL” like below.
// 1000 ms, 60 sec, 60 min, 24, hours, 14 days unsigned long wait = 1000UL * 60 * 60 * 24 * 14; Serial.print("wait = "); Serial.println(wait); // Prints: wait = 1209600000
You need to be careful where you place the “UL.” If you place it at the end, the result is still an overflow because of the order in which the math occurs. You could also add “UL” to all the constants. More on that in a bit.
unsigned long wait = 1000 * 60 * 60 * 24 * 14UL; // Prints: wait = 329728
By the way, pay attention to compiler warnings. It may try to tell you about this issue:
warning: integer overflow in expression [-Woverflow] unsigned long wait = 1000 * 60 * 60 * 24 * 14;
There are other methods to force wider data types to be used. This method is my favorite. Next, we look at a related issue that occurs when mixing integers and floats.
#2 Why does Arduino division result in a zero?
Look at this code example. What do you expect the serial monitor to print?
float example = 3 / 5 * 2; Serial.print("example = "); Serial.println(example); // Prints: example = 0.00
You might assume that the serial monitor prints “1.20”. Afterall, that is the answer you get when you plug it into your calculator. However, the Arduino math comes back with “0.00”. Why does that happen?
Similar to the example above, all of the constants on the right of the equal sign are integers. So each intermediate math operations get stored an integer data type.
A straightforward way to fix this problem is to tell the compiler that one of the constants is a floating point variable. Just adding “.0” promotes the operation to floating point math. Later I’ll talk about preference order which makes a difference on where you add the .0. If you want to be safe, add it to all integer constants.
float example = 3.0 / 5.0 * 2.0; Serial.print("example = "); Serial.println(example); // Prints: example = 1.20
What happens if the variable “example” changes into an integer? The decimal point does not get stored. Let’s look at two examples.
#3 Arduino math does not Round
Starting off, here is the same math operation from above but with “example” declared as an integer instead of a float.
int example = 3.0 / 5 * 2; Serial.print("example = "); Serial.println(example); // Prints: example = 1
An important thing to realize is that when doing decimal math that ends up in an integer, the decimal is truncated. The decimal is not rounded. For the second example I changed the math a little bit.
int example = 5.0 / 3; Serial.print("example = "); Serial.println(example); // Prints: example = 1
Even though the result is actually “1.666…”, Arduino math returns “1” not “2.” The integer does not get rounded up (or down) based on the decimal. The code truncates or drops, the decimal entirely. (Note that there are rounding functions are available in Math.h.)
#4 Arduino’s float Precision
Floating point variables mean that the decimal point can float around. The precision of the number is limited to a certain number of digits. That is not the same as saying a certain number of decimals. In the case of 8-bit AVR Arduino boards, the float type is 32-bits which makes the limit 6 or 7 digits.
float example1 = 1.23456789; float example2 = 123456.789; Serial.print("example1 = "); Serial.println(example1,9); Serial.print("example2 = "); Serial.println(example2,4); // Prints: // example1 = 1.234567880 // example2 = 123456.7890
Here is the result from my serial monitor.
Even though “example1” and “example2” have the same number of digits, the decimal point is in a different place. Notice that example1 lost a tiny bit of precision at the end of the digit string. The last valid digit is an “8” and not a “9”.
Meanwhile, “example2” was okay.
You might be thinking that a double is better than a float since the variable type is twice as big. On the 32-bit based Arduino platforms, this point might be the case. However, on the 8-bit AVR based boards, the avr-gcc compiler does not support the double type. So a float and a double work the same.
#5 Order of operations
I do not want to turn this into a comment-bait Facebook post. C does have a defined operator preference. Basic operators occur in the order of Multiplication and division, then addition and subtraction. Check this reference for a complete list of operator precedence. Parenthesis, of course, is calculated first.
Based on what you learned in this post and knowing there is a specific order of operations, what value does this code generate? The answer may shock you!
int example = 3 * ((5 + 2) / 4) - 1; Serial.print("example = "); Serial.println(example);
Keep parenthesis and the order of operations in mind when you try to promote constants to larger variable types. You need to add “UL” or “.0” to the first operation to make sure the entire operation gets promoted.
These are the most common issues I have run into with Arduino math. In most cases the math is working fine, it is the compiler doing something I did not expect. Next time your math seems to be off, double-check these points. Or float-check them!
Question: What Arduino or C-related math ‘issues’ have you run into wiht your code? You can leave a comment by clicking here.
You might also want to check out my related post, Arduino Myths everyone believes but are not true.. It explains why I call it the “Arduino Library” and not its own language.