1. 从代码到物理世界:为什么我们需要理解FFT的每一步?

大家好,我是老张,在智能硬件和信号处理这块摸爬滚打了十来年。今天想和大家聊聊一个非常具体、也常常让新手工程师头疼的问题:当你拿到一份TI毫米波雷达的原始数据(一个.bin文件)和一段MATLAB处理代码时,如何才能真正看懂它?你可能会看到代码里一堆fftfftshiftreshape操作,最后输出了目标的距离、速度、角度。但如果你只是机械地运行代码,而不明白每一行计算背后的物理意义,一旦参数变了、雷达型号换了,或者数据出现异常,你就完全懵了。

这就像给你一张地图,你只知道跟着红线走能到目的地,但完全看不懂比例尺、等高线和图例。换一张地图,你就寸步难行。我们这篇文章的目标,就是帮你把这张“地图”彻底看懂。我们将紧扣“从MATLAB代码到物理世界”这个核心,把抽象的FFT算法和TI雷达硬件参数(比如采样点数、脉冲数)死死地绑在一起,手把手带你走完一个完整的“代码行 -> 物理量”的转换过程。

我会基于一个真实的TI AWR1642雷达采集的.bin文件数据,用最直白的语言,拆解从原始数据到三维点云的每一步。你会发现,原来MATLAB里每一个矩阵维度的变化,都对应着雷达发射、接收的物理过程;原来FFT之后得到的那个“峰值”的索引值,直接换算一下就是几十米外的物体距离。我不讲空洞的理论,就讲我实际调代码、看数据、踩坑总结出来的经验。准备好了吗?我们开始吧。

2. 解码雷达的“原始语言”:深入理解bin文件与硬件参数

在动手写任何一行MATLAB代码之前,我们必须先和雷达硬件“对上暗号”。TI雷达采集完数据后,存成一个.bin文件。这个文件对于没接触过的人来说,就像一本用外星语写的书。我们的第一个任务,就是学会这门“语言”的语法和词汇。

2.1 硬件参数:雷达的“出厂设置”

这些参数决定了数据的基本结构,是你后续所有计算的基石。我们结合TI AWR1642的典型配置来理解:

  • IQ正交采样:这是雷达接收数据的根本方式。它告诉我们,雷达接收到的每一个信号点,都不是一个简单的实数,而是一个复数。这个复数包含实部(I)和虚部(Q),共同决定了信号的幅度和相位。在存储时,通常实部和虚部各用16位(2字节)表示,所以一个IQ采样点占4个字节。理解这一点至关重要,因为读文件时,你必须按4字节一组来读。
  • 采样点数:雷达发射一个频率连续变化的信号(称为一个Chirp),在接收回波时,会以固定的时间间隔进行采样。这个“固定的采样次数”就是采样点数。它直接决定了雷达能探测的最远距离。采样点数越多,距离分辨率越高。我们例子中设为256。
  • 脉冲数:雷达为了测量速度,会连续发射多个完全相同的Chirp,这一串Chirp称为一帧。单个Chirp的数量就是脉冲数。它决定了雷达能测量的最大不模糊速度和速度分辨率。我们例子中设为128。
  • 发射天线与接收天线:TI雷达通常有多个发射天线和接收天线,通过排列组合形成虚拟天线阵列,这是实现角度测量的基础。例子中是1发4收,可以形成4个虚拟接收通道。

把这些参数想象成盖房子的蓝图:采样点数是房子的“长度”(距离维),脉冲数是房子的“宽度”(速度维),而天线数量决定了你能盖多少“层”(角度维)。蓝图错了,房子肯定盖歪。

2.2 bin文件结构:数据是怎么“堆”在一起的?

知道了参数,我们来看数据是怎么存放的。假设我们有一个125MB的.bin文件,包含了250帧数据。那么一帧有多大呢?我们来算一下:

一帧大小 = 4字节/采样点 × 256采样点/Chirp × 128个Chirp × 4个RX通道 × 1个TX通道 = 524,288 字节

这52万多个字节,在文件里可不是杂乱无章堆着的。TI的传输格式通常是这样的:按照“快时间”和“慢时间”的顺序交织存储。更具体地说,对于第一个接收天线,它会把第一个Chirp的256个IQ采样点(共256×4字节)依次存完;然后存第二个Chirp的256个点……直到存完128个Chirp。接着,再以同样的方式存储第二个接收天线的数据,以此类推。

用MATLAB读取后,你会看到一个超级长的列向量。我们的第一个任务,就是根据上面理解的“蓝图”,把这个长长的向量,重新排列成一个我们能理解的、有物理意义的三维数据立方体。这个过程就像把一长串连续的乐高积木,按照说明书拼成一栋三层小楼。接下来,我们就进入MATLAB实战环节。

3. MATLAB实战第一步:数据重塑与三维数据立方体构建

拿到那个长长的数据向量后,直接做FFT是没意义的。我们必须先进行“数据重塑”,把它整理成符合我们认知的格式。这个过程在代码里主要靠reshape函数完成,但每一步reshape的维度参数,都充满了物理意义。

3.1 第一步:从字节到复数

原始数据是按字节流的,每4个字节代表一个复数采样点(前2字节是实部I,后2字节是虚部Q)。在MATLAB中,我们通常先将其读取为uint16类型(因为每个I或Q是16位),然后组合成复数。

% 假设 rawData 是 uint16 类型的列向量,长度是 262144*2? 等等,这里需要小心!
% 更常见的操作是直接以二进制形式读取并转换
fid = fopen('1642SRR2m.bin', 'r');
% 读取一帧数据,注意:一帧的字节数我们已经算出来了
frameBytes = fread(fid, 524288, 'uint8=>uint8'); % 先按字节读
fclose(fid);

% 将字节数据转换为 int16(考虑ADC的输出通常是有符号的)
% 这里假设数据格式是:I0, Q0, I1, Q1, ...
% 即每两个字节组成一个16位有符号整数
allData = typecast(frameBytes, 'int16'); % 现在 allData 是 int16 的向量

% 将奇偶元素分离为实部和虚部
I = allData(1:2:end); % 奇数位是实部
Q = allData(2:2:end); % 偶数位是虚部

% 组合成复数向量
complexData = complex(I, Q); % 现在 complexData 的长度是 262144

这一步做完,我们就把原始的、冰冷的字节流,变成了有明确物理意义(信号幅度和相位)的复数序列。它的长度是 256采样点 × 128Chirp × 4RX = 131072 个复数?等等,这里有个关键点:我们之前算的一帧大小524288字节,对应的是 524288 / 2 = 262144 个 int16。而每个复数由两个int16组成,所以复数个数应该是 262144 / 2 = 131072 个。这和我们的计算(2561284=131072)对上了!这是一个重要的自检环节。

3.2 第二步:构建三维数据立方体

现在我们有了一维的复数向量complexData,长度为131072。接下来,我们要把它还原成雷达采集时的三维结构:

  1. 维度1:快时间维(采样点,代表距离)
  2. 维度2:慢时间维(Chirp,代表速度)
  3. 维度3:空间维(接收天线通道,代表角度)
% 已知参数
numADCSamples = 256; % 采样点数
numChirps = 128;     % 每帧Chirp数
numRxAntennas = 4;   % 接收天线数

% 关键重塑步骤:理解这个顺序是核心!
% 假设数据存储顺序是:对于每个RX天线,存储所有Chirp的所有采样点
% 即:RX1(Chirp1的所有采样点, Chirp2的所有采样点, ...), 然后RX2, RX3, RX4
dataCube = reshape(complexData, numADCSamples, numChirps, numRxAntennas);

这行代码是魔法发生的地方。reshape函数按照(采样点数, Chirp数, 接收天线数)的顺序,把一维向量重新排列。现在,dataCube是一个256×128×4的三维矩阵。你可以这样理解:

  • dataCube(:, 1, 1) 表示第一个接收天线上,第一个Chirp的256个采样点(随时间变化的回波信号)。
  • dataCube(100, :, 1) 表示第一个接收天线上,所有128个Chirp的第100个采样点(随时间变化的慢变信号,蕴含速度信息)。
  • dataCube(100, 50, :) 表示在第100个采样时刻、第50个Chirp时,4个接收天线同时接收到的信号(蕴含角度信息)。

这个三维数据立方体,就是我们后续所有FFT处理的起点。它完美对应了物理世界:距离、速度、角度三个维度的信息,已经各就各位,等待被提取。

4. 三维FFT处理核心:逐层剥开距离、速度与角度信息

数据立方体准备好了,接下来就是最核心的FFT处理三部曲。记住,每一次FFT,都是在特定的维度上,将时域(或空间域)的信号转换到频域,而频域的索引值直接对应着我们要的物理量

4.1 第一维FFT:距离维FFT(Range FFT)

这是对“快时间”维做FFT,也就是对每个Chirp内的256个采样点做FFT。

% 进行距离维FFT,通常会在采样点数基础上做补零以提高频率分辨率
rangeFFTLength = 256; % 可以设置为2的幂次,如256或512
rangeFFT = fft(dataCube, rangeFFTLength, 1); % 在第一个维度上做FFT

物理意义解读: 雷达发射的Chirp信号频率是随时间线性变化的。物体反射回波后,接收到的信号与发射信号混频,会产生一个固定的“差拍频率”。这个差拍频率与物体的距离成正比。对单个Chirp做FFT(即距离维FFT),就是在分析这个差拍频率的频谱。频谱上出现峰值的位置(索引),就对应着特定的差拍频率,从而可以计算出距离。

MATLAB操作细节

  • fft(..., N, dim)中的dim=1指定了在第一个维度(采样点维度)上进行变换。
  • 做完FFT后,通常取绝对值(abs)来观察幅度谱。你会看到一个频谱图,横轴是FFT点数(对应频率),纵轴是幅度。峰值对应的索引idx_range,通过公式距离 = (采样率 * idx_range) / (FFT点数 * 调频率斜率) 来计算。这是连接代码索引和物理世界距离的第一座桥梁。

4.2 第二维FFT:速度维FFT(Doppler FFT)

在距离FFT的基础上,我们对每个距离单元(range bin),沿着Chirp维度(第二个维度)再做一次FFT。

% 进行速度维FFT
dopplerFFTLength = 128; % 通常等于Chirp数
dopplerFFT = fft(rangeFFT, dopplerFFTLength, 2); % 在第二个维度上做FFT

% 非常重要:将零频分量移到频谱中心,以便观察正负速度
dopplerFFT = fftshift(dopplerFFT, 2);

物理意义解读: 由于目标在运动,每个Chirp的回波信号相位会有一个微小的变化。这个相位变化率就是多普勒频率,它与目标的径向速度成正比。对同一个距离单元上的128个Chirp信号做FFT(即速度维FFT),就是在分析这个多普勒频率的频谱。频谱峰值的位置,就对应着目标的速度。

MATLAB操作细节

  • dim=2指定在Chirp维度进行。
  • fftshift是关键一步。因为速度有正有负(目标靠近或远离雷达),对应的多普勒频率也有正负。原始的FFT结果零频在两边,正负频率分居两侧,不直观。fftshift将其重新排列,让零频位于频谱中间,负速度在左边,正速度在右边。峰值索引idx_doppler经过换算(速度 = (波长 * (idx_doppler -中心点)) / (2 * 总 chirp时间)),就得到了目标速度。

4.3 第三维FFT:角度维FFT(Angle FFT)

最后,在距离-速度二维处理的基础上,我们沿着接收天线维度(第三个维度)做FFT。

% 进行角度维FFT,点数可以灵活设置,通常比天线数多(如180)以提高角度分辨率
angleFFTLength = 180; % 常用180,对应-90度到+90度
angleFFT = fft(dopplerFFT, angleFFTLength, 3); % 在第三个维度上做FFT
angleFFT = fftshift(angleFFT, 3); % 同样将零角度(正前方)移到中心

物理意义解读: 多个接收天线构成了一个阵列,目标反射的波前到达不同天线时,存在微小的波程差,表现为信号相位的差异。这个相位差与目标的到达角有关。对同一个距离-速度单元上的多个天线信号做FFT(即角度维FFT),就是在分析这个空间频率的频谱。频谱峰值的位置,就对应着目标的角度。

MATLAB操作细节

  • dim=3指定在接收天线维度进行。
  • 同样使用fftshift将正前方(0度)调整到频谱中心。
  • 角度FFT的点数angleFFTLength可以设置得比实际天线数多(比如4根天线设180点),这是一种“补零”操作,能让你在频谱图上更精细地观察峰值位置,但并不会提高真实的角度分辨率(分辨率由天线孔径决定)。峰值索引idx_angle通过公式角度 = arcsin( (idx_angle -中心点) * 波长 / (FFT点数 * 天线间距) ) 计算。

经过这三重FFT,我们得到了一个三维复数矩阵angleFFT,它的三个维度分别对应距离、速度、角度。这个矩阵的每一个元素,都代表了在特定距离、速度、角度上存在目标的“可能性”(幅度大小)。寻找这个三维空间中的局部峰值,就能检测出多个目标。

5. 从峰值索引到物理量:完成最后一步转换

假设我们通过CFAR等检测算法,在三维FFT结果矩阵中找到了一个峰值点,其索引为(row, col, pag),分别对应距离维、速度维、角度维的索引。现在,我们要把这三个索引值,转换成实实在在的物理量:距离R、速度v、角度angle。这是打通代码和物理世界的“临门一脚”。

5.1 距离计算

距离的计算依赖于差拍频率fb。在距离维FFT中,峰值索引row对应一个数字频率。这个数字频率需要转换成模拟频率fb

% 已知系统参数
fs = 10e6;          % 采样率,例如10 MHz,这个值由雷达配置决定
N_range = 256;      % 距离维FFT点数
K = 60e12;          % 调频率斜率,单位Hz/s,雷达发射Chirp的频率变化速度,由雷达配置决定
c = 3e8;            % 光速

% 计算差拍频率 fb
% 注意:row 是1-based索引,而FFT频率分量从0开始。
fb = (row - 1) * fs / N_range;

% 计算距离 R
% 注意:这里有一个关键点!对于运动目标,差拍频率fb实际上包含了距离引起的频率和多普勒频率fd。
% 严格公式应为:R = c * (fb - fd) / (2 * K)
% 但在计算距离时,我们通常先忽略fd的影响(因为fd通常远小于fb),或者使用更精确的联合估计。
% 这里先给出简化公式:
R = c * fb / (2 * K);

关键点讨论:这里引出了一个实际工程中的细节。fb是直接从距离FFT峰值索引算出的频率,它同时包含了由距离决定的频率和由速度决定的多普勒频移。在TI的公式中,更精确的距离计算是 R = c * (fb - fd) / (2 * K)。这意味着我们需要先估算出速度fd,才能反哺回来得到更精确的距离。在实际代码中,有时会先忽略fd进行初步估算,有时则会迭代计算。理解这个关系,能帮你避免在精度要求高的场景下踩坑。

5.2 速度计算

速度的计算依赖于多普勒频率fd。在速度维FFT中,峰值索引col对应多普勒频率。

% 已知系统参数
M = 128;            % 速度维FFT点数(等于Chirp数)
Tc = 40e-6;         % 一个Chirp的周期(包括发射和空闲时间),由雷达配置决定
lambda = 3.9e-3;    % 雷达波长,例如77GHz雷达波长约3.9mm,由载波频率决定

% 计算多普勒频率 fd
% 注意:因为之前用了fftshift,零速在中心。col是1-based索引,需要换算到以零速为中心的索引。
fd = (col - M/2 - 1) / (M * Tc); % 这个公式给出了以零速为中心的正负频率

% 计算速度 v
v = lambda * fd / 2;

公式解读(col - M/2 - 1)这一步,就是把峰值索引col(假设1到128)转换成一个以零为中心、范围在[-M/2, M/2-1]的索引。然后除以M*Tc(总观察时间),就得到了模拟多普勒频率fd。速度公式v = lambda * fd / 2是雷达测速的基本公式,其中除以2是因为波程是双程的。

5.3 角度计算

角度的计算依赖于空间频率fw。在角度维FFT中,峰值索引pag对应空间频率。

% 已知系统参数
Q = 180;            % 角度维FFT点数
d = lambda / 2;     % 接收天线间距,通常设计为半波长以获得最大不模糊角度范围

% 计算空间频率 fw
% 同样,因为用了fftshift,零角度(正前方)在中心。
fw = (pag - Q/2 - 1) / Q;

% 计算角度(弧度)
theta = asin(fw * lambda / d); % 因为 d = lambda/2,所以公式可简化为 asin(2 * fw)
% 转换为度
angle_deg = theta * 180 / pi;

物理意义:空间频率fw反映了信号相位在不同天线间变化的快慢。asin函数将其映射到角度。这里有一个重要假设:天线间距d必须小于等于半波长,否则会出现角度模糊(即多个不同的真实角度对应同一个fw)。TI的芯片天线通常已按此设计好。

5.4 整合与验证

将以上计算封装成一个函数,输入三维索引和雷达参数,输出物理量。在实际项目中,我强烈建议你单独测试这个转换函数。用一些已知的、简单的场景(比如把一个角反射器放在雷达正前方固定位置)来验证你的计算是否正确。参数fsKTc等一定要从你的雷达配置脚本或数据手册中准确获取,一个参数错了,结果可能差之千里。

6. 超越基础:实际工程中的关键细节与避坑指南

如果你按照前面的步骤走通了,恭喜你,你已经掌握了从TI雷达数据到目标信息的主干道。但真实的工程项目就像越野,主干道之外还有很多沟坎。下面分享几个我踩过坑才明白的关键细节。

6.1 静态杂波抑制与MTI滤波

雷达收到的信号里,最强的往往不是运动目标,而是静止的背景(比如墙壁、地面)。这些静止杂波会在速度维FFT的零频(零速)附近产生一个巨大的峰值,淹没旁边低速运动的目标信号。解决方法是在做速度维FFT之前,先进行静态杂波抑制

最常用的是“动目标显示”滤波器。一个简单有效的方法是,对每个距离单元,沿着Chirp维度做相邻Chirp的差分。

% 简单的两脉冲对消器(MTI)
% dataCube 是距离FFT前的数据或距离FFT后的数据(通常在距离FFT后做)
[mtiFiltered] = zeros(size(dataCube));
for i = 2:numChirps
    mtiFiltered(:, i, :) = dataCube(:, i, :) - dataCube(:, i-1, :);
end
% 然后对 mtiFiltered 进行速度维FFT

这个操作相当于一个高通滤波器,会滤除零频附近的静止杂波。但要注意,它也会削弱非常低速的目标。你需要根据应用场景(比如检测行人还是车辆)来权衡。

6.2 相位校准与角度校准

理论上,天线阵列应该是完全一致的,但实际硬件中,每个接收通道的放大器、混频器、走线长度都有微小差异,导致即使信号从同一方向来,不同通道接收到的信号也存在固定的相位偏差。如果不校准,角度FFT的结果就会不准,测角出现系统误差。

校准方法:通常需要在暗室或开阔场,在雷达正前方较远处放置一个点目标(角反射器)。采集该目标的数据,然后计算每个接收通道相对于参考通道的固定相位差。在后续处理中,对所有数据先进行相位补偿。

% 假设 refPhase 是一个 1 x numRxAntennas 的向量,存储了每个通道需要补偿的相位(弧度)
% calibDataCube 是校准后的数据
for rxIdx = 1:numRxAntennas
    calibDataCube(:, :, rxIdx) = dataCube(:, :, rxIdx) * exp(-1j * refPhase(rxIdx));
end

这个步骤对于高精度测角应用至关重要,但很多入门教程会忽略。

6.3 三维峰值搜索与CFAR检测

三维FFT之后,我们得到一个能量立方体。如何自动找到里面的目标?你不能简单地说“找到最大值”,因为可能有多个目标,而且还有噪声。这就需要用到恒虚警率检测算法。

CFAR的核心思想是,在每个检测单元周围划出一个“保护单元”和“训练单元”。用训练单元的背景噪声水平来估计当前检测单元的噪声阈值,超过阈值才判为目标。在三维空间中,这变得比较复杂,因为你要在三个维度上滑动窗口。

% 这是一个概念性代码,实际CFAR实现更复杂
thresholdCube = someCFARalgorithm(abs(angleFFT).^2); % 计算阈值立方体
detectionMap = (abs(angleFFT).^2) > thresholdCube; % 生成二值检测图
% 然后对 detectionMap 中的连通区域寻找局部极大值,得到目标索引列表

我建议初学者可以先使用MATLAB的cfarDetector2D等现成工具,或者从简单的二维CFAR开始理解原理,再扩展到三维。自己实现一个高效、准确的三维CFAR是个不小的挑战。

6.4 参数选择对性能的影响

FFT点数、补零、加窗这些操作不是随便选的,它们直接影响性能:

  • FFT点数:距离/速度维FFT点数通常等于采样点数/Chirp数,补零可以增加频谱显示点数,让峰值更易观察,但不提高真实分辨率。角度维FFT点数可以远大于天线数,同样是为了显示更平滑。
  • 加窗:在做FFT前,对数据加窗(如汉明窗)可以抑制频谱旁瓣,减少目标间的相互干扰,但代价是主瓣略微展宽(分辨率轻微下降)。在有多目标且能量相差大的场景下,加窗非常有用。
  • 距离/速度/角度分辨率:这三个是系统硬限制。距离分辨率 = c / (2 * 带宽);速度分辨率 = λ / (2 * 帧时间);角度分辨率与天线孔径有关。你的FFT处理无法突破这些理论分辨率。

理解这些工程细节,能让你从“代码能跑”提升到“结果可靠、性能优化”。毫米波雷达的信号处理是一条很深的赛道,每一个环节都有优化的空间。希望这篇结合了具体代码和物理意义的解析,能为你打下扎实的基础。当你再看到那些FFT代码时,眼前浮现的不再是抽象的数学变换,而是电磁波在空间中的传播、反射,以及目标在距离-速度-角度三维空间中的清晰画像。这才是工程师该有的感觉。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐