简介:面向毫米波雷达生命体征探测方向的Matlab仿真工具,可在无硬件条件下模拟人体呼吸、心跳等生命体征信号,支持信号分析与算法验证,兼容Matlab 2014a、2019b、2024b等主流版本。压缩包仅122KB,共5个文件,涵盖m脚本、mlx实时脚本、md说明文档、zbak备份文件及一个zip备份包,结构精简,便于按需选用。工具内置可直接运行的示例数据集,无需自行搭建模型即可快速开展实验;核心参数开放调整,可针对不同呼吸频率、心率变异等生理场景进行定制,代码分段注释清晰,适合入门学习与二次开发。目前已有60人学习下载,适用于电子工程、生物医学工程及计算机科学等专业的高年级课程实践、专题研讨与毕业设计,借助该仿真平台可实践滤波、特征提取等典型信号处理流程,深入理解毫米波雷达在健康监测中的工作原理。
1. 毫米波雷达生命体征模拟系统:没有硬件也能把呼吸心跳测到 0.1Hz 精度
毫米波雷达做人体生命体征检测,最难的不是算法,而是没有硬件的时候你根本拿不到像样的数据。这套模拟系统就是干这个的:用 Matlab 把雷达参数和人体生理参数合成为贴近真实的 FMCW 中频回波,再走完距离 FFT、相位提取、呼吸心跳分离的完整信号链。做车内儿童存在检测、居家健康监护这类课题,前期算法验证基本靠它顶住。适合做毕业设计、雷达算法预研,以及想理解微动测量原理的工程师。拿到代码先别急着改参数,先把默认配置跑一遍,再对照本文逐步替换,你会发现每一个参数改动都会真实反映在输出波形里。
2. FMCW 回波生成:把呼吸 4mm、心跳 0.3mm 的微动写进 IF 信号
2.1 为什么 FMCW 能测到毫米级位移
FMCW 雷达发射线性调频信号,目标回波经过双程时延 τ=2R/c 后与本地振荡器混频,得到的中频信号频率是 f_IF=S·τ=S·2R/c,其中 S=B/Tc 是调频斜率。距离信息从频率里读,但生命体征的位移实在太小:呼吸幅度约 2~12mm,心跳只有 0.2~0.5mm,在距离维 FFT 后完全分辨不出来——1GHz 带宽对应的距离分辨率约 0.15m,而胸腔微动只有毫米量级。真正能抓住微动的物理量是相位:中频信号里携带载波相位 φ=4πR/λ。77GHz 的波长 λ≈3.9mm,胸腔每移动 1mm,相位变化约 3.2rad。呼吸 4mm 对应约 12.9rad,心跳 0.3mm 对应约 0.97rad,在 SNR 足够时都能被相位检测捕获。
所以整个模拟系统的理论根基非常明确:距离维 FFT 负责把人体从环境中分离出来,相位维负责把亚毫米微动放大。仿真里最核心的环节就是把呼吸和心跳的位移模型写进 IF 信号的相位项,而不是把位移直接当作信号本身。很多初学者上来就生成一个正弦心跳信号,再叠加上呼吸正弦,那跟雷达就没有关系了——雷达看到的是由位移引起的相位调制,不是位移本身。这一步想明白了,后面的频谱估计才有意义。
2.2 模拟系统的参数设计
仿真参数不是拍脑袋定的。IF 频率不能超过 ADC 采样率的一半;帧率要高于心跳频率的至少两倍,实际取 20~30fps;距离分辨率要能把人体目标区分开。我一般先列一张参数表再写代码。
| 参数 | 取值 | 依据 |
|---|---|---|
| 载频 fc | 77GHz | 毫米波雷达主流频段,波长 3.9mm |
| 带宽 B | 1GHz | 距离分辨率约 0.15m |
| chirp 时长 Tc | 80us | 配合采样率得到 160 个快时间采样点 |
| ADC 采样率 fs | 2MHz | 5m 处 IF 频率约 416kHz,留足余量 |
| 帧率 frame_rate | 30fps | 心跳 2Hz 时仍有 15 倍过采样 |
| 总时长 T_dur | 30s | 频率分辨率约 0.033Hz,足够分辨呼吸心跳 |
| 呼吸幅度/频率 | 4mm / 0.25Hz | 模拟安静状态成人呼吸 15 次/分 |
| 心跳幅度/频率 | 0.3mm / 1.2Hz | 模拟正常心率 72 次/分 |
这里有个容易被忽略的细节:单 chirp 持续时间只有 80us,在这段时间内胸腔位移相对帧时间来说几乎静止,所以每帧内多个 chirp 的相位可以视为同一时刻的快照,不同帧之间的相位变化才反映呼吸心跳。因此慢时间维的采样率就是帧率,后续所有滤波和频谱计算都用 frame_rate 作为采样率,而不是 ADC 采样率 fs。这个混用会导致滤波器系数算错,我在实际调试中见过不止一次。
2.3 生成 IF 回波信号
下面是核心仿真代码。它生成总共 900 个慢时间快照,对应 30 秒数据,每个快照是一个长度为 160 的复数序列;再叠加上高斯白噪声,SNR 从变量传入,方便后面做信噪比扫描。
% sim_if_signal.m 生成完整IF回波数据 % 雷达参数 fc = 77e9; c = 3e8; B = 1e9; Tc = 80e-6; fs = 2e6; N = round(fs * Tc); % 单chirp采样点数 160 S = B / Tc; % 调频斜率 1.25e13 Hz/s % 慢时间参数 frame_rate = 30; T_dur = 30; M = T_dur * frame_rate; % 总帧数 900 n = (0:N-1).'; % 生理参数 R0 = 1.0; % 雷达到胸腔平均距离 A_b = 4e-3; f_b = 0.25; % 呼吸幅度4mm 频率0.25Hz A_h = 0.3e-3; f_h = 1.2; % 心跳幅度0.3mm 频率1.2Hz frame = zeros(N, M); for m = 1:M t = (m-1) / frame_rate; % 慢时间 x = A_b*sin(2*pi*f_b*t) + A_h*sin(2*pi*f_h*t); % 胸腔位移 R = R0 + x; tau = 2 * R / c; % 中频复数信号:差频项 + 载波相位项 frame(:, m) = exp(1j * 2*pi * (S*tau*(n/fs) + fc*tau)); end % 加高斯白噪声,SNR 单位 dB snr_dB = 20; sig_pow = mean(abs(frame(:)).^2); noise_pow = sig_pow / (10^(snr_dB/10)); frame = frame + sqrt(noise_pow/2) * ... (randn(size(frame)) + 1j*randn(size(frame)));代码里有几个点需要解释。快时间维用 n/fs 表示采样时刻,差频相位项写成 S·τ·(n/fs),这是 FMCW 中频信号的主项;载波相位项 fc·τ 随慢时间缓慢变化,微动信息就藏在它里面。噪声是复高斯白噪声,sqrt(noise_pow/2) 分别乘在实部和虚部,保证总噪声功率等于 noise_pow。这里的模型故意忽略了幅度衰减、多径和非线性失真,对算法验证够用;也正因为如此,真机上出现的问题往往比仿真更严重,这一点到第 5 章再展开。
2.4 静态杂波与多目标扩展
实际雷达回波里不只有胸腔一个强目标,还有墙体、桌面等静态反射体,它们在中频上表现为固定距离门上的直流分量。在仿真里给每个 chirp 叠加一个固定复数偏置就能模拟,处理时对应“去直流”或 MTI 杂波对消。多目标场景(比如车内同时检测驾驶员和乘客)则需要把两个不同距离的微动源叠加到 IF 信号里,每一帧是两个目标的复指数相加,幅度按距离衰减分别设置。这一步我建议放到基础版本跑通之后再加,因为两个目标的距离间隔一旦小于距离分辨率 0.15m,距离 FFT 后就会重叠在同一个 bin,相位提取直接失效,这是多目标仿真里最容易翻车的地方。
3. 距离 FFT 与相位提取:从 160 点 ADC 序列里抠出慢时间相位
3.1 距离维 FFT 与目标距离门选择
把上一节生成的 frame 沿快时间维做 FFT,就得到一帧的距离像。FFT 点数取 256,把 160 个实际采样点补零到 256,补零后的距离 bin 间隔约 0.094m,目标在 1m 处对应约第 11 个 bin。要选对目标距离门,常见做法是对整段数据先做慢时间平均,在平均距离像上找最大值,这样最稳;我实际调试时更习惯逐帧峰值搜索后取中值,因为单帧噪声大时峰值会跳一两个 bin,中值能把异常拉回来。如果峰值跳变超过 1 个 bin,说明 SNR 太低或目标幅度接近旁边杂波,要回到上一章把 SNR 提到 20dB 以上。
% range_fft.m —— 距离维FFT与稳定bin选择 N_fft = 256; rd = fft(frame, N_fft, 1); % 沿快时间维做FFT rd = rd(1:N_fft/2, :); % 只取正频率 % 整段平均距离像,选全局最大bin rd_avg = mean(abs(rd), 2); [~, pos] = max(rd_avg); % 补零后距离bin间隔 dR = c / (2*B) * (N / N_fft); % c/(2B)≈0.15m,补零后约0.094m range_est = pos * dR;这里加不加窗值得说一下。加 hann 窗会把旁瓣从 -13dB 压到 -31dB 左右,适合近距离强反射体旁边有弱目标的情况;但主瓣会变宽,峰值幅度会降低。生命体征仿真里目标本身不弱,我默认不加窗,保持主瓣锐度;一旦在 2.4 节加了静态杂波,再把窗加上,否则杂波旁瓣可能压过心跳目标。补零只是插值,不改变物理分辨率,所以 dR 公式里乘 N/N_fft 是必需的,这个修正因子新手经常漏。在 1m 处目标会被放到 10.67 附近,补零后看起来在第 11 个 bin,实际上用未补零的 160 点做峰值搜索也会得到同一个目标距离,只是幅度峰值更宽。
3.2 相位提取与 unwrap 的顺序问题
相位提取的顺序有讲究:取目标 bin 的复数,用 angle 得到 -π~π 的相位,再用 unwrap 沿慢时间维解开。但 unwrap 不是银弹,当 SNR 低、相位噪声大时,2π 跳变点可能选错,解缠结果出现台阶。我做过对比:SNR 从 30dB 降到 15dB,unwrap 出错概率从约 0 升到约 5%,这意味着 30 秒仿真里能看到一次明显的阶梯。更稳的方式是相邻帧差分相位再累加,如下面代码的 robust 方式。
% phase_extract.m —— 相位提取与两种方式对比 s = rd(pos, :); % 方式一:直接 angle + unwrap raw = unwrap(angle(s)); % 方式二:帧间差分累计,抗单帧跳变 diff_p = angle(s(:, 2:end) .* conj(s(:, 1:end-1))); robust = [0, cumsum(diff_p)]; % 与真值比较,验证相位提取精度 t_slow = (0:M-1) / frame_rate; true_x = A_b*sin(2*pi*f_b*t_slow) + A_h*sin(2*pi*f_h*t_slow); lambda = c / fc; err1 = max(abs(detrend(raw,1) - 4*pi*true_x/lambda)); err2 = max(abs(detrend(robust,1) - 4*pi*true_x/lambda));差分方式的限制是帧间位移必须小于 λ/4,约 0.975mm。呼吸幅度 4mm 时,30fps 下相邻帧胸腔位移约 0.21mm,完全满足;但如果把帧率降到 5fps,这一项变成 1.26mm,就超限了,所以这个坑的本质还是帧率约束。代码里的 true_x 是从生理参数构造的位移真值,err1/err2 用来验证相位提取精度,这是整个模拟系统最直观的自检手段——如果这一步误差超过 0.2rad,后面的频域估计不用看肯定偏。
3.3 detrend 的去留与直流泄漏
解缠后的相位序列里除了微动信号,还有一个与距离 R0 成正比的常值相位,以及可能随硬件偏置变化的慢趋势。常值相位无所谓,但趋势项如果不去掉,会在低频处产生很强的直流分量,呼吸 0.25Hz 离得很近,带通滤波后的边缘效应会很严重。我的习惯是先 detrend(phase, 1) 再做滤波,除非后面用 VMD——VMD 会自己分配一个低频模态吸收趋势,pre-detrend 反而会把能量打散。detrend 第二个参数传 1 表示线性拟合去趋势,传 0 只去均值。源码默认传 1,因为远距离静态目标和硬件偏置带来的相位斜率很多不是常数,30 秒仿真里二阶以上的趋势很少见,不必加高阶。另一个容易忽视的点是滤波前先确认相位序列方向:filtfilt 要求输入是向量,如果是行向量就保持行向量,窗函数也要对应转置,否则 Matlab 会隐式扩展出维度灾难,结果全是 NaN。
4. 呼吸心跳分离:带通滤波与 VMD 两条路线的参数落地
4.1 带通滤波:最能落地的默认方案
相位序列去掉趋势后,就是包含呼吸、心跳、噪声的低频信号。经典路线是用带通滤波器把两个频段切开:呼吸频段取 0.1~0.5Hz,对应 6~30 次/分;心跳频段取 0.8~2.5Hz,对应 48~150 次/分。中间留 0.5~0.8Hz 保护带,防止滤波器滚降互相串扰。滤波器阶数我分情况处理:呼吸信号幅度大、信噪比高,三阶 Butterworth 足够;心跳幅度小、容易被边缘效应污染,用四阶。零相位滤波必须用 filtfilt,普通 filter 会引入固定群延迟,心跳波形相位偏移,频率估计虽然不受影响,但波形形状全错,后续做心率变异性 HRV 分析时会直接报废。
% filter_bank.m —— 呼吸/心跳分离与频率估计 fs_pt = frame_rate; % 慢时间采样率=帧率 % 带通滤波器设计 [b_b, a_b] = butter(3, [0.1 0.5]/(fs_pt/2), 'bandpass'); [b_h, a_h] = butter(4, [0.8 2.5]/(fs_pt/2), 'bandpass'); % 零相位滤波 breath = filtfilt(b_b, a_b, phase_proc); heart = filtfilt(b_h, a_h, phase_proc); % 频率估计:加窗FFT + 峰值搜索 Nf = 4096; win = hann(length(breath)).'; f_ax = (0:Nf-1)/Nf * fs_pt; BF = abs(fft(breath .* win, Nf)); HF = abs(fft(heart .* win, Nf)); [~, ib] = max(BF(2:floor(Nf/2))); [~, ih] = max(HF(2:floor(Nf/2))); fb_est = f_ax(ib+1); fh_est = f_ax(ih+1);参数说明:截止频率分母 fs_pt/2 是归一化换算,千万不要拿快时间采样率 fs 来算,否则滤波器会落在完全错误的频段。FFT 频率分辨率等于 1/T_dur,30 秒数据做 4096 点 FFT 后分辨率约 0.033Hz,对应呼吸频率约 0.004 次/分的误差,够用。峰值搜索从索引 2 开始是为了避开 0Hz 直流,最后 f_ax(ib+1) 把索引偏移回去。我习惯把呼吸波形、心跳波形、频谱、真值四条曲线画在同一张图上,先眼睛看过一遍再谈数字,直接输出估计频率容易忽略滤波振铃这类局部异常。
4.2 VMD 分离:当谐波和频谱混叠把滤波逼到墙角
带通滤波的短板是假设呼吸和心跳频带不重叠。真实数据里呼吸波形往往带二次、三次谐波,毕竟呼吸不是标准正弦,0.5Hz 的二次谐波正好落在心跳带通边界上,很容易被误判为心跳频率。这种时候我优先试 VMD,变分模态分解,MATLAB R2021a 之后自带 vmd 函数,不需要额外工具箱。
% vmd_sep.m —— VMD分离呼吸与心跳 [imfs, ~] = vmd(phase_proc, 'NumIMFs', 3, ... 'InitialAlpha', 2000, 'Tol', 1e-6); % imfs是3xN矩阵,通常最低频的呼吸在第1行,心跳在第2行 % 对每行做FFT并取峰值得到模态中心频率vmd 参数里最敏感的是 NumIMFs 和 InitialAlpha。NumIMFs 固定为 3,一个给呼吸、一个给心跳、一个吸收趋势和噪声残差;如果提前做了 detrend,可以减到 2,但我不推荐。InitialAlpha 控制模态带宽,2000 是经验起点:alpha 越大模态带宽越窄、越不容易串扰,但太大会把呼吸的多普勒展宽硬压成一条线,丢失真实波形形态;alpha 太小又会让模态互相重叠。VMD 的代价是计算量,900 帧的小数据还好,一旦把慢时间长度加到上千点,这步会明显变慢,跑批的时候建议放到最后再执行。
4.3 两条路线的选型逻辑
带通滤波和 VMD 各有适用场景。带通滤波计算量几乎可忽略,适合在线实时处理、干净模拟数据;VMD 数秒起步,抗谐波能力强,适合离线分析、波形复杂、呼吸谐波明显时。选型一句话:先跑带通,如果心跳估计值和呼吸频率存在倍数关系,怀疑谐波串扰,再上 VMD。不要一上来就 VMD,alpha 的玄学调试会让你浪费大量时间。验证方式也很直接,固定一组生理参数,对 SNR 从 30dB 到 10dB 扫描,记录两种方法的频率估计误差,画成曲线,就能看到 VMD 在低信噪比下的收益和带通滤波在计算量上的优势。
5. 避坑清单:毫米波雷达生命体征仿真最常见的 5 个翻车点
5.1 unwrap 后出现台阶,呼吸波形断裂
现象:解缠后的相位序列带有明显跳变台阶,带通滤波后呼吸波形上下分裂,看起来不像正弦,更像是方波。
原因:帧间相位差接近 π 时,unwrap 对跳变方向的判断会被噪声带偏;尤其是帧率设置偏低、SNR 又在 15dB 附近时,错误概率快速上升。
解决:换成帧间差分相位累计,先检查帧间位移有没有超过 λ/4;超过就先提帧率再谈解缠。用 3.2 节的 robust 方式替换 unwrap,并把 SNR 保持在 18dB 以上。如果位移本来就超限,换任何解缠算法都救不回来,只能降帧率或降载频。
5.2 心跳频率稳定偏到呼吸的 2 倍上
现象:带通滤波后心跳峰值出现在 0.5Hz 附近,与呼吸频率精确两倍,且幅度不大,但就是不像 1.2Hz 的真实心率。
原因:呼吸波形不是严格正弦,产生二次谐波;0.5Hz 谐波落进心跳带通低频边缘,能量比真实心跳还大。
解决:对呼吸带通输出估计出 f_b 后,在心跳频率峰值搜索时排除 2×f_b±0.05Hz 邻域。或者直接切到 VMD 看第二个模态的频谱,如果模态中心在 1.2Hz 附近,说明真实心跳没丢,只是被谐波盖住了。
5.3 距离门跳变,相位提取全成了噪声
现象:输出的相位序列像白噪声,detrend 后没有任何周期性;回看距离像,峰值在多帧之间跳来跳去。
原因:每帧独立取最大值时,不同帧的最强点可能来自旁边旁瓣或噪声峰;SNR 不足时尤其明显。
解决:用整段数据先做慢时间平均距离像,取全局最大 bin 作为固定目标位置,再做抛物线插值细化:
rd_avg = mean(abs(rd), 2); [~, pos] = max(rd_avg); % 抛物线插值获得亚bin位置 a = rd_avg(pos-1); b = rd_avg(pos); c = rd_avg(pos+1); pos_sub = pos - 0.5*(a-c)/(a-2*b+c);抛物线插值能补偿栅栏效应带来的 ±0.5bin 抖动,对相位精度有明显帮助。注意 pos 在边界时要退化为直接用 pos,否则索引越界。
5.4 帧率太低,心跳被混叠到低频
现象:把帧率改成 5fps 想省计算量,心跳估计变成 0.8~0.9Hz,或者带通输出波形明显失真。
原因:帧率不满足奈奎斯特条件时,高于 fs_pt/2 的频率分量会折叠回低频。心跳 1.2Hz 在 5fps 下还未直接混叠,但带通上界是 2.5Hz,这个频段里的任何分量都会折叠。
解决:frame_rate 不低于 4×f_h_max,一般取 20~30fps。如果帧率被硬件限制,就把心跳带通上界同步降下来,但代价是测不了真实场景里的高心率。帧率是慢时间采样率,所有滤波计算都用它,混淆成 ADC 采样率的人基本都会在这一步翻车。
5.5 SNR 参数写错单位,加噪后信号直接炸掉
现象:把 snr_dB 按线性值 20 写进公式,噪声功率变成 sig_pow/20,结果噪声几乎为零;反过来把 20dB 当成 20 倍噪声,波形完全淹没。
原因:信噪比参数最容易搞混 dB 和线性倍数,差着数量级。
解决:统一用 10^(snr_dB/10) 做换算,并打印实际 SNR 验证:
snr_check = 10*log10(sig_pow / noise_pow); assert(abs(snr_check - snr_dB) < 1e-6);这个断言代码建议留在所有仿真脚本里,SNR 错了后面的所有分析都白做。
6. 体动干扰注入:把仿真从实验室推到真实场景的最后一步
6.1 体动建模与检测
真实环境里最大的干扰不是噪声,而是人自己动:翻身、说话、伸手拿东西,体动幅度可达几厘米,比心跳信号大两个数量级。把体动注入仿真后,你会看到相位序列出现一次突然的大偏移,带通滤波输出出现长尾振铃。我的做法是在呼吸心跳位移上叠加一个窗函数形状的短时脉冲,再用滑动窗口标准差检测体动段的起止:
% body_move.m —— 体动注入与检测 t_slow = (0:M-1)/frame_rate; move = zeros(1, M); burst = 8e-3 * hann(31); % 幅度8mm,持续约1秒 move(200:230) = burst; x_mixed = true_x + move; % 滑动窗口标准差检测 wlen = 15; th = 3 * std(true_x); sd = zeros(size(x_mixed)); for k = wlen+1 : M-wlen sd(k) = std(x_mixed(k-wlen:k)); end move_mask = sd > th;检测到体动段后直接剔除,不参与心跳频率估计;体动结束后不要立刻继续用原来的相位基线,先重新 detrend 一段再继续。这个处理是源码里最不起眼却最关键的一步。
6.2 三个让仿真跑得更快的小习惯
第一,把参数集中成一个结构体 cfg,不要散落在各个脚本里,改帧率时不会漏改一个变量。第二,避免在循环里反复调用 fft 和 butter,滤波器系数提前算好,帧数据先存矩阵再一次性 FFT,速度能快一个数量级。第三,仿真时长默认取 30 秒以上,但绘图时只画最近 4096 点,避免低频分量把图淹掉。
那是我第一次做体动注入时,把一段翻身数据直接送进心跳估计,得出了 87 次/分的“正常心率”,后来看了波形才知道整段都污染了。从那以后我每次仿真都会强制走一遍体动注入和滑动窗口检测,确认输出信号的时间位置,再谈频率。希望帮到你。
本文还有配套的精品资源,点击获取