深入Wi-Fi链路层:用ESP32玩转广播帧收发,打造无网络设备发现系统
你有没有遇到过这样的场景?
想让两个智能灯泡在没有路由器的情况下互相“打招呼”,或者希望一个传感器在检测到异常时瞬间唤醒整个房间的设备——但又不想依赖复杂的IP网络和MQTT服务器?
这时候,传统的TCP/IP通信就显得有点“笨重”了。协议栈层层封装、连接建立耗时、还得有AP中转……延迟动辄上百毫秒。
那有没有更轻、更快、更直接的方式?
答案是:绕开IP,直击Wi-Fi的MAC层。
今天,我们就来聊聊如何用一块不到10块钱的ESP32,实现真正的“无线自由”——不通过任何路由器或热点,仅靠Wi-Fi物理层发送和接收自定义广播包,完成设备间的即时通信与发现。
这不是理论推演,而是完全可以跑在你手头开发板上的实战技术。准备好了吗?我们从零开始,一步步拆解。
为什么选择ESP32做这件事?
在众多嵌入式芯片中,ESP32能脱颖而出,不是没有理由的。
它内置了完整的Wi-Fi基带处理器(PHY)和射频前端,更重要的是,乐鑫官方提供的ESP-IDF SDK开放了对底层Wi-Fi功能的强大控制接口。这让我们有机会跳出“连Wi-Fi → 获取IP → 发HTTP”的固定套路,深入到802.11协议栈的最底层。
特别是以下两个关键能力:
- ✅ 支持混杂模式(Promiscuous Mode):可以监听空中所有经过的Wi-Fi帧
- ✅ 提供原始帧发送接口(
esp_wifi_80211_tx):允许构造任意格式的802.11帧并发射出去
这两个功能组合起来,相当于给ESP32装上了“无线嗅探器+信号发射器”,让它不仅能“听”到周围的一切Wi-Fi动静,还能主动“说话”,哪怕没人给它分配IP地址。
先搞明白一件事:什么是Wi-Fi广播帧?
别被术语吓到。我们先来打个比方。
想象你在一间大教室里想找人:“张三!你在吗?”
如果你喊得足够响,全班人都能听见。这就是广播。
在Wi-Fi世界里也一样。每个数据包都有一个“目标MAC地址”。如果这个地址是ff:ff:ff:ff:ff:ff,那就表示:“嘿,所有人注意!”——这就是广播帧。
通常情况下,Wi-Fi网卡只会处理发给自己的帧(比如目的MAC匹配本机),其他的一律丢弃。但如果我们把网卡设为“混杂模式”,它就会变得“耳听八方”:不管是不是叫它的名字,全都收下来交给程序处理。
而如果我们自己构造一个帧,并强行把它发出去,哪怕它不是一个标准Beacon或Probe Request,只要格式合法,其他处于混杂模式的设备也能收到。
这就为我们打开了自定义无线协议的大门。
如何让ESP32“听见”所有Wi-Fi帧?
关键API:esp_wifi_set_promiscuous()
这是开启“顺风耳”模式的核心函数。
我们需要做的步骤非常清晰:
- 初始化Wi-Fi模块(但不启用STA或AP)
- 设置工作模式为
WIFI_MODE_NULL - 启用混杂模式
- 注册一个回调函数,用于处理每一个捕获到的帧
来看一段真正可用的代码:
#include "esp_wifi.h" #include "esp_log.h" static const char *TAG = "SNIFFER"; void sniffer_callback(void* buf, wifi_promiscuous_pkt_type_t type) { // 只关心管理帧(例如Beacon、Probe等) if (type != WIFI_PKT_MGMT) return; wifi_promiscuous_pkt_t *packet = (wifi_promiscuous_pkt_t*)buf; uint8_t *payload = packet->payload; uint16_t len = packet->rx_ctrl.sig_len; ESP_LOGI(TAG, "捕获帧 | 长度:%u RSSI:%d dBm 速率:%d Mbps", len, packet->rx_ctrl.rssi, packet->rx_ctrl.rate / 10); // 打印前16字节看看内容(通常是帧头) for (int i = 0; i < len && i < 16; i++) { printf("%02x ", payload[i]); } printf("\n"); } void start_sniffer_on_channel(int channel) { // 标准初始化配置 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL)); // 不作为STA/AP运行 // 启用混杂模式 ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(sniffer_callback)); // 锁定信道(2.4GHz常见信道1/6/11) esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); ESP_LOGI(TAG, "✅ 混杂模式启动,正在监听信道 %d", channel); }重点说明几个细节:
WIFI_MODE_NULL是关键:我们不需要联网,只是想当个“旁观者”,所以不能设置成STA或AP。- 回调函数运行在中断上下文:不要在里面做耗时操作(如
printf、malloc),否则可能造成系统崩溃。建议只做简单解析或将数据拷贝到队列中延后处理。 rx_ctrl结构体很实用:里面包含了RSSI(信号强度)、数据率、噪声等信息,可用于测距、环境感知等高级应用。- 默认只监听当前信道:如果你想扫描多个信道,可以用定时器周期性调用
esp_wifi_set_channel()切换。
如何让ESP32主动“喊话”?发一个自定义广播包!
光听不行,我们还要会说。
ESP-IDF提供了一个隐藏技能:esp_wifi_80211_tx()。它可以绕过整个TCP/IP协议栈,直接将一段内存中的字节作为802.11帧发送出去。
听起来是不是很像“发Raw Packet”?没错,就是这么硬核。
要点提醒:
- 必须提前设置好Wi-Fi模式(即使只是NULL模式)
- 发送信道必须与当前监听信道一致
- 帧长度不能超过约114字节(受硬件限制)
- 默认以最低速率(1Mbps)发送,穿透力更强
动手写一个广播帧
我们来构造一个类似Beacon帧的管理帧,但它不是真的Beacon,而是携带自定义信息的“伪广播”。
uint8_t custom_beacon_frame[128]; size_t frame_len = 0; void build_custom_broadcast_frame(uint8_t *mac_addr) { uint8_t *p = custom_beacon_frame; // 1. Frame Control 字段:类型=管理帧,子类型=Beacon (0x80) *p++ = 0x80; // FC: Management / Beacon *p++ = 0x00; // To DS=0, From DS=0, no retry, no PM, etc. // 2. Duration ID (2字节) *p++ = 0x00; *p++ = 0x00; // 3. Destination Address: 广播地址 memcpy(p, "\xff\xff\xff\xff\xff\xff", 6); p += 6; // 4. Source Address: 使用本机MAC memcpy(p, mac_addr, 6); p += 6; // 5. BSSID: 可以和SA相同 memcpy(p, mac_addr, 6); p += 6; // 6. Sequence Control (Fragment + Sequence Number) *p++ = 0xc0; *p++ = 0x6c; // 序列号可变,这里固定 // 7. Timestamp (8字节) - 简化为全0 memset(p, 0, 8); p += 8; // 8. Beacon Interval (2字节): 100 TU (~102.4ms) *p++ = 0x64; *p++ = 0x00; // 9. Capability Info (2字节): ESS基本能力 *p++ = 0x01; *p++ = 0x00; // 10. 自定义Payload(例如设备ID、状态码) const char *vendor_data = "DEV_ID=SENSOR_01|TEMP=25C"; size_t data_len = strlen(vendor_data); // 添加Tagged Parameter: Vendor Specific (Tag Number 221) *p++ = 221; // Tag Number *p++ = data_len; // Tag Length memcpy(p, vendor_data, data_len); p += data_len; frame_len = p - custom_beacon_frame; }发送出去!
void send_broadcast_packet() { uint8_t mac[6]; esp_wifi_get_mac(WIFI_IF_STA, mac); // 获取本机MAC build_custom_broadcast_frame(mac); esp_err_t ret = esp_wifi_80211_tx( WIFI_IF_STA, // 接口类型 custom_beacon_frame, // 帧缓冲区 frame_len, // 帧长度 false // 是否等待信道空闲(false = 立即发送) ); if (ret == ESP_OK) { ESP_LOGI("TX", "📡 广播帧已发出 (%u 字节)", frame_len); } else { ESP_LOGE("TX", "❌ 发送失败: %s", esp_err_to_name(ret)); } }现在,只要另一个ESP32在相同信道上开着混杂模式,就能收到这条消息!
实战应用场景:去中心化的设备发现系统
设想这样一个系统:
- 多个ESP32节点分布在不同房间
- 每个节点既是“广播者”也是“监听者”
- 它们每隔500ms广播一次自己的身份信息(ID、电量、状态)
- 同时持续监听信道6,一旦发现特定ID的设备出现,立即触发动作(如点亮LED)
完全无需Wi-Fi路由器,也不需要互联网,纯粹靠“空气传播”完成通信。
这种架构特别适合:
- 🏠 智能家居本地联动(开门→开灯)
- 🏭 工业现场的状态广播
- 🔋 超低功耗传感网络(配合深度睡眠)
- 🚨 紧急唤醒机制(火灾报警器触发所有终端)
常见问题与避坑指南
❓ 能干扰别人的Wi-Fi吗?违法吗?
技术本身无罪,但使用需谨慎。
- ✅ 合法用途:产品开发、测试、科研实验
- ❌ 禁止行为:伪造认证帧、频繁发送干扰正常通信、进行未授权的渗透测试
建议:
- 控制发送频率 ≤ 10次/秒
- 使用非重叠信道(1、6、11)
- 避免在密集Wi-Fi环境中高频操作
❓ 手机或其他设备能收到这些帧吗?
大多数手机Wi-Fi芯片不允许进入混杂模式(除非Root + 特殊驱动),所以普通App无法捕获这类帧。但如果接收端也是ESP32或其他支持raw rx的设备(如树莓派+ALFA网卡),就没问题。
❓ 如何提高接收成功率?
- 接收端轮询切换信道(如每200ms切一次)
- 利用RSSI过滤弱信号(排除误报)
- 在Payload中加入CRC校验或魔数(Magic Number),防止解析错误帧
❓ 可以加密吗?
802.11原始帧本身不支持加密,但我们可以在Payload里加料:
// 示例:简单异或加密 for (int i = 0; i < data_len; i++) { vendor_data[i] ^= 0xAA; // 密钥0xAA }虽然不够安全,但对于防伪造、防误读已经够用了。
性能实测参考(基于ESP32-WROOM-32)
| 项目 | 数值 |
|---|---|
| 最大发射功率 | +19.5 dBm |
| 接收灵敏度(1 Mbps) | -94 dBm |
| 单帧最大长度 | ~114 bytes |
| 发送延迟 | < 1 ms |
| 典型通信距离 | 开阔地 80~100米 |
| 功耗(广播间隔1s) | ~20mA |
⚡ 小技巧:结合定时器+light-sleep,可将平均功耗降至1mA以下!
写在最后:这不是终点,而是起点
当你第一次看到串口打印出“Received RSSI: -67”的那一刻,你会意识到——
你不再只是一个“连Wi-Fi的用户”,而是成了无线世界的“参与者”。
你可以让设备在无网络环境下彼此感知,可以用极低延迟传递关键状态,甚至可以用RSSI做粗略定位。
而这套能力,只需要一块ESP32 + 几十行代码就能实现。
未来随着ESP32-C系列(RISC-V内核)和Wi-Fi 6的支持逐步完善,这类底层通信将变得更加高效、节能、智能。
所以,别再只用ESP32发HTTP请求了。试着打开它的另一面,去探索那些藏在协议深处的可能性。
毕竟,真正的创新,往往始于对“常规”的打破。
如果你也正在尝试类似的项目,欢迎在评论区分享你的想法和踩过的坑。我们一起把无线玩出花来。 🛰️