PLFM_RADAR:FPGA+STM32+Python全链路脉冲线性调频雷达实战 1. 项目缘起与整体设计思路PLFM_RADAR 这个标题第一次看到的人大概率会愣一下。PLFM 是 Pulse Linear Frequency Modulation 的缩写中文叫脉冲线性调频说白了就是一种雷达发射信号的调制方式RADAR 就是雷达。合在一起这是一个以脉冲线性调频体制为核心的雷达信号处理项目。它不是一个纯软件仿真玩具而是一套横跨 FPGA 硬件逻辑、STM32 控制层、Python 上位机与 GUI 交互的完整链路工程。我做这个项目的出发点很朴素市面上讲雷达原理的书很多讲 FPGA 高速 ADC 采样的教程也不少但把这两件事真正串起来、从射频前端一直做到屏幕上能看到目标距离和速度的完整工程案例少得可怜。大部分资料要么停在 MATLAB 仿真要么只给你一段 Verilog 让你自己悟。所以 PLFM_RADAR 的目标就是把这整条链路打通做成一个可以复现、可以拆开单独学习、也可以整体运行的实战项目。这套系统能做什么简单说它发射一个线性调频的脉冲信号接收回波后做去斜处理或者脉冲压缩通过 FFT 得到目标的距离信息通过多个脉冲之间的相位变化得到速度信息最后把结果显示在 PC 端的 GUI 界面上。适合谁来参考我认为有三类人一是正在做 FPGA 高速信号处理、想找一个完整项目练手的工程师二是做 STM32 嵌入式控制、想了解雷达时序控制逻辑的开发者三是电子类毕业设计选题卡壳的同学这个题目既有硬件又有软件还有算法工作量饱满且不落俗套。整体架构上我把它分成四层。第一层是射频前端与 ADC 采集负责把模拟回波变成数字样本第二层是 FPGA 逻辑负责高速数据流的接收、缓存、预处理和 FFT第三层是 STM32负责系统时序控制、参数配置、与上位机的通信第四层是 Python 上位机加 GUI负责数据接收、二次处理和可视化。这四层之间通过明确的接口解耦任何一层出问题都可以单独替换或调试这是我做复杂系统一贯坚持的原则——能解耦就解耦能分模块就分模块绝不把鸡蛋放在一个篮子里。为什么这么分层因为雷达系统的调试难度是叠加的。如果你把采集、处理、显示揉在一个芯片里一旦结果不对你根本不知道是 ADC 采样时序错了、还是 FFT 点数配错了、还是上位机解析错了。分层之后每一层都有独立的验证手段ADC 层可以用已知信号源验证FPGA 层可以用仿真加在线逻辑分析仪验证STM32 层可以用串口打印验证上位机层可以用离线数据验证。这套方法论我在多个高速采集项目里反复验证过确实能把调试时间压缩一半以上。2. 核心硬件选型与关键参数解析2.1 FPGA 与 STM32 的分工逻辑很多人会问为什么不用一颗芯片全搞定非要 FPGA 加 STM32 两颗这个问题我在项目初期也纠结过。答案在于两类芯片的擅长领域完全不同。FPGA 擅长的是并行、高速、确定性时序比如 ADC 采样率 100MSPS 的时候每个时钟周期都要处理一个样本这种活只有 FPGA 干得动。而 STM32 擅长的是流程控制、协议栈、人机交互比如解析上位机命令、管理状态机、驱动显示屏这些用 FPGA 做反而又累又贵。具体分工是这样的FPGA 负责 ADC 数据接收、数字下变频、抽取滤波、FFT 运算、数据打包STM32 负责发射触发时序、接收 FPGA 的中断和状态、通过串口或网口把处理结果传给上位机、响应上位机的参数配置命令。两者之间用 FSMC 或者 SPI 通信我实测下来 FSMC 并行总线在数据量大的时候更稳SPI 适合低速配置通道。提示FPGA 和 STM32 之间的电平标准一定要提前确认3.3V 对 3.3V 最省心如果一边是 1.8V 一边是 3.3V务必加电平转换否则轻则通信不稳重则烧 IO。2.2 高速 ADC 采样的几个硬指标雷达项目的性能天花板很大程度上由 ADC 决定。选 ADC 的时候我重点关注四个参数采样率、分辨率、输入带宽、信噪比。采样率决定了你能处理的最大信号带宽按照奈奎斯特准则采样率至少要大于信号带宽的两倍实际工程里我一般留 2.5 倍以上的余量。分辨率方面雷达信号动态范围大我倾向选 14 位或 16 位的 ADC12 位在弱目标检测时会明显吃力。输入带宽这个参数容易被忽略。有些 ADC 标称采样率很高但模拟输入带宽只有几十兆高频信号进去就衰减得厉害。我踩过这个坑后来选型时一定把输入带宽和采样率一起看。信噪比和有效位数直接决定雷达的灵敏度这个在数据手册里都有选型时对比一下就行。采样时钟的质量同样关键。时钟抖动会直接恶化信噪比我一般用专用的时钟芯片或者 FPGA 内部的 MMCM 加外部晶振绝不用普通晶振直接驱动 ADC。实测下来同样的 ADC换一个好时钟信噪比能提升好几个 dB这个投入非常值。2.3 存储与缓存的容量计算雷达数据是流式的来得多来得快缓存设计不好就会丢数据。我算一下假设 ADC 采样率 100MSPS16 位宽那就是每秒 200MB 的数据量。FPGA 内部 RAM 根本存不下必须外挂 DDR。DDR 的带宽要大于数据产生速率还要留出读写切换的开销我一般按 1.5 倍余量设计。DDR 读写这块有个经典难题多端口并发访问。ADC 写数据、FFT 读数据、STM32 读结果三个端口同时抢 DDR仲裁做不好就乱套。我的做法是在 FPGA 内部用乒乓 Buffer 做一级缓冲把突发的大块数据先存进 Block RAM再批量搬进 DDR这样 DDR 的访问就是大块连续的效率高很多。这个思路和网络里的包缓存是一个道理先攒够一批再统一处理避免频繁的小数据访问。3. FPGA 逻辑实现与信号处理链路3.1 从 ADC 到数字下变频的完整流程ADC 送进来的数据是实信号直接做 FFT 会有镜像干扰所以第一步是数字下变频把信号搬到基带。具体做法是把 ADC 数据分别乘以本地振荡器的余弦和正弦得到 I 路和 Q 路。本地振荡器用 DDS 生成频率设成发射信号的中心频率。这一步在 FPGA 里就是两个乘法器加一个 DDS资源消耗不大但效果立竿见影。下变频之后是抽取滤波。因为下变频后信号带宽变窄了没必要保持原来的高采样率抽取可以大幅降低后续处理的数据量。抽取倍数要根据信号带宽来定我一般让抽取后的采样率是信号带宽的 2.5 到 4 倍。抽取滤波器用 CIC 加补偿 FIR 的组合CIC 负责大倍数抽取FIR 负责补偿 CIC 的幅频失真这个组合是业界标准做法实测效果很稳。注意CIC 滤波器的位宽增长要算清楚抽取倍数 N 和级数 M 决定了内部位宽要增加 M 乘以 log2(N) 位位宽不够会溢出结果全错这个坑我见过太多人踩。3.2 FFT 的选型与实现细节FFT 是雷达信号处理的核心。FPGA 里做 FFT 有几种选择自己写 R2SDF 结构、用厂商 IP 核、或者用 HLS 生成。我的建议是除非你是为了学习 FFT 内部结构否则直接用厂商 IP 核稳定、省时间、性能还有保证。点数选择上1024 点适合快速验证4096 点是比较平衡的选择8192 点以上资源消耗就上来了。FFT 的输入要做加窗处理否则旁瓣会很高弱目标容易被强目标的旁瓣淹没。我一般用汉明窗或者布莱克曼窗汉明窗主瓣窄但旁瓣抑制一般布莱克曼窗旁瓣抑制好但主瓣宽具体选哪个看你的应用场景。加窗系数可以预先算好存在 ROM 里和输入数据逐点相乘。FFT 输出的复数结果要算模值也就是求平方和再开方。开方在 FPGA 里比较费资源如果对精度要求不高可以用近似算法比如 alpha max 加 beta min 的线性近似误差在百分之几以内但资源省一大截。这个取舍要看你的系统对幅度精度的要求。3.3 数据打包与跨时钟域处理FPGA 处理完的数据要传给 STM32这中间涉及跨时钟域。ADC 采样时钟、FFT 处理时钟、通信时钟往往是不同频率不同相位的直接传数据必然出亚稳态。我的做法是用异步 FIFO 做隔离写端用处理时钟读端用通信时钟FIFO 深度要足够容纳一次突发传输的数据量。数据打包格式我设计成帧头加长度加数据加校验的结构。帧头用固定的魔术字方便接收端同步长度字段告诉接收端这一帧有多少字节校验用 CRC16能检出绝大多数传输错误。这个格式看起来简单但非常实用我在多个项目里都用这套接收端解析逻辑清晰出错也容易定位。4. STM32 控制层与通信协议设计4.1 系统时序控制的实现雷达系统的时序非常讲究。发射脉冲、接收窗口、数据采集、处理、传输这些动作有严格的先后顺序和精确的时间关系。STM32 在这里的角色就是总指挥用定时器产生精确的触发信号控制发射机什么时候发、接收机什么时候开、FPGA 什么时候开始采集。我用的是 STM32 的高级定时器因为它支持多路互补输出和死区控制做时序控制很灵活。定时器的预分频和自动重载值要根据脉冲重复频率来算。假设脉冲重复频率是 1kHz那周期就是 1ms定时器时钟 72MHz 的话自动重载值就是 72000 减 1。这个计算看着简单但实际调试时要注意定时器更新事件和实际输出之间的延迟我一般用示波器实测一下把偏差补偿掉。提示STM32 定时器的触发输出到实际引脚有几十纳秒的延迟做高精度时序时要把这个延迟算进去否则发射和接收的窗口会对不齐。4.2 与 FPGA 的通信接口STM32 和 FPGA 之间的通信我前面提到用 FSMC。FSMC 的本质是把 FPGA 当成一个外部存储器来访问STM32 往特定地址写数据FPGA 就能收到反过来 FPGA 把数据放到数据总线上STM32 读特定地址就能拿到。这种方式速度快编程也简单不用自己写协议。地址映射要规划好我一般把配置寄存器、状态寄存器、数据 FIFO 分到不同的地址段。配置寄存器是只写的STM32 写参数FPGA 读状态寄存器是只读的FPGA 写状态STM32 读数据 FIFO 是双向的看具体用途。地址译码在 FPGA 里用简单的组合逻辑就能实现注意时序要满足 FSMC 的建立保持时间要求。如果数据量不大SPI 也够用而且引脚少、布线简单。SPI 的坑在于时钟极性和相位的配置STM32 和 FPGA 两边必须一致否则数据错位。我一般用模式 0也就是时钟空闲低电平、第一个边沿采样这个模式最通用。4.3 上位机通信协议STM32 和 PC 之间的通信我用的是串口转 USB波特率 921600 或者更高。协议设计上命令帧和数据帧分开。命令帧是 PC 发给 STM32 的包含命令码和参数数据帧是 STM32 发给 PC 的包含雷达处理结果。命令码我设计了一套简单的协议0x01 是启动采集0x02 是停止采集0x03 是配置参数0x04 是查询状态。每个命令后面跟相应的参数比如配置参数命令后面跟脉冲重复频率、FFT 点数、增益等。这种设计的好处是扩展方便以后加新功能只要加新命令码就行不用改协议框架。数据帧的格式和 FPGA 到 STM32 的格式保持一致这样 STM32 基本就是透传不需要做复杂的解析和重组减少了出错的可能。实测下来921600 波特率下传 4096 点的 FFT 结果一帧大概几十毫秒对于 1kHz 的脉冲重复频率来说数据率是匹配的不会堆积。5. Python 上位机与 GUI 可视化实现5.1 串口数据接收与解析Python 这边我用 pyserial 库做串口接收。接收线程独立运行把收到的字节流放进队列主线程从队列取数据做解析。这种生产者消费者模式能避免界面卡顿是 GUI 程序的标准做法。解析的时候先找帧头找到帧头后读长度然后按长度读数据最后校验 CRC。如果校验失败就丢弃这一帧继续找下一个帧头。这个逻辑看起来简单但实际写的时候要注意缓冲区管理不能每次来一个字节就找一次帧头那样效率太低。我的做法是维护一个字节缓冲区每次收到数据就追加进去然后从缓冲区里找完整的帧找到就取出来处理剩下的留着等下次数据。注意串口接收一定要设超时否则程序可能永远阻塞在 read 上。我一般设 0.1 秒超时超时后检查一下缓冲区如果有半截帧就等下一批数据如果长时间没有新数据就报警提示连接断开。5.2 雷达数据的二次处理STM32 传上来的数据是 FFT 模值但直接显示效果不好需要做二次处理。第一步是取对数把线性幅度转成 dB这样动态范围压缩了弱目标也能看见。第二步是做背景噪声估计我用的是单元平均恒虚警检测也就是 CFAR。CFAR 的原理是滑动窗口用待检测单元周围的参考单元估计噪声电平然后设一个阈值超过阈值的就认为是目标。CFAR 的参数很关键参考单元数和保护单元数要根据目标大小来定。参考单元太少噪声估计不稳参考单元太多邻近目标会互相干扰。我一般用 16 到 32 个参考单元保护单元 2 到 4 个这个范围在多数场景下都能用。阈值系数根据虚警率要求来定要求虚警率低就设高一点要求检测概率高就设低一点这是个权衡。5.3 GUI 界面设计与交互逻辑GUI 我用的是 PyQt5功能强大、文档全、社区活跃。界面布局上主区域是雷达的 PPI 显示或者距离-多普勒二维图旁边是参数配置面板底部是状态栏显示连接状态和数据率。PPI 显示用 QGraphicsView 做把极坐标的距离和角度转成笛卡尔坐标画点。距离-多普勒图用 QImage 直接操作像素把二维数据映射成颜色效率比一个个画点高得多。颜色映射我用的是 jet 或者 viridisjet 对比度高但容易误导viridis 感知均匀但对比度低看具体需求选。参数配置面板用 QSlider 加 QSpinBox 的组合滑块调快速数字框调精确。配置改变后通过串口发给 STM32STM32 再转发给 FPGA。这里要注意线程安全GUI 线程不能直接操作串口必须通过信号槽机制把配置命令发给接收线程由接收线程统一发送。6. 常见问题与排查技巧实录6.1 数据链路类问题数据链路的问题最让人头疼因为涉及环节多任何一个环节出问题都表现为数据不对。我的排查思路是从后往前先确认上位机能不能收到数据再确认 STM32 有没有发出数据再确认 FPGA 有没有处理出数据最后确认 ADC 有没有采到数据。上位机收不到数据先看串口有没有打开、波特率对不对、线有没有接对。这些基础问题占了故障的一半以上别嫌简单先排除。STM32 没发出数据看发送函数有没有被调用、DMA 有没有配好、发送完成标志有没有置位。FPGA 没处理出数据看时钟有没有锁、复位有没有释放、FIFO 有没有溢出。ADC 没采到数据看采样时钟有没有、输入信号幅度对不对、SPI 配置有没有写进去。现象可能原因排查方法上位机无数据串口未打开或波特率错误检查设备管理器端口号确认波特率一致数据全为零ADC 未配置或时钟未锁用示波器测采样时钟读 ADC 配置寄存器数据乱码波特率不匹配或校验错误降低波特率测试检查 CRC 计算数据断续FIFO 溢出或缓冲区太小增大 FIFO 深度检查数据率是否超限FFT 结果异常窗函数未加或位宽溢出检查加窗逻辑核对 CIC 位宽计算6.2 时序与同步类问题时序问题往往表现为偶发错误很难复现也很难定位。我的经验是凡是偶发问题先怀疑跨时钟域和亚稳态。用异步 FIFO 隔离不同时钟域用两级触发器做同步这些是基本功不能省。还有一个常见问题是复位释放的时机。如果复位释放时时钟不稳定或者多个模块的复位释放不同步就会出现各种奇怪的现象。我的做法是用一个复位同步器把外部复位同步到各个时钟域确保每个模块在自己的时钟域里看到干净的复位信号。提示调试时序问题时在线逻辑分析仪是你的好朋友。把关键信号引出来设置好触发条件抓一次波形比看一百行代码都管用。6.3 算法与精度类问题算法类问题表现为结果不对但数据链路正常。比如 FFT 出来的峰值位置不对可能是点数配错了目标距离有偏差可能是采样率或者调频斜率算错了速度符号反了可能是 I Q 路接反了。距离计算这个我重点说一下。雷达距离等于光速乘以时间除以二时间等于频率差除以调频斜率。所以距离等于光速乘以频率差除以二倍调频斜率。这个公式里的每个参数都要准确光速是常数频率差从 FFT 峰值位置算调频斜率是发射信号的设计参数。任何一个参数错了距离就错了。我一般用一个已知距离的目标做标定反推系统参数这样最可靠。速度计算靠的是多普勒效应多个脉冲之间的相位差对应速度。这里要注意相位解模糊速度大了相位会超过 360 度需要解模糊算法。简单的做法是限制最大速度让相位差不超过 180 度这样就不会模糊。如果必须测高速目标就要用多脉冲解模糊或者变脉冲重复频率的方法。7. 实操心得与经验总结做这个项目最大的体会是雷达系统的调试是一个系统工程不能头痛医头脚痛医脚。我建议在项目初期就建立一套完整的验证方案每个模块都有独立的测试方法和测试数据这样集成的时候才能快速定位问题。另一个体会是参数的计算和记录非常重要。雷达系统里参数特别多采样率、调频斜率、脉冲宽度、脉冲重复频率、FFT 点数、抽取倍数这些参数之间有关联改一个可能影响一片。我习惯用一个配置文件或者一个表格把所有参数列出来改的时候统一改避免遗漏。还有一点仿真和实测的差距往往比想象的大。MATLAB 里跑得好好的算法搬到 FPGA 里可能因为定点化精度不够而性能下降。我的做法是先在 MATLAB 里做定点仿真确定每个环节需要的位宽然后再写 FPGA 代码这样能少走很多弯路。最后分享一个小技巧雷达数据里噪声是不可避免的但噪声也是有用的。通过分析噪声的统计特性可以判断系统工作是否正常。如果噪声的方差突然变大可能是增益配错了如果噪声的均值偏移了可能是直流偏置没去掉。养成看噪声的习惯能提前发现很多问题。这个项目后续还可以扩展的方向不少比如加动目标显示滤除杂波、加脉冲压缩提高距离分辨率、加恒虚警检测自动提取目标。每一个方向都可以单独作为一个子课题深入研究这也是雷达这个领域有意思的地方永远有新的东西可以学。