☰
ESP32隐藏的无线电通路:用CSI信道状态信息实现无线感知
2026/10/20 1:05:44 网站建设 项目流程

如果你只把ESP32当一块能连WiFi、跑蓝牙的开发板,那你很可能和它最值钱的那部分功能擦肩而过。我在搭一套室内人体存在检测方案时,原本只想用WiFi信号强度(RSSI)判断房间里有没有人,结果在翻官方SDK头文件的过程中,发现了一条连手册都没仔细说明的无线电通路——WiFi射频前端在接收每个数据包的同时,会顺带输出一组叫做CSI(信道状态信息)的底层数据。这篇文章就记录我是怎么发现它、打开它,并用它做了三个能复现的实测。适合正在做无线感知、智能家居、无摄像头安防、射频调试的开发者参考,哪怕你是刚接触微控制器的新手,只要会烧录官方SDK,也能跟着走通。

1. 一次抓包排查,让我撞见WiFi驱动里躺着的"影子接口"

先说项目背景。我手头有个智能家居的小项目,想在房间里做一个不依赖摄像头的存在检测:判断屋里到底有没有人,人在哪个区域活动。最初的想法很朴素——放一块ESP32在房间里,监听周围的WiFi流量,通过RSSI波动来判断有没有活物在动。

实测跑了一周,结论是:RSSI确实会随人走动而波动,但信号质量太差了。它的问题在于,RSSI是一个把所有路径能量揉在一起的平均标量,对环境的敏感度很低。人站在一米外和五米外,数值变化可能只有1~2dBm;而路由器自身功率波动、隔壁房间的微波炉干扰,也能造成同样量级的跳变。更麻烦的是,当人坐下来不动、只是轻微呼吸时,RSSI几乎纹丝不动,完全区分不了"房间里有人但很安静"和"房间空着"。

于是我把方向转向更底层的抓包:开启WiFi的混杂模式(promiscuous mode),让板子不关联任何网络,直接在空气里接收所有802.11帧。这样能拿到每个包的RSSI、信道、速率、帧类型,信息量确实比之前多了一截。但折腾几天后我意识到,这些字段统统是"接收结果",描述的是"信号到头了,强度多少",而不是"信号在空间里怎么走的"。我需要的是路径信息,是无线电波在墙壁、地板、家具、人体之间反复反弹后留下的痕迹。

就在我准备放弃、打算去买毫米波雷达模块的时候,一次常规的SDK代码检索救了这个项目。我在驱动目录里搜关键词,想看看WiFi收包路径上还有什么没暴露的字段,结果跳出来一整套以 CSI 命名的接口:配置函数、使能函数、回调注册函数。头文件里的注释只有可怜的三四行,官方那个几百页的技术参考手册里翻遍也没有专门章节讲它。API文档里倒是有一小段,但写得极其克制,只说"用于获取信道状态信息",没有示例,没有使用场景,连参数说明都含糊其辞。

我当时的第一个反应是:这该不会是给工厂产线校准用的内部接口,或者某个废弃版本留下的僵尸代码吧?于是去翻了这套SDK的历史提交记录,发现这批CSI接口很早就加进了WiFi驱动,而且一直有人维护,但它主要用于射频校准、吞吐调优这类内部场景。顺藤摸瓜又找到几个学术性质的WiFi感知项目,这些项目用ESP32做过穿墙检测、人体动作识别,用的正是这条路。看到这我基本确定了:这不是后门,而是WiFi接收机在解调每个数据包时本来就必须要算的一笔账——信道均衡。为了让收到的OFDM符号还原成正确的比特,接收机必须估计出无线信道对每个子载波的幅度和相位影响,这个估计结果就是CSI。驱动默认算完就扔,我们做的不过是把它半路截下来。

就这么一个"别人算完就扔"的中间量,成了我后来整套感知方案的基石。它确实没有出现在常规手册里,但对做无线感知的人来说,它比手册里任何一张引脚图都值钱。

2. 为什么说CSI是一条"无线电通路":把电波走过的路拍成一张CT

理解这条通路之前,得先纠正一个直觉。很多人以为WiFi信号从路由器到手机,是像手电筒光束那样走一条直线。实际完全不是这么回事。2.4GHz频段的电波波长大概12厘米,遇到房间里的金属、墙壁、人体、地板,会发生反射、衍射、散射。发射机发出的能量会沿着无数条路径到达接收机:有直接从空中过来的直射径,有打到天花板再弹下来的反射径,有穿过隔墙衰减后的透射径,还有在两面墙之间来回弹好几次的多次反射径。我们手机上显示"信号满格",其实收到的是一大堆路径回波的叠加结果。

这些路径的长度不同,到达时间不同,经历的衰减不同,所以到达接收天线时,每条路径贡献的幅度和相位都不一样。这一整套路径集合,就是你设备之间那条真实的"无线电通路"。人往房间里一站,相当于在通路上塞进一个会吸收、反射电波的障碍物;人一走动,所有路径的长度和衰减系数都在变。

RSSI这个指标的问题就在这里——它把所有路径的能量加起来求了个总功率,压缩成一个小数。房间平均亮度从60变成62,你能从中推断出"有个人站在窗前挡住了光"吗?很难。RSSI就是这个"平均亮度",信息粒度太粗了。

而WiFi本身用的是OFDM(正交频分复用)调制。一条20MHz的信道内部,其实是几十个并行的窄带子载波,每个子载波占据一小段频率,各自独立承载数据。不同频率的子载波,对同一组物理路径的响应是不同的:频率高一点的子载波,对某些路径的相位旋转更敏感;频率低一点的子载波,对另一些障碍物的衰减反应更剧烈。如果把整条信道看成一条宽阔的公路,每个子载波就像是公路上的一个车道,每个车道感受到的路况细节都不一样。

CSI做的事情,就是逐个车道地测量路况。它给出的是每个子载波上无线信道的复数响应H(f) = 幅度 × e^(j相位)。在20MHz带宽下,ESP32会给出64个子载波的实部加虚部,一共128个int8数值。你可以从里面算出每个子载波上的信号幅度和相位偏移。

拿CT来类比最贴切:RSSI是一张"器官平均密度报告",CSI则是把人体一层层切开拍摄的断层扫描片。房间里多了一个人、一扇门被打开、窗帘被风吹动、甚至一段墙皮后面的水管开始流水,都会在CSI的某些子载波上留下可观测的变化——因为不同路径对不同频率的敏感度不一样,所以CSI能把"环境发生了变化"这件事,拆解成一组高维的、可区分的信号特征。

所以标题里说的"无线电通路",并不是指ESP32藏了第二块射频芯片。它就是同一个WiFi收发前端,在正常收包的同时,额外暴露出来的一个"可以看到通路本身"的数据出口。这个出口面向的不是数据比特,而是物理世界。对做环境感知的人来说,这相当于给一块十块钱出头的开发板,白送了一个传感器阵列。

3. 撬开通路:配置、回调、数据格式一步步拆开

光知道概念没用,得让数据流起来。下面是我验证后能稳定复现的完整流程,用的是官方SDK,硬件就是普通ESP32开发板加一根外接天线。

3.1 准备工作里的三个关键前提

第一个前提是SDK版本。CSI接口在不同时期的API命名有变化,建议用较新的版本,老版本有些字段行为不一样。第二个前提是必须有一个稳定的发包源。CSI不是主动探测雷达,它只在收到802.11帧时才会产生数据,没人给你发包,这条通路就是哑的。最简单的方法是让ESP32以STA模式连上家里的路由器,路由器会周期性广播beacon帧,天然就是稳定的发包源。第三个前提是关掉WiFi省电模式,否则在空闲时收包会断断续续,CSI数据流也跟着一卡一卡,根本没法做时序分析。

3.2 初始化代码与参数含义

先看初始化部分的代码,这段放在WiFi已经启动之后运行:

#include "esp_wifi.h" static void csi_cb(void *ctx, wifi_csi_info_t *data); void csi_init(void) { // 1. 配置CSI采样参数 wifi_csi_config_t csi_cfg = { .lltf_en = 1, // 用长训练序列估计信道 .htltf_en = 1, // 用HT长训练序列估计信道 .stbc_htltf2_en = 0, // 不需要STBC第二段数据 .ltf_merge_en = 1, // 把多段训练序列结果合并 .channel_filter_en = 0, // 先关掉信道平滑滤波 .manu_scale = 0, // 使用自动增益 .shift = 0, // 手动缩放值,自动增益时无效 .manu_scale_filter = 0, .profile = 0, }; esp_wifi_set_csi_config(&csi_cfg); // 2. 打开总开关 esp_wifi_set_csi(true); // 3. 注册回调(部分SDK版本叫esp_wifi_set_csi_rx_cb) esp_wifi_set_csi_cb(csi_cb); // 4. 关闭省电模式,保证数据流连续 esp_wifi_set_ps(WIFI_PS_NONE); }

这几个配置字段初次接触容易一头雾水,我按自己的理解解释一下。lltf和htltf分别是802.11帧头里的长训练序列,接收机靠它们来估计信道,开这两个就是让CSI尽量利用帧里的参考信号。ltf_merge_en是是否把多段训练序列的估计结果合并,合并之后数据更稳,我建议开。channel_filter_en是驱动的信道平滑滤波,我建议第一次实验先关,看原始数据长什么样,后面遇到幅度漂移太严重再打开试试。manu_scale和shift是手动增益缩放,默认先别动,后面踩坑环节我会说什么时候需要碰它。

3.3 回调里的数据到底长什么样

回调函数是数据出口,结构体定义大致是这样:

typedef struct { wifi_pkt_rx_ctrl_t rx_ctrl; // 这个包的射频控制信息 uint8_t *buf; // 原始802.11帧数据 uint8_t len; // 原始帧长度 signed char *csi_data; // CSI数据数组 uint8_t csi_len; // CSI数据长度 } wifi_csi_info_t;

最关键的是csi_data和csi_len。在20MHz带宽、单天线的典型配置下,csi_len通常是128字节,对应64个子载波,每个子载波用2个int8表示:第一个是虚部Q,第二个是实部I。注意是有符号数,直接用的话幅度算法是:

float amp = sqrtf((float)(I * I + Q * Q));

回调里拿到数据后,我建议第一件事不是算特征,而是把数据塞进一个环形队列,丢给另一个任务去处理。原因后面踩坑环节细说,这里先记住:回调函数里绝对不能做耗时运算和串口打印。

再来看rx_ctrl,这里面有channel、rssi、rate、sig_mode这些字段。我做实验时会用sig_mode和rate过滤一下,只处理OFDM格式的数据包,因为802.11b的CCK包不携带CSI信息,混进来只会污染数据集。回调处理代码可以参考这个骨架:

static void csi_cb(void *ctx, wifi_csi_info_t *data) { if (data->csi_len < 128) return; // 只处理完整的20MHz单流数据 wifi_pkt_rx_ctrl_t rx =>#include "esp_now.h" void espnow_init(void) { esp_now_init(); // 填对端MAC地址,广播地址为FF:FF:FF:FF:FF:FF esp_now_peer_info_t peer = {0}; memcpy(peer.peer_addr, broadcast_mac, 6); peer.channel = 1; peer.ifidx = ESP_IF_WIFI_STA; esp_now_add_peer(&peer); } void espnow_send_cmd(void) { uint8_t cmd = 0xA1; esp_now_send(broadcast_mac, &cmd, sizeof(cmd)); }

注意一个关键点:节点A同时跑WiFi连接和ESP-NOW,两者共用射频前端,信道必须一致。我直接把ESP-NOW的信道设成和路由器一样的信道,实测稳定。这套系统跑下来,整个链路从"无线电波感知环境"到"无线电波传输指令",全程没有任何线缆依赖,而且两个功能共用的是同一块芯片里的同一个射频通路。这就是标题里"通路"二字在我项目里的落点:它既感知,又传输,一块开发板干了两件事。

5. 踩坑记录:我一开始拿到的数据根本不能用

CSI这条路刚上手时,被我自己的天真坑了好几天。这里把踩过的坑按排查链路完整列出来,你可以按顺序自查。

5.1 第一个坑:收了一堆包,回调却一个都不触发

CSI只在收到OFDM调制格式的包时才会产生。如果周围设备的速率被锁定在802.11b的CCK模式,你的板子收包正常,但CSI回调就是静默。我排查这个问题的过程是:先开混杂模式确认信道上有包,然后打印rx_ctrl里的sig_mode和rate字段,发现全是CCK。解决方法是让ESP32自己主动向路由器发包——调成STA模式,定期向网关地址发一个UDP数据包,或者直接ping网关。自己发出去的包也会触发自己收包路径上的CSI回调,至少能保证数据源是OFDM格式。

5.2 第二个坑:天线分集让数据忽高忽低

如果开发板上有板载天线,还接了外接天线,或者模块支持天线分集切换,那CSI的来源天线可能每个包都在变。我一开始看到的现象是,64个子载波的幅度整体成片跳动,像被人反复调音量。后来排查发现,分集逻辑会根据信号强度自动选择天线,两套天线对着不同方向,各自看到的信道自然完全不同。解决方法是尽量用单一外接天线,并确认没有开启分集切换。这类问题用眼睛看波形最直观:整体电平骤变、但内部细节没变化,多半就是天线切换了。

5.3 第三个坑:省电模式把数据流掐断了

默认的WiFi省电模式在空闲时会进入睡眠,beacon帧也不是每个都收,CSI数据流自然断断续续。我一度以为是丢包,后来查驱动说明才想起来还有省电这回事。解决很简单,初始化里加一行esp_wifi_set_ps(WIFI_PS_NONE)。这个坑对做吞吐测试的人无所谓,但对做感知的人来说是致命的,因为时间连续性丢失意味着方差特征的计算根本不准。

5.4 第四个坑:自动增益导致的幅度漂移

CSI的I/Q数值受接收机AGC(自动增益控制)影响。环境变化引起信号总功率波动时,AGC会调整增益,导致CSI幅度的整体基线漂移。这种漂移有时候比人体动作造成的真实信号变化还大,直接算方差的话,会得到一堆假警报。我的对策是两件套:一是初始化时将channel_filter_en打开,让驱动做平滑滤波;二是在算法层面对每个窗口的数据做归一化,即减均值、除以标准差,把基线漂移消掉再算特征。

5.5 第五个坑:int8量化噪声吃掉细节

CSI数据是8位有符号整数,当信号较弱时,I/Q值可能只有几个LSB在跳,量化噪声占比很大,算出来的相位精度惨不忍睹。这个坑没有完美解法,只能避重就轻:尽量用幅度特征,不要轻易用原始相位;如果一定要用相位差,就多包平均以降噪。我在做方向判别实验时,相位数据基本没派上用场,核心决策全靠幅度。

5.6 第六个坑:回调函数里做重活导致丢包

CSI回调是驱动上下文里直接调用的,里面写得越轻越好。我最初图省事,直接在回调里算方差、打印日志、甚至驱动屏幕刷新,结果丢包率急剧上升,数据时间序列稀疏到没法用。正确写法是回调里只做拷贝和入队,其余计算全部移到主任务或独立任务;环形队列大小要给足,我用的是每项128字节、深度256。另外,串口打印务必关掉或大幅降频。

5.7 第七个坑:SDK升级后数据格式变了

这个坑最阴险。我曾经在旧版SDK上跑通一整套数据格式解析和算法,升级到新版后,csi_len变了,子载波顺序也变了,画出来的波形直接从"S型"变成"Z型"。排查到最后发现是SDK驱动的PHY参数调整导致数据排列变化。经验教训就一条:升级SDK后,先重跑一遍数据可视化脚本,确认格式没有变化再继续开发,别盲目信任二进制兼容。

6. 手册之外还有两条相邻的暗门,以及必须守住的边界

CSI是这条无线电通路的核心,但它不是全部。顺着同样的思路往底层再挖,还能摸到两条和它相邻的隐藏通道,但它们的使用边界也更明显。

6.1 暗门一:原始帧收发,不连网络直接和空气对话

除了CSI,WiFi驱动还暴露了原始帧收发能力。开启混杂模式后,可以不关联任何AP就接收空气中的802.11帧,并解析出完整的MAC头、帧体;同时,esp_wifi_80211_tx这类接口允许你在不入网的状态下,把自定义构造的802.11帧直接发出去。这意味着你可以不走TCP/IP协议栈,在片子和片子之间建立一套完全自定义的裸射频协议。配合CSI使用,它还能充当"主动探针":你用原始帧发一个特定信号,再用CSI去观测这个信号走过的路径变化,等于把被动感知升级成了主动探测。这类玩法非常适合做设备间快节奏应答、自定义遥控协议、以及射频相关教学实验。

6.2 暗门二:直接操作PHY寄存器读取频谱能量

还有一个更野的方向:绕过所有驱动API,直接操作WiFi基带PHY寄存器,把射频前端下变频后的能量数据读出来。社区里确实有逆向项目把ESP32变成过一个粗糙的2.4GHz频谱观测工具,能看到整个频段上的能量起伏。但这个方向的代价很大——寄存器映射几乎没有任何文档,SDK版本一升级就可能全部失效,而且操作不当还可能让WiFi射频前端进入异常状态。我建议把它当作考古项目来看看热闹就好,别放进任何生产级方案里。

6.3 边界在哪:它不是雷达,也不是无限权力

说了这么多花活,我得泼两盆冷水。第一盆,CSI不是雷达,别期待它测距、测速、成像。20MHz信道带宽对应的理论距离分辨率大约在7.5米量级,做"房间里有没有人"绰绰有余,做"这面墙后的人在哪个精确位置"完全不可能。第二盆,所有实验都要在自己的设备之间进行,别拿这套能力去干扰别人家的网络,也别改装发射功放把功率提上去。ESP32本来就在ISM频段合法工作,你只是在同一个频段、同一个功率约束下多看了几眼底层数据,这没问题;但越过功率和干扰的红线就是另一回事了。

回到标题的疑问。ESP32里并没有藏着一块传说中的独立无线电芯片,它真正藏着的是WiFi射频前端对外暴露的底层数据通路。这条通路没有出现在普通的开发手册里,但它和芯片里任何一条引脚的信号一样真实。你不掏一分钱硬件成本,就能把十块钱的模组变成一台能"看见"环境变化的传感器,这才是这条通路最值钱的地方。

最后再说几句个人体会。拿到CSI之后,你最该做的第一件事不是上网找现成的机器学习模型,而是花一个周末把子载波幅度实时画成热力图,然后人站到不同位置、推门、开柜子、拉窗帘,眼睛盯着波形建立直觉。我做出来的所有能用的特征,基本都是从这段"看图"经历里反推的。还有一点经验:不同官方SDK版本下CSI的表现细节确实有差异,如果哪一天你升级SDK之后数据突然变得很奇怪,先回退版本或者重新校准数据格式,别一上来就怀疑天线坏了。

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

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

立即咨询