写这套 PLFM_RADAR 的时候,我正在做一个让人头疼的办公环境改造需求:既要感知工位上有没有人,又不能在摄像头底下让同事觉得被盯着,还要做到人坐在那里一动不动也能稳定识别出“有人在”。折腾了一圈传感方案,最后把目光落在了毫米波雷达上,于是就有了这个项目。PLFM 就是 Platform 的缩写,RADAR 代表核心传感器类型——整套项目做下来,它就是一套基于毫米波雷达的人体存在感知平台框架,我把它当作一个标准化的“感知底座”来设计,而不是一颗单纯的雷达 demo。它解决的问题很明确:在低成本嵌入式设备上,用一颗毫米波雷达同时实现运动目标检测、静态人体存在感知、区域入侵判断,以及可选的呼吸率提取,并且把结果以标准事件结构输出,方便上层业务直接使用。适合正在做智能家居、办公节能、安防联动或者养老人体感应方案的朋友参考,尤其是那些不想碰摄像头、又对隐私敏感的场景。
主控端我用的是英飞凌的 BGT60TR13C 毫米波雷达模块,配了一块自研底板,整个固件在裸机环境下跑通,数据通过串口输出。开发过程中踩了不少坑,从雷达盲区到电源纹波干扰,再到静态人体检测的参数收敛,都做了完整记录。这篇文章会把设计思路、硬件选型、核心算法实现、调试教训一次讲透,结尾附上我自己实测的标定数据和调参经验,希望能帮你少走几个月弯路。
1. 项目整体设计与思路拆解
1.1 项目命名与核心定位
先说说这个 이름 的由來。PLFM_RADAR 里 PLFM 是我们项目内部的统一前缀,凡是沉淀成平台化组件的模块都会带这个前缀;RADAR 直指传感器类型。一开始我也想过叫“人体存在检测”或者“Human Presence Detection”,但那种命名太功能化,体现不出平台架构的含义。
这套框架的核心定位不是“写一个能跑的雷达例程”,而是“定义一套能被不同业务场景复用的雷达感知接口”。什么意思?就是我把雷达厂商 SDK 暴露出来的底层 API 全部收敛到一个统一的抽象层里,上层不关心你用的是英飞凌还是 TI 的芯片,只需要拿到统一格式的距离能量谱、目标状态、事件回调。这样以后换传感器、换芯片,业务代码基本不用动,这是“平台”两个字真正的价值。
从业务角度来看,PLFM_RADAR 把雷达信号处理拆成三个层次:底层是传感器驱动和原始帧采集,中层是信号处理算法(距离变换、背景对消、目标检测、存在状态机),上层是应用策略(区域入侵、呼吸率计算、联动事件输出)。这种分层结构在嵌入式项目里非常实用,只要每一层接口定义清楚,多人协作和核心维护难度都会低很多。
1.2 为什么我不选 PIR、摄像头,而选毫米波雷达
这个决定是整个项目的起点,也是站在今天回看最值得复盘的一个决策。做人体感知,主流方案就那么几类,每一类都有各自明显的取舍。
被动红外(PIR)方案最便宜,功耗极低,但最大的致命伤是:人一旦保持静止一段时间,它就会“看丢”。我实测过,普通的 PIR 传感器在静止坐姿下二十分钟到半个小时后,输出大概率会翻成无人状态。这个缺陷在做办公节能、卫生间管理、睡眠监测这类场景时根本没法接受。
摄像头方案在识别准确率上无可挑剔,但隐私问题在办公场景和卧室场景是硬伤,同时光线变化对算法影响也很大,夜间还得补光,功耗和成本直接拉高。超声波方案虽然也能检测运动,但对非刚体和低速目标的分辨能力很弱,远距离人体回波很容易被环境多径淹没。
毫米波雷达采用 FMCW 调频连续波体制,利用电磁波的反射和频率差来测距测速。它对缓动目标敏感,能捕捉人体微动(呼吸导致胸腔每秒几毫米的位移),所以可以做到真正的“有人存在”检测。同时它不采集图像信息,不会留下可以还原身份的画面,隐私问题天然规避。成本上,一颗 60GHz 雷达芯片的方案价格已经做到和好一点儿的 PIR 模组加 MCU 的组合相当,完全在商业项目的接受范围内。
选型的时候我还额外考虑了一点:毫米波雷达可以输出目标距离和角度信息,这意味着同一个模组不仅能回答“有没有人”,还能回答“人在哪个区域”“是不是靠近了边界”,一个传感器支持三种不同业务能力,性价比很高。
1.3 平台化的核心架构决策
项目最开始我其实也是先写了一个能用的 demo,然后才回头做平台化重构的。第一版代码把所有逻辑都堆在 main 函数里,初始化雷达、读数据、算 FFT、判断存在、组串口包,一坨全在一起。当时能跑,但换个场景就得大改。重构之后,我总结出三个核心架构决策。
第一,传感器驱动必须抽象。我在雷达硬件外面封装了一个radar_hw_if结构体,里面只留初始化、配置、启动采集、停止采集、数据回调这几个标准接口。具体厂商 SDK 的 API 全部塞到对应实现文件里,上层一律不直接调用。
第二,算法模块保持无状态与隔离。FFT、背景对消、阈值检测这些函数都不存全局状态,全部通过传入结构体的方式实现。这样做的好处是可以在电脑上用采集到的原始帧数据离线回放、调参,不用反复烧录固件,调试效率高很多。
第三,输出采用统一事件结构。我定义了一个全局的radar_platform_event_t结构体,把是否有人的布尔值、目标距离(单位米)、目标所在区域编号、目标置信度、呼吸率(如果有)打包在一起,通过串口或回调发给上层。上层拿到的是一个干净的结果,而不是乱七八糟的频谱,这样对接做联动逻辑的同事就不用理解雷达原理了。
这套架构在项目过程中证明是扛得住的。后面调试的时候我换过一次雷达模组固件版本,驱动层改了大约两百行,上层业务逻辑一行没动。对嵌入式项目来说,这个隔离收益非常可观。
2. 硬件选型与开发环境准备
2.1 毫米波雷达模块怎么选:BGT60TR13C 的实测感受
雷达芯片这个领域,目前市面上可选的就那么几家:英飞凌、TI、恩智浦、Acconeer。我最后选的是英飞凌的 BGT60TR13C,因为它在成熟度和文档完整度上做得最均衡。
BGT60TR13C 是一颗 60GHz FMCW 雷达芯片,支持一发三收,通过开关切换可以得到不同的角度分辨率。它的封装很小,集成度很高,外围电路精简,可以做到一个一块钱硬币大小的模组。对于做产品来说,这个尺寸和结构非常有吸引力。它有内置的帧配置机制,扫频带宽等参数可以通过寄存器配置,探测距离可以在几米到十几米之间灵活调节。
实际测下来,这颗芯片的相位噪声表现不错。呼吸检测这种应用对相位稳定性要求极高,胸腔位移幅度只有几毫米,反映到中频信号的相位变化很微弱,如果芯片的相位噪声大,呼吸特征很容易淹没在噪声里。这点上 BGT60TR13C 给我的体验是可靠的,也是我愿意在文章里推荐它的原因。
| 对比维度 | 英飞凌 BGT60TR13C | TI IWR6843 | 恩智浦 MW1001 |
|---|---|---|---|
| 工作频段 | 60GHz | 60GHz | 10GHz(UWB脉冲) |
| 天线配置 | 1T3R | 3T4R | 1T1R |
| 探测原理 | FMCW连续波 | FMCW连续波 | UWB脉冲 |
| 集成度 | 高,几乎单芯片方案 | 中,外围较多 | 高 |
| 开发难度 | 较低,SDK完善 | 中等 | 中等 |
| 适合场景 | 存在检测、低功耗 | 多目标跟踪、角度成像 | 高精度距离测量 |
对中小团队或者个人开发者来说,英飞凌的生态在这几个方案里是最友好的。它的 SDK 封装了存在检测的参考实现,能大大缩短起步时间。不过要注意,参考实现终归是参考,实际场景里的误报过滤还是要自己动手调,这部分后面会展开。
2.2 开发环境与工具链搭建
我的开发环境是嵌入式常见的组合:主控 MCU 用的是一颗 ARM Cortex-M4F 内核的型号,主频跑 160MHz,外扩了 64KB 的 SRAM 做雷达 FFT 数据缓冲。雷达模块通过 SPI 接口与主控连接,数据率大概在几十 Mbps 量级,SPI 如果是低速模式会明显拖后腿,所以我直接开了 25MHz 时钟。
软件工具链用的是 ARM GCC + 命令行 Makefile,没用 IDE。调试方式上,硬件调试器看变量,串口打印看运行日志,另外我把雷达原始帧数据设计了一个专用调试模式,通过串口把中频采样数据导出来,直接在 PC 上用 Python 脚本离线解析,配合 matplotlib 画频谱热图。这个工具链组合帮我省了大量的调参时间,强烈建议做雷达项目的朋友复制这套工作流。
固件层面我用了裸机状态机架构,没有上 RTOS。原因很简单:雷达算法的计算密集点集中在 FFT 和阈值处理上,实时性要求虽然高,但用中断加轮询就能满足。如果上 RTOS 反而要考虑任务调度带来的延时抖动,对稳定输出不利。当然如果你的产品还有屏幕显示、网络协议栈等复杂交互,用 FreeRTOS 也行,但注意给雷达处理单独分配最高优先级任务。
2.3 工程结构与初始化流程
工程文件布局我采用了非常标准的分层结构:driver/放雷达硬件驱动和 SPI 底层,algo/放信号处理和检测算法,app/放业务策略和事件输出,main.c只做初始化和调度。这样看起来平平无奇,却是我踩了一遍坑之后才沉淀下来的布局。
初始化流程最关键的一点是:雷达芯片上电后不要立刻开始采数据。要让模块先完成内部自校准,等待时钟稳定。我实测如果上电后立即采数,前两帧的数据经常是坏的,幅度异常大或者出现直流漂移,很容易造成误判。
基本初始化步骤是这样的:
/* 雷达驱动初始化的核心调用示例 */ radar_platform_config_t config = { .frame_rate_hz = 10, /* 帧率:每秒钟采集10帧 */ .sweep_bandwidth_hz = 1000000000UL, /* 扫频带宽 1GHz */ .num_samples = 128, /* 每chirp采样点数 */ .num_chirps = 32, /* 每帧chirps数 */ .detection_range_m = 6.0f, /* 探测距离 */ .if_gain_db = 40, /* 中频增益 */ }; radar_platform_init(&config); /* 等待自校准完成(至少200ms) */ radar_platform_wait_for_ready();配置参数里最需要关注的其实是detection_range_m和sweep_bandwidth_hz的关系。FMCW 雷达的距离分辨率和扫频带宽成反比:带宽越宽,距离分辨率越高。我用 1GHz 带宽,理论上对应的距离分辨率是 15 厘米,足够把人跟墙体杂波区分开。但这会带来更高的中频信号频率,对主控的 FFT 点数要求也更高。这个权衡后面在算法部分细说。
3. 核心功能模块的实操实现
3.1 人体存在检测:从 FFT 到状态机
人体存在检测是第一优先级功能,也是整个 PLFM_RADAR 框架的心脏。它的核心原理说起来并不复杂:毫米波雷达发射调频连续波,遇到人体反射回来后与发射信号混频,产生一个差频信号。这个差频信号的频率和目标距离成正比,所以对差频信号做 FFT 就能得到目标的距离信息。而人体相对雷达的径向运动,哪怕幅度很小,都会在对应距离 bin 上引起信号幅度和相位的波动,利用这个波动就能判断“有没有一个活物在附近”。
实际代码里,我把每个帧的原始采样数据处理成距离 FFT 幅度谱,然后和一个动态维护的背景模型做比较。背景模型是怎么来的?在雷达安装好、确认环境里没有人的时候,采集几十帧数据取平均,作为“空房间基准”。之后每一帧的新数据与基准相减,差值超过阈值的距离 bin 就是候选目标。
背景模型需要动态更新。我用了指数加权移动平均,每个新帧只有 2% 的权重更新到背景模型里。这样房间里的空调风道、墙体温漂、设备散热这些缓慢变化会被逐渐吸收进背景,不会导致长期运行后误报,但快速度移动的人体不会被吸收。
判决逻辑上用了一个简单的滑动窗口投票:统计最近 20 帧里有多少帧有超过阈值的能量峰值,如果超过 12 帧判定有人,低于 6 帧判定无人。这种“多数表决”的处理方式看起来简单,实际效果非常好,能滤掉飞虫、窗外光线变化引起的瞬时噪声。
状态机设计上我没有只用“有人/无人”两个状态,而是在两态中间加了一个“确认中”状态。刚上电或者无人状态久了之后,检测到疑似目标并不会立刻翻转输出,而是保持 3 秒的确认窗口,窗口内持续有目标证据才会完全切到有人状态。对应地,从有人切到无人也会经过一个 10 秒的确认延时,防止人蹲下系鞋带、拿手机这类的短暂遮挡直接被判定为无人。
/* 存在检测的核心判断逻辑 */ static bool presence_vote_algo(float *range_energy, size_t num_bins) { uint32_t vote = 0; for (int i = 0; i < PRESENCE_VOTE_WINDOW; i++) { float *frame = &range_history[i * num_bins]; if (frame_has_motion_evidence(frame, num_bins)) { vote++; } } return (vote >= PRESENCE_ON_THRESHOLD); }3.2 静态生命体征感知:呼吸率检测的实操细节
存在检测只能判断“有人在”,但如果人完全静止不动,纯靠运动能量检测依然会失效。毫米波雷达在这个场景下的杀手锏能力是呼吸检测。这个功能是我后期移植进来的,收益巨大:不被摄像头接受的睡眠监测场景,雷达加上呼吸检测就能做。
原理是这样:人呼吸时胸腔会以每分钟 12 到 20 次的频率起伏,幅度大约 4 到 12 毫米。这个微小的位移会让雷达回波的相位产生周期性变化。在频谱上,这个变化出现在非常低的频率段,大约是 0.15Hz 到 0.4Hz(也就是每分钟 9 到 24 次)。正常情况下这个频段的能量非常低,只要我们能稳定提取出来,就说明有个“活着的目标”在。
实现上我塞了一个高阶低通滤波器链。先把原始 I/Q 信号的相位解卷绕出来,得到微动位移序列,然后对这条序列做带通滤波,把 0.1Hz 以下和 0.5Hz 以上的成分都滤掉,剩下的就是呼吸主频。之后再做一次短时 FFT,找到频谱峰值对应的频率乘以 60 就是每分钟呼吸次数。
这里有个关键的坑:相位解卷绕必须处理跳变。因为相位是周期性的,实际位移超过半个波长的时候,解出来的相位会跳变 2π,如果不做相位恢复,呼吸波形直接乱掉。我用了一个简单的 unwrap 函数,检测相位变化超过 180 度就自动加减 360 度。
呼吸率这个参数单独拿出来用还能进一步过滤误检。比如说,区域入侵检测里经常遇到的问题是:一个悬挂的窗帘被风吹动会产生帧间显著的运动,但它的频谱上不会有规律的呼吸峰。我把“检测到微动 + 存在呼吸峰”同时作为静态目标判定的条件,误报率大幅下降。
/* 呼吸检测主循环简化示意 */ float frequency_domain_peak_indf(double *phase_samples, uint32_t len, float fs) { double *samples = apply_butterworth_bandpass(phase_samples, len, 0.1f, 0.5f, fs); fft(samples, len); uint32_t peak_bin = find_max_bin_magnitude(samples, len, 0.1f, 0.5f, fs); float breath_hz = peak_bin * fs / len; return breath_hz * 60.0f; /* 转为每分呼吸次数 */ }3.3 区域入侵与边界判定:让雷达学会“划分地盘”
光有存在检测还不够,很多应用场景需要知道“人到底在哪块区域”。PLFM_RADAR 在 3.3 版本里加入了多区域划分功能,原理是在距离维上把探测范围切成若干段,每段对应一个区域编号。
这里再回头讲一下距离分辨率的工程意义。我配置的 FMCW 带宽是 1GHz,距离分辨率是 15 厘米。这意味着一个 6 米长的房间会被切分成 40 个距离 bin。做区域判定就是把连续的多个 bin 归并成区。比如我要划分成“近区(0-2米)、中区(2-4米)、远区(4-6米)”,对应的就是把距离 bin 1 到 13 归为近区,14 到 27 归为中区,28 到 40 归为远区。
划分完之后每一帧的目标检测结果会标记出目标落在哪个距离 bin,再映射到区域编号,接下来就可以做区域级别的策略。我在项目里做了一个边界警戒的演示:当目标从远区移动到中区并持续 5 秒以上,输出一个“进入”事件;当目标从中区继续进入近区,输出“逼近”事件。这实际上就是雷达版的电子围栏,用在办公过道、展厅区域、卫生间门口的自动统计都很顺手。
需要注意的是,单雷达模式下做区域划分距离维信息是可靠的,但横向角度维只靠一发三收的开关切换也只能做到比较粗略的左中右区分。如果真要做大范围的二维多分区追踪,建议直接上 TI 的多发多收方案。这是硬件能力的天花板,不是软件算法能补回来的。
3.4 数据融合与对外输出:统一事件结构接口
各种检测能力搭完之后,最后要解决的是“结果怎么给别人用”。我踩过的很典型的坑是把所有检测结果散落得到处都是,一会儿串口打存在检测,一会儿 I2C 上报呼吸率,上层对接代码简直要疯掉。PLFM_RADAR 的做法是定义一个全局统一的事件结构体,所有检测结果都走这一个出口:
typedef struct { bool presence; uint8_t zone_id; float target_distance_m; float respiration_rate_bpm; uint8_t confidence; uint32_t timestamp_ms; } radar_platform_event_t;struct 里加了一个置信度字段,它是由存在投票得分率计算出来的。0 到 100 之间,越高代表判决依据越充分。上层可以拿它做进一步过滤,比如夜间安防联动要求置信度大于 70 才拉高告警,这样能最大限度避免猫、窗帘、植物造成的误报。
输出通道上,我同时支持了串口 ASCII JSON 和二进制帧两种格式。二进制帧用于和主控之外的其他 MCU 对接,优点是解析快、不需要内存池处理字符串。JSON 主要是开发期调试用的,直接在串口终端看结果非常直观。两种格式都在代码里做了 CRC8 校验,防止粘包或者被干扰后误解析。
这套统一事件接口的价值在实际联调中体现得非常明显。我这边把事件通过串口送给一个做灯光联动的子板,那边只需要解析这个结构体就能实现“人在灯亮、人走灯灭、静止 5 分钟自动降低亮度”的逻辑,完全不用关心雷达内部怎么实现的。
4. 实测经验、调参心得与常见问题排查
4.1 三种典型安装方式下的标定数据与实测表现
项目做完以后,我在三种典型安装方式下做了实测:桌面近距安装、墙角 45 度斜装、天花板正装。每种方式我都记录了实测数据,方便你对照自己的安装场景来预估效果。
桌面近距安装(雷达离人约 0.5 到 1 米),存在检测和呼吸检测的表现最佳。这个距离下雷达回波强度大,呼吸波形清晰,实测呼吸率误差在每分钟 1 次以内。但雷达存在一个近距盲区,目标离雷达过近(小于 10 厘米)时中频信号饱和,检测反而会丢失。所以桌面安装尽量不要让人脸或胸口正对着雷达贴上去。
墙角 45 度斜装是覆盖大房间的常用方式。雷达安装在墙角高度 2.2 米左右,朝着房间对角线方向。这种方式下,运动检测覆盖半径能到 6 到 8 米,静态存在检测的有效半径会缩到 3 到 4 米。原因很简单:斜装时人体正对雷达的投影面积减小,胸腔微动能够引起的相位变化幅度也变小。
天花板正装是智能卫生间、卧室场景最合理的方案。雷达装天花板高度 2.5 米,人基本处于雷达正下方。此时人体距雷达最近,但雷达波束几乎垂直照射,人体胸腹运动的径向分量变得很小,呼吸检测效果反而变差。要解决的话需要雷达波束有一个倾斜角,或者雷达芯片本身支持波束成形方向调整。我用 BGT60TR13C 的天线设计做了 30 度倾斜角安装测试,呼吸率检测的成功率从 60% 左右提升到了 88%。
| 安装方式 | 安装高度 | 运动检测半径 | 静态存在检测距离 | 呼吸检测体验 |
|---|---|---|---|---|
| 桌面近距 | 0.3米 | 3-4米 | 1.5米内有效 | 优秀 |
| 墙角斜装 | 2.2米 | 6-8米 | 3-4米有效 | 良好 |
| 天花板正装 | 2.5米 | 5-6米 | 2-3米有效 | 一般,需倾斜角 |
4.2 常见问题速查表与排查思路
做毫米波雷达调试的这段时间,我整理了一份问题速查表,都是实际遇到过并定位到根因的问题。这些问题在大多数雷达 SDK 的官方文档里不会写得很细,但实际项目里十个有七个会碰到。
| 常见现象 | 根本原因 | 处理办法 |
|---|---|---|
| 上电后前几帧数据异常跳变 | 模块自校准未完成 | 初始化后延时 200ms 以上再开始采集 |
| 距离 FFT 后每个 bin 都有高能量 | 中频增益配置过高,饱和 | 降低 if_gain_db 或减小发射功率 |
| 人静止时存在检测频繁丢失 | 纯运动检测阈值太高 | 开启微动与呼吸特征检测辅助判决 |
| 串口偶尔出现乱码或数据卡顿 | SPI 传输速率不足或 DMA 未生效 | 开启 DMA 模式,SPI 时钟调高到 25MHz |
| 雷达到了凌晨误报增多 | 背景模型未更新,环境温漂累计 | 增加背景动态更新权重到 5% |
| 安装高角度时检测距离明显缩短 | 人体径向投影面积变小 | 调整安装角度,或降低检测阈值并接受一定误报 |
| 两个雷达互相干扰 | 射频信号同频互泄露 | 为两台雷达配置不同的起始频率或扫频段 |
有一个问题排查过程值得单独说一下:有一个版本的固件在环境温度上升后误报率直线增加,一开始我以为是算法阈值问题,后来用热成像仪看了下现场,发现是空调出风口上方的墙面温度变化导致雷达回波相位产生漂移,被误判成了慢速运动目标。后面我加了一个温度补偿校正系数,用板载温度传感器每 60 秒校准一次背景模型,误报问题就自然消失了。
4.3 我的调参顺序与几个独家建议
调参这件事,顺序很重要。我最开始犯的错误是一上手就去怼检测灵敏度,结果到处误报,越调越乱。后来我总结出一套固定顺序,每次换新环境都能在半天内完成收敛。
第一步:先把背景学好。安装好雷达后,让人全部离场,采集 60 秒的空环境数据做背景初始化。这步做得越干净,后面一切阈值都好设。第二步:先压运动检测,再开静态检测。运动检测很容易观察到效果,先把大块运动目标调到稳定跟住、不闪烁,再去调呼吸检测这种弱信号。第三步:最后才加业务逻辑。区域联动、灯光控制这些上层逻辑一定是建立在稳定检测之上的,如果检测本身还在胡乱跳动,上层调得再精细也没用。
除了顺序之外,我特别想分享一个关于阈值设置的教训:不要追求极致的灵敏度,要留余量。RTLS 的监测场景会很诱人地希望把雷达探测距离拉满,但你把所有参数推到极限之后,系统对环境的适应性会变得很差。外面的空调、窗帘、甚至一只飞虫都会触发告警。我自己的经验是把运动检测阈值设为理论值加上 6dB 余量,这样虽然距离上损失了大约 15%,但长期运行时段的稳定性高了好几个量级。
最后再提一个适合低成本产品落地的方案:雷达存在检测配合 PIR 做“双传感器融合”。PIR 负责低功耗唤醒,雷达负责持续静态探测。平时雷达工作在低数据率的巡逻模式,PIR 一旦检测到人就会给 MCU 一个中断,雷达马上切换为高性能模式做存在确认。这个组合在电池供电的智能门锁和节能开关项目里特别吃香,成本增加很少,功耗和体验却能兼顾。PLFM_RADAR 目前已经在往这个方向演进,让雷达检测在绝大多数时间里都不需要满负荷运转。
如果你刚开始接触毫米波雷达,我的建议是不要急着把每个功能都做到完美,先用这个平台框架跑通“存在检测→事件输出”的最小闭环,把环境背景和安装问题解决之后,再一步步加上呼吸感知和区域判定。雷达调试的乐趣恰恰在于:它给出的信号里永远有噪声,但当你学会分辨那些噪声里的规律,你真正要感知的那个世界,就会变得清晰起来。