1. CSI信号为什么能用来"看"人,而不是只能"连"网
先说个反直觉的事儿:Wi-Fi从诞生那天起,就一直在"浪费"信息。我们平时只关心它能不能上网、速度快不快,但Wi-Fi信号在空气中传播时,会被人体、家具、墙壁反射、散射、衍射,这些物理痕迹全都体现在信道状态信息(CSI,Channel State Information)里。换句话说,每一次你从路由器旁边走过,Wi-Fi信号其实都"拍了一张照片",只是我们过去从来不读这些照片。
RSSI(接收信号强度)只能告诉你信号整体是强是弱,就像一个站在远处的人只能看到房间里灯是亮是灭。而CSI能告诉你信号在多个频率子载波上的幅度和相位变化,相当于给每个子载波都装了一个微型传感器。人体动作——尤其是跌倒这种大位移、高速度的动作——会引起CSI时序上的显著模式变化:幅度波动加剧、特定子载波出现尖峰、相位差出现不连续跳变。这些特征,就是跌倒检测算法要抓的核心线索。
用CSI做跌倒检测,相比摄像头、红外阵列、毫米波雷达这些方案,最大的优势是三个:一是无感部署,直接复用现有Wi-Fi基础设施,不需要额外买传感器硬件(除了一个能采CSI的接收端);二是不侵犯隐私,CSI是射频信号,不是图像,不会拍到任何面部或身体细节,放在卧室、卫生间这种私密场景没有心理负担;三是穿墙能力,Wi-Fi在2.4GHz频段穿透性尚可,隔着一堵墙仍然能捕捉到隔壁房间的人体动作,这是摄像头和普通红外方案做不到的。
那问题来了:CSI这么好,为什么过去没多少人用?因为传统的CSI采集需要专用网卡(比如Intel 5300、Atheros网卡)配合Linux系统打补丁,研究门槛高,设备体积大,功耗也不友好。直到ESP32系列芯片开始支持CSI采集,这个技术才算真正走到了嵌入式开发者面前——你手上那块几十块钱的开发板,就能变成一个"Wi-Fi雷达"接收机。
1.1 为什么选ESP32而不是树莓派或专用网卡
我早期做CSI实验用的是树莓派加网卡方案,稳定性和数据采集率都不错,但整套系统功耗高、体积大、启动慢,而且需要在Linux内核层打补丁,每次换内核版本都是噩梦。ESP32的出现改变了这个局面:它本身就是Wi-Fi收发芯片,CPU和射频前端集成在一块硅片上,SDK提供了CSI采集接口,一条esp_wifi_set_csi配置下去,就能拿到每个Wi-Fi数据包对应的CSI复数矩阵。
选ESP32还有一个实际考量:部署形态。跌倒检测最终要落到家庭、养老院、病房这类场景,你不能在每个房间里放一台树莓派,但可以放一个ESP32模组做接收端,搭配现有的无线路由器发射信号,这样的节点成本压到几十块钱,功耗可以做到电池供电几个月(如果牺牲部分采样率)。另外,ESP32社区活跃度极高,Arduino、ESP-IDF、MicroPython生态都很成熟,就算你不是射频专业出身,也能在社区方案基础上快速跑起来。
1.2 不是所有ESP32都支持CSI:选型避坑
这是最容易踩的第一个坑。我在群里见过不止一个人买了老款ESP32(经典款,双核240MHz那款),折腾半天发现SDK里根本没有CSI接口。实际上,CSI采集能力是从部分新一代芯片(如ESP32-C3、ESP32-S3等)开始引入的,具体支持情况在不同芯片版本上有差异。经典ESP32在多数官方SDK配置下不提供CSI数据获取接口,贸然用老芯片会导致项目停滞。
我自己的经验是:优先选择ESP32-S3或ESP32-C3系列做CSI开发。原因有三点。第一,这两个系列在官方仓库中能找到带CSI采集能力的demo或补丁,资料相对齐全。第二,CPU性能足够跑基本的特征提取和阈值判断逻辑,不需要外挂MCU。第三,S3还带向量指令和足够大的PSRAM,后面如果要上轻量级神经网络做分类,性能预算更充裕。开发时务必先查清楚你手上的芯片型号和SDK分支是否开启CSI功能,别等硬件到手再发现问题。
2. ESP32 CSI采集环境搭建:从SDK配置到第一帧数据
2.1 开发环境的三个推荐路线
CSI开发的基础环境搭建,我会根据读者的基础分成三个路线。
路线一:ESP-IDF原生开发(推荐)。CSI接口本质上依赖乐鑫的Wi-Fi驱动层,ESP-IDF提供了最完整的配置和控制能力。建议使用官方推荐的稳定版本IDF,配合特定的CSI补丁或使用支持CSI的较新版本代码分支。搭建过程就是常规的IDF环境配置:安装工具链、设置IDF_PATH、编译烧录。重点是编译之前要先把Wi-Fi配置和CSI相关的宏定义打开,具体可见下文。
路线二:Arduino框架快速验证。如果只是想先看CSI数据长什么样,Arduino开发方式的上手成本最低。有开发者把CSI采集封装成了Arduino库,你只需在menuconfig或头文件里配置Wi-Fi信道、采样率,然后调用回调函数接收CSI数据。这个路线的缺点是底层控制粒度不够细,过滤器和采样调度做不了太深入的定制,但验证信号特征足够了。
路线三:MicroPython。我个人的建议是——别用它做CSI。MicroPython的Wi-Fi层封装太厚,CSI数据是高速实时流,Python解释器的性能会成为瓶颈。除非你只做极低采样率的原理演示,否则会浪费大量时间在性能调优上,还未必调得出来。
2.2 Wi-Fi协议模式与信道选择的隐性门槛
CSI数据的质量,很大程度上取决于你让ESP32工作在什么Wi-Fi模式下。这里有个容易忽略的细节:CSI是Wi-Fi数据包的物理层附属信息,所以ESP32必须参与到Wi-Fi数据包的收发过程中才能采集。
建议将ESP32配置为Station模式(即作为站点连接到一个路由器),并且让路由器与ESP32之间要持续有数据包传输。如果网络空闲,数据包太少,CSI采样率就上不来。这就需要人为制造流量,常见做法是让ESP32周期性向路由器发送Ping包,或者用另一个设备做UDP小包打流,目的就是保证信道上有足够的包可供CSI采样。实测中,UDP打流比Ping的效果更稳定,因为Ping包默认优先级不高,速率上不去。
信道选择也有讲究。2.4GHz频段在家庭环境里拥挤不堪,蓝牙、微波炉、邻居的Wi-Fi都在这个频段打架。建议在部署现场先用手机App扫描一下信道占用情况,选一个相对空闲的信道固定住(禁用自动信道切换),否则路由器一跳频,你的CSI数据流就断片了。5GHz频段虽然干扰更少,但ESP32的CSI支持情况、信号穿透力和覆盖范围需要单独测试,不是首选。
2.3 编译配置和CSI回调的骨架代码
下面这段是基于ESP-IDF风格的CSI采集初始化逻辑,我把它写成代码块,实际开发时你要结合具体的SDK版本调整API名称。这段代码的意义是让你明白CSI采集的配置流程,而不是直接复制就能跑。
#include "esp_wifi.h" #include "esp_wifi_types.h" // CSI回调函数:每收到一个CSI帧,这里就会被调用 static void csi_data_handler(void *ctx, wifi_csi_info_t *data) { //>// 伪代码,核心逻辑演示 #define WINDOW_SIZE 50 // 根据采样率调整 #define SLIDE_STEP 10 float buffer[WINDOW_SIZE]; // 环形缓冲区,存每帧的平均幅度 void on_new_csi_frame(float avg_mag) { // 入队 push_to_ring_buffer(buffer, avg_mag, WINDOW_SIZE); // 每当积累SLIDE_STEP个新帧,计算一次特征 if (frames_since_last_process >= SLIDE_STEP) { float variance = calc_variance(buffer, WINDOW_SIZE); float kurtosis = calc_kurtosis(buffer, WINDOW_SIZE); // 送识别器 fall_detector_update(variance, kurtosis); frames_since_last_process = 0; } }3.3 容易踩的性能陷阱:在MCU上处理CSI要省着点
ESP32虽然有240MHz双核(指S3),但CSI回调频率高了之后,CPU占用率会迅速飙升。我踩过的坑有两个。
第一个坑是在CSI回调函数里直接做浮点运算和打印。CSI回调是高频中断上下文,在这里做大量printf或复杂数学运算,会拖垮整个Wi-Fi协议栈,导致掉包甚至重启。正确做法是回调里只做最轻量的拷贝(把原始CSI数据拷入DMA缓冲区或环形队列),把特征计算放到独立的任务中执行。如果内存够,甚至可以双缓冲:一个缓冲区在接收,一个在做特征提取。
第二个坑是忽略子载波选择。64路子载波的数据都算没必要,很多研究证明低频段子载波对身体整体运动更敏感,高频段子载波容易被小尺度反射物(比如手部动作)干扰。我的做法是在预处理阶段就只保留一部分子载波的数据参与特征计算,既能降噪还能减少计算量。具体选哪些子载波,建议你在自己的部署环境里跑一批数据,做相关性分析后确定。
4. 跌倒识别器:先跑通阈值法,再决定要不要上模型
4.1 定制的阈值规则比一上来就跑模型更务实
现在很多教程一上来就让你训练SVM、随机森林或者神经网络来分类跌倒。大方向没错,但我不建议第一次做就上模型。原因很现实:模型训练需要标注数据,你需要采集大量动作样本(摔倒、坐下、躺下、下蹲、走路、跑动),每一条都要打标签,这是一件极其耗时费力的工作,而且每个环境的样本分布都不一样,换一个房间模型效果可能就崩了。
我的建议是先把阈值规则跑通,让整个系统能端到端工作,然后再考虑用模型提升准确率。阈值检测的思路很简单:在滑动窗口内,如果方差超过某个阈值T1,并且峰度超过T2,紧接着下一个窗口方差迅速跌落到接近零,就判定为一次跌倒事件。这套规则背后的物理逻辑很直白——跌倒就是"剧烈运动到静止"的突变过程。
4.2 阈值标定的实操过程
阈值不是拍脑袋定的,要靠在目标环境里采集数据来标定。具体步骤如下。
第一步,采集日常活动基线数据。让测试者在房间里正常活动:走路、坐下、起立、弯腰、下蹲,各持续几分钟。把所有窗口的方差和峰度记录下来,求出分布区间。
第二步,采集模拟跌倒数据。在安全防护下(铺厚垫子),让测试者面向、背向、侧向从站立位置倒下,记录跌倒过程中的特征值。注意跌倒的多样性很重要:不同方向、不同速度、从椅子滑落、从床上滚落,都要覆盖。
第三步,设定阈值留出安全余量。阈值不能刚好卡在日常数据和跌倒数据的分界线上,因为实时环境会有噪声波动。我的经验是取日常活动分布的第95百分位数,和跌倒数据分布的第5百分位数,两者之间取一个中位点的70%-80%位置作为阈值。宁可多报几次误警,也要保证跌倒不漏报,因为这直接关系到后续告警的可信度。
阈值法跑通后,你会发现一个尴尬但真实的问题:某些动作和跌倒的特征高度相似,比如猛地坐到沙发上、故意快速蹲下。这时候阈值法已经不够用了,再考虑引入模型。
4.3 模型路线怎么平滑升级
我在CSI跌倒检测上验证过几条模型路线,从易到难排序:
逻辑回归或决策树:用前面提取的3-5个特征做分类。训练集不需要太大,几百条样本就能训练,模型体积小,ESP32完全跑得动。适合作为从阈值法到模型法之间的过渡。
随机森林或梯度提升树:效果比逻辑回归好一些,对特征间非线性关系捕捉更强。但模型体积稍大,在ESP32上用C语言重新实现会稍微费事一些,不过有现成的微型模型推理库可以用。
一维卷积神经网络或LSTM:直接把时序幅度数据作为输入,让网络自己学特征。效果上限最高,但训练数据需求量也最大,而且ESP32-S3上跑推理速度需要实测。我建议先把前两种做扎实,确有必要再上深度学习,否则开发周期会翻好几倍。
无论选哪种模型,训练数据的环境覆盖度都是决定落地成败的因素。采集数据的时候,一定要覆盖多人、多房间、多时间段,否则模型很容易过拟合到你那个测试现场。
4.4 误报抑制:让系统学会"等一下再报警"
做一个跌倒检测系统,最让用户反感的不是漏报,而是误报。如果系统三天两头来个假报警,老人和家属都会选择关掉它。所以要在识别器后面加一个二次确认机制,我把这条经验写在最前面——它救了我的项目很多次。
二次确认的常用办法:第一次触发跌倒条件时,不立即告警,而是进入一个确认窗口(比如3秒)。在这个窗口内持续观察:如果幅度方差持续低(人确实没有后续动作),那就可以判定为跌倒;如果窗口内又出现了明显的活动特征(比如人站起来走动),那就撤销报警。这个机制的物理依据是:正常跌倒后,人要么保持静止,要么会有挣扎/尝试爬起的动作,这会在CSI上产生持续的低幅波动信号。通过确认窗口能极大降低"突然蹲下捡东西"这类场景的误报率。
5. 部署实战:链路布局、抗干扰与性能边界
5.1 发射端与接收端的布局三原则
CSI跌倒检测的性能上限,有一半在部署位置上就被决定了。我把踩过的布局坑总结成三个原则。
第一,收发端之间必须有一个清晰的菲涅尔区。通俗地说,人要在ESP32和路由器之间的信号通道附近活动,不要躲在信号阴影区。实测中,人在收发连线附近0.5米范围内活动时,CSI特征最明显;距离连线超过3米,信号敏感度大幅下降。如果房间太大,就要考虑部署多组收发链路做冗余。
第二,收发端高度应略低于人体躯干位置。比如离地80-120厘米。这个高度范围内,人体躯干和四肢的运动对信号遮挡最大,特征最明显。如果路由器放地上或天花板上,信号路径很高,跌倒时身体在低位的运动反而不容易被捕捉到。
第三,避免大金属物体和鱼缸在链路中间。金属反射会让CSI数据出现严重的多径干扰,鱼缸(水)对2.4GHz信号的吸收极强,会让链路预算崩溃。部署前踩点的时候,试着在目标位置来回走动,实时看CSI幅度的波动幅度,波动越大说明链路越敏感,这个位置就越适合检测。
5.2 多链路冗余与数据融合的取舍
单个收发对解决不了的一个问题是:人背对链路时,身体遮挡弱,特征不明显。有一个实验数据供你参考:单链路方案对面向链路方向的跌倒检出率能到90%以上,但背向链路方向会掉到60%-70%,这个差距在实装时是不可接受的。
解决方法是部署多链路冗余:在房间相对的两个角落分别放一个ESP32接收端,共享同一个路由器(或两个不同的发射AP),两边同时采集CSI,特征取最大值或加权融合后送入识别器。理论上多链路还能帮你判断跌倒发生的大致区域(哪条链路的特征先触发,人就在那附近),这对后续的现场处置很有价值。
但是多链路会带来同步问题:两个接收端的CSI帧没有统一时间戳,融合时会出现时间错位。我的做法是让两块ESP32都从同一个发射端打流,然后以UDP包的序号作为准时间基准,把两条链路的特征序列对齐到同一个滑窗上。这个方法不需要精密的IEEE 1588同步,工程上完全可行。
5.3 家庭环境的动态干扰:从微波炉到智能家居
家庭环境里最大的干扰源,按危害程度排序,我遇到的是:微波炉 > 蓝牙设备群 > 邻居的Wi-Fi。微波炉一开,2.4GHz频段的底噪能飙升20dB以上,CSI数据几乎完全失真,这个问题基本无解,只能靠感知端做规避:检测到微波炉的干扰特征(全子载波幅度同时飙升、持续数分钟)时,暂停检测并标记"环境干扰"。蓝牙设备如音箱、鼠标会造成偶发性的窄带干扰,表现为个别子载波出现随机尖峰,可以通过子载波异常剔除来缓解。邻居的Wi-Fi主要是信道争用,表现为CSI帧间隔不稳定,规律性变差,可以通过切换到空闲信道来规避。
你需要在检测算法上层做一个"环境状态估计器",周期性地评估CSI数据质量:平均信噪比、子载波连续异常率、帧到达间隔抖动。一旦数据质量低于阈值,就自动调整识别阈值或干脆进入低灵敏度模式。这个思路比费劲去消除所有干扰要务实得多。
5.4 检测延迟与可靠性的权衡
最后说说系统指标。我实测的ESP32 CSI跌倒检测系统,在特征窗口2秒、二次确认3秒的配置下,从跌倒发生到触发告警,总延迟大约在4-6秒之间。这个延迟对多数应用场景是可接受的,毕竟跌倒后家属或监护者不是要求毫秒级响应。但如果想压缩延迟,可以把特征窗口缩短到1秒、二次确认缩短到1.5秒,代价是误报率会上升。这是一个你必须在实际场景里做取舍的权衡,我建议在初始版本把可靠性放在第一位,延迟放在第二位。
有一个容易被忽略的细节是:CSI数据流的中断恢复。ESP32在运行中如果重连Wi-Fi、动态信道切换,CSI采集会短暂中断,如果你的滑窗特征在中断期间还在累计,会产生一个假的"冲击尖峰"。所以要在代码里实现一个"连续帧计数"机制:如果超过200ms没有收到CSI帧,就重置滑窗,而不是继续往里填数据。这个小细节,能让你的系统避免很多莫名其妙的误报。
写在最后的个人经验与下一步方向
这套基于ESP32和Wi-Fi CSI的跌倒检测方案,我从最开始用树莓派加网卡做原型,到最后用ESP32-S3完成单板落地,中间最大的体会是:CSI开发的核心难点不在芯片,而在对无线信道物理规律的理解。你要始终记住,CSI信号是环境、人体、设备三者共同作用的结果,所以任何在实验室里调好的参数,到现场都可能需要重新标定。我现在的习惯是,每次部署到新环境,先把阈值法的参数做成可通过Wi-Fi调试口在线调整的形式,而不是烧死在固件里,这样能极大缩短现场调试的时间。
下一步我计划做两件事:一是把模型推理换成更新的微型神经网络架构,在ESP32-S3上跑实时推理,目标是提升对"似跌倒"动作的区分度;二是加入低功耗模式,让系统在检测到房间无人时自动降采样率,延长电池供电的续航时间,毕竟跌倒检测的高价值场景恰恰是夜间和独居老人家中,设备续航决定了用户是否愿意长期开启它。希望这篇记录能帮你少走一些弯路,也欢迎在评论区交流你们实测中遇到的CSI波形特征,尤其是不同环境下的相位校准经验。