摘要:本文针对"惯组与雷达同轴固连、双动态跑车追踪"的特殊场景,深度剖析了从绝对经纬度到平台相对角度的坐标转换体系。文章涵盖核心数学推导、工业级 C++/Qt 代码实现、以及极简桌面端 UI 架构设计,为相控阵雷达或电扫天线的 FPGA 底层引导提供了一套完整闭环的解决方案。
第一部分:坐标转换体系的深度解析
在跑车动态追踪场景中,追踪车(惯组+雷达)与目标车均处于高速运动状态。由于地球并非完美球体,且车辆自身存在姿态变化,绝对经纬度(LLA)不能直接进行加减运算。
我们需要通过两次"降维打击",把复杂的地球空间,降维到车辆前方的局部平面:
- LLA →→ ECEF(地心直角系) :
将基于 WGS-84 椭球模型的经纬高,转换为以地心为原点、以米为单位的绝对三维直角坐标。这是空间平移和矢量计算的基础。 - ECEF →→ ENU(东北天坐标系) :
以追踪车所在位置为切点,利用旋转矩阵,将地心矢量拉回到局部水平面(东、北、天)。这消除了地球曲率的影响。 - ENU →→ Body(平台/载体系) :
由于雷达与惯组刚性固连,直接将惯组的欧拉角姿态构建为旋转矩阵的转置(逆向算回),把东北天矢量乘进去,彻底映射到车头正前方(X)、车体右侧(Y)、车顶上方(Z)的平台坐标系。 - 引导解算 :
在平台坐标系下,利用 arctan2(Y,X)arctan2(Y,X) 和 arctan2(Z,X2+Y2)arctan2(Z,X2+Y2) 算出相对方位角和俯仰角,打包发送给 FPGA。
第二部分:工业级核心算法代码 (C++/Qt)
以下是完整的系统库头文件 RadarGuideSystem.h,包含坐标转换引擎、解算引擎及二进制协议打包。
cpp
scss
#ifndef RADARGUIDESYSTEM_H
#define RADARGUIDESYSTEM_H
#include <QVector3D>
#include <QMatrix4x4>
#include <QByteArray>
#include <QtMath>
// ==========================================
// 1. 基础数据结构
// ==========================================
struct GeoPos {
double lon; // 经度 (度)
double lat; // 纬度 (度)
double alt; // 高度 (米)
};
struct Attitude {
double yaw; // 航向角 (度)
double pitch; // 俯仰角 (度)
double roll; // 横滚角 (度)
};
struct GuideCommand {
double azCmd; // 平台方位引导角 (度)
double elCmd; // 平台俯仰引导角 (度)
double distance; // 距离 (米)
bool valid;
};
// ==========================================
// 2. 核心数学引擎 (坐标系降维)
// ==========================================
class MathEngine {
public:
static constexpr double WGS84_A = 6378137.0;
static constexpr double WGS84_E2 = 0.00669437999014;
static constexpr double WGS84_B = 6356752.314245;
// LLA -> ECEF
static QVector3D llaToEcef(const GeoPos& pos) {
double lonRad = qDegreesToRadians(pos.lon);
double latRad = qDegreesToRadians(pos.lat);
double sinLat = sin(latRad), cosLat = cos(latRad);
double sinLon = sin(lonRad), cosLon = cos(lonRad);
double N = WGS84_A / sqrt(1.0 - WGS84_E2 * sinLat * sinLat);
return QVector3D(
(N + pos.alt) * cosLat * cosLon,
(N + pos.alt) * cosLat * sinLon,
(N * (1.0 - WGS84_E2) + pos.alt) * sinLat
);
}
// ECEF -> LLA (Bowring算法,防极区死锁)
static GeoPos ecefToLla(const QVector3D& ecef) {
double x = ecef.x(), y = ecef.y(), z = ecef.z();
double lonRad = atan2(y, x);
double p = sqrt(x * x + y * y);
if (p < 1e-6) {
double latRad = (z > 0) ? (M_PI / 2.0) : (-M_PI / 2.0);
return { qRadiansToDegrees(lonRad), qRadiansToDegrees(latRad), fabs(z) - WGS84_B };
}
double theta = atan2(z * WGS84_A, p * WGS84_B);
double sinTheta = sin(theta), cosTheta = cos(theta);
double ep2 = (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
double latRad = atan2(z + ep2 * WGS84_B * sinTheta * sinTheta * sinTheta,
p - WGS84_E2 * WGS84_A * cosTheta * cosTheta * cosTheta);
for (int i = 0; i < 2; ++i) {
double sinLat = sin(latRad);
double N = WGS84_A / sqrt(1.0 - WGS84_E2 * sinLat * sinLat);
double alt = p / cos(latRad) - N;
latRad = atan2(z, p * (1.0 - WGS84_E2 * N / (N + alt)));
}
double sinLat = sin(latRad);
double N = WGS84_A / sqrt(1.0 - WGS84_E2 * sinLat * sinLat);
double alt = p / cos(latRad) - N;
return { qRadiansToDegrees(lonRad), qRadiansToDegrees(latRad), alt };
}
// 获取 ECEF 到 ENU 的旋转矩阵
static QMatrix4x4 getEcefToEnuMatrix(const GeoPos& refPos) {
double lon = qDegreesToRadians(refPos.lon);
double lat = qDegreesToRadians(refPos.lat);
double sinLon = sin(lon), cosLon = cos(lon);
double sinLat = sin(lat), cosLat = cos(lat);
QMatrix4x4 mat;
mat(0,0) = -sinLon; mat(0,1) = cosLon; mat(0,2) = 0;
mat(1,0) = -sinLat * cosLon; mat(1,1) = -sinLat * sinLon; mat(1,2) = cosLat;
mat(2,0) = cosLat * cosLon; mat(2,1) = cosLat * sinLon; mat(2,2) = sinLat;
return mat;
}
// LLA -> ENU (相对偏差)
static QVector3D llaToEnu(const GeoPos& target, const GeoPos& ref) {
QVector3D dEcef = llaToEcef(target) - llaToEcef(ref);
return getEcefToEnuMatrix(ref) * dEcef;
}
// ENU -> LLA (绝对坐标)
static GeoPos enuToLla(const QVector3D& enu, const GeoPos& ref) {
QVector3D dEcef = getEcefToEnuMatrix(ref).transposed() * enu;
return ecefToLla(llaToEcef(ref) + dEcef);
}
// 欧拉角 -> 旋转矩阵 (Cbn)
static QMatrix4x4 eulerToMatrix(const Attitude& att) {
double y = qDegreesToRadians(att.yaw);
double p = qDegreesToRadians(att.pitch);
double r = qDegreesToRadians(att.roll);
double cy = cos(y), sy = sin(y);
double cp = cos(p), sp = sin(p);
double cr = cos(r), sr = sin(r);
QMatrix4x4 mat;
mat(0,0) = cy*cp; mat(0,1) = sy*cp; mat(0,2) = -sp;
mat(1,0) = cy*sp*sr - sy*cr; mat(1,1) = sy*sp*sr + cy*cr; mat(1,2) = cp*sr;
mat(2,0) = cy*sp*cr + sy*sr; mat(2,1) = sy*sp*cr - cy*sr; mat(2,2) = cp*cr;
return mat;
}
};
// ==========================================
// 3. 雷达引导引擎
// ==========================================
class GuideEngine {
public:
static GuideCommand compute(
const GeoPos& imuPos, const Attitude& imuAtt,
const GeoPos& targetPos,
const QVector3D& leverArm_imu_to_radar,
const QMatrix4x4& installErrorMat)
{
GuideCommand cmd = {0, 0, 0, false};
// 1. 姿态矩阵
QMatrix4x4 Cbn = MathEngine::eulerToMatrix(imuAtt);
// 2. 计算雷达天线的绝对位置 (补偿杆臂)
QVector3D leverEnu = Cbn.transposed() * leverArm_imu_to_radar;
GeoPos radarPos = MathEngine::enuToLla(leverEnu, imuPos);
// 3. 计算绝对视线矢量 (ENU)
QVector3D losEnu = MathEngine::llaToEnu(targetPos, radarPos);
cmd.distance = losEnu.length();
if (cmd.distance < 10.0) return cmd;
// 4. 转到载体系 (车头坐标系)
QVector3D losBody = Cbn * losEnu;
// 5. 扣除雷达安装误差
QVector3D losRadar = installErrorMat * losBody;
// 6. 解算方位/俯仰
double azRad = atan2(losRadar.y(), losRadar.x());
double elRad = atan2(losRadar.z(), sqrt(losRadar.x()*losRadar.x() + losRadar.y()*losRadar.y()));
cmd.azCmd = qRadiansToDegrees(azRad);
cmd.elCmd = qRadiansToDegrees(elRad);
cmd.valid = true;
return cmd;
}
};
// ==========================================
// 4. 协议打包引擎 (发 FPGA)
// ==========================================
class ProtocolPackager {
public:
static QByteArray packForFPGA(const GuideCommand& cmd) {
QByteArray payload(12, 0);
quint8* p = reinterpret_cast<quint8*>(payload.data());
p[0] = 0xAA; p[1] = 0x55;
qint16 azInt = static_cast<qint16>(cmd.azCmd * 100.0);
qint16 elInt = static_cast<qint16>(cmd.elCmd * 100.0);
// 大端模式写入
p[2] = (azInt >> 8) & 0xFF; p[3] = azInt & 0xFF;
p[4] = (elInt >> 8) & 0xFF; p[5] = elInt & 0xFF;
quint32 distInt = static_cast<quint32>(cmd.distance * 10.0);
p[6] = (distInt >> 24) & 0xFF; p[7] = (distInt >> 16) & 0xFF;
p[8] = (distInt >> 8) & 0xFF; p[9] = distInt & 0xFF;
p[10] = cmd.valid ? 0x01 : 0x00;
quint8 crc = 0;
for(int i = 0; i < 11; ++i) crc ^= p[i];
p[11] = crc;
return payload;
}
};
#endif // RADARGUIDESYSTEM_H
第三部分:桌面端 UI 架构设计
为了符合军工/工业测试软件的严谨风格,界面摒弃了花哨的卡片式排版。采用 "上-中-下"三段式逻辑流:上(上帝视角双源状态) →→ 中(解算聚焦) →→ 下(硬件控制与下发)。
text
ini
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ ┌─ 一、 空间实体状态 (Entities) ──────────────────────────────────────────────────────────┐ │
│ │ ┌─ 1. 惯组 (雷达平台) ──────────────┐ ┌─ 2. 目标 (动态实体) ──────────────┐ │ │
│ │ │ 经度: [ 104.060000 ] E │ │ 经度: [ 104.075000 ] E │ │ │
│ │ │ 纬度: [ 30.670000 ] N │ │ 纬度: [ 30.685000 ] N │ │ │
│ │ │ 高度: [ 500.00 ] m │ │ 高度: [ 550.00 ] m │ │ │
│ │ │ 航向: [ 45.00 ] ° │ │ 运动模型: [ 匀速直线 ▼ ] │ │ │
│ │ │ 俯仰: [ 2.00 ] ° │ │ 注入延迟: [ 50 ] ms │ │ │
│ │ │ 横滚: [ -1.00 ] ° │ │ │ │ │
│ │ │ 杆臂: X[1.5] Y[0.0] Z[1.2] m │ │ │ │ │
│ │ └───────────────────────────────────┘ └───────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
│ ┌─ 二、 引导解算指令 (Guidance Result) ──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 【 平台指令 (Body Frame) 】 │ │
│ │ 方位指令 (Az): [ + 12.50° ] 目标距离: [ 2450.5 m ] │ │
│ │ 俯仰指令 (El): [ + 0.80° ] 绝对方位: [ 45.2° ] │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
│ ┌─ 三、 执行流控制与下发 (Execution Flow) ───────────────────────────────────────────────┐ │
│ │ 串口端口: [ COM3 ▼ ] 波特率: [ 921600 ▼ ] [ 打开串口 ] [ 暂停发送 ] │ │
│ │ TX 原始报文: [ AA 55 04 E2 00 50 00 00 5F B3 01 7C ] │ │
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ 系统日志 (System Log) │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ [12:00:01.123] [SYS ] 平台与目标数据链路建立完成,进入 100Hz 实时解算。 │ │
│ │ [12:00:01.200] [CALC] 目标机动,杆臂动态补偿生效,方位角解算更新。 │ │
│ │ [12:00:01.500] [TX ] 成功发送指令至 FPGA: AA 55 04 E2 ... │ │
│ └─────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
Qt 布局落地建议
- 顶部区域 :使用一个
QGroupBox(标题为"空间实体状态"),内部放一个QHBoxLayout。左边是"惯组状态"的QGroupBox,右边是"目标状态"的QGroupBox。 - 中间区域 :由于是核心数据,方位和俯仰指令的
QLineEdit需设置只读,且通过setStyleSheet将背景调为浅绿(#E8F5E9),字体加粗、字号放大,确保操作员一眼看清。 - 底部区域 :串口配置用
QComboBox,TX 报文用一个宽大的QLineEdit显示。 - 最底部日志 :使用
QPlainTextEdit,并设定其高度随窗口自适应变化。利用QTextCharFormat对日志的前缀(SYS, CALC, TX)设置不同颜色,方便排查问题。