Skip to content

feat(sensors/gps): map PVT UTC to HRT for EKF time alignment#27387

Draft
dakejahl wants to merge 1 commit into
PX4:mainfrom
dakejahl:feat/uavcan-gnss-pvt-timestamp
Draft

feat(sensors/gps): map PVT UTC to HRT for EKF time alignment#27387
dakejahl wants to merge 1 commit into
PX4:mainfrom
dakejahl:feat/uavcan-gnss-pvt-timestamp

Conversation

@dakejahl
Copy link
Copy Markdown
Contributor

@dakejahl dakejahl commented May 19, 2026

Summary

Map the GNSS PVT UTC timestamp to the FC's HRT timebase in VehicleGPSPosition with an asymmetric leaky-max filter on (UTC_PVT - HRT_recv), so EKF GPS samples align with IMU samples to within minimum observed latency (a few ms) instead of the constant-latency SENS_GPS*_DELAY 110 ms guess.

Problem

VehicleGPSPosition falls back to timestamp - SENS_GPS_DELAY (default 110 ms) when the driver leaves timestamp_sample unset. The DroneCAN GNSS driver parses Fix2.gnss_timestamp into sensor_gps.time_utc_usec but only uses it for setting the system clock and the PPS path — the precise PVT instant is discarded for EKF fusion. On common no-PPS DroneCAN setups the EKF therefore aligns GPS against IMU with a fixed-latency assumption that ignores actual receiver-internal processing plus CAN-bus latency (which varies with bus load). Serial GPS drivers that populate time_utc_usec hit the same fallback.

Solution

New UtcToHrtMapper helper in sensors/vehicle_gps_position runs above the SENS_GPS_DELAY fallback. For each sensor_gps message with valid time_utc_usec, it estimates offset := UTC - HRT from

cand_k = UTC_PVT,k - HRT_recv,k = offset - latency_k,   latency_k >= 0

as the supremum of cand over recent observations — one-sided noise (latency cannot be negative) makes max-of-cand the right estimator, the same technique used by NTP/PTP min-RTT filters. Implemented as an asymmetric leaky-max: snap up immediately on lower-latency observations or UTC > HRT drift, decay at 100 ppm between observations so the offset can also track UTC < HRT drift over long flights. Residual timestamp_sample bias is approximately min(latency) — three orders of magnitude better than the 110 ms fallback.

Lives in VehicleGPSPosition so every sensor_gps source benefits, not just DroneCAN. Per-receiver state (_utc_to_hrt_mapper[GPS_MAX_RECEIVERS]) so different bus/cable latencies between receivers do not bias each other. Hard reset on a backward cand jump > 100 ms (leap-second insertion, receiver recovery) — bounded re-bootstrap instead of hours-of-decay recovery. Strict cross-sample monotonicity clamp on the emitted timestamp so snap-ups during the convergence transient do not get rejected by the EKF GPS-buffer's _min_obs_interval_us gate.

Falls through to SENS_GPS_DELAY when UTC is unavailable (e.g., before first fix). PpsTimeSync still overrides post-blending when PPS hardware is wired; this only improves the no-PPS path.

Tested by building px4_fmu-v6x_default, px4_sitl, and running gps_blending unit tests. Needs flight-log validation on hardware with a DroneCAN GNSS before un-drafting.

@github-actions github-actions Bot added kind:feature Request or change that adds new functionality. scope:drivers Device drivers and hardware interfaces. scope:middleware DDS, ROS 2, Cyphal/UAVCAN, zenoh, or bridge layers. labels May 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

🔎 FLASH Analysis

px4_fmu-v5x [Total VM Diff: 304 byte (0.02 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +304  +0.0%    +304    .text
    [NEW]    +224  [NEW]    +224    UtcToHrtMapper::map()
    +9.6%     +52  +9.6%     +52    sensors::VehicleGPSPosition::Run()
    +5.5%     +12  +5.5%     +12    sensors::VehicleGPSPosition::ParametersUpdate()
    +1.6%     +12  +1.6%     +12    sensors::VehicleGPSPosition::VehicleGPSPosition()
     +25%      +4   +25%      +4    sensors::VehicleGPSPosition::PrintStatus()
  +0.0%    +646  [ = ]       0    .debug_abbrev
  +0.0%     +32  [ = ]       0    .debug_aranges
  +0.0%     +64  [ = ]       0    .debug_frame
  +0.0% +2.17Ki  [ = ]       0    .debug_info
  +0.0%    +741  [ = ]       0    .debug_line
   -42.9%      -3  [ = ]       0    [Unmapped]
    +0.0%    +744  [ = ]       0    [section .debug_line]
  +0.0%    +304  [ = ]       0    .debug_loclists
  -0.0%     -11  [ = ]       0    .debug_rnglists
     +50%      +1  [ = ]       0    [Unmapped]
    -0.0%     -12  [ = ]       0    [section .debug_rnglists]
  +0.0%    +367  [ = ]       0    .debug_str
  -0.8%      -2  [ = ]       0    .shstrtab
  +0.0%     +46  [ = ]       0    .strtab
    [NEW]     +27  [ = ]       0    UtcToHrtMapper::map()
    +0.1%     +19  [ = ]       0    [section .strtab]
   -26.7%     -16  [ = ]       0    ___ZL19param_get_cplusplustPf.isra.0_veneer
     +67%     +16  [ = ]       0    __nxsem_tickwait_veneer
  +0.0%     +64  [ = ]       0    .symtab
    [NEW]     +64  [ = ]       0    UtcToHrtMapper::map()
    +0.3%     +32  [ = ]       0    [section .symtab]
   -14.3%     -32  [ = ]       0    ___ZL19param_get_cplusplustPf.isra.0_veneer
     +67%     +32  [ = ]       0    __nxsem_tickwait_veneer
    -1.9%     -16  [ = ]       0    px4::wq_configurations::nav_and_controllers
   -33.3%     -16  [ = ]       0    sensors::VehicleGPSPosition::PrintStatus()
  -2.5%    -304  [ = ]       0    [Unmapped]
  +0.0% +4.37Ki  +0.0%    +304    TOTAL

px4_fmu-v6x [Total VM Diff: 304 byte (0.02 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +304  +0.0%    +304    .text
    [NEW]    +228  [NEW]    +228    UtcToHrtMapper::map()
    +9.6%     +52  +9.6%     +52    sensors::VehicleGPSPosition::Run()
    +5.5%     +12  +5.5%     +12    sensors::VehicleGPSPosition::ParametersUpdate()
    +1.6%     +12  +1.6%     +12    sensors::VehicleGPSPosition::VehicleGPSPosition()
     +25%      +4   +25%      +4    sensors::VehicleGPSPosition::PrintStatus()
    -3.4%      -4  -3.4%      -4    PpsTimeSync::correct_gps_timestamp()
  +0.0%    +646  [ = ]       0    .debug_abbrev
  +0.0%     +32  [ = ]       0    .debug_aranges
  +0.0%     +64  [ = ]       0    .debug_frame
  +0.0% +2.17Ki  [ = ]       0    .debug_info
  +0.0%    +749  [ = ]       0    .debug_line
    [NEW]      +5  [ = ]       0    [Unmapped]
    +0.0%    +744  [ = ]       0    [section .debug_line]
  +0.0%    +304  [ = ]       0    .debug_loclists
  -0.0%     -11  [ = ]       0    .debug_rnglists
    +100%      +1  [ = ]       0    [Unmapped]
    -0.0%     -12  [ = ]       0    [section .debug_rnglists]
  +0.0%    +367  [ = ]       0    .debug_str
  +0.9%      +2  [ = ]       0    .shstrtab
  +0.0%     +46  [ = ]       0    .strtab
    [NEW]     +27  [ = ]       0    UtcToHrtMapper::map()
    +0.1%     +19  [ = ]       0    [section .strtab]
  +0.0%     +64  [ = ]       0    .symtab
    +100%     +32  [ = ]       0    PpsTimeSync::correct_gps_timestamp()
    [NEW]     +32  [ = ]       0    UtcToHrtMapper::map()
    +0.3%     +32  [ = ]       0    [section .symtab]
    -1.9%     -16  [ = ]       0    px4::wq_configurations::nav_and_controllers
   -33.3%     -16  [ = ]       0    sensors::VehicleGPSPosition::PrintStatus()
  -7.2%    -304  [ = ]       0    [Unmapped]
  +0.0% +4.38Ki  +0.0%    +304    TOTAL

Updated: 2026-05-19T03:07:30

Replaces the constant-latency SENS_GPS*_DELAY guess (110 ms) with an
asymmetric leaky-max filter on (UTC_PVT - HRT_recv) per receiver,
reducing residual GPS-vs-IMU time-alignment bias from ~110 ms to the
minimum observed latency (a few ms on typical CAN or serial GPS).

For each cand = UTC_PVT - HRT_recv = offset - latency (latency >= 0),
the true offset is the supremum of cand. The filter snaps up to a new
max immediately (tracks low-latency observations and growing UTC-HRT
delta) and decays slowly between observations at 100 ppm so the offset
can also track a shrinking delta when HRT runs faster than UTC. Without
the decay, long-flight clock drift in that direction would accumulate
unbounded bias.

UtcToHrtMapper lives in VehicleGPSPosition so every sensor_gps source
benefits, not just DroneCAN. Per-receiver state prevents one receiver's
latency from biasing another's. Hard reset on a backward cand jump
larger than 100 ms (leap-second insertion, receiver recovery). Strict
cross-sample monotonicity clamp on the emitted timestamp so snap-ups
during the convergence transient do not get rejected by the EKF GPS
buffer's _min_obs_interval_us gate.

Falls through to the SENS_GPS_DELAY path when UTC is unavailable
(e.g., before first fix). PpsTimeSync still overrides post-blending
when PPS hardware is wired.

Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
@dakejahl dakejahl force-pushed the feat/uavcan-gnss-pvt-timestamp branch from 67db04c to b997fd6 Compare May 19, 2026 03:01
@github-actions github-actions Bot added risk:safety-critical May affect arming, failsafe, control, navigation, or flight safety. scope:sensors Sensor pipeline, calibration, voting, or sensor validation. labels May 19, 2026
@dakejahl dakejahl changed the title feat(drivers/uavcan): use GNSS PVT timestamp for EKF time alignment feat(sensors/gps): map PVT UTC to HRT for EKF time alignment May 19, 2026
@dakejahl dakejahl requested a review from bresch May 21, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind:feature Request or change that adds new functionality. risk:safety-critical May affect arming, failsafe, control, navigation, or flight safety. scope:drivers Device drivers and hardware interfaces. scope:middleware DDS, ROS 2, Cyphal/UAVCAN, zenoh, or bridge layers. scope:sensors Sensor pipeline, calibration, voting, or sensor validation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant