CSI is often drawn as a row of bars, one per subcarrier, and people assume those bars are the whole story. They are only half of it. Each subcarrier is actually a complex number, and that number splits into two very different measurements. Knowing which half to use, and when, is most of the skill in working with CSI.
01The complex sample behind every subcarrier
When the ESP32 hands you CSI, each subcarrier arrives as two signed bytes: an imaginary part and a real part, often called Q and I. Together they describe a single point in the complex plane. That point is the channel's effect on that one frequency: how it scaled the signal and how it rotated it.
You can describe that same point two ways. In rectangular form it is just the real and imaginary values. In polar form it is an amplitude, the distance from the origin, and a phase, the angle. Both forms hold the same information, but they expose different things about the room, and they behave very differently when you try to use them. If the per-subcarrier structure is new, start with CSI subcarriers and OFDM explained.
02What amplitude tells you
Amplitude is the magnitude of the complex sample, the square root of the real part squared plus the imaginary part squared. Physically it tells you how strongly that subcarrier arrived, which depends on how the channel reflected, absorbed and recombined the signal at that frequency.
Its great virtue is that it is easy to use almost raw. Amplitude does not care about the absolute timing of the signal, so it is largely immune to the clock and frequency errors that wreck phase. With nothing more than light smoothing and an empty-room baseline, amplitude already reacts cleanly to a person entering the link, walking through it, or waving a hand nearby. This is why most beginner CSI projects, including our capture how-to, start by summing amplitude into a single energy number.
The trade-off is sensitivity. Amplitude responds well to movement that changes how much energy reaches the receiver, but it is less sharp for very small, fine motion where the main effect is a change in path length rather than in strength.
03What phase tells you
Phase is the angle of the complex sample, the two-argument arctangent of the imaginary part over the real part. It encodes how much the signal was delayed on its way to the receiver, and delay is directly tied to the length of the path the signal traveled.
That link to distance is what makes clean phase so valuable. A path length change of a fraction of a wavelength, roughly a few centimeters at 2.4 GHz, produces a clear, measurable phase shift. This is why phase, once sanitized, is the preferred signal for fine work: estimating distance and angle, tracking the slow rhythm of breathing, and separating movements that barely register in amplitude. The difference across multiple antennas also underpins direction and localization work.
04Why raw phase is noisy
Raw CSI phase looks almost random because it carries several errors stacked on top of the real channel. None of them are faults in your board; they are inherent to measuring phase between two independent radios.
- Carrier frequency offset (CFO). The transmitter and receiver oscillators never run at exactly the same frequency. The small difference adds a rotating phase error that changes from packet to packet.
- Sampling frequency offset (SFO). Their sampling clocks also differ slightly, which adds a phase slope that grows linearly across the subcarriers.
- Random phase offset. Each packet can start with an unknown phase from the receiver's timing recovery, so the whole curve jumps by an arbitrary amount every packet.
- Wrapping. Phase only lives in a range of two pi, so a real trend gets chopped into sawtooth jumps that must be unwrapped before they mean anything.
The standard fix, phase sanitization, unwraps the phase and then removes the linear component, the slope and offset that the clock errors add, usually by fitting a straight line across the subcarriers and subtracting it. What remains is far closer to the true channel phase. We cover this step inside the wider pipeline in cleaning Wi-Fi CSI data.
05Which to use for which task
| Task | Better signal | Why |
|---|---|---|
| Presence, coarse motion | Amplitude | Robust and usable almost raw |
| Fine motion, gestures | Clean phase | Sensitive to small path changes |
| Breathing / vital signs | Both, phase after cleaning | Slow path changes show in phase |
| Distance / ranging | Clean phase | Phase maps to path length |
| Quick first prototype | Amplitude | No sanitization needed |
The practical path is clear. Prototype with amplitude because it works immediately, then add sanitized phase when you need the extra sensitivity it buys. Jumping straight to raw phase is the fastest way to get a project that does not work and a signal you cannot explain.
It is also worth knowing that amplitude and phase do not have to be used separately. Many of the strongest CSI systems feed both into a machine-learning model as two channels of the same image, with subcarriers on one axis and time on the other, and let the model learn how to combine them. That still only works if the phase has been sanitized first, so the rule holds: clean before you combine. If you are building something simple on a single board, amplitude alone will carry you a long way, and you can always layer phase on top once the amplitude version is solid.
06Computing both on an ESP32
The math is short. Inside your CSI callback, loop over the subcarriers and turn each interleaved pair into an amplitude and a phase. The snippet below does exactly that and prints one subcarrier so you can see the two numbers.
// Amplitude and phase per subcarrier from ESP32 CSI bytes. // Runs inside the CSI callback. Tested on Arduino-ESP32 3.0.x. #include <math.h> static void onCsi(void* ctx, wifi_csi_info_t* info) { if (!info || !info->buf) return; int8_t* d = info->buf; // interleaved [imag, real] int8 int n = info->len / 2; // subcarrier count for (int i = 0; i < n; i++) { int im = d[i * 2]; // imaginary (Q) int re = d[i * 2 + 1]; // real (I) float amp = sqrtf((float)(re * re + im * im)); // magnitude float phase = atan2f((float)im, (float)re); // radians, -pi..pi // amp is usable now; phase still needs sanitizing before use. if (i == n / 2) { // print one central subcarrier Serial.printf("sc=%d amp=%.1f phase=%.2f rad\n", i, amp, phase); } } }
Watch the output and the contrast is immediate. The amp figure moves smoothly as you move. The raw phase jumps around even when the room is still, because of the offsets above. That is your cue to sanitize before trusting it. For the full capture scaffolding this drops into, see how to capture Wi-Fi CSI on an ESP32, and always test on your own network.
07FAQ
What is the difference between amplitude and phase in CSI?
Each CSI subcarrier is a complex number. Amplitude is its magnitude, how strong that subcarrier arrived, and it reflects how much the channel attenuated or reinforced that frequency. Phase is its angle, how much the signal was delayed or shifted, and it is very sensitive to tiny changes in path length. Amplitude is easy to use raw, while phase needs cleaning first.
Why is raw CSI phase so noisy?
Raw phase carries several hardware errors on top of the real channel. Carrier frequency offset between the two radios, sampling frequency offset in the clocks, and random phase offsets that change packet to packet all corrupt it. These add unknown slopes and jumps across subcarriers, so raw phase looks like noise until it is sanitized.
Which is better for motion detection, amplitude or phase?
Amplitude is the simpler and more robust starting point for motion and presence, because it is usable straight from the board with light filtering. Clean phase is more sensitive to fine motion and useful for distance and breathing, but only after sanitization. Many practical systems use amplitude first and add cleaned phase when they need the extra sensitivity.
How do I compute amplitude and phase from ESP32 CSI bytes?
The ESP32 gives interleaved signed bytes, an imaginary and a real value per subcarrier. Amplitude is the square root of real squared plus imaginary squared, and phase is the two-argument arctangent of imaginary over real. Compute both inside a loop over the subcarriers, then sanitize the phase before you use it.