☰
ESP32隐藏无线电通路:不连WiFi也能嗅探2.4GHz信号
2026/10/30 3:49:25 网站建设 项目流程

1. 一次“掉线”调试,撞见了ESP32的第二张脸

半年前我在调一个低功耗环境监测节点,ESP32放在窗台,为了省电故意没有连接家里路由器,只靠休眠和定时醒来上报。结果在串口监视器里,我却时不时看到一些奇怪的十六进制帧和MAC地址。当时第一反应是“代码写错了”,但仔细翻代码,我根本没初始化任何网络连接。后来才知道,ESP32在WiFi协议栈“空闲”的时候,射频前端仍然在监听空中的802.11帧;而这种监听能力,恰恰是很多开发者忽略掉的一条无线电通路。

现在我敢说,ESP32这个芯片里藏了一条“连官方入门文档都没写清楚”的无线电通路。它不需要外接电视棒、不需要SDR模块,也不需要把协议栈拨到极客模式,只要把WiFi底层接口打开,就能直接拿到空中2.4GHz频段上的原始帧信息和信号强度。顺着这条路往下走,你还能用ESP-NOW在两块板子之间建立一条不经过路由器、不组TCP/IP网络的裸通信链路。

这篇文章不是纯理论科普,而是把我从“发现这个现象”到“复现这个通路”再到“踩完一圈坑”的完整过程写出来。适合手里正好有ESP32开发板、平时用Arduino IDE或ESP-IDF的朋友,也适合想低成本做一个无线环境感知小工具的人。代码我会给,参数我会讲清楚,但更重要的是,我会告诉你官方手册为什么没把这条路标出来。

2. 为什么说这是一条“手册上查不到”的通路

先把结论抛出来:ESP32的WiFi模块支持一种叫混杂模式(Promiscuous Mode)的底层工作状态。在这个状态下,射频前端可以解码信道上所有被捕获的802.11帧,而不仅仅是发给这台设备的帧。与此同时,你还能拿到每个数据帧的RSSI(接收信号强度指示)、信道号、调制方式等物理层信息。

问题就出在这里:你去翻乐鑫官方那本几百页的《ESP32 Technical Reference Manual》,里面讲了很多WiFi/BLE协议栈的内容,但极少专门把“怎么在杂乱的信道上捞原始帧”这件事单独拎出来讲。真正可用的配置方式,散落在SDK源码、示例工程、GitHub issue和社区博客里。对于只用过WiFi.begin()的普通开发者来说,这不就是一条“查不到”的通路吗?

也别把它想得太玄。打个比方:你家宽带入户,大家都在用路由器上行下行;但交换机背后还有一个专门给网管抓包用的镜像端口。这个端口不会出现在产品说明书的“一键上网”章节里,但懂底层的人知道它的存在。ESP32的混杂模式就是类似的“镜像端口”,而ESP-NOW则是利用这段底层通路做自定义数据传输的“内线电话”。

这里还有一层容易被忽略的背景:ESP32的射频前端是WiFi和蓝牙共用的。也就是说,同一个2.4GHz收发链路,既处理802.11帧,也处理BLE广播。所以你在做WiFi帧监听时,其实也在“顺路”感知这个频段上的其他无线活动——蓝牙设备、无线鼠标、甚至微波炉泄露出来的杂波,都会以能量波动的形式出现在RSSI结果里。对喜欢动手的人而言,这就等于免费获得了一台简易射频能量探测器。

3. 这条路到底通到哪里:RF前端与原始帧的交汇点

要说清楚这条通路的本质,得稍微看一眼ESP32的射频接收链路。天线收到的信号先经过低噪声放大器,再混频成中频或基带,最后交给数字基带做AGC(自动增益控制)和解调。解调完成后,802.11 MAC层会进行CRC校验、地址过滤、去重等一系列处理,然后才把数据帧交到协议栈手里。

混杂模式所做的,就是越过地址过滤这一步:只要信道上存在一个能通过CRC校验的802.11帧,它就会带着完整的MAC层头信息被拷贝回调函数中。这意味着你能看到:

  • 帧头里的type和subtype,区分beacon、probe request、data帧等;
  • BSSID、ESID(SSID名称)等网络标识;
  • 物理层附加的sig_mode、mcs、rssi等参数。

用官方SDK的写法,你只需要定义一个结构体指针指向收到的数据包,然后在回调函数里按字段解析即可。核心代码逻辑并不复杂,但前提是你知道去哪找这些结构体定义——它们藏在esp_wifi_types.h或Arduino封装下的esp_wifi.h里,而不是常见教程会带你看的地方。

这里我列一个对比表,帮助理解这条“隐藏通路”和传统路径的差异:

观察维度传统WiFi连接模式混杂模式原始帧通路外接SDR方案
是否连接AP必须连接不需要连接不需要连接
看到的信号应用层数据物理层帧+RSSI大量原始IQ数据
成本板子本身板子本身需要RTL-SDR等硬件
官方教程覆盖度高低低
适合应用联网、上传数据环境感知、设备侦测广播解码、频谱分析

所以,“藏了一条无线电通路”这个说法,严格说不是“芯片藏了秘密”,而是“芯片能力就在那里,但官方文档的编排方式让它对大多数人隐藏了”。我们要做的,是把它从手册的旮旯里挖出来,变成自己能用上的功能。

4. 实测第一步:不连WiFi,扫出周围所有2.4GHz设备

我用最常用的Arduino IDE来演示,因为大部分人上手ESP32都是这条路。先说明环境:

  • 开发板:ESP32 DevKit V1(带USB-UART),你也可以用NodeMCU-32S、D1 Mini ESP32、S2/S3,代码差异不大;
  • 软件:Arduino IDE 2.x,安装esp32 by Espressif开发包,版本2.0.17左右即可;
  • 不需要外部SDR模块,不需要天线改装。

4.1 安装开发板时的一个坑

如果你在Arduino IDE里通过“开发板管理器”在线安装ESP32包,网络不稳时很容易卡在某个文件下载上。实际上,乐鑫官方提供离线包,可以在GitHub Release页面找esp32-arduino-lib-xxx-windows.tar.gz之类的压缩包,手动解压到~/Arduino/hardware/espressif/esp32目录。这样既避免反复下载,也方便团队统一版本。

4.2 扫描代码:把射频前端的监听口打开

下面这段代码是整篇文章的基石。它做的事情是:

  1. 初始化WiFi为WIFI_MODE_STA,但不调用WiFi.begin();
  2. 设置混杂模式接收滤波,只放行管理帧;
  3. 每隔一段时间换一个信道,确保扫完1、6、11这些常用信道;
  4. 在回调函数里解析beacon帧,提取SSID、BSSID和RSSI,并通过串口打印。
#include <WiFi.h> typedef struct { uint8_t mac[6]; uint8_t ssid[33]; int8_t rssi; uint8_t channel; uint8_t enc; } wifi_ap_record_light; void snifferCallback(void* buf, wifi_promiscuous_pkt_type_t type) { wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf; wifi_promiscuous_pkt_hdr_t* hdr = &pkt->rx_ctrl; uint8_t* payload = (uint8_t*)pkt->payload; uint8_t frameType = payload[0] & 0x0C; // frame type字段 uint8_t frameSubtype = (payload[0] >> 4) & 0x0F; // 只关心Beacon帧: type=0 (management), subtype=8 if (frameType == 0 && frameSubtype == 8) { // 固定偏移:BSSID在frame body之前, SSID在frame body的固定位置 uint8_t bssid[6]; char ssid[36] = {0}; memcpy(bssid, payload + 10, 6); int ssid_len = sendBeaconInfo(payload, ssid); int rssi = hdr->rssi; if (ssid_len > 0 && rssi > -100) { // 打印或保存 } } } // 简化版SSID解析 int sendBeaconInfo(uint8_t* payload, char* ssid) { // beacon frame body起始于固定偏移36字节 uint8_t* body = payload + 36; uint8_t len = body[1]; if (len > 32) len = 32; memcpy(ssid, body + 2, len); ssid[len] = 0; return len; } void setup() { Serial.begin(115200); WiFi.mode(WIFI_MODE_STA); wifi_config_t conf = {}; esp_wifi_set_config(WIFI_IF_STA, &conf); esp_wifi_set_promiscuous(true); esp_wifi_set_promiscuous_filter( &(wifi_promiscuous_filter_t){ .filter_mask = WIFI_PROMIS_FILTER_MANAGE } ); esp_wifi_set_promiscuous_rx_cb(&snifferCallback); } void loop() { // 每隔500ms切换信道,1、6、11轮流扫 delay(500); }

这段代码初看没问题,但实际跑起来有几个点必须注意:第一,esp_wifi_set_promiscuous_true之前最好先设置一个空白的wifi_config,否则某些版本固件会报错;第二,Beacon帧的SSID字段不是每次都从固定偏移读,因为部分AP开启了隐藏SSID,SSID长度可能为0;第三,回调函数里绝对不要调用Serial.print这类耗时的操作,否则WiFi栈会被阻塞,造成看门狗重启。正确做法是把解析结果通过xQueueSend放到队列里,让loop()去打印。

4.3 观察到的现象

我放在办公室跑了20分钟,串口输出里出现了周围十几台设备的beacon,包括:

  • 公司两个频段的路由器,RSSI在 -45 dBm 到 -65 dBm 之间浮动;
  • 同事桌面上的无线投屏器,信道一直固定在6;
  • 会议室的AP,虽然在另一个房间,但依然能被扫到;
  • 偶尔出现的RF噪声尖峰:RSSI跳到 -30 dBm 但看不到有效帧,这通常是蓝牙音频设备或无线鼠标在某个频点上的瞬时跳变。

这个结果已经足够说明问题:不需要外接SDR,ESP32自身就能扮演一个“2.4GHz频段嗅探器”。而这条通路恰恰是官方手动翻半天也未必翻得到的功能。把它做成一个OLED频谱显示器,或者配合微处理器做信道繁忙统计,都是很自然的扩展方向。

5. 进阶通路:用ESP-NOW搭一条不依赖路由器的裸数据链路

如果说混杂模式是“接收端的隐藏通路”,那ESP-NOW就是“发送端的隐藏通路”。ESP-NOW是乐鑫在WiFi协议栈之上、IP层之下做的一种MAC层通信协议。它不需要AP连接,不需要Socket,不需要TCP/IP,只要你知道对方设备的MAC地址,就可以直接发数据帧。本质上,它就是把你要传的数据塞进了一个自定义action frame里,通过前面那条射频通路发射出去。

5.1 ESP-NOW为什么适合做“裸链路”

我在一个户外采集项目里用它传雨滴传感器数据。当时备选方案有三个:蓝牙连接、HTTP over WiFi、ESP-NOW。

  • 蓝牙需要配对,对临时组网不友好,功耗也高;
  • HTTP over WiFi需要路由器,户外环境根本没AP;
  • ESP-NOW则像是两台对讲机:开机、拨到同一信道、按“通话”,完事。

而且它的实测延迟很低。我测试发送4字节负载,两块开发板相距5米无障碍,往返时间能稳定在10毫秒以内。对比蓝牙SPP动辄几十毫秒的连接握手,ESP-NOW打通后基本是“即发即收”的状态。

5.2 一对一的发送接收代码(最小可用版)

发送端:

#include <esp_now.h> #include <WiFi.h> uint8_t peerMac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; esp_now_peer_info_t peerInfo; void setup() { WiFi.mode(WIFI_STA); esp_now_init(); esp_now_add_peer(&peerInfo); } void loop() { int data = analogRead(34); // 读取传感器 esp_now_send(peerMac, (uint8_t*)&data, sizeof(data)); delay(100); }

接收端:

#include <esp_now.h> #include <WiFi.h> void onData(const uint8_t* mac, const uint8_t* data, int len) { int sensorValue; memcpy(&sensorValue, data, sizeof(sensorValue)); Serial.println(sensorValue); } void setup() { WiFi.mode(WIFI_STA); esp_now_init(); esp_now_register_recv_cb(onData); } void loop() { // 空 }

代码很简单,但“简单”背后有两个很容易踩的坑:

  1. 双方都必须先WiFi.mode(WIFI_STA)。很多人在发送端用WIFI_AP_STA,结果接收端偶尔收不到数据。因为ESP-NOW工作在STA模式下最稳定,AP模式会受到虚拟接口调度影响。
  2. esp_now_add_peer之前,建议用esp_now_set_peer_key或直接填peerInfo中的channel字段。如果两块板子默认信道不一致,虽然它们会在所有信道上监听,但发包选择的信道一旦隔离,就会造成“明明初始化成功但就是收不到”的现象。最简单的做法是把两块板子都固定在信道1。

5.3 数据通路的边界在哪里

ESP-NOW虽然好用,但它不是万能的。它只能在乐鑫自家的芯片之间通信——ESP32、ESP32-S2、ESP32-S3、ESP32-C3等,无法直接发给普通手机或电脑。所以它适合做“封闭生态”里的设备组网,不适合做“异构网关”。同时,ESP-NOW帧没有重传机制,在信道拥挤时丢包率会上升。我给户外节点做了三次重传+序号校验,实际场景下丢包率从15%降到了2%左右。这是任何裸数据通路都必须面对的工程问题:简单,但不能依赖它“一定送达”。

6. 避坑指南:从天线到供电的实战经验

这条路通到底层,灵敏度高,也意味着受影响的因素多。我在复现过程里踩了一圈坑,挑几个最值得说的。

6.1 天线位置和朝向,直接决定能不能听到“远方”

ESP32开发板上的PCB天线通常就在板子边缘。理论上,距离5米内的设备都能扫到,但如果你把板子塞进金属外壳、或者紧贴着金属面,RSSI会断崖式下降。我用外置IPEX天线后,同样的路由器从 -60 dBm 变成了 -52 dBm,提升立竿见影。做环境感知类项目,天线最好悬空,并且至少远离大地和金属支架10厘米以上。

6.2 供电纹波会污染RSSI

很多人用充电宝或开发板自带的AMS1117稳压给ESP32供电。实测发现,笔记本USB口的电压比较干净,RSSI波动在 ±2 dBm 左右;换成劣质DC-DC降压模块后,RSSI波动能到 ±6 dBm。原因是射频前端的LNA对电源噪声敏感。如果你要对多个设备做RSSI测距,尽量使用低纹波的LDO供电,并在天线支路加一个便宜的陶瓷电容。

6.3 回调函数里禁止做重活

这是新手最容易犯、也最危险的一个错误。混杂模式回调函数的运行上下文跟WiFi任务绑定,你如果直接在里面做malloc、Serial.print、甚至复杂的字符串拼接,轻则丢包,重则触发任务看门狗重启。正确姿势是:回调里只做结构体拷贝,然后通过FreeRTOS队列塞给loop()。记住,那条无线电通路每时每刻都在把帧数据塞进来,你不能让它等着你慢慢打印。

6.4 信道切换会让ESP-NOW短暂中断

如果你一边开着混杂模式扫描信道,一边跑ESP-NOW收发,就会发现掉帧很严重。这是因为切换信道相当于把射频前端重新调谐,而ESP-NOW默认在固定信道上监听,扫其他信道时必然漏掉那边的包。想同时做好“监听环境”和“数据通信”,要么分时切换,要么把接收端的监听信道固定在ESP-NOW所在信道,牺牲一部分全频段感知能力。我在最终方案里选择放弃全频扫描,只在信道6上做监听,才换来稳定的双向通信。

6.5 合法合规问题要提前想清楚

这一点必须写在前面:混杂模式能看到的802.11帧,在空中本来就是“广播”给所有能够解调的设备看的,尤其是beacon帧,它本身就是用来宣告“我在这”的。用它做环境感知、信道占用统计没有问题。但不要试图去破解别人WiFi密码、解密WPA2加密数据或干扰其他设备运行。做周边设备的被动侦测,也要限定在自己设备、公共频谱资源或明确授权的场景内。否则再好的技术,也只会给自己惹麻烦。

7. 最后扩展:这条通路还能往哪个方向走

把上面两段代码组合起来,你可以得到一台近乎零成本的“2.4GHz无线信号侦察器”:一边抓beacon算RSSI,一边用ESP-NOW把数据发到边上的汇聚节点。再进一步,给板子接一块0.96英寸OLED,就能在屏上画出不同WiFi信道的信号强度热力条。我做过一个版本,在会议室里跑了一个下午,能很直观地看出哪几个信道拥挤、哪几个信道空闲,对部署办公室AP挺有用。

如果你手头有ESP32-C3或ESP32-S3,代码改动也不大。C3的功耗更低,适合放在户外用电池跑;S3的ADC更精准,适合把传感器数据也塞进这条通路。只要思想转变过来:ESP32不只是“连上WiFi的微控制器”,它本身就是一个带着完整2.4GHz射频前端的无线电平台。那些没被写进官方手册的寄存器、过滤器和回调接口,才是这个芯片真正值钱的地方。

我自己调通混杂模式那天,正好在下雨,笔记本屏幕上的串口飘着一行行真实存在的路由器beacon,那一刻确实有点“听天空说话”的感觉。你现在如果手边正好有一块ESP32,不妨试一下把官方那个“连接WiFi”的固定思路放到一边,打开回调,看看你能听到什么。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询