ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Qt5实现LFM雷达信号上位机:从波形生成到脉冲压缩

用Qt5实现LFM雷达信号上位机:从波形生成到脉冲压缩 简介基于QT5.9框架开发的雷达信号上位机程序面向雷达信号处理与测试工程师解决LFM等波形参数配置和信号下发问题。界面直观支持LFM、NLFM、正弦波、常规脉冲等多种信号可通过以太网实时传输并以INI文件保存、导入参数适合雷达系统调试、教学演示和二次开发。压缩包共95个文件其中包含17个cpp源码、16个h头文件、8个ui界面布局、6个ini参数配置以及png图标和编译好的exe可执行程序整体大小仅2.38MB目录结构清晰便于按模块查阅。已有2677人学习下载。资源提供了完整工程源码、界面文件、参数配置示例及程序图标等全套内容可直接编译运行也能帮助读者深入理解QT上位机与雷达信号仿真结合的设计思路并可作为模板快速扩展新波形。1. 用Qt5把LFM雷达信号搬进上位机从波形生成到界面显示雷达信号处理里LFM线性调频信号几乎是所有脉冲压缩雷达的标配。但很多雷达软件工程师会陷入一个分工陷阱算法在Matlab里验证得很漂亮到了Qt5上位机里只能在界面上画一条静态曲线参数一改就要重新编译回波延迟验证更是只能靠肉眼对比。另一个常见误区是把上位机只当成“显示器”信号生成、频谱分析、匹配滤波全由下位机或离线脚本做上位机只负责贴一下结果。实际上用Qt5做上位机时完全可以把LFM信号生成、FFT频谱、脉冲压缩和距离游标全部集成到一个Widgets界面里既适合作为雷达模拟器也适合作为硬件回波的调试工具。这篇博客面向写过Qt Widgets但没搭过完整信号链的人讲一条最常用的工程路径。2. LFM雷达信号建模先算清时宽、带宽和调频斜率2.1 LFM信号离散生成的数学基础与参数表LFM信号复基带表达为 s(t)exp(jπKt²)KB/T。K的正负决定扫频方向B和T共同构成时宽带宽积TB。距离分辨率是c/(2B)所以带宽越大脉压峰越窄。数字域生成的核心是按tn/fs累加相位即phase[n]πK*(n/fs)²。注意这里不需要用exp(j2π∫f dt)的方式去逼近直接用解析相位表达式更准确。表 2-1 信号建模参数表参数符号单位典型值约束采样率fsHz100e6fs≥2B零中频脉冲时宽Ts20e-6决定距离门长度带宽BHz10e6距离分辨率为15m调频斜率KHz/sB/T正/负对应上/下扫频采样点数N1fs*T补零到2的幂更有利伺服控制类上位机常把信号源参数写死但雷达上位机里我建议把fs、T、B、K全部做成界面输入框因为用户在联调时最常改的就是带宽和时宽。K如果直接由B/T算出界面里就只保留B、T、fs三个输入减少误操作。表里的fs最小值为2B是针对复基带。当你在界面中画实信号时fs要乘以2以上这在下文3.2节会体现。2.2 先用Python验证波形和频谱后续移植到Qt写Qt代码前先花五分钟用Python把信号参数验证一遍。下面这段代码生成一个LFM复信号并打印瞬时频率范围。import numpy as np def gen_lfm(fs, T, B): N int(fs * T) t np.arange(N) / fs K B / T phase np.pi * K * t * t return np.exp(1j * phase), t fs 100e6 T 20e-6 B 10e6 x, t gen_lfm(fs, T, B) inst_freq np.diff(np.unwrap(np.angle(x))) / (2*np.pi) * fs print(inst_freq[0], inst_freq[-1])这里用np.unwrap解卷绕再用np.diff计算相位变化的速率换算成瞬时频率。如果打印结果接近0和10e6说明生成正确如果起始频率不为0说明相位编写时漏了0.5因子。注意np.pi * K * t * t相当于2π * 0.5 * K * t²所以不再额外乘0.5。这段Python之后可以直接照着写成C界面验证时可以对照两边的波形是否一致。说到实际移植Qt端生成信号我一般用一个独立的函数输入fs、T、B输出QVector 实部和虚部。下面这段C代码结构与Python是一一对应的。void generateLfm(double fs, double T, double B, QVectordouble real, QVectordouble imag) { const int N static_castint(fs * T); double K B / T; real.resize(N); imag.resize(N); for (int i 0; i N; i) { double t i / fs; double phase M_PI * K * t * t; real[i] std::cos(phase); imag[i] std::sin(phase); } }这段代码里M_PI在MSVC下可能需要定义_USE_MATH_DEFINES。每次循环都调用cos/sin耗时主要在数学函数上N百万点量级时大约几十毫秒可以接受。更快的做法是用相位增量递推例如每次相位增加δπK(2n1)/fs²用一次cos和sin计算同时缓存向量表。但为了保证匹配滤波的参考信号与回波完全一致我仍然建议用直接公式避免递推误差。2.3 送进上位机前要确定的三个边界条件第一FFT点数。如果直接用NfsT当fsT不是2的幂时kissfft依然能算但耗时会增加。我习惯补零到最近的2的幂补零不会影响脉压主峰反而让频谱显示更平滑。第二数据类型。Qt上位机里数据多存为double的QVector如果改用float当T1ms、B100MHz时相位项达到约10^10量级float有效位数只有6-7位生成信号会有可观测的相位噪声。第三时宽带宽积TB。TB超过1000后一个脉冲的采样点达到百万以上无论是FFT还是QCustomPlot重绘都会明显卡顿。所以我在界面里会加一个点数提示当N超过500万时警告用户而不是像普通上位机那样无脑刷新。3. 搭建Qt5雷达上位机工程结构、绘图与FFT选型3.1 qt5上位机工程的最小工程文件Qt Widgets是雷达上位机的主流选择本段给出最小工程配置。使用qmake时pro文件里要加入图表和并发模块QT core gui widgets charts concurrent CONFIG c17 TARGET radar_signal_host SOURCES main.cpp mainwindow.cpp kiss_fft.cpp HEADERS mainwindow.h kiss_fft.hcharts如果只使用QCustomPlot可以不加但Qt Charts在滚动频谱和瀑布图上有官方支持后续扩展会省事。concurrent模块用于线程池后面4.2节会用到。kissfft编译时只需要把实现文件加入SOURCES不需要额外链接库。选择kissfft而不是fftw的原因fftw是GPL闭源上位机可能涉及许可证问题kissfft的许可证更宽松接口也简单。若用fftw注意它的plan创建不是线程安全的初始化时要加锁。3.2 用QCustomPlot绘制时域波形接口、参数和刷新QCustomPlot画LFM波形典型代码void MainWindow::plotTimeWave(const QVectordouble real, double fs) { const int N real.size(); QVectordouble x(N), y(N); for (int i 0; i N; i) { x[i] i / fs; y[i] real[i]; } ui-plotTime-graph(0)-setData(x, y, true); ui-plotTime-xAxis-setRange(x.first(), x.last()); ui-plotTime-yAxis-setRange(-1.2, 1.2); ui-plotTime-replot(QCustomPlot::rpQueuedReplot); }参数说明setData的第三参数为true表示x已经递增QCustomPlot会跳过排序检查。axis范围直接在每次刷新时重设省去坐标轴自适应计算的消耗。如果画复数信号graph只有y轴两条所以要么画实部要么用pow(实部²虚部², 0.5)先取包络再把包络作为另一条graph叠加。坐标范围±1.2是经验值因为正弦幅度为1再留出10%的余量。3.3 用kissfft计算LFM频谱配置分配和功率谱提取频谱显示不能每次都在槽函数里分配FFT配置应在构造函数初始化。下面是一组最小实现kiss_fft_cfg fftCfg nullptr; int fftSize 0; void MainWindow::initFft(int N) { if (fftSize ! N) { if (fftCfg) kiss_fft_free(fftCfg); fftCfg kiss_fft_alloc(N, 0, nullptr, nullptr); fftSize N; } } QVectordouble MainWindow::computePowerSpectrum(const QVectordouble signal) { const int N signal.size(); QVectorkiss_fft_cpx in(N), out(N); for (int i 0; i N; i) { in[i].r signal[i]; in[i].i 0.0; } kiss_fft(fftCfg, in.data(), out.data()); QVectordouble power(N / 2); for (int i 0; i N / 2; i) { double re out[i].r / N; double im out[i].i / N; power[i] re * re im * im; } return power; }kiss_fft_alloc的第二个参数0为正变换1为逆变换。归一化放在每个频率点而不是在FFT时统一缩放这样后续脉冲压缩的幅度只与信号幅度相关。注意如果信号长度N不是2的幂kissfft虽然正常计算但速度会明显下降。频谱图在QCustomPlot中通常画为填充图我用QCPGraph::setChannelFillGraph把一个graph作为底视觉上比默认连线更舒服。关于FFT库选型可参考下表对比点kissfftfftw许可证BSD类似GPL逆变换通过is_inverse1额外plan任意点数支持支持最新版维护长期稳定频繁更新3.4 用QTimer控制界面刷新节奏仿真模式下上位机不需要处理真实回波时我会用一个定时器触发整帧刷新。这样做的好处是波形观察稳定也方便录屏。refreshTimer new QTimer(this); refreshTimer-setInterval(50); refreshTimer-setTimerType(Qt::PreciseTimer); connect(refreshTimer, QTimer::timeout, this, MainWindow::onRefreshTick); refreshTimer-start();setInterval(50)就是20FPS。若你想实时性更强可以改8ms配合Qt::PreciseTimer但CPU占用会显著上升。实际雷达显示目标变化很快50ms足够人眼观察。刷新函数里只生成新数据、替换graph曲线和replot不要在这里做文件IO或睡眠。如果有多个视图我在onRefreshTick里调用updateAllPlots集中管理。4. 上位机实时采集链路从串口、UDP到多线程刷新的常见坑4.1 收雷达数据的接口选择串口和UDP的取舍雷达传感器的数据出口可能是串口、网口也可能是内部采集卡。上位机开发里最常见的还是串口和UDP。串口速率一般不超过几MBps需要处理粘包UDP速率高但可能丢包和乱序。下面的表列出三种方式的选择建议数据来源典型速率数据格式Qt类需要关注的坑串口 1 MBps帧头数据QSerialPort粘包、分包UDP网口10-100 MBps包序号IQ数据QUdpSocket丢包、乱序raw文件回放任意裸数据QFile一次读入过大我在开发初期总是先用文件回放因为可以离线复现问题。文件回放时注意Qt5里中文路径用QStringLiteral显示路径用QFileInfo规范化后再打开能避免地址乱码。切换到串口和UDP后保留文件回放作为联动调试模式三者在界面上用一个下拉框切换。4.2 用QtConcurrent把FFT放到后台线程如果FFT在界面线程执行数据点一多就会卡。常见做法是用QtConcurrent::run把计算任务放到全局线程池。下面是一个安全的做法void MainWindow::processData(const QVectordouble raw) { QVectordouble snapshot raw; QtConcurrent::run([this, snapshot]() { QVectordouble spectrum computePowerSpectrum(snapshot); emit spectrumComputed(spectrum); }); }注意捕获列表按值拷贝了snapshot这保证了lambda运行时数据不被主线程覆盖。emit spectrumComputed是跨线程信号Qt会自动使用QueuedConnection在主线程槽函数里执行。如果你希望任务可取消需要把QFuture保存为成员变量并在析构里调用cancel。使用QtConcurrent的一个坑是不要在线程里直接调用ui相关方法哪怕只是修改一个QString也会因为线程亲和性问题产生不可预测崩溃。必须通过信号/槽回到主线程。4.3 界面槽函数里的重绘优化当频谱和波形数据到达主线程后槽函数里的绘图操作也要优化。QCustomPlot的setData默认会检查x数据是否递增用第三个参数跳过void MainWindow::onSpectrumComputed(const QVectordouble spectrum) { QVectordouble freq(spectrum.size()); double df fs / (spectrum.size() * 2); for (int i 0; i spectrum.size(); i) freq[i] i * df; ui-plotSpectrum-graph(0)-setData(freq, spectrum, true); ui-plotSpectrum-replot(QCustomPlot::rpQueuedReplot); }这里df是频率分辨率fs/N。注意我传入的频率点数量是spectrum.size()而spectrum是N/2所以fs/(2*N)这个换算要仔细。如果坐标轴范围不变最好在初始化时分别设好range刷新时只修改数据不重新setRange能省下两次坐标轴重算。4.4 qt5无法拖拽文件的一个替代方案写上位机时很多用户想直接拖拽raw文件进入窗口但经常发现qt5无法拖拽文件。原因是QWidget默认关闭了拖拽接受。正确的最小实现是重写两个事件并在构造函数开启setAcceptDrops(true)void MainWindow::dragEnterEvent(QDragEnterEvent* e) { if (e-mimeData()-hasUrls()) e-acceptProposedAction(); } void MainWindow::dropEvent(QDropEvent* e) { QString path e-mimeData()-urls().first().toLocalFile(); QFileInfo info(path); if (info.canonicalFilePath().isEmpty()) { return; } loadRawFile(info.canonicalFilePath()); e-acceptProposedAction(); }代码中QFileInfo和canonicalFilePath是为了处理中文路径问题。Qt5里QFile::exists对部分中文路径会返回false先用canonicalFilePath可以拿到规范化绝对路径如果为空就说明文件不存在直接返回。如果你想同时支持多个文件就把urls()遍历一遍但是第一次实现阶段推荐只读第一个避免误操作。4.5 多线程关闭与线程池排空主窗口析构时后台线程池里可能还有任务正在运行。直接销毁MainWindow会导致lambda中访问已释放对象。我的处理方式是MainWindow::~MainWindow() { if (fftCfg) { kiss_fft_free(fftCfg); fftCfg nullptr; } QThreadPool::globalInstance()-waitForDone(); }调用waitForDone会阻塞到线程池所有任务结束。如果FFT的数据量很大用户可能觉得界面卡顿所以更好的方案是在closeEvent里弹出一个模态等待框后台跑完再真正关闭。但为了简洁多数场景下waitForDone足够。注意kiss_fft_free只能在主线程释放因为cfg分配在主线程如果没有初始化为nullptr也需要判空。5. 脉冲压缩算法在Qt中的落地与验证5.1 匹配滤波的核心参考信号共轭与FFT循环卷积LFM雷达信号上位机的核心功能往往是把回波和参考信号做脉压得到目标的延时峰。频域实现是R(k)乘以H(k)的共轭再逆FFT。由于FFT天然做循环卷积我一般在生成长度N的参考信号时把回波信号也补零到相同长度保证延迟小于序列长度时峰不出现在错误位置。这里要注意参考信号应当使用复基带LFM且与发射信号保持一致的K、B、T。5.2 在Qt中实现频域脉冲压缩的完整代码void MainWindow::pulseCompress(const QVectordouble rx, const QVectordouble ref, QVectordouble result) { const int N rx.size(); kiss_fft_cfg fwd kiss_fft_alloc(N, 0, nullptr, nullptr); kiss_fft_cfg inv kiss_fft_alloc(N, 1, nullptr, nullptr); QVectorkiss_fft_cpx R(N), H(N), Y(N), out(N); for (int i 0; i N; i) { R[i].r rx[i]; R[i].i 0; H[i].r ref[i]; H[i].i 0; } kiss_fft(fwd, R.data(), R.data()); kiss_fft(fwd, H.data(), H.data()); for (int i 0; i N; i) { double r R[i].r, imR R[i].i; double h H[i].r, imH H[i].i; Y[i].r r * h imR * imH; Y[i].i imR * h - r * imH; } kiss_fft(inv, Y.data(), out.data()); result.resize(N); for (int i 0; i N; i) { result[i] std::hypot(out[i].r, out[i].i) / N; } kiss_fft_free(fwd); kiss_fft_free(inv); }这里的复数乘法是R * conj(H)共轭体现在虚部变号。如果你的参考信号是实数序列也要先转换成kiss_fft_cpx再FFT不能直接把double数组传进去。逆变换输出out是复数但脉压结果在时域是复数还是实数对于实信号得到的脉压结果理论上实部是卷积结果虚部约等于0但直接用hypot取模可以避免相位问题。除以N是因为kissfft的逆变换没有归一化不除会得到一个放大了N倍的幅度。5.3 验证脉冲压缩正确性的三个检查点第一个检查点用不匹配的参考信号对回波做压缩结果不应出现尖峰。第二个检查点构造一个delay点延迟的回波压缩结果应当在delay附近形成主峰。如果峰值出现在0点说明循环卷积时回波的补零没有做对。第三个检查点旁瓣电平应在-13dB左右。若旁瓣过高大概率是LFM参考信号的幅度不是恒定1或者相位表达式里漏掉了π因子。下面这个自检函数可以直接加到程序启动逻辑里bool MainWindow::selfTestPulseCompress() { QVectordouble ref, real, imag, result; generateLfm(fs, T, B, real, imag); int delay 256; QVectordouble rx(real.size(), 0.0); for (int i delay; i real.size(); i) rx[i] real[i - delay]; pulseCompress(rx, real, result); int peak 0; for (int i 1; i result.size(); i) if (result[i] result[peak]) peak i; return std::abs(peak - delay) 1; }自检的合理容差是±1个采样点。如果偏差超过1说明参考信号与回波的起始频率不一致或者是FFT长度设置有问题。自检通过后弹出状态信息“脉冲压缩自检通过”这样每次启动时都能确认算法链没有回归。5.4 用游标读取脉压峰对应的距离最后把脉压结果画到界面后我在xAxis上增加一个无限游标用于拖拽后读取距离。游标的处理比较简单在mouseMove事件里取得像素对应的x值再换算成距离R x * c / 2。由于脉压结果的时间轴通常很长我会先把x轴范围限定到主峰附近的窗口也就是按下鼠标时缩放到峰值的±50点。这样操作员能直接看到距离数字而不是自己数刻度。本文还有配套的精品资源点击获取
返回列表