PLFM_RADAR 是我给自己的低成本毫米波雷达项目起的代号。PLFM 的全称我一般写成 Practical Lightweight Frequency-Modulated Radar翻译过来就是务实型轻量调频雷达。这个项目的目标特别明确用尽可能低的硬件成本把人动没动、人怎么动、人在不在呼吸这件事做成一套可离线可实时的完整链路而不是只跑通官方演示程序或者只写个 FFT 验证一下波形。项目本身涉及的环节比较杂射频硬件、ADC 原始数据采集、距离/多普勒信号处理、微多普勒特征提取、轻量级模型训练再到嵌入式部署和实时可视化。当时做这个项目前前后后花了四个多月踩了不少坑也验证了不少网上教程没写过的细节。如果你正准备在智能家居、养老监护或者接着做人体感知方向想用毫米波雷达从零搭一套自己的数据流这篇文章应该能帮你节省大量试错时间。1. 项目定位PLFM_RADAR 要解决的真实需求1.1 为什么选择雷达感知而不是摄像头、红外、超声智能感知领域常用的非接触式传感方案无外乎四种摄像头、PIR 红外、超声波、毫米波雷达。我在定这个项目的第一天就把前三个都排除了。摄像头的问题不是技术上的而是场景上的。室内监控最敏感的就是隐私用户对房间里有镜头这件事天然有抵触心理哪怕你的图像数据完全本地处理也没法说服所有人。而且摄像头在暗光环境下需要补光算力消耗也大一台树莓派跑一个人体姿态模型基本就满载了。我不否认摄像头方案效果最好但在产品化层面它并不是所有场景的最优解。PIR 红外传感器确实便宜几块钱一个但它只能告诉你这个地方有没有热源在动没法区分是人在挥手还是猫在跑更没法检测呼吸这类微弱生理信号。它的响应还依赖人体与传感器的相对运动方向人正对传感器慢慢靠近时输出信号会非常微弱经常漏报。超声波方案有一个绕不开的问题多径效应。室内墙角、家具反射会让回波路径变得乱七八糟距离解算一塌糊涂。而且超声波模块的波束角通常很大空间分辨率很受限想靠它识别坐着还是站着这种细粒度动作基本没戏。毫米波雷达的好处恰好覆盖了这些短板。它不采集图像没有隐私问题它是主动无线电感知完全不受环境光线影响它可以直接输出目标的距离、速度、角度信息哪怕人体静止不动只要胸腔还有毫米级的起伏多普勒通道就能看到信号。唯一的门槛是信号处理链路比 PIR 复杂不少但这正是这个项目存在的意义。1.2 设计目标与关键指标做项目最忌讳什么都想要。PLFM_RADAR 在我的设计里不是万能的 4D 高清成像雷达它只对标下面这几个可量化的目标。指标目标值说明硬件成本核心板控制在 500 元以内使用 TI IWR1443BOOST 评估板二手价格更低探测距离最远 8 米覆盖典型室内房间最长边功耗整机平均低于 0.4W相比摄像头方案有明显优势识别延迟单帧处理到输出动作结果小于 100ms保证实时性不产生明显迟滞感动作识别准确率9 类活动平均 F1 大于 0.85参数量控制在手写特征 轻量级模型的水平扩展性同一个框架可切到呼吸、心跳检测不需要换硬件仅换后处理链路这些指标写成表格很简单真正逐项达成花了很长时间。尤其是功耗和识别准确率两者直接打架帧率越高、发射天线开得越多多普勒信息越丰富分类准确率越好但功耗也跟着涨。这个平衡点不是一上来就能找到的我在后面的章节里会详细说。1.3 项目代码结构与技术选型我比较反感那种把所有逻辑塞进一个 main.cpp的工程组织方式。PLFM_RADAR 的代码从一开始就按信号处理链路的自然边界拆开大概长这样plfm_radar/ ├── config/ # 雷达波形参数、CFAR参数、模型超参 ├── capture/ # DCA1000原始数据采集与数据格式转换 ├── dsp/ # 距离FFT、多普勒FFT、杂波抑制、CFAR ├── feature/ # 微多普勒特征提取、时频图生成 ├── model/ # 特征选择、模型训练、C数组导出 ├── target/ # 部署到IWR1443的嵌入式代码 └── viz/ # PyQtGraph实时可视化底层语言的选择上嵌入式侧用 C这是硬性条件PC 侧的数据分析和模型训练用 Python。实时可视化我用的是 PyQtGraph这个选择在后面的调试章节会解释原因。整体思路是雷达固件只负责输出距离-多普勒谱特征提取和分类在 PC 侧或主控 MCU 上做这样方便迭代算法不用反复烧录板子。2. 硬件选型的得失IWR1443BOOST 与 DCA1000 的组合2.1 为什么不直接用市面上的商用雷达模块做雷达感知市面上有很多现成的模块比如 24GHz 的多普勒雷达模块接上供电就能输出模拟多普勒信号有些人拿它配合 Arduino 做存在检测。我最初也买了一个来试印象很直接硬件简单到不需要写任何信号处理代码但你能拿到的信息量也贫乏得可怜。这种模块输出的是人体运动的多普勒波形本质上是把雷达前端混频、滤波之后的结果直接模拟输出距离信息完全丢掉了。你只知道有东西在动但不知道东西在哪、向哪个方向动、速度是多少。想靠它区分跑步、走路、挥手几乎不可能。所以项目一开始我就排除了这类模块。另一个极端是直接上高端评估平台比如 TI 的 AWR2243、级联四芯片的 4D 成像方案。这些方案性能确实强但成本也高得离谱一套级联系统开发板价格可以到几万元做产品预研还行个人项目和研究入门性价比太低。我需要一个中间态能拿到原始 ADC 数据能自由决定波形参数价格不要太离谱最好社区资料还足够多。2.2 IWR1443BOOST 的硬件底子选来选去最后定了 TI 的 IWR1443BOOST。它属于 76GHz 到 81GHz 频段的毫米波雷达评估板板上有 3 个发射天线、4 个接收天线射频前端和数字信号处理部分集成在一个芯片里。芯片内部有一颗 ARM Cortex-R4F 用于控制和通信还有一颗 C674x DSP 用于浮点运算这个架构很适合做前端处理在板载 DSP、复杂算法在外部主控的分布式方案。这个评估板单独工作时通过 XDS110 调试接口烧录固件输出到串口的是雷达处理过后的点云或者检测结果。但如果只是用官方 demo你只能拿到 TI 已经封装好的数据算法上几乎没有自由度。要真正做研究必须搭配 DCA1000 采集卡它通过 LVDS 接口把 ADC 原始数据实时传到 PC 端采样数据流可以到几百 Mbps。这套组合让我能够完全掌控从波形生成到最终结果的整个信号链路这才是项目的核心价值所在。还有一点很加分IWR1443 的工作频段在 77GHz 附近对应波长大约 3.9 毫米。波长越短相同速度产生的多普勒频移就越大这意味着微小的运动更容易被察觉。呼吸引起的胸腔起伏大概只有几毫米在这个波长下仍然能够产生可识别的相位变化这也是我后来敢在这个平台上做呼吸检测尝试的原因。2.3 供电、USB线材和板级启动的细节评估板买回来之后我第一周就被玄学问题折磨得不轻程序跑着跑着雷达突然重启有时候一连几十个包正常突然就断流。排查到最后问题完全不在代码而在供电。IWR1443BOOST 板子上的射频发射一开瞬时电流会往上冲。如果用普通 USB 线连电脑供电导线内阻一旦偏大板上电压就会被拉低到复位阈值以下芯片直接重启。这个故障和你写没写对代码没有任何关系它就那么安静地重启然后串口重新打印初始化信息。我用示波器量了 5V 引脚发现启动瞬间有接近 800mV 的跌落换成一根短线粗线并且用带电源的 USB HUB 供电之后问题彻底消失。另外板上有一组 DIP 开关SOP用于配置启动模式不同的开关组合决定是从 QSPI 启动、从 UART 下载还是调试模式。如果你烧录固件的方式和 SOP 设置不一致串口完全没反应。我这里分享一个经验每次给雷达上电前先拨好 SOP然后等板上 LED 从闪烁变成常亮再发数据不要一上电就急着灌串口命令雷达固件初始化需要几百毫秒到一两秒抢跑只会丢命令。还有一个容易被忽略的板级细节PCB 天线就在板子边缘天线正前方尽量不要放金属物体包括你的手。调试时我习惯用手拿板子一开始没意识到手离天线阵太近也会改变辐射特性导致近距离杂波时大时小。后来做了一个小支架把板子固定好数据才稳定下来。3. 信号处理链路从 ADC 采数到距离-多普勒图3.1 FMCW 的原理和波形参数反推PLFM_RADAR 采用的是调频连续波体制具体来说就是线性调频连续波LFMCW。原理用大白话说就是发射频率随时间线性上升的电磁波遇到目标反射回来后把发射信号和回波信号混频得到一个频率与目标距离成正比的差频信号。目标越远回波延迟时间越长发射信号和回波信号的频率差就越大。距离与差频的关系可以写成f_b S * 2R / c其中 S 是调频斜率chirp 带宽除以 chirp 持续时间单位 Hz/sR 是目标距离c 是光速。如果目标有径向速度回波还会叠加多普勒频移f_d 2v / λ所以一个完整的 FMCW 处理流程是先对单个 chirp 内的 ADC 采样点做 FFT得到距离维谱再对多个 chirp 之间的同一距离门做 FFT得到多普勒维谱。两者组合起来就是常说的距离-多普勒图Range-Doppler MapRDM。波形参数不是随手拍的而是从指标反推的。我的需求是最大探测距离 8 米、最远可以识别 3.5 米每秒的人体运动。以 77GHz 载频为例波长约 3.9mm由最大速度限制可以得到 chirp 重复周期不能超过 λ / (4 * vmax)也就是约 278 微秒。我留了一点余量选 200 微秒。距离分辨率和带宽直接相关ΔR c / (2B)如果想做到 4 厘米左右带宽 B 要接近 3.75GHz。IWR1443 支持最大 4GHz 调频带宽我选了 3.6GHz距离分辨率约 4.2 厘米。这些参数用 Python 算一遍非常直观import numpy as np c 299792458.0 fc 77e9 lam c / fc B 3.6e9 T_chirp 200e-6 S B / T_chirp fs 2.5e6 N_samples int(fs * T_chirp) N_chirps 128 range_res c / (2 * B) max_range fs * c / (2 * S) max_speed lam / (4 * T_chirp) vel_res lam / (2 * T_chirp * N_chirps) print(f距离分辨率: {range_res*100:.2f} cm) print(f最大不模糊距离: {max_range:.2f} m) print(f最大不模糊速度: {max_speed:.2f} m/s) print(f速度分辨率: {vel_res:.3f} m/s)我最后用的波形参数是带宽 3.6GHzchirp 持续 200 微秒ADC 采样率 2.5MHz每 chirp 采 500 个点一帧包含 128 个 chirp帧周期 50ms。这套参数下理论最大不模糊距离约 8.3 米速度分辨率约 0.076m/s足够把站立不动和缓慢挥手区分开。3.2 2D-FFT 的工程实现细节拿到 ADC 原始数据之后第一步是把它整理成一个二维矩阵形状是 (chirp 数, 采样点数)。然后沿两个维度分别做 FFT。这里的工程细节比教科书上写的要复杂一些主要是加窗、补零和多普勒轴的搬移。加窗是非常重要的。如果不加窗距离维 FFT 之后会有严重的主瓣泄漏一个强反射目标会在旁边产生一堆旁瓣把弱目标淹没。这里我选择的是 Hamming 窗它不是旁瓣抑制能力最强的窗函数Blackman-Harris 更强但主瓣展宽小距离分辨率损失在可以接受的范围。实际测试下来Hamming 窗在人体感知场景下的综合表现是最稳的。多普勒维的 FFT 还多一个步骤fftshift。因为目标速度有正有负多普勒轴默认中心是零频正频率代表目标靠近负频率代表远离所以要把零频移到谱图中间否则后续可视化时看到的图谱是半张图容易让人误解目标分布。我实现的核心代码大概是这样import numpy as np def range_doppler_fft(adc_chirps, n_range512, n_dopplerNone): # adc_chirps: shape (num_chirps, num_samples) num_chirps, num_samples adc_chirps.shape if n_doppler is None: n_doppler num_chirps win_r np.hamming(num_samples) win_d np.hamming(num_chirps) # 距离维 FFT range_fft np.fft.fft(adc_chirps * win_r[np.newaxis, :], nn_range, axis1) # 多普勒维 FFT并搬移零频到中心 doppler_fft np.fft.fftshift( np.fft.fft(range_fft * win_d[:, np.newaxis], nn_doppler, axis0), axes0 ) rdm np.abs(doppler_fft) return rdm代码很简单但有一个坑如果直接把原始 ADC 数据从 DCA1000 读出来就转成 float 做 FFT性能会非常差。DCA1000 输出的原始数据是 int16 格式里面还分实部和虚部正确做法是先按实部虚部交错的方式解析成复数数组再做 FFT。我在初版代码里用文本格式保存调试数据结果一秒 20 帧的数据量就把硬盘写爆了而且还拖慢了整个链路。后面改成二进制格式一帧 128x500 点的复数矩阵只占几百 KB处理速度才跟上。3.3 静态杂波抑制和 CFAR 检测距离-多普勒图出来之后不能直接拿去喂给分类器因为里面包含大量不符合需求的信号。最典型的是静态杂波墙壁、家具、地板反射的电磁波它们的距离是固定的多普勒频率为零在 RDM 上表现为中间一行特别亮。如果不处理这行强能量会主导后续特征提取。静态杂波抑制的做法有很多传统雷达里最常用的是 MTI 滤波器也就是沿 chirp 维做高通滤波。但在我的代码里最简单直接有效的方式是零多普勒频带置零减背景把每一帧 RDM 的零多普勒行列出来做一个慢速滑动平均背景模型然后从当前帧里减掉。这个思路有点像视频监控里的背景差分实测下来能把固定桌椅的反射压到噪声底附近而人体运动引起的多普勒信号不会受影响。杂波抑制之后需要从 RDM 里找到目标区域。这里我用的是 CFAR恒虚警率检测。CFAR 的作用是给每个检测单元计算一个自适应阈值背景能量高时阈值自动抬高背景安静时阈值自动降低从而保证虚警概率基本恒定。我选了最经典的一维 CA-CFAR先在距离维上做一轮找候选距离门再在多普勒维上做一轮确定目标的速度区间。CFAR 的三个参数要反复调保护单元、训练单元、虚警率。保护单元太大会漏掉紧挨着的目标太小强目标的旁瓣会污染噪声估计。我实测下来距离维保护窗设 4 个单元、训练窗设 18 个单元、虚警率设 1e-4在 8 米室内场景下效果比较平衡。这里有一个经验虚警率不要照抄论文里的 1e-6那是高SNR军事场景的参数室内人体回波本来就弱设这么严苛会把大量有效目标直接滤掉。4. 微多普勒与轻量级识别从特征到结果4.1 为什么不用点云而是用微多普勒TI 的毫米波 SDK 可以直接输出目标点云每个点带距离、速度、角度信息。很多人做人体识别时理所当然地用点云喂模型但我实际测下来发现这条路在低成本雷达上走不通。这里有一个关键原因单个雷达帧的点云稀疏且不稳定。点云数量受 CFAR 阈值影响很大人离雷达远一点、衣服吸收性好一点可能只有两三个点而且帧与帧之间点的位置跳动很大。用稀疏不稳定点云直接训练分类模型模型会把大量容量浪费在补全缺失信息上效果自然不理想。微多普勒是完全不同的思路。它不需要精确追踪目标位置而是把一段时间的多普勒谱变化当成一个整体特征来使用。人的肢体运动会在多普勒谱上形成独特的调制模式挥动的手臂周期性加速减速会在时频图上画出类似正弦波的纹路跑步时腿部摆动频率更快多普勒谱宽明显增加站立呼吸时只有低频微弱的谱成分。这些模式比点云的位置信息要稳定得多也更适合低算力分类器。4.2 特征工程与数据集构建PLFM_RADAR 用的特征不是直接把 RDM 像素全部摊平那样维度太高。我选择先做时频分析连续多帧 RDM 的多普勒维投影拼成一幅时频图spectrogram然后从时频图里提取一组压缩后的统计特征。具体做法是把一帧时频图例如 2 秒窗口、20fps、共 40 帧看成二维图像。它的横轴是时间纵轴是多普勒速度颜色深浅表示能量。从这张图上我提取以下特征特征族具体特征物理含义频谱质心平均多普勒频率及其标准差目标整体运动速度快慢频谱展宽多普勒谱的方差、峰度、偏度运动肢体数量和多变性能量分布正负多普勒能量比靠近/远离方向偏置周期特征自相关峰值和周期位置肢体摆动频率数据采集上我找了 10 个不同身高体重的人在办公室、宿舍、走廊三个场景各采一轮。动作类别是无活动、走路、跑步、站立坐下、从椅子起身、挥手、弯腰捡东西、原地跳跃、模拟跌倒。全部采集下来大约 12000 帧有效样本。这里有个非常实际的经验跌倒样本不是真的让人摔而是志愿者用快速下蹲并且身体姿态倾斜来模拟真实跌倒场景数据还是很难拿到的。4.3 模型选择和边端部署特征维度只有十几个完全不需要深度学习大模型。我先用随机森林做了 baseline在验证集上宏平均 F1 大约 0.84。然后我换了一个轻量 1D-CNN直接吃时频图的低频分量压缩向量宏平均 F1 提升到 0.89。这两者的差别在于随机森林对人工特征比较敏感需要人为设计好特征与动作的对应关系而 CNN 能自动找到不同动作在多普勒时频图上的细微差别。部署到板载平台时我没有把 CNN 原样跑在 R4F 上而是先把模型参数导成 C 数组再用作者自己写的轻量推理代码在 DSP 上执行。实际跑下来单帧推理不到 20ms在 50ms 帧周期内完全够用。这也算是一个心得在单片机平台上不要一上来就想着上 TensorFlow Lite MicroC 数组加手工推理在很多低维度小模型场景下又快又省而且可控性强。4.4 实测结果和最容易混淆的类别最终在独立测试集上9 类活动的平均 F1 是 0.87单类最高的无活动F1 到 0.97最低的是弯腰捡东西和从椅子起身F1 在 0.78 左右。这两类混淆的原因不复杂动作持续时间短也就一两秒但前后都伴有明显的无活动帧如果特征窗口没对齐模型很容易把过渡状态当成完整动作。另一个容易混淆的是原地跳跃和跑步。跳跃时身体上下起伏频率高跑步时腿前后摆动产生多普勒谱宽大谱形其实不一样但如果人跑步时上身左右晃动时频图看起来和跳跃非常接近。我的解决办法是多普勒正负频段能量比这个特征要重点加权。跑步时多普勒谱对称性更强跳跃时则呈现明显的上下行频移交替这个特征能有效把两者的差异放大。这里也想提醒一句模型准确率再高都不能忽略无活动类别。实际落地时绝大多数时间是没有人活动的状态如果误报率太高产品会非常不可用。我在训练时对无活动类别做了小幅过采样并且把误报惩罚权重调高了一点最终把虚警率压到了每 10 分钟不超过一次。5. 工程化落地中的几个更新功耗、干扰、同步、可视化5.1 呼吸检测的相位链路和杂波干扰做完了活动识别我又在 PLFM_RADAR 上加了呼吸检测因为养老监护场景里这个需求很刚性。呼吸检测不能用 RDM 的幅度信号直接做幅度变化太微弱了。要用的信号是距离维 FFT 后人体目标的相位因为呼吸引起的胸腔位移会体现在回波相位上相位对亚毫米级位移非常敏感。但相位信号有一个大坑如果目标不在雷达正前方或者人体稍微移动一下某个距离门里的相位就可能发生跳变解出来的呼吸波形噪声巨大。我最后采用的办法是先锁定最强距离门然后只对这个距离门做时间维相位解算同时加一个滑动中值滤波来剔除因为身体晃动产生的相位跳变点。即便如此呼吸检测的最佳探测距离也只能做到 2 米以内超过 3 米基本不可用。这不是算法的锅而是单颗低成本雷达在相位信噪比上的物理极限。5.2 雷达间的互相干扰我在测试中发现一个多媒体电子爱好者大概率也会遇到的问题两台毫米波雷达同时开机距离-多普勒图上会出现明显的对角线亮线或者十字形干扰条纹。原因是第二台雷达的发射信号进入第一台雷达的接收机后会与第一台雷达的本振信号混频产生不受控的差频分量。解决办法有很多种成本最低的是在算法上做中值滤波干扰条纹在 RDM 上是孤立的高亮像素块中值滤波可以很干净地把它抹掉。如果想从物理层面避免两台雷达可以错开 chirp 启动时刻或者配置不同的起始频率。我在项目里用的是硬件上错开发射窗口 算法上中值滤波的双保险实测两台雷达放同一个房间互相干扰率从肉眼可见降到了几乎不可感知。5.3 功耗优化从 0.8W 到 0.35W功耗这个指标一开始完全没关注直到拿手持电源测试发现快充电池两个小时就见底了才意识到需要系统性地做功耗调优。实测下来这个板子默认配置的功耗在 0.8W 附近主要耗电大户有三个射频发射链路、板载 DSP 全速运行、UART 打印。先砍 UART 打印这最立竿见影。调试阶段我开着 921600 波特率的实时日志数据吞吐大而且纯粹耗电。把打印关掉后功耗直接降了 0.1W 左右。然后关发射通道IWR1443 有 3 个发射天线但我的算法只用距离和多普勒信息不需要角度估计所以只保留一个发射通道另外两个直接关掉。最后降低处理频率DSP 时钟从 600MHz 降到 300MHz虽然单帧处理时间变长了一些但依然远小于 50ms 帧周期。这一通操作之后整机功耗降到约 0.35W配合一块 10000mAh 的电池可以连续跑接近 10 个小时完全够做移动部署实验。如果未来用 IWR6843 这类更偏低功耗的芯片还能再往下压。5.4 雷达数据与 IMU 数据的同步问题我做了一个小扩展在雷达旁边加了一颗六轴 IMU想融合雷达和惯性数据判断人的姿态。一开始我把雷达帧的接收时间戳和 IMU 的时间戳直接对齐结果发现算法的效果非常奇怪姿态判断比真实动作晚了差不多 120ms。原因是我当时用的时间戳是数据到达 PC 的时间而不是雷达实际采集的时间。雷达通过 DCA1000 流式传数据到电脑数据在 USB 缓冲区和网卡队列里排队延时会随机跳动。解决办法是让雷达在每帧开始采样时通过 GPIO 输出一个脉冲同时主控记录这个脉冲和 IMU 中断的时间戳再在软件里做插值对齐。这是嵌入式系统里老生常谈的时间戳应该尽量靠近传感器采样源头原则但只有踩过坑才能真正理解它有多重要。5.5 实时可视化工具链的选型做雷达调试没有实时可视化几乎等于盲调。我最初用 matplotlib 的动画模式画 RDM每帧刷新都卡得不行而且跑几分钟后内存明显增长原因在于 matplotlib 的 event loop 不支持高频原子刷新累积的对象没有及时释放。后面换成 PyQtGraph性能完全是另一个数量级它可以做到稳定 20fps 刷新距离-多普勒图和时频图CPU 占用还能控制在单核 30% 以内。对第一次做雷达可视化的朋友我建议直接上 PyQtGraph别在 matplotlib 上浪费时间。另外可视化界面里记得把范围-多普勒热力图多普勒时频图动作类别输出三个面板放在同一个窗口同步刷新这样调参时一眼就能看到前因后果节省大量时间。最后分享一个我在 PLFM_RADAR 调试中最受益的小经验数据链路每一层的输入输出一定要做落盘。不管你是用 DCA1000 采集 ADC 原始数据还是用 CFAR 检测目标都会遇到查看这个模块是否正常的时刻。把每一级的结果以二进制格式加上一个简单的版本头写下来再写一个同样简单的读取脚本排查问题的速度会快非常多。