Arduino Memory Saving Tips How To Avoid Using Floats
Hey guys! Running out of memory on your Arduino projects? It's a common problem, especially when you start doing more complex stuff. One major memory hog is using float
variables. You're right, including floating-point calculations pulls in a whole library (or parts of it), and that can eat up precious space, especially on smaller boards like the Arduino Uno or Nano. So, what can we do to avoid float
and reclaim some memory? Let's dive in!
Why Floats are Memory Hogs on Arduino
First, let's quickly understand why floats are so memory-intensive on Arduinos. Unlike integers, which can be directly represented in binary, floating-point numbers need a more complex representation (usually following the IEEE 754 standard). This involves storing the sign, mantissa, and exponent, requiring more bytes per variable (4 bytes for a float
compared to 2 bytes for an int
). But it's not just the variable size; the real kicker is the library. Arduinos don't have native floating-point hardware, so the compiler has to include software routines to perform floating-point arithmetic (addition, subtraction, multiplication, division, etc.). These routines take up a significant chunk of program memory (flash) and working memory (RAM). Therefore, avoiding floats will free up valuable resources and allow you to add more features to your projects.
Strategies to Avoid Using Floats
Okay, so we know floats are memory-hungry. The big question is: how do we avoid them? Don't worry, there are several techniques you can use to achieve the same results without sacrificing memory. The trick is to think about how you're using floats and find integer-based alternatives. Let's explore some common scenarios and solutions:
1. Integer Math with Scaling
This is the most common and versatile technique. The core idea is to perform all your calculations using integers but represent fractional values by scaling them up. Think of it like working with cents instead of dollars – you're still representing the same value, just with a different unit. Here's how it works:
- Choose a Scale Factor: Decide on a power of 10 to use as your scale factor (10, 100, 1000, etc.). The larger the scale factor, the higher the precision you'll retain, but also the larger the numbers you'll be dealing with, so you might need to use
long
integers (4 bytes) instead ofint
(2 bytes). You should choose an appropriate scale factor according to your accuracy requirements. For example, if you need to represent a temperature with a precision of 0.1 degrees, a scale factor of 10 would be sufficient. If you need 0.01-degree precision, a scale factor of 100 would be more suitable. Also, make sure that the scaled values do not overflow the integer data type you are using. - Multiply Inputs by the Scale Factor: When you receive an input value that would normally be a float, multiply it by your scale factor and store it as an integer. For example, if you are measuring voltage and the reading is 3.14 volts, with a scale factor of 100, you would store it as 314.
- Perform Integer Calculations: Do all your arithmetic operations using these scaled integer values. Remember to consider the scale factor when performing operations. For example, when multiplying two scaled values, the result will be scaled by the square of the scale factor, so you will need to divide the result by the scale factor to get the correct scaled result.
- Divide for Display (if needed): When you need to display or output the result, divide it by the scale factor to get the original fractional value. When printing the result, you can manually insert the decimal point at the correct position.
Example:
Let's say you want to calculate the average of two temperature readings, and you need a precision of 0.1 degrees. You can use a scale factor of 10.
const int SCALE_FACTOR = 10;
int temp1 = 255; // Represents 25.5 degrees
int temp2 = 268; // Represents 26.8 degrees
long sum = (long)temp1 + temp2; // Use long to prevent overflow
int average = sum / 2; // Integer division
Serial.print("Average temperature: ");
Serial.print(average / SCALE_FACTOR); // Integer division gives the integer part
Serial.print(".");
Serial.print(average % SCALE_FACTOR); // Modulo gives the fractional part
Serial.println(" degrees");
In this example, we used integers to store temperatures scaled by 10. The average is calculated using integer arithmetic, and the result is displayed with one decimal place by separating the integer and fractional parts.
2. Fixed-Point Arithmetic
Fixed-point arithmetic is a more structured way of handling scaled integers. Instead of just scaling by a power of 10, you conceptually divide your integer variable into two parts: an integer part and a fractional part. The number of bits allocated to each part determines the range and precision of your fixed-point numbers. While you don't explicitly declare a "fixed-point" data type in Arduino, you manage the integer as if it were split.
-
Define Integer and Fractional Parts: Determine how many bits you'll use for the integer part and the fractional part. For example, in a 16-bit integer, you could use 8 bits for the integer part and 8 bits for the fractional part. This is often represented as Q8.8 format (8 integer bits, 8 fractional bits). Choosing the right split depends on the range and precision required for your application. More bits for the integer part allow you to represent larger numbers, while more bits for the fractional part provide higher precision.
-
Scaling: Just like in the previous method, you multiply your input values by a scale factor. The scale factor is determined by the number of bits you've allocated to the fractional part. If you have 8 fractional bits (Q8.8), your scale factor would be 2^8 = 256. So, you multiply your floating-point value by 256 and store it in an integer.
-
Arithmetic Operations: Addition and subtraction are straightforward. You can perform them directly on the fixed-point integers. Multiplication and division are a bit trickier. When multiplying two fixed-point numbers, you need to divide the result by the scale factor to get the correct fixed-point value. When dividing two fixed-point numbers, you need to multiply the result by the scale factor. These adjustments are necessary because multiplying or dividing scaled numbers changes the scaling.
Example:
const int FRACTIONAL_BITS = 8; const int SCALE_FACTOR = 1 << FRACTIONAL_BITS; // 2^8 = 256 int fixed_a = 5 * SCALE_FACTOR; // Represents 5.0 int fixed_b = 2.5 * SCALE_FACTOR; // Represents 2.5 // Multiplication long product = (long)fixed_a * fixed_b; // Use long to prevent overflow int fixed_product = product / SCALE_FACTOR; // Correct the scaling Serial.print("Product: "); Serial.println((float)fixed_product / SCALE_FACTOR); // Convert back to float for display // Division int fixed_quotient = ((long)fixed_a * SCALE_FACTOR) / fixed_b; // Correct the scaling Serial.print("Quotient: "); Serial.println((float)fixed_quotient / SCALE_FACTOR); // Convert back to float for display
This example demonstrates fixed-point multiplication and division using a Q8.8 format. The results are converted back to floats for display purposes, but all calculations are performed using integers.
Fixed-point arithmetic gives you more control over precision and range, but it requires a bit more care when performing calculations, especially multiplication and division.
3. Look-Up Tables
If you're dealing with functions that are computationally expensive (like trigonometric functions or square roots) and you don't need extreme precision, consider using look-up tables (LUTs). A look-up table is simply an array that stores pre-calculated results for a range of input values. Instead of calculating the function every time, you look up the result in the table. This can save a lot of processing time and memory (by avoiding the floating point functions themselves).
- Determine Input Range and Resolution: Decide on the range of input values you need to cover and the desired resolution. For example, if you need sine values for angles from 0 to 90 degrees with 1-degree resolution, you'll need a table with 91 entries.
- Pre-calculate Values: Calculate the function values for each input value and store them in an array. You can do this in your code during setup or even generate the table offline and include it in your sketch.
- Look Up Values: In your main code, when you need the function value, use the input value as an index into the table to retrieve the pre-calculated result.
Example:
const int TABLE_SIZE = 91; // 0 to 90 degrees
int sine_table[TABLE_SIZE];
void setup() {
Serial.begin(9600);
// Pre-calculate sine values
for (int i = 0; i < TABLE_SIZE; i++) {
sine_table[i] = sin(i * PI / 180.0) * 1000; // Scale by 1000 for precision
}
}
void loop() {
int angle = 45; // Example angle
int sine_value = sine_table[angle]; // Look up the value
Serial.print("Sine of ");
Serial.print(angle);
Serial.print(" degrees: ");
Serial.println((float)sine_value / 1000); // Convert back to float for display
delay(1000);
}
In this example, we pre-calculate sine values for angles from 0 to 90 degrees and store them in the sine_table
. When we need the sine of an angle, we simply look it up in the table. The values are scaled by 1000 to provide some precision, and they are converted back to floats for display purposes.
Look-up tables are incredibly efficient for frequently used functions, but they do require memory to store the table itself. The trade-off is between memory usage and computational speed and the avoidance of floating-point math.
4. Re-think Your Algorithm
Sometimes, the best way to avoid floats is to step back and re-think your algorithm. Can you achieve the same result using a different approach that relies more on integer math? This might involve changing the way you represent data or breaking down calculations into smaller, integer-friendly steps. This approach can be more challenging but can lead to more memory-efficient code.
For example, consider calculating the distance between two points on a 2D plane. The standard formula involves square roots and floating-point operations. However, if you only need to compare distances (e.g., to find the closest point), you can compare the squared distances instead. This avoids the square root calculation, saving both memory and processing time. By focusing on integer calculations and comparisons, you can often simplify the algorithm and reduce memory usage.
5. Libraries for Integer Math
There are also libraries available that can help with more advanced integer math operations, such as fixed-point arithmetic or large integer calculations. These libraries provide optimized functions and data structures for working with integers, making it easier to avoid floats. Some popular libraries include the Fixed-Point Arduino library and the BigNumber library. The Fixed-Point Arduino library, for example, provides classes and functions for performing fixed-point arithmetic operations, making it easier to work with scaled integers. The BigNumber library allows you to work with very large integers that exceed the size of standard integer data types, which can be useful in applications that require high precision.
Conclusion
So, there you have it! Several ways to free up memory on your Arduino by avoiding those pesky floats. From scaled integer math to look-up tables and algorithmic tweaks, there are plenty of options to choose from. The best approach will depend on your specific project and the trade-offs you're willing to make between memory usage, precision, and code complexity. By carefully considering your needs and applying these techniques, you can create more efficient and feature-rich Arduino projects, even on memory-constrained devices. Happy coding, and remember, every byte counts! Remember to choose the method that best suits your needs and constraints. Each method has its trade-offs in terms of memory usage, precision, and computational complexity. Experiment with different techniques to find the optimal solution for your specific application.