跳转至

10 STM32H743 移植指南(AHRS-Board 工程)

参考工程:F4 原版 PSINS_STM32F4xx_Keil4_V2.1(wiki 内路径 docs/惯性导航/assets/PSINS_STM32F4xx_Keil4_V2.1/,请本地浏览;本文引用的具体源码文件链接可直接点开); H743 目标工程已存在本地:D:\01_Job\Project\AHRS-Board\software\STM32H743_AHRS-Board\(CubeMX 框架 + HAL 驱动 + 19 维自写 ESKF)。 移植目标:复用 F4 工程里验证过的 PSINS C++ 算法层(CMahony、CSINSGNSS、CKFApp 19 维 ESKF),取代 H743 现有的 C 版 15 维 ESKF,并把 F4 的软件 I2C/MS5611、MPU9250 驱动、UBX GPS 解析、DMA 串口输出、PCCMD PC 指令适配到 H743 硬件。


一、工程差异速览

维度 F407 原版(STM32F407VGT6, 168 MHz) H743 AHRS-Board(STM32H743VIH6, 480 MHz)
驱动框架 标准外设库(StdPeriph) v1.0.1 HAL + CubeMX(代码自动生成)
浮点单元 Cortex-M4 FPU(单精度) Cortex-M7 FPU + DP(单/双精度) + I/D Cache
存储架构 192 KB SRAM / 1 MB Flash,统一地址 DTCM / ITCM / AXI SRAM / SRD1~4 分域,需 MPU 配置
时钟源 HSE 8 MHz → PLL → 168 MHz (AHB) → 84 (APB1) / 42 (APB2) HSE → PLL1 → 480 MHz (sys) → 240 (AXI) → 120 (APB3) / 240 (HPB2/4) 视 CubeMX 配置
SPI 总线 SPI1(MPU9250)软件 CS SPI1 ICM42688 + SPI4 BMI088 + SPI6 IIM42652/RM3100(硬件 CS)
I2C 总线 软件 I2C(GPIO 模拟)读 MS5611 I2C1 IST8310 + I2C2 BMP581 + I2C3 MS5611(硬件 I2C)
UART/GPS USART2 + UBX-NAV-PVT 解析 USART3 + UM982(北天星,NMEA/UM982 私有协议)
输出串口 USART1 DMA2 Stream7 USART1(CubeMX 配置)→ 可选 DMA1 Stream
采样定时器 TIM2 100 Hz IMU / TIM3 50 Hz BARO CubeMX 配置 TIMx(常用 TIM6 1 kHz 时基 + 分频)
算法层 C++:CMahony / CSINSGNSS / CKFApp,19 维 ESKF C 实现:ins_mahony_ahrs.c(Mahony 简化版)+ ins_eskf_15d.c(15 维,无杆臂/dt)
传感器类型 MPU9250(9DoF)+ MS5611 + UBLOX ICM42688 + BMI088 + IIM42652(IMU 3 选 2 热备)+ IST8310 + RM3100(磁力 2 选)+ BMP581 + MS5611(气压)+ UM982(GNSS 双天线 RTK)
坐标系约定 ENU,pos=[lat,lon,h], vn=[vE,vN,vU](见 00_总览 §7 NED,world≡NED,body→NED(见 ins_eskf_15d.h#L7-L8

坐标系不兼容是移植最核心的差异。若直接复用 PSINS 的 C++ 类,要么在接口层做 ENU↔NED 变换,要么统一改到同一坐标系(建议以项目约束为准:本项目强约束 NED,因此复用算法层时必须做 NED→ENU 喂入算法 → 结果 ENU→NED 输出,或修改 PSINS 内部 earth/Cnb 约定为 NED)。


二、移植策略与分层边界

2.1 保留层(H7 现有工程不动)

  • CubeMX 生成的初始化代码(SystemClock_Config, MX_GPIO_Init, MX_SPIx_Init, MX_I2Cx_Init, MX_USARTx_Init, MX_TIMx_Init, MX_DMA_Init);
  • 板级总线适配:h743_hal_bus.c/.hboard_h743vih6_sensors.c/.h
  • 传感器驱动层(ICM42688P / BMI088 / BMP581 / IST8310 / RM3100 / UM982)。

这些直接使用 H7 HAL 编写,与 StdPeriph 完全不同,不值得把 F4 的软件 SPI/I2C 移植过来,H7 硬件外设更强大。

2.2 可直接复用的算法层(F4 → H7)

以源码方式加入 H7 工程的 Include Path & Source Group:

Psinscore/            # 原样拷贝
├── PSINS.h           # 定义 CVect3/CQuat/CMat3/CKalman/CSINS/CSINSGNSS...
├── PSINS.cpp         # ~3500 行,所有 C++ 算法实现
└── KFApp.h / KFApp.cpp   # CKFApp(CKFApp 重写,改 P/Q/R 适配 MEMS 等级)

工程设置: - Keil:新建 Group "PSINS Core",包含 PSINS.cppKFApp.cpp,编译器选 Arm Compiler 6,语言 C++14; - CubeIDE:同样在 Makefile 或 .cproject 中加入源文件,C++ 编译选项 -std=c++17 -fno-exceptions -fno-rtti; - Include Path 增加 Psinscore/ 目录; - C 宏定义增加 PSINS_AHRS_MEMSPSINS.h#L26),PSINS_MATRIX_MAX_DIM=19; - 强烈推荐在 H7 上也用 -O2;PSINS 有大量 CMat3 拷贝/相乘,H7 M7 Cache 开后 O2 比 -O0 快 8~12 倍。

2.3 重写 / 新写的 Glue 层

Core/Src/psins_port.cpp + Core/Inc/psins_port.h      // C 接口封装 C++ 对象(extern "C")
Core/Src/psins_adapter.cpp                           // 采样结构体 ↔ CVect3/CSINS 参数
Core/Src/psins_uart.cpp                              // Out_Frame 兼容版打包 + DMA 发送

三、算法层复用步骤

3.1 坐标变换(NED ↔ ENU,移植关键)

项目 H7 使用 NED,而 PSINS 默认 ENU。在每个融合周期内:

NED → ENU(喂给 PSINS):

qnb_enu = T_enu_ned ⊗ qnb_ned      (若保留 H7 原有初值)
vn_enu  = [vN, vE, -vD]            即 [vE_enu, vN_enu, vU_enu] = [vN_ned, vE_ned, -vD_ned]
pos_enu = [lat, lon, h]            与 NED 相同(LLH 不随导航系轴方向变)
wm_enu  = wm                       角速度在 body 系,不变
vm_enu  = vm                       比力在 body 系,不变
mag_enu = [mx, my, mz]             磁力 body 系不变

ENU→NED 变换矩阵 \(T\)(3×3):

\[ T_{ned}^{enu} = \begin{bmatrix}0&1&0\\1&0&0\\0&0&-1\end{bmatrix}, \qquad R^{ned}_b = T R^{enu}_b T^T \]

实践上更轻量的做法是:不要每次积分完转换四元数,而是在初始化时把 NED 参考值(重力方向、初值 pos/vn)变成 ENU,让 PSINS 全程在 ENU 下运行,只在对外输出接收 GNSS 量测时做轴交换:

// PSINS 内部 ENU → 对外输出 NED
void psins_get_attitude_ned(float roll, float pitch, float *yaw_ned) {
    double r, p, y_e;  CSINS::q2att(qnb, r, p, y_e);
    *yaw_ned = (float)y_e;   // yaw 数值相同但定义变化,见下面说明
    // 推荐:直接在输出前用 y 轴定义修正
    // *yaw_ned = - (float)y_e + M_PI_2;   // NED ψ 定义:北顺时针;ENU ψ 定义:东逆时针
}

yaw 的 ENU↔NED 修正最易出错: - ENU下 q2att.qnbPSINS.cpp#L907)yaw 按 atan2(-2(q0q2 - q1q3), ...) 输出; - NED下 ins_eskf_15d_get_euler yaw 按不同 DCM 行的 ZYX 反变换; - 工程上建议:用同一组合成真值(给定 body→NED 四元数 → 生成传感器数据 → 喂给 ENU 算法 → 对比输出)验证修正公式,而不要硬记符号。

3.2 采样数据桥接

H7 采样结构体为 ins_imu_sample_taccel_mps2/gyro_rads float ×3,μs 时间戳),PSINS 接口是 CVect3 wm [rad 角增量]CVect3 vm [m/s 速度增量]

// psins_port.cpp
extern "C" void psins_feed_imu(const ins_imu_sample_t *imu, float dt)
{
    static CVect3 eb_fixed{-4.0*DPS, 1.3*DPS, 0.0};   // 沿用 F4 的经验零偏
    CVect3 wm{imu->gyro_rads.x * dt - eb_fixed.i * dt,
              imu->gyro_rads.y * dt - eb_fixed.j * dt,
              imu->gyro_rads.z * dt - eb_fixed.k * dt};
    CVect3 vm{imu->accel_mps2.x * dt,
              imu->accel_mps2.y * dt,
              imu->accel_mps2.z * dt};
    // 注意 NED/ENU 修正:PSINS ENU,加速度 f=-gU;H7 NED f=+gD
    vm.k = -vm.k;   // 把 body-z 的 NED 符号翻转到 ENU,按安装矩阵调整
    s_mahony.Update(wm, vm, dt);
    s_kf.Update(&wm, &vm, 1, dt, 5);
}

具体符号取决于传感器 body 轴方向。务必用 board_h743vih6_sensors.csensors_install_rot(若存在)乘完后再判断。

3.3 CSINSGNSS 初始化

H7 现在的 eskf15_init 只初始化本地 NED 结构体。替换为 CSINSGNSS 初始化时:

// psins_port.cpp
static CKFApp *s_kf = nullptr;
static CMahony *s_mahony = nullptr;

extern "C" void psins_init(double lat0_deg, double lon0_deg, double h0_m)
{
    static uint8_t buf[sizeof(CKFApp) + sizeof(CMahony)] __attribute__((aligned(8)));
    s_kf = new (buf) CKFApp(0.01);               // placement new,避免 heap
    s_mahony = new (buf + sizeof(CKFApp)) CMahony(10.0);

    CVect3 pos0 = LLH(lat0_deg, lon0_deg, h0_m);  // 注意内部转 rad/lat 前 lon
    CSINS sins0(qI /*初始姿态,之后由GPS重对齐*/, O31, pos0);
    s_kf->Init(sins0, /*grade=*/1);              // grade 1 = MEMS grade 默认 P/Q
}

关键:禁止 new/malloc 从堆分配——H7 CubeMX 启动堆默认很小,且 PSINS 对象 ≈ 21~25 KB。使用 placement new 在静态 buf 上构造,地址需 8 字节对齐(CVect3/CMat3 含 double)。F4 工程中 KFApp 对象也在栈上,H7 的 DTCM 放 buf 更好,同时避免在中断/线程上下文切换中被改。

3.4 GNSS 量测注入

H7 的 UM982 输出 [lat, lon, h] 与 [vel_n_mps, vel_e_mps, vel_d_mps](NED),映射到 PSINS ENU:

extern "C" void psins_feed_gnss(const ins_gnss_sample_t *g)
{
    if(g->fix < 3 || g->sats < 6) return;
    CVect3 gpos_enu{g->latitude_deg*DEG, g->longitude_deg*DEG, g->altitude_m};   // lat, lon, h
    CVect3 gvn_enu {g->vel_e_mps,                 // ENU.vE = NED.vE
                    g->vel_n_mps,                 // ENU.vN = NED.vN
                   -g->vel_d_mps};                // ENU.vU = -NED.vD

    // 延迟补偿(H7 要用 GNSS 时戳与当前 μs 差值)
    s_kf->posGNSSdelay = s_kf->vnGNSSdelay = -(float)(g->delay_sec);
    s_kf->SetMeasGNSS(gpos_enu, gvn_enu);
}

动态模式下 yaw 初始化逻辑沿用 F4 的 tracking-angle:

if(!yaw_inited) {
    float vn = sqrtf(g->vel_n_mps*g->vel_n_mps + g->vel_e_mps*g->vel_e_mps);
    if(vn > 3.0f) {
        double yaw_enu = atan2(-g->vel_e_mps, g->vel_n_mps);   // 与 F4 atan2(-gvn.i, gvn.j) 等价
        CSINS sins0(a2qua(CVect3(0,0,yaw_enu)), gvn_enu, gpos_enu);
        s_kf->Init(sins0, 1);
        yaw_inited = 1;
    }
}

四、驱动层复用/适配清单

4.1 MS5611 驱动

F4 原版 ms5611.c H7 现有 te_ms5611.c + 硬件 I2C3
软件 I2C 比特级模拟:IIC_Start/Stop/Ack/ReadByte HAL_I2C_Mem_Read/Writ(硬件 I2C)
PROM 0xA0~0xAE 8 字校准系数 同样寄存器地址 0xA0~0xAE
转换命令 0x48 (D1 OSR=4096) / 0x58 (D2 OSR=4096) 相同命令
TIM3 50 Hz 中断驱动 CONV→ADC 读 可复用 H7 50 Hz TIM 通道

适配结论:不用移植软件 I2C,直接用 H7 te_ms5611.c 现成 HAL 版本即可;若其结果与 F4 对不上,可直接把 F4 的温度补偿公式(二阶修正:T2 = dT>2^31? x:0 等)拷贝覆盖 te_ms5611_convert

4.2 MPU9250 → ICM42688/BMI088 寄存器级差异

F4 的 MPU9250 驱动在 mpu9250.c 中:SPI 9 字节连续读加速度/温度/陀螺,AK8963 磁力挂在 I2C Aux。

H7 不使用 MPU9250,无移植需求,只需把 F4 主循环的尺度变换/零偏扣除部分(wm = Gyro/65.5f * DPS - eb)以 H7 驱动返回的 accel_mps2 / gyro_rads 替代——H7 驱动已经做了 scaling,直接使用即可。

4.3 GPS 解析

F4 的 GPS_PVT_Decode usart.c#L7-L52 是 UBX-NAV-PVT,H7 unicore_um982.c 是北天星协议。二者不同:

  • F4 UBX-PVT offset:24=lon, 28=lat, 42=hgt, 58=vN, 62=vE, 66=vD;
  • H7 UM982:查看 unicore_um982.c 具体解析——若有 BESTPOS/BESTVEL 输出,直接取 lat/lon/h 和 NED 速度;
  • 若保留 UM982 的 GPS_Delay(PPS → PVT 时间滞后),需要在 H7 EXTI(PPS 引脚)上记时戳,对应 F4 stm32f4xx_it.c#L210GPS_Delay = MCU_ms_cnt*10 - PPS_cnt

4.4 串口 DMA 输出

F4 的 Out_Frame 定义(./05_串口输出帧.md)以 head=0x56aa55aa 起始,顺序 t → Gyro[3] → Accel[3] → Magn[3] → mBar → Att[3] → Vn[3] → Pos[5] → ... → checksum

H7 现有输出在 ins_output.c(可能二进制/自定义协议)。若要兼容 F4 的上位机解析工具: - 定义相同的 Out_Frame 结构体; - 使用 HAL_UART_Transmit_DMA(&huart1, (uint8_t*)&outFrame, sizeof(outFrame)) 替换; - USART1 TX DMA 的 CubeMX 配置:DMA1 Stream X,Memory-to-Peripheral,高优先级,半满/传输完成中断回调。

4.5 PC 命令(PCCMD)系统

F4 中 PC_cmd 是 32-bit 字,强转 PCCMD{cmd1,cmd2}: - cmd2 = 0x1111/0x2222/0x3333/0x4444 切换模式; - cmd1 = 0xa5a5 退出当前模式。

H7 新增 PSINS 模式时同样可复用:在 usart2/uart_idle 收到 PC 4 字节帧后,pcmd->cmd2 = 0x1111 → 切到 PSINS Mahony。对应 H7 的 psins_switch_mode(uint16_t mode)


五、时钟/中断/时序重映射

F4 → H7 外设挂载差异(关键移植点):

功能 F4 H743 AHRS-Board 建议配置 移植要点
IMU 采样节拍 TIM2 APB1, 100 Hz, 中断读 MPU9250 TIM6(基本定时器)APB4 1 kHz → 软件分频 10/20/50/100 Hz 多路 H7 多传感器,统一时基
气压计节拍 TIM3 APB1 50 Hz TIM6 分频得到,或独立 TIM7 H7 可把气压采样挂 DMA I2C 或 IMU 节拍回调
磁力计节拍 无独立节拍,MPU9250 同步 100 Hz 独立 RM3100 DRDY 外部中断(PCx),精度更高 RM3100 推荐用硬 DRDY,不轮询
GNSS PPS EXTI Line X,驱动 PPS_cnt UM982 PPS → EXTI Line X,记录 64-bit DWT 时间戳 H7 DWT 可直接 1 μs 粒度
输出节拍 IMU 同步 100 Hz DMA USART DMA 输出可降到 50 Hz,节省带宽 用户可选 100/50 Hz
主循环调度 裸机 while(1) + flag 轮询 可继续裸机,或接入 FreeRTOS:Sensor Task(最高优先级),算法 Task(次高),通信 Task(次低) H7 480 MHz 裸机即可,RTOS 需注意栈大小(KFApp 需要 24 KB 栈/静态对象)

中断优先级建议(从高到低):

  1. PPS EXTI(捕获时间戳,<1 μs 抖动)
  2. TIM6 时基 / IMU SPI DMA 完成(2~5 μs 抖动)
  3. GNSS USART IDLE / GPS PVT 解析
  4. UART 输出 DMA TC
  5. Systick(1 ms)

5.1 H7 Cache 一致性(致命陷阱)

PSINS 使用 double 数组 & CMat3 大对象(19×19 double ≈ 2.9 KB Pk),H7 的 Cortex-M7 带 I-Cache & D-Cache:

  1. MPU 配置:把 DTCM(0x20000000,64 KB)配置为 Non-Cacheable(因为 DTCM 不挂 Cache);SRAM1/2/3(0x30000000+)用 Write-Through + Read-Allocate。避免 Write-Back 模式下 DMA 读取的 RAM ≠ CPU 看到的值
  2. DMA 发送前:SCB_CleanDCache_by_Addr((uint32_t*)&outFrame, sizeof(outFrame))
  3. 传感器接收缓冲区:__attribute__((section(".noncacheable"))) 放在 Non-Cache SRAM 区,或 SCB_InvalidateDCache_by_Addr 在读前。

CubeMX .sct 脚本新增段:

LR_IROM1 ... {
  ...
  RW_NONCACHE 0x30048000 UNINIT 0x00008000  {  *.o (.noncacheable)  }
}

5.2 MDMA / BDMA

F4 的 DMA2 Stream7 是简单 DMA;H7 有 MDMA(支持 Mem-Mem、链表)。串口输出帧可以直接把多个 Sensor Buffer 用 MDMA 链表一次性打包,减少 CPU 参与。但在移植第一步保持兼容,先复用 HAL_UART_Transmit_DMA,之后再优化。


六、编译问题处理清单(已知踩坑)

问题 原因 修复
undefined reference to operator new/delete H7 工程默认未链接 C++ stdlib 启用 uC++ 微库或提供 operator new(size_t, void*) placement only;在 psins_port.cpp 中提供 void* operator new(size_t) { while(1); } stub 防止误堆分配
sizeof(CVect3) != 24 或 struct 错位 对齐不一致 #pragma pack(4)(见 PSINS.h#L20)必须全局生效;H7 HAL 头若用 #pragma pack(8) 会冲突,改为 pack(push,4) ... pack(pop) 包起来
PSINS.cpp : error: #279: controlling expression is constant ARMCC5/6 对 while(1)assert 的警告 Keil C++ Warning 选项设 -W,或在 PSINS.cpp 顶部 #pragma diag_suppress 279
printf("%f") 打印 double 出 0.000000 Armclang microlib 单精度 printf 在 debug 时用 PSINS.cpp 内自写格式化;或启用 --library_type=std
HardFault_Handler 进入 通常是 double 非对齐访问(H7 M7 不支持非对齐 LDRD) ① 所有静态 CVect3/CMat3 对象 alignas(8) 声明;② *(CVect3*)src 强转时确认 src 指针 8 字节对齐;③ 打开 BusFault/UsageFault 调试位,读 BFAR/UFSR
IsZero() 判零逻辑导致 SetMeasGNSS 全被跳过 H7 传的伪速度有 [0,0,0.01] → F4 IsZero 阈值 2.2e-16; 保持正确即可,0.01 不会误判

七、验收与回归

移植后要通过 4 个最低回归门槛,依次验证:

  1. 编译链接-W -O2 --brief-diagnostics 0 Error,栈大小报告显示 KFApp 运行栈 < 10 KB(若使用 RTOS,栈设 16 KB 足够)。
  2. Mahony 静态对齐:静态 60 s,欧拉角波动 < 1°,yaw 若接磁力计则稳定。对齐后与 H7 现 ins_mahony_ahrs.c 输出偏差 < 2°。
  3. SINS/GNSS 组合回放:用 F4 已有数据集(data/11.txt)或现场采集 5 分钟行车 GNSS+IMU,PC 端把同样数据分别灌给 F4 工程(MATLAB psins_uart_load.m)和 H7 的移植版本,结果 ENU→NED 后:
  4. 姿态 RMSE < 1°;
  5. 速度 RMSE < 0.2 m/s;
  6. 位置 CEP50 < 2 m(同 GPS 精度量级)。
  7. 长期稳定性:裸机 24 h 运行,观察 Pk 是否合理,姿态/位置不发散;周期性 HardFault 计数 = 0;exyzInt, Pk.diag 不突破 Pmax/Pmin

八、分阶段实施计划(按复杂度排序)

阶段 内容 预计 交付
P0 编译 C++ 工程:加入 PSINS.h/.cpp, KFApp.h/.cpp 到 H7 MDK,写 psins_port.cpp/.h 提供 extern "C" C 接口;验证 CMahony::Update 对象构造/析构无堆分配 1 天 H7 工程可跑通 void mahony_test() 循环
P1 接 H7 ICM42688 采样喂给 mahony,静态 10 min 精度验收;输出 att 到现有 UART 协议 1~2 天 姿态结果和 H7 现有 ins_mahony_ahrs 可比
P2 移植 CKFApp 19 维 ESKF:psins_init / psins_feed_imu / psins_feed_gnss / psins_get_nav 四个 C 接口写好 2 天 静态 KF 收敛(vn→0,pos→GPS 固定点)
P3 NED↔ENU 变换校验:合成数据(已知轨迹)对比 PSINS MATLAB 与 H7 输出的差 1 天 单元测试/回放脚本
P4 端口外设驱动:UM982 协议 → SetMeasGNSS;MS5611/气压高程作 baro_z 附加观测;Out_Frame DMA 输出兼容 2~3 天 上位机 PSINS 工具直接识别 H7 数据流
P5 优化/鲁棒性:Cache 清理、MPU、nSteps 配置、杆臂估计、Markov 零偏、异常值剔除 3~5 天 实车路测报告

P0~P3 是可运行 MVP,P4~P5 是产品化级


九、保留选项:直接重写 vs 直接复用 PSINS

现在 H7 已经有 ins_eskf_15d.c,功能上覆盖了 CSINSGNSS 的 15 维核心,但缺少:

  • 杆臂 l (3) 与 时间误差 dt (1) → 共 4 个状态;
  • GPS 延迟补偿 avpi;
  • 时间分布式 KF(TDUpdate nSteps 摊薄);
  • CSINS 高阶地球模型(含 \(C_b^n\)\(\omega_{ie}\)\(g_{cc}\))矩阵 Maa/Mav/Map/Mva/Mvv/Mvp/Mpv/Mpp 等显式 Ft 自动生成;
  • PSINS MATLAB 工具链 1:1 对应。

若最终目标是和 PSINS MATLAB 对齐,应直接复用 PSINS C++ 层,而不是在 eskf15d.c 上迭代补功能——长期维护成本更低,且 MATLAB→C→硬件 1:1 映射便于定位精度问题。如果只做最小功能出货,eskf15d.c 已足够。


参考资料


导航