The Short Answer: Hardware FPU vs. Software Emulation
Yes, the ESP32 can absolutely do floating point math, but there is a massive catch that traps many embedded developers: the ESP32 hardware Floating Point Unit (FPU) only supports 32-bit single-precision (float). It does not have hardware support for 64-bit double-precision (double).
The original ESP32 (Xtensa LX6 core), the ESP32-S3 (Xtensa LX7), and the RISC-V based ESP32-C3/C6 all feature a hardware FPU that executes single-precision IEEE 754 instructions (like ADD.S and MUL.S) in 1 to 4 clock cycles. However, if you declare a variable as a double, or use standard C math functions that default to double-precision, the ESP32 falls back to software emulation. The compiler inserts calls to software libraries (like __adddf3 or __muldf3), which can take hundreds of cycles per operation, effectively making your math 10x to 50x slower.
float for your variables, and always use the f-suffixed versions of C math functions. Use sinf() instead of sin(), sqrtf() instead of sqrt(), and fabsf() instead of fabs(). If you pass a float into sin(), the compiler silently casts it to a 64-bit double, performs slow software math, and casts it back.
For a deeper look at how the Xtensa architecture handles the FPU at the silicon level, refer to the Espressif FPU API Guide. Understanding this distinction is the difference between a buttery-smooth 10kHz control loop and a stuttering mess.
Project Build: Real-Time IIR Filter Using Hardware Floats
To prove the hardware FPU's capabilities, we will build a high-speed Infinite Impulse Response (IIR) low-pass filter. We will read a noisy analog signal from a potentiometer, apply a single-precision floating point filter in real-time, and output the smoothed result to an LED via PWM. This requires thousands of float multiplications per second.
Parts List
- Microcontroller: ESP32 DevKit V4 (specifically the ESP32-WROOM-32E module variant)
- Input: 10kΩ Linear Taper Potentiometer
- Output: 5mm Blue LED + 330Ω current-limiting resistor
- Misc: Half-size breadboard, male-to-male jumper wires
Pin Mapping Table
| Component | ESP32 Pin | GPIO Number | Notes |
|---|---|---|---|
| Potentiometer Wiper | ADC1_CH0 | GPIO 36 (VP) | Input signal (0-3.3V) |
| Potentiometer VCC | 3V3 | - | Do not use 5V on ADC pins |
| Potentiometer GND | GND | - | Common ground |
| LED Anode (+) | GPIO 2 | GPIO 2 | Via 330Ω resistor |
| LED Cathode (-) | GND | - | Common ground |
Complete Compilable Code
This code targets the modern ESP32 Arduino Core 3.x API. It includes explicit error handling for floating point overflow (inf) and invalid operations (nan), which are common when tuning IIR filter coefficients.
/*
* Real-Time Single-Precision IIR Low-Pass Filter
* Target: ESP32-WROOM-32E (DevKit V4)
* Core: ESP32 Arduino Core 3.x
*/
#include
#include
// --- Pin Definitions ---
#define PIN_ADC_INPUT 36 // ADC1_CH0
#define PIN_LED_OUTPUT 2 // Built-in LED / External LED
// --- Filter Configuration ---
// Alpha determines the smoothing. 0.01 = heavy smoothing, 0.9 = light smoothing.
// MUST be declared as float to utilize hardware FPU.
const float ALPHA = 0.05f;
float filteredValue = 0.0f;
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
// Configure ADC
analogReadResolution(12); // 12-bit (0-4095)
// Configure LED PWM using modern Core 3.x API
// 5000 Hz frequency, 8-bit resolution (0-255)
ledcAttach(PIN_LED_OUTPUT, 5000, 8);
Serial.println("ESP32 Hardware FPU IIR Filter Initialized.");
Serial.println("Format: Raw_ADC, Filtered_Float");
}
void loop() {
// 1. Read raw ADC value and cast to float immediately
int rawAdc = analogRead(PIN_ADC_INPUT);
float rawFloat = static_cast(rawAdc);
// 2. Apply IIR Low-Pass Filter: Y[n] = alpha * X[n] + (1 - alpha) * Y[n-1]
// Using 'f' suffix on constants ensures the compiler doesn't promote to double
filteredValue = (ALPHA * rawFloat) + ((1.0f - ALPHA) * filteredValue);
// 3. Error Handling: Check for Float Corruption (NaN or Infinity)
// This can happen if ALPHA is miscalculated or memory is corrupted
if (isnan(filteredValue) || isinf(filteredValue)) {
Serial.println("ERR: Float overflow detected. Resetting filter state.");
filteredValue = rawFloat; // Fallback to raw reading
}
// 4. Map the filtered float (0-4095) to PWM output (0-255)
// Using hardware-accelerated float math for mapping
float pwmFloat = (filteredValue / 4095.0f) * 255.0f;
uint8_t pwmOut = static_cast(constrain(pwmFloat, 0.0f, 255.0f));
ledcWrite(PIN_LED_OUTPUT, pwmOut);
// 5. Output for Serial Plotter
Serial.print(rawFloat);
Serial.print(",");
Serial.println(filteredValue);
// Run at roughly 1kHz to demonstrate FPU throughput
delay(1);
}
How to Extend or Simplify the Build
- Simplify: If you just want to test the ADC without the math overhead, remove the IIR filter block and map
rawFloatdirectly to the PWM output. This isolates the ADC noise so you can see it on the Serial Plotter. - Extend: Upgrade the math to a second-order Biquad filter, or add an I2C sensor like the BME280. You can use the hardware FPU to run a Kalman filter fusing the barometric pressure and accelerometer data to calculate precise altitude changes in real-time.
Debugging Floating Point Errors on the ESP32
When floating point math goes wrong on the ESP32, it rarely crashes the chip outright. Instead, it produces silent data corruption or severe performance bottlenecks. If your project fails or behaves erratically, here are the first three things to check.
1. Check for the 'Double' Trap and Missing 'f' Suffixes
Symptom: Your control loop is running at 50Hz instead of the expected 5kHz, or the ESP32 feels 'laggy'.
The Fix: Audit your code for implicit double promotions. If you write float x = sin(angle) * 3.14;, you have triggered software emulation. The literal 3.14 is a 64-bit double by default in C/C++, which forces the compiler to promote sin(angle) to a double, perform the multiplication in software, and truncate it back to a float. Change it to float x = sinf(angle) * 3.14f;. Always append f to your decimal literals.
2. Check for 'nan' and 'inf' Serial Outputs
Symptom: Your Serial Monitor prints nan (Not a Number) or inf (Infinity), and your PWM output locks at 0 or 255.
The Fix: This happens when you divide a float by zero, or take the square root of a negative number (e.g., sqrtf(-1.0f)). In IIR or PID filters, this usually means your feedback coefficient has caused exponential growth, overflowing the 32-bit IEEE 754 maximum limit of ~3.4 × 10^38. Add the isnan() and isinf() checks shown in the code block above to catch and reset the state before it locks up your hardware.
3. Check RTOS Task FPU Context Switching
Symptom: You see the exact error string Guru Meditation Error: Core 1 panic'ed (Coprocessor exception), or your float variables randomly change values when switching between FreeRTOS tasks.
The Fix: This is a deep-cut ESP-IDF issue. The hardware FPU registers are not automatically saved during a FreeRTOS context switch to save RAM and CPU cycles. If Task A uses the FPU, gets interrupted, and Task B uses the FPU, Task A's registers are overwritten. If you are writing pure ESP-IDF code or using advanced Arduino multithreading, you must enable CONFIG_FREERTOS_FPU_IN_ISR or ensure FPU context saving is enabled in your sdkconfig. For standard single-threaded Arduino loop() code, this is not an issue.
nan propagation in a lithium-ion BMS algorithm could result in a failed cutoff and a thermal event. Always validate float outputs against integer bounds before triggering high-current MOSFETs.
Frequently Asked Questions
Does the ESP32-S3 have better floating point performance than the original ESP32?
Yes, but only for specific workloads. Both the original ESP32 (Xtensa LX6) and the ESP32-S3 (Xtensa LX7) feature single-precision hardware FPUs. However, the ESP32-S3 includes PIE (Processor Instruction Extensions) and vector instructions. This allows the S3 to perform SIMD (Single Instruction, Multiple Data) operations, meaning it can process multiple 32-bit floats in a single clock cycle. If you are doing matrix math for machine learning (like ESP-WHO face detection) or heavy DSP audio filtering, the S3 will vastly outperform the original. For simple scalar math (like a PID loop), the performance difference is negligible.
Why is my ESP32 math running slower than an 8-bit Arduino Uno?
If your ESP32 is losing a footrace to a 16MHz ATmega328P, you have almost certainly fallen into the double trap. On the 8-bit AVR architecture (Arduino Uno), the compiler treats double and float as the exact same 32-bit type. Code written for the Uno using double runs fine. But when you port that exact code to the ESP32, the ESP32 Arduino Core correctly treats double as a 64-bit type. Because the ESP32 lacks a 64-bit hardware FPU, it suddenly has to emulate 64-bit math in software, resulting in a massive performance penalty compared to the Uno's native 32-bit math. Change your doubles to floats and the ESP32 will instantly outrun the Uno.
Can I use 64-bit doubles on the ESP32 if I really need the precision?
Yes, you can use them, but you must accept the performance cost. A 32-bit float gives you about 7 decimal digits of precision. If you are calculating GPS coordinates or doing astronomical integrations where 7 digits isn't enough, you must use 64-bit doubles (which provide ~15 digits). Just be aware that every double operation will invoke software emulation libraries. To mitigate the speed loss, offload your 64-bit math to the secondary core (Core 0) using xTaskCreatePinnedToCore, leaving Core 1 free to handle time-critical Wi-Fi and RTOS tasks using fast 32-bit floats. For more on data type limits, see the Arduino Float Reference.






