I realize that using float results in loading all or at least parts of the floating point library, which consumes a significant amount of memory.
How can I avoid using float
?
2 Answers 2
As suggested by @halfbit, you can get rid of floating point by choosing units that let you conveniently work with integers, like millivolts instead of volts. I would go a bit further and suggest you use, whenever feasible, the ADC step as a voltage unit.
Here is an example. Assume you want to turn a light on whenever the resistance of an LDR goes above 5 kΩ. Here is how this could be coded naively (I'm dropping the hysteresis for the sake of simplicity):
#define PULL_UP_RESISTANCE 10e3 // 10 kΩ pull-up
#define THRESHOLD_RESISTANCE 5e3 // 5 kΩ threshold
void loop() {
int reading = analogRead(A0);
float voltage = reading * 5.0 / 1024;
float resistance = voltage / (5.0 - voltage) * PULL_UP_RESISTANCE;
if (resistance > THRESHOLD_RESISTANCE)
digitalWrite(LIGHT_PIN, HIGH);
else
digitalWrite(LIGHT_PIN, LOW);
}
If you do the math ahead of time, you can figure out what ADC reading matches the chosen threshold. The code becomes:
#define THRESHOLD_READING 341 // matches 5 kΩ on the LDR
void loop() {
int reading = analogRead(A0);
if (reading > THRESHOLD_READING)
digitalWrite(LIGHT_PIN, HIGH);
else
digitalWrite(LIGHT_PIN, LOW);
}
Much simpler, and no run-time arithmetics other than a comparison. You can even leverage the preprocessor to do the math for you. Floating point math done at compile-time is essentially free:
#define THRESHOLD_READING \
((int)(THRESHOLD_RESISTANCE/(THRESHOLD_RESISTANCE+PULL_UP_RESISTANCE)*1024))
You may want to make this a constexpr int
if you want a hard warranty
that it is computed at compile-time.
Sometimes you may want to use a voltage unit that is smaller than an ADC step. For example, you may filter ADC readings using a simple first-order low-pass filter, which follows the recurrence relation
y[n] = α x[n] + (1−α) y[n−1]
and can be coded like this:
float low_pass_filter(float x)
{
static float y;
y += alpha * (x - y);
return y;
}
This is not directly amenable to integer arithmetics, as alpha
is
smaller than one, and even the quantity alpha * (x - y)
may often be
smaller than one. This can be mitigated by adding a large DC gain G to
the filter. The recurrence relation becomes:
y[n] = Gα x[n] + (1−α) y[n−1]
For α = 1/16 and G = 64, it can be coded like this:
uint16_t low_pass_filter(uint16_t x)
{
static uint16_t y;
y += 4*x - (y+8) / 16;
return y;
}
Here, the DC gain mitigates the rounding errors introduced by using
integers. The values of α and G are chosen as powers of two, this way
the multiplication and the division above are optimized by the compiler
into bit shifts, which makes the filter very fast. Note that a DC gain
of 64 makes an ADC reading (10 bits) just fit into a uint16_t
.
The DC gain could be interpreted as a change of unit: the filtered readings are in units of 5 V/216 instead of 5 V/210. One could also say that the unit stays the same (one ADC step), but the values are formatted as 10.6 fixed-point numbers.
Fixed-point arithmetic is a large topic that deserves whole books. Suffice to say here that it can be used to do complex math (trigonometry, logarithms...) efficiently if you do not require high precision.
-
I voted up, because that would have been my second answer. :). I do exactly that in my programs, but I could not find the right words to explain. That's why I started with «There are many tricks» It would be nice if you mention in your Answer, that "it's a little bit harder to debug"halfbit– halfbit2025年07月28日 20:23:30 +00:00Commented Jul 28 at 20:23
There are many tricks to get around the floating library (not to use float):
The simplest is: don't use volts as the unit, but millivolts.
But wait, before I explain how to do that, I want to explain that this is not about 1023 vs. 1024. Opinions differ, but my answer is unbiased. Both sides can use my answer. There is a nice thread in the Arduino forum that shows the partly hardened fronts. dividing by 1023 or 1024 the final verdict on analogread
I am not getting involved in this discussion 1023 or 1024 and ask you not to do so either.
My answer is useful for both schools, and you will be surprised at the results.
Typically, many sketches (programs) start as follows:
int x = analogRead(inPin);
float v = x * 5 / 1023 ;
// or in the other school:
float v = x * 5 / 1024 ;
With this solution, you have a good part of the floating point library on board.
Just do not use Volts as base, use mV. This does not mean 5V but 5000mV.
int x = analogRead(inPin);
short v = x * 5000L / 1023 ;
// or in the other school:
short v = x * 5000L / 1024
Now the variable v holds mV (without fraction).
The trick here is that the compiler (and subsequently the CPU) must first use 5000L as LONG (32bit), so (maximum) 5000 ×ばつ 1000 -> 5,000,000 have space and are then divided by 1000 again, so that the (maximum) result (5000) has space again in a SHORT.
If you think that this is not precise enough for you, work with 1/10mV (0.1 mV)
unsigned short y = 50000L * x / 1024;
The result is slightly more accurate, but you have to be careful to work with unsigned short.
Both variants are also available in the 1023 version! The simple approach here is not to take 5000L or 50000L, but 5005L or 50049L.
This is a kind of integer arithmetic magic. You get the numbers by multiplying the initial value (5000) by 1025(!) and then dividing by 1024. (and a little trial and error). The maths behind this is not trivial.
But why don't I just divide by 1023? The reason for this is that dividing by 1024 as a simple "shift bit to the right", namely 10 bit, is better for the CPU than dividing by the number 1023, which is cruel for the CPU.
I have prepared a sketch that allows everyone to see the barely different results for themselves and to test whether and how much memory they can save.
I provide 15 rows of results here, 5 from the beginning, 5 from the middle and the last 5.
The list has 7 columns:
The first column is the value of analogRead in HEX.
This is followed by 2 ×ばつ 3 columns. In each case the logic for "float", 5000 mv and 50000 1/10 mv. The first three in the 1024 logic, the other three in the 1023 logic.
0x000 | f 0.0000 | 0000 | 00000 | f 0.0000 | 0000 | 00000 |
0x001 | f 0.0048 | 0004 | 00048 | f 0.0048 | 0004 | 00048 |
0x002 | f 0.0097 | 0009 | 00097 | f 0.0097 | 0009 | 00097 |
0x003 | f 0.0146 | 0014 | 00146 | f 0.0146 | 0014 | 00146 |
0x004 | f 0.0195 | 0019 | 00195 | f 0.0195 | 0019 | 00195 |
.
.
.
0x1fe | f 2.4902 | 2490 | 24902 | f 2.4926 | 2492 | 24926 |
0x1ff | f 2.4951 | 2495 | 24951 | f 2.4975 | 2497 | 24975 |
0x200 | f 2.5000 | 2500 | 25000 | f 2.5024 | 2502 | 25024 |
0x201 | f 2.5048 | 2504 | 25048 | f 2.5073 | 2507 | 25073 |
0x202 | f 2.5097 | 2509 | 25097 | f 2.5122 | 2512 | 25122 |
.
.
.
0x3fb | f 4.9755 | 4975 | 49755 | f 4.9804 | 4980 | 49804 |
0x3fc | f 4.9804 | 4980 | 49804 | f 4.9853 | 4985 | 49853 |
0x3fd | f 4.9853 | 4985 | 49853 | f 4.9902 | 4990 | 49902 |
0x3fe | f 4.9902 | 4990 | 49902 | f 4.9951 | 4995 | 49951 |
0x3ff | f 4.9951 | 4995 | 49951 | f 5.0000 | 5000 | 50000 |
The sketch is clearly free for cut and paste!
(the function say is the same as printf in std c.)
void say(char const* fmt, ...) {
va_list vars;
char buf[20];
va_start(vars, fmt);
vsnprintf(buf, sizeof(buf), fmt, vars);
Serial.print(buf);
}
void setup() {
Serial.begin(115200);
Serial.println("----------------------");
for (int i=0; i<1024; ++i) {
say("0x%3.3x | ", i);
float volts_24= i * 5.0/1024;
say("f %d.%4.4u | ", (int)volts_24, (unsigned int)((volts_24-(int)volts_24)*10000));
unsigned short mVolts = i * 5000l / 1024;
say("%4.4d | ", mVolts);
unsigned short mVolts10 = i * 50000l / 1024;
say("%5.5u | ", mVolts10);
float volts_23= i * 5.0/1023;
say("f %d.%4.4u | ", (int)volts_23, (unsigned int)((volts_23-(int)volts_23)*10000));
// unsigned short mVolts_1023 = i * (5001l*1024/1023) / 1024;
unsigned short mVolts_1023 = (i * 5005l) / 1024;
say("%4.4d | ", mVolts_1023);
unsigned short mVolts10_1023 = (i * 50049l) / 1024;
say("%5.5u | ", mVolts10_1023);
say("\n");
}
}
void loop() {
}
-
AFAIK, the key term here is "fixed-point arithmetic". From docs.arduino.cc/language-reference/en/functions/analog-io/… each integer value is in an increment of 4.9 mV. It doesn't make sense to debate 1023 when the docs say the exact resolution. And the exact value won't be that precise anyway.qwr– qwr2025年07月29日 19:20:00 +00:00Commented Jul 29 at 19:20
-
@qwr: that is the reason I wrote «I am not getting involved in this discussion 1023 or 1024 and ask you not to do so either.»halfbit– halfbit2025年07月30日 23:32:36 +00:00Commented Jul 30 at 23:32
float
." But since you had this straight-forward idea for sure and still post this question, what is the specific issue?