1. 项目缘起:为什么我要折腾一台相位法测距雷达
PLFM_RADAR 这个名字拆开看就是PhaseLaserFrequencyModulationRADAR,直译过来是相位式激光调频测距雷达。但在我这个项目里,它其实是一个以 FPGA 做高速时序控制、STM32 做系统调度与人机交互的混合架构测距系统。核心原理并不复杂:发射一束经过正弦调幅的激光,接收端把回波光信号转换成电信号,通过测量发射调制信号与接收调制信号之间的相位差,反推出目标距离。相位差和距离之间是线性关系,测相精度直接决定测距精度,这也是为什么整个项目最吃功夫的地方在“相位测量”而不是“激光发射”。
我最初动这个念头,是因为市面上能买到的激光测距模块大多是脉冲飞行时间法(dTOF),量程大但毫米级精度很难做,而且模块内部算法封闭,想改点东西几乎不可能。相位法测距在短距离(几米到几十米)内可以做到毫米甚至亚毫米级分辨率,正好适合室内定位、工业形变监测、精密装配引导这类场景。问题是,公开的相位法测距完整工程极少,要么是纯理论推导,要么是拿现成评估板跑个 demo,真正把 FPGA 高速采样、数字鉴相、STM32 系统控制串起来的资料几乎找不到。所以我决定自己搭一套,把 PLFM_RADAR 做成一个可复现、可扩展的开源项目骨架。
这套东西适合谁看?如果你正在做FPGA 高速 ADC 采样、STM32 与 FPGA 协同、数字信号处理中的相位提取,或者你单纯想找一个能同时练 FPGA 和 STM32 的综合性项目,那这篇内容应该能帮你省下不少试错时间。我会把方案选型、参数计算、实操步骤、踩过的坑全部摊开讲,不藏私。
2. 整体架构设计与关键选型逻辑
2.1 为什么是 FPGA + STM32 的双核架构
先回答一个最常被问到的问题:为什么不用纯 FPGA 或者纯 STM32 搞定?纯 STM32 的问题在于采样率。相位法测距需要至少几十 MHz 的调制频率,根据奈奎斯特采样定理,ADC 采样率得是调制频率的两倍以上,实际工程中为了做数字正交解调,往往需要 4 倍甚至更高。STM32 内置 ADC 在 12 位分辨率下勉强能跑到几 MSPS,但一旦要同时处理多路信号、做实时数字滤波和相位解算,CPU 根本扛不住。纯 FPGA 倒是能解决采样和实时处理,但人机交互、参数存储、上位机通信、屏幕显示这些事用 FPGA 做性价比极低,开发周期也长。
所以最终方案是:FPGA 负责高速数据通路,STM32 负责控制通路和业务逻辑。具体分工如下表:
| 模块 | 承担角色 | 关键器件/接口 | 理由 |
|---|---|---|---|
| 信号采集 | FPGA | 高速 ADC(如 AD9226,12 位 65 MSPS) | 需要严格时序和高速并行采样 |
| 数字鉴相 | FPGA | 内部 DSP 切片做乘法与累加 | 实时性要求高,STM32 算不过来 |
| 调制信号生成 | FPGA | DDS IP 核或查找表 | 频率和相位可精确控制 |
| 系统调度 | STM32 | 主控 MCU | 任务管理、状态机、异常处理 |
| 人机交互 | STM32 | ILI9341 屏、按键、串口 | 成熟生态,开发快 |
| 数据存储 | STM32 | SPI Flash / SD 卡 | 参数与日志保存 |
| 上位机通信 | STM32 | UART / USB CDC | 方便调试和数据回传 |
这个分工的核心逻辑是:让擅长的人干擅长的事。FPGA 的强项是并行和确定性时序,STM32 的强项是流程控制和丰富外设。两者通过 SPI 或 FSMC 总线交换数据,STM32 发命令、读结果,FPGA 只管把相位差算出来放到寄存器里。
2.2 调制频率与量程的取舍计算
相位法测距有一个绕不开的矛盾:调制频率越高,理论分辨率越高,但无模糊量程越短。因为相位差只能在 0 到 2π 之间测量,超过一个周期就出现模糊。距离和相位差的关系是:
距离 D = (c × Δφ) / (4π × f_mod)
其中 c 是光速,Δφ 是相位差,f_mod 是调制频率。当 Δφ = 2π 时对应一个“无模糊距离”,即 D_max = c / (2 × f_mod)。
我选了几个典型频率算了一下:
| 调制频率 f_mod | 无模糊量程 D_max | 理论相位分辨率(假设 0.1°)对应距离 |
|---|---|---|
| 1 MHz | 150 m | 约 0.42 mm |
| 5 MHz | 30 m | 约 0.083 mm |
| 10 MHz | 15 m | 约 0.042 mm |
| 20 MHz | 7.5 m | 约 0.021 mm |
可以看到,10 MHz 调制下无模糊量程 15 米,对于室内和一般工业场景完全够用,同时理论分辨率已经进入亚毫米级。再高的话量程太短,实际使用中稍微远一点就模糊了。所以我最终把主调制频率定在10 MHz,同时预留一个1 MHz 的粗测频率用于解模糊,两个频率配合可以把量程扩展到 150 米同时保持高精度。这就是多频相位法测距的基本思路。
2.3 ADC 采样率与数字鉴相方案选择
10 MHz 调制信号,如果采用数字正交解调(I/Q 解调),采样率至少要是调制频率的 4 倍,即 40 MSPS 以上。我选的是AD9226,12 位、最高 65 MSPS,实际配置在 50 MSPS 工作。这样每个调制周期采 5 个点,虽然不算特别充裕,但配合后续的数字滤波和相干累加,信噪比可以接受。
数字鉴相我试过两种方案:
第一种是过零检测法,把发射和接收信号分别过比较器变成方波,然后用 FPGA 测两个方波上升沿的时间差。这个方法实现简单,但对噪声极其敏感,信号稍微有点毛刺就误触发,实测抖动很大,直接放弃。
第二种是正交解调法(I/Q 解调),也是我最终采用的方案。原理是把 ADC 采到的信号分别乘以同相分量 cos(ωt) 和正交分量 sin(ωt),然后各自做低通滤波,得到 I 和 Q 两路直流分量。相位差就是 atan2(Q, I)。这个方法的优势在于对噪声有天然的抑制能力,因为乘法加累加本质上是一个相干积分过程,噪声被平均掉了。FPGA 里用 DSP48 切片做乘加非常高效,一个 12 位乘 12 位的乘法器可以跑到几百 MHz,完全跟得上 50 MSPS 的采样率。
3. 核心细节解析与实操要点
3.1 FPGA 内部数字鉴相的具体实现
正交解调在 FPGA 里的实现分三步:混频、滤波、相位计算。
混频部分,我需要生成与调制信号同频的 cos 和 sin 参考序列。这里有个细节:参考信号的相位必须和发射调制信号严格对齐,否则测出来的相位差里会混入一个固定偏移。我的做法是用同一个 DDS 核同时产生发射调制信号和接收端解调用的参考信号,保证两者同源同相。DDS 的相位累加器位宽选 32 位,输出 12 位正弦查找表地址,频率控制字根据 10 MHz 和 50 MHz 采样时钟计算:
频率控制字 = (f_mod / f_clk) × 2^32 = (10e6 / 50e6) × 2^32 ≈ 858993459
这个值直接写进 DDS 的频率控制寄存器即可。
混频之后是低通滤波。因为调制频率是 10 MHz,采样率 50 MHz,混频后有用信号变成直流附近,而二倍频分量在 20 MHz 附近。我设计了一个32 阶的 FIR 低通滤波器,截止频率设在 1 MHz,用 FPGA 的乘法器阵列实现。滤波器的系数用 MATLAB 的 fir1 函数生成,量化成 12 位后写入 ROM。这里有个经验:滤波器阶数不是越高越好,阶数太高会消耗大量 DSP 资源,而且群延迟变大,对实时性有影响。32 阶在资源占用和滤波效果之间是一个比较平衡的点。
相位计算部分,I 和 Q 两路滤波后的值送到一个 CORDIC 核里算 atan2。CORDIC 是 FPGA 里算三角函数的经典方法,只需要移位和加法,不需要乘法器,非常适合硬件实现。我用的是一个 16 级迭代的流水线 CORDIC,延迟 16 个时钟周期,精度大约 0.01°。算出来的相位值是一个 16 位定点数,范围对应 -π 到 +π,STM32 读过去之后再根据公式换算成距离。
3.2 STM32 与 FPGA 的通信接口设计
STM32 和 FPGA 之间我选了SPI 接口,而不是 FSMC 并行总线。原因很简单:SPI 占用的引脚少,PCB 布线容易,而且我的数据量不大——每次测距只需要读回一个相位值和一个状态字,几十个字节而已。SPI 时钟跑到 10 MHz,传输这点数据连一毫秒都用不了。
通信协议我定义得很简单:STM32 作为主机,FPGA 作为从机。STM32 先拉低片选,然后发送一个 8 位命令字,FPGA 根据命令字决定返回什么数据。命令字定义如下:
| 命令字 | 功能 | 返回数据 |
|---|---|---|
| 0x01 | 启动单次测距 | 状态字节 |
| 0x02 | 读取相位差 | 2 字节相位值 |
| 0x03 | 读取信号强度 | 2 字节幅度值 |
| 0x04 | 设置调制频率 | 状态字节 |
| 0x05 | 读取 FPGA 版本 | 4 字节版本号 |
这里有个坑我踩过:SPI 的时钟极性和相位必须和 FPGA 端严格匹配。STM32 的 SPI 可以配置成四种模式,我一开始用了默认的 Mode 0,结果 FPGA 端采样数据错位,读回来的相位值全是乱的。后来改成 Mode 3(CPOL=1,CPHA=1)才正常。建议在 FPGA 端用示波器或者逻辑分析仪抓一下 SCK 和 MOSI 的波形,确认采样沿和数据变化沿的关系。
3.3 激光发射与接收模拟前端的关键参数
模拟前端是整个系统里最容易被忽视但最影响性能的部分。发射端我用的是一个10 mW 的 650 nm 激光二极管,驱动电路是一个恒流源加高速 MOSFET 调制。调制深度直接决定接收端信号的信噪比,我实测调制深度在 80% 以上时,接收信号幅度比较理想。
接收端用的是APD(雪崩光电二极管),因为普通 PIN 光电二极管在几米距离上回波信号太弱,APD 内部有增益,可以显著提高灵敏度。APD 的偏置电压需要精确控制,太高会击穿,太低增益不够。我用的是一个可调高压模块,通过 STM32 的 DAC 输出控制电压,再经过升压电路产生 APD 所需的偏压。调试时先用示波器观察接收信号的幅度,慢慢调偏压直到信号幅度最大且没有明显噪声。
跨阻放大器(TIA)的反馈电阻选的是100 kΩ,配合 APD 的结电容,带宽大约在 20 MHz 左右,刚好覆盖 10 MHz 调制信号。这里要注意:反馈电阻越大,增益越高,但带宽越低。如果发现接收信号幅度够但波形失真,很可能是带宽不够,需要减小反馈电阻或者换用更高带宽的运放。
4. 实操过程与核心环节实现
4.1 硬件搭建与上电调试顺序
硬件部分我分成三块板子:FPGA 核心板、ADC 采集板、STM32 控制板。三块板子通过排针连接,方便单独调试。上电顺序很重要,我建议按以下步骤来:
- 先单独给 STM32 上电,烧录一个最简单的 LED 闪烁程序,确认最小系统正常。这一步能排除电源、晶振、复位电路的问题。
- 再单独给 FPGA 上电,用 JTAG 下载一个 LED 闪烁的 bit 流,确认 FPGA 配置电路正常。如果是第一次用某款 FPGA,还要确认芯片包和下载器驱动装好了。
- 连接 ADC 板,给 ADC 提供独立的时钟源(我用的是 50 MHz 有源晶振),用逻辑分析仪抓 ADC 的输出数据线,确认有数据在跳变。
- 最后连接模拟前端,先不接激光器,用信号发生器给接收端一个 10 MHz 的正弦波,观察 ADC 采到的波形是否正常。
这个顺序的核心逻辑是从数字到模拟、从简单到复杂,每一步只引入一个新变量,出问题容易定位。我见过有人一上来就把所有板子插好上电,结果某个电源短路把 FPGA 烧了,损失惨重。
4.2 FPGA 工程的模块划分与关键代码
FPGA 工程我用的是 Verilog,顶层模块下面分了几个子模块:
clk_gen:时钟管理,产生 50 MHz 采样时钟和 100 MHz 系统时钟。dds_mod:DDS 调制信号生成,输出 10 MHz 正弦波。adc_capture:ADC 数据采集,把 12 位并行数据打拍同步到系统时钟域。iq_demod:正交解调,包含混频器和 FIR 低通滤波器。cordic_atan2:CORDIC 相位计算。spi_slave:SPI 从机接口,与 STM32 通信。reg_file:寄存器组,存放相位值、幅度值、状态字。
关键代码片段——DDS 频率控制字的计算和写入:
// 参数定义 parameter PHASE_ACC_WIDTH = 32; parameter FREQ_CTRL_WORD = 32'd858993459; // 10MHz @ 50MHz采样时钟 // 相位累加器 reg [PHASE_ACC_WIDTH-1:0] phase_acc; always @(posedge clk_50m) begin phase_acc <= phase_acc + FREQ_CTRL_WORD; end // 取高12位作为查找表地址 wire [11:0] lut_addr = phase_acc[PHASE_ACC_WIDTH-1:PHASE_ACC_WIDTH-12];FIR 滤波器的实现我用的是转置型结构,每个时钟周期处理一个采样点,32 阶滤波器需要 32 个乘法器。如果 DSP 资源紧张,可以改成折叠结构,用 4 个乘法器分 8 个周期算完,但那样吞吐率会下降。我用的 FPGA 有 240 个 DSP 切片,32 个乘法器只占一小部分,所以直接用全并行结构。
4.3 STM32 端任务调度与屏幕显示
STM32 端我用的是FreeRTOS,建了三个任务:
task_measure:负责发起测距、读取相位、计算距离,优先级最高。task_display:负责刷新 ILI9341 屏幕,显示距离值和信号强度,优先级中等。task_comm:负责串口通信,把数据打包发给上位机,优先级最低。
任务之间通过队列传递数据,避免全局变量竞争。测距任务的伪代码如下:
void task_measure(void *pvParameters) { float distance; uint16_t phase_raw; while (1) { spi_send_cmd(0x01); // 启动测距 vTaskDelay(pdMS_TO_TICKS(10)); // 等待FPGA完成 phase_raw = spi_read_phase(); // 读相位 distance = phase_to_distance(phase_raw); xQueueSend(distance_queue, &distance, 0); vTaskDelay(pdMS_TO_TICKS(100)); // 10Hz刷新率 } }ILI9341 屏幕的驱动我踩过一个坑:读 ID 返回 0xA1A1 并不一定代表屏幕正常。有些兼容芯片的 ID 就是 0xA1A1,但初始化序列和原厂 ILI9341 不同。我一开始按标准 ILI9341 的初始化序列写,屏幕一直白屏,后来换了卖家提供的初始化代码才点亮。所以买屏幕的时候一定要找卖家要初始化代码,不要迷信数据手册。
4.4 距离解算与校准流程
相位差转距离的公式前面已经给过,但实际使用中还需要校准。因为电路上存在固定的相位延迟(走线长度、运放延迟、滤波器群延迟等),这些延迟会叠加到测量结果里,导致零点偏移。校准的方法是:在已知距离上测量,反推固定偏移量。
我用了两个校准点:0.5 米和 5 米。先测 0.5 米时的相位差 Δφ1,再测 5 米时的相位差 Δφ2,然后解一个二元一次方程组:
D1 = k × Δφ1 + b D2 = k × Δφ2 + b
求出 k 和 b 之后,后续测量值代入即可。实测下来,校准后 0.5 米到 10 米范围内的误差可以控制在 ±2 毫米以内。如果要求更高,可以增加校准点数量,用最小二乘法拟合。
5. 常见问题与排查技巧实录
5.1 相位测量值跳动大怎么办
这是最常见的问题,表现为读回来的距离值一直在跳,波动范围可能有好几厘米。排查思路按以下顺序来:
| 可能原因 | 排查方法 | 解决方案 |
|---|---|---|
| 接收信号太弱 | 用示波器看 TIA 输出幅度 | 提高激光功率或 APD 偏压 |
| 电源噪声大 | 用示波器看电源纹波 | 增加 LC 滤波,模拟和数字电源分开 |
| 参考信号相位漂移 | 检查 DDS 和 ADC 时钟是否同源 | 确保同一时钟源驱动 |
| FIR 滤波器截止频率太高 | 观察滤波后 I/Q 波形 | 降低截止频率到 500 kHz |
| 相干累加次数不够 | 增加累加周期数 | 从 100 次增加到 1000 次 |
我遇到过一次特别诡异的跳动,最后发现是ADC 的时钟线和激光驱动线在 PCB 上走得太近,高速开关信号耦合到了时钟线上,导致采样时钟抖动。把这两根线分开走、中间加地线隔离之后,跳动从 ±5 厘米降到了 ±2 毫米。所以 PCB 布局在混合信号系统里怎么强调都不过分。
5.2 STM32 和 FPGA 通信失败怎么查
SPI 通信失败通常有三种表现:读回全 0、读回全 1、读回数据错位。对应的排查步骤:
- 读回全 0:先检查片选信号有没有拉低,再检查 FPGA 端 SPI 从机模块有没有被正确综合进去。可以用 FPGA 的在线逻辑分析仪(如 SignalTap 或 ILA)抓一下 SPI 接口的信号。
- 读回全 1:通常是 MISO 线没有正确驱动,检查 FPGA 端 MISO 有没有配置成输出,或者有没有上拉电阻冲突。
- 数据错位:几乎都是时钟极性相位不匹配,参考前面说的改成 Mode 3 试试。
还有一个容易被忽略的点:STM32 的 SPI 引脚复用要配置正确。我有一次把 SPI1 的引脚配到了 SPI2 的复用功能上,结果怎么都不通,查了半天才发现是引脚映射搞错了。
5.3 量程不够或出现模糊怎么办
如果目标距离超过无模糊量程,相位差会绕圈,测出来的距离会突然跳到一个错误值。解决方法是多频解模糊。具体做法是:先用 1 MHz 粗测得到一个粗略距离(无模糊量程 150 米),再用 10 MHz 精测得到一个高精度但可能模糊的距离,然后用粗测值确定精测值的周期数。
举个例子:粗测得到 12.3 米,精测得到 2.3 米(因为 10 MHz 下 12.3 米对应的相位差超过了 2π,实际测出来是 2.3 米)。那么真实距离就是 12.3 米,精测值用来提供毫米级的小数部分。这个逻辑在 STM32 里实现,几行代码就能搞定。
5.4 FPGA 资源不够用的优化技巧
如果你的 FPGA 比较小(比如 EP4CE10 这类入门级芯片),可能会遇到资源不够的问题。我总结了几条优化经验:
- FIR 滤波器改折叠结构:用 4 个乘法器分时复用,资源占用降到 1/8,代价是吞吐率下降,但 10 MHz 调制信号对吞吐率要求不高,完全够用。
- CORDIC 改迭代结构:流水线 CORDIC 用 16 个寄存器,迭代 CORDIC 只用 1 个,但需要 16 个时钟周期算一次。因为相位计算不需要每个采样点都算,可以攒够一批数据再算,所以迭代结构完全可行。
- DDS 查找表用块 RAM:不要用分布式 RAM 实现正弦查找表,块 RAM 更省逻辑资源。
- SPI 从机用状态机实现:不要用现成的 IP 核,自己写一个简单的状态机,资源占用小很多。
6. 实测数据与性能评估
6.1 不同距离下的测量误差
我在实验室里用卷尺量了几个标准距离,每个点测 100 次取平均,结果如下:
| 实际距离 | 测量均值 | 误差 | 标准差 |
|---|---|---|---|
| 0.5 m | 0.502 m | +2 mm | 1.2 mm |
| 1.0 m | 0.998 m | -2 mm | 1.5 mm |
| 2.0 m | 2.003 m | +3 mm | 1.8 mm |
| 5.0 m | 4.997 m | -3 mm | 2.1 mm |
| 10.0 m | 10.005 m | +5 mm | 3.5 mm |
可以看到,近距离误差在 ±3 毫米以内,10 米处误差稍微大一点,但也在 ±5 毫米以内。标准差随着距离增加而增大,这是因为回波信号强度随距离平方衰减,信噪比下降导致相位测量抖动增加。如果需要在更远距离保持精度,可以增加激光功率或者增加相干累加次数。
6.2 刷新率与响应时间
系统当前配置下,单次测距耗时约 10 毫秒(包括 FPGA 采集 1000 个周期做相干累加,以及 SPI 传输和 STM32 计算),实际刷新率约 100 Hz。如果降低累加次数到 100 次,刷新率可以提高到 500 Hz,但精度会下降。这是一个典型的精度与速度的权衡,根据具体应用场景调整即可。
对于静态测量场景(如形变监测),可以用 1000 次累加追求最高精度;对于动态跟踪场景(如机械臂末端定位),可以用 100 次累加保证实时性。
6.3 温度漂移与长期稳定性
连续运行 2 小时后,我发现测量值有大约 1-2 毫米的漂移。排查后发现主要来源是APD 的偏压随温度变化,以及激光二极管的波长随温度漂移。解决方法是加一个温度传感器(如 DS18B20),实时监测环境温度,然后在 STM32 里做温度补偿。补偿系数需要实验测定:在 20°C 和 40°C 下分别测量同一距离,算出漂移量,然后线性插值。
这个补偿做完之后,2 小时漂移降到了 0.5 毫米以内,基本满足工业级应用需求。
7. 后续扩展方向与个人体会
这套 PLFM_RADAR 骨架目前只实现了单点测距,但扩展空间很大。比如把单点激光换成振镜扫描,就可以做二维扫描测距,生成点云;把 FPGA 里的鉴相算法从正交解调换成 FFT,可以同时处理多个调制频率,实现更鲁棒的解模糊;把 STM32 的串口通信换成以太网,就可以接入工业网络做远程监测。
我在这个项目里最大的体会是:混合架构系统的难点从来不在单个模块,而在模块之间的接口和时序。FPGA 内部逻辑再复杂,只要仿真通过了基本不会出大问题;但 FPGA 和 STM32 之间的 SPI 通信、ADC 和 FPGA 之间的高速数据接口、模拟前端和数字后端之间的电源与地处理,这些“边界”才是最容易翻车的地方。所以我的建议是,每连接一个新模块,先用最简单的测试程序验证接口,确认无误后再往上叠功能。不要想着一次性把所有代码写完再调试,那样出了问题你根本不知道是哪一层的问题。
另外,仪器真的很重要。一台 200 MHz 带宽的示波器和一个逻辑分析仪,能帮你省下至少一半的调试时间。我一开始想用万用表和 LED 凑合,结果在 SPI 时序问题上卡了整整两天,后来借了一台逻辑分析仪,十分钟就定位到了问题。该花的钱还是要花。