ESP32-S3 CSI: yes it works — and when you need a 5 GHz chip instead
By Wu Jun · Updated 2026-10-29 · ESP32-S3, Wi-Fi CSI, esp-csi, 2.4 GHz, ESP32-C5 · 6 min read
The ESP32-S3 is the chip most people reach for when they outgrow the original ESP32: more RAM, more compute, USB, and a lower price than a Raspberry Pi. The question is always the same — does it do CSI, and is it good enough?
Short answer
- Yes, the ESP32-S3 supports Wi-Fi CSI. Espressif states that all ESP32 series chips support CSI, S3 included, and the ESP-IDF programming guide documents the S3 CSI payload in detail [1] [2].
- It is a 2.4 GHz part. The S3 CSI tables cover 20 MHz and 40 MHz channels only; there is no 5 GHz band to measure.
- You get tens to a few hundred complex subcarrier values per packet, not the 256 subcarriers an 80 MHz router gives you.
- If you specifically want 5 GHz CSI on Espressif silicon, the ESP32-C5 is the dual-band (2.4 GHz + 5 GHz) CSI part [3].
1. What the vendor docs actually say
The ESP-IDF guide for the ESP32-S3 describes CSI as "the channel frequency responses of sub-carriers … estimated when packets are received from the transmitter". Each subcarrier is stored as two signed bytes — imaginary part first, then real part — and up to three training fields can be present: LLTF, HT-LTF and STBC-HT-LTF [2].
That last detail matters more than it looks: the payload size per packet is not fixed. It depends on the packet type, the channel width and whether STBC is used. The S3 table ranges from 128 bytes (20 MHz non-HT, 64 complex values) up through larger HT-LTF cases. Log the reported length instead of hard-coding a subcarrier count — exactly the same rule we give for router CSI.
The ESP32-S3 also has a documented hardware quirk: when first_word_invalid is set, the first four bytes of the CSI
buffer are invalid and must be discarded [2]. Forgetting this puts a constant notch at the start
of every measurement.
2. Getting CSI out of an S3 (the four calls)
The ESP-IDF flow is short [2]:
- enable
Wi-Fi CSIinmenuconfig(Components config → Wi-Fi → Wi-Fi CSI); - register a receive callback with
esp_wifi_set_csi_rx_cb(); - configure it with
esp_wifi_set_csi_config(); - turn it on with
esp_wifi_set_csi().
Two practical notes from the same page:
- the callback runs in the Wi-Fi task — copy data to a queue and process it elsewhere;
- a connected station only receives packets from its AP, so the docs suggest enabling promiscuous mode to harvest more CSI frames.
On the application side, espressif/esp-csi ships ready-made sender/receiver examples and a csi_data_read_parse.py
parser; ESP32-CSI-Tool is the CSV-oriented alternative for datasets, and ESPectre wraps the whole thing into an
ESPHome presence sensor [1] [4] [5].
3. One ESP32-S3 plus a router
For a first bench test, the least ambiguous topology is one ESP32-S3 associated with a 2.4 GHz AP while the
ESP32 sends pings to the gateway and receives the replies. It proves the receive callback, CSI configuration,
serial output and parser without requiring a second ESP32. The current first-party example for this topology is
examples/get-started/csi_recv_router [1].
The commands below follow the example repository as inspected on 2026-10-29. This is C-grade vendor documentation and source code, not an E-grade measurement by this site: we have not attached an ESP32-S3 to the published router run.
git clone https://github.com/espressif/esp-csi.git
cd esp-csi/examples/get-started/csi_recv_router
# Activate the ESP-IDF you intend to use, then verify it before building.
. "$IDF_PATH/export.sh"
idf.py --version
idf.py set-target esp32s3
idf.py menuconfig
idf.py -p /dev/ttyUSB0 -b 921600 flash monitor
In menuconfig, open Example Connection Configuration and enter the 2.4 GHz SSID/password. The example's
sdkconfig.defaults enables Wi-Fi CSI and selects a 921600 baud monitor. The Component Manager also downloads
esp_csi_gain_ctrl, so the first build needs network access even after ESP-IDF itself is installed.
The current router example deliberately asks for LLTF only:
wifi_csi_config_t csi_config = {
.lltf_en = true,
.htltf_en = false,
.stbc_htltf2_en = false,
.ltf_merge_en = true,
.channel_filter_en = true,
.manu_scale = true,
.shift = true,
};
That is a compatibility choice, not the maximum payload. If you enable HT-LTF or STBC-HT-LTF, re-check the
resulting len, packet type and bandwidth instead of assuming the old array shape. Do not mix rows captured
with different LTF flags in one model or one CSI matrix.
The example also sets the ping interval to 1000 / CONFIG_SEND_FREQUENCY with CONFIG_SEND_FREQUENCY=100.
That is a request rate, not a guaranteed CSI rate: the AP must answer, the radio must win airtime, the
callback queue must keep up, and the serial link must carry the text. At 921600 baud, long CSI rows and verbose
logging can saturate the monitor. Measure received CSI_DATA rows per second and queue drops before treating
the stream as 100 Hz.
4. Parse the payload without inventing units
The first-party output contains one CSI_DATA row per callback. For the S3 format, the important fields are
len, first_word, rssi, rate, sig_mode, mcs, bandwidth, stbc, channel, local_timestamp
and data [2] [1]. A minimal decoder should preserve those fields:
def decode_s3_csi(row):
raw = list(row["data"])
if len(raw) != row["len"]:
raise ValueError(f"len says {row['len']}, data has {len(raw)} bytes")
if row["first_word"]:
raw = raw[4:] # four invalid bytes = two complex samples
if len(raw) % 2:
raise ValueError("CSI byte count must be even")
return [(raw[i], raw[i + 1]) for i in range(0, len(raw), 2)]
Each returned tuple is one (imaginary, real) pair. len is a byte count, not a subcarrier count;
len(data) / 2 is the complex-sample count only after the invalid prefix is removed. sig_len describes the
received Wi-Fi frame and is not the CSI payload length either.
Before collecting a dataset, reject or quarantine rows that fail all of these checks:
| Check | Why |
|---|---|
non-zero, even len and matching data length |
catches CSV truncation and parser mistakes |
first_word is logged, then handled consistently |
otherwise the first two complex samples carry a fixed artefact |
| timestamp is monotonic and uses one unit | the S3 field is a device timestamp, not wall-clock time |
| channel, bandwidth, STBC and LTF flags stay in each row | payload shape changes with packet type |
| actual rows/s and drops are recorded | configured ping frequency is not measured capture rate |
Start with a short serial capture, inspect the distribution of len, channel, bandwidth and sig_len, then
choose a storage format. For more than a few minutes, prefer binary UDP or framed binary serial output over
text CSV; JSON and comma-separated integers are convenient for debugging but expensive at sustained rates.
5. The 5 GHz question
Here is the trade-off nobody puts on the box:
| Chip | Band | CSI | Notes |
|---|---|---|---|
| ESP32 (classic) / ESP32-S3 | 2.4 GHz | yes | cheapest entry, 20/40 MHz |
| ESP32-C6 | 2.4 GHz | yes | Wi-Fi 6 (2.4 GHz only) |
| ESP32-C5 | 2.4 + 5 GHz | yes | dual-band Wi-Fi 6, WIFI_BAND_MODE_AUTO [3] |
| Router (MT7986 + mt76 CSI) | 5 GHz | yes | 80 MHz / 256 subcarriers / 8 chains |
Espressif's dual-band support is real — the C5 API exposes a band mode that operates on both 2.4 GHz and 5 GHz channels [3] — but note that no Espressif chip currently gives you an 80 MHz CSI view. For that class of resolution (breathing, fine motion) the router route is the practical option, and it is the one we built and measured on this site.
So the honest decision rule:
- Learning, IoT, smart-home presence: ESP32-S3 is plenty, and it is cheap [5].
- 5 GHz experiments on an MCU: ESP32-C5.
- Whole-home, high-resolution, 24/7 sensing: a CSI-capable router, not an ESP32.
6. Where this leaves you
The ESP32-S3 is a legitimate CSI platform with first-party documentation — treat the "S3 can't do CSI" claim as wrong, and treat "S3 does 80 MHz" as equally wrong. Pin down the band and the channel width first, then pick the hardware.
Related: ESP32 CSI getting started · ESP32 human detection · router CSI reproduction · 2.4 GHz vs 5 GHz
References and data sources
Every external claim carries a source and an evidence grade (grading scheme). Grade E marks first-party, reproducible measurements from this project.
- Grade C · official docs
espressif/esp-csi — Applications based on Wi-Fi CSI
Espressif Systems
github.com/espressif/esp-csi · verified: GitHub API 2026-10-29 (stars 1,723; README confirms the complete ESP32 series list and three sender/receiver routes) · The official README states: "All ESP32 series support CSI, including ESP32 / ESP32-S2 / ESP32-C3 / ESP32-S3 / ESP32-C5 / ESP32-C6 / ESP32-C61." - Grade C · official docs
ESP-IDF — Wi-Fi Channel State Information (ESP32-S3)
Espressif Systems
docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-guides/wifi-driver/wifi-vendor-features.html#wi-fi-channel-state-information · verified: official docs fetched 2026-10-29 (ESP32-S3 CSI section: LTF/byte-count table and the first_word_invalid note) - Grade C · official docs
ESP-IDF — esp_wifi API (ESP32-C5): band mode 2.4G + 5G
Espressif Systems
docs.espressif.com/projects/esp-idf/en/stable/esp32c5/api-reference/network/esp_wifi.html · verified: official docs fetched 2026-10-11 (text includes "band mode 2.4G + 5G (WIFI_BAND_MODE_AUTO)" and 802.11anacax) - Grade C · official docs
StevenMHernandez/ESP32-CSI-Tool — active/passive CSI extraction for ESP32
S. M. Hernandez
github.com/StevenMHernandez/ESP32-CSI-Tool · verified: GitHub API 2026-10-11 (stars 571, MIT, last push 2024-07-16) - Grade C · official docs
francescopace/espectre — Wi-Fi CSI motion sensing for ESP32 (ESPHome)
F. Pace
github.com/francescopace/espectre · verified: GitHub API and official README fetched 2026-10-29 (stars 9,700; GPLv3; ESPHome source path, Direct API port 62587 and supported chips documented) · The official project provides ESPHome, native MQTT, Matter and browser flashing paths; one ESP32 covers one area and does not identify people or count occupants.