Skip to content

Commit b997fd6

Browse files
committed
feat(sensors/gps): map PVT UTC to HRT for EKF time alignment
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>
1 parent 9ca5cf3 commit b997fd6

5 files changed

Lines changed: 194 additions & 1 deletion

File tree

src/modules/sensors/vehicle_gps_position/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ px4_add_library(vehicle_gps_position
3838
gps_blending.hpp
3939
PpsTimeSync.cpp
4040
PpsTimeSync.hpp
41+
UtcToHrtMapper.cpp
42+
UtcToHrtMapper.hpp
4143
)
4244
target_link_libraries(vehicle_gps_position
4345
PRIVATE
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/****************************************************************************
2+
*
3+
* Copyright (c) 2026 PX4 Development Team. All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions
7+
* are met:
8+
*
9+
* 1. Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
* 2. Redistributions in binary form must reproduce the above copyright
12+
* notice, this list of conditions and the following disclaimer in
13+
* the documentation and/or other materials provided with the
14+
* distribution.
15+
* 3. Neither the name PX4 nor the names of its contributors may be
16+
* used to endorse or promote products derived from this software
17+
* without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
22+
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
23+
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
24+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
25+
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
26+
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
27+
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28+
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29+
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30+
* POSSIBILITY OF SUCH DAMAGE.
31+
*
32+
****************************************************************************/
33+
34+
#include "UtcToHrtMapper.hpp"
35+
36+
hrt_abstime UtcToHrtMapper::map(uint64_t utc_us, hrt_abstime hrt_recv_us)
37+
{
38+
if (utc_us == 0) {
39+
return 0;
40+
}
41+
42+
const int64_t cand_offset = static_cast<int64_t>(utc_us) - static_cast<int64_t>(hrt_recv_us);
43+
44+
// Hard reset on a UTC discontinuity: a backward jump in cand larger than
45+
// any plausible latency spike means the previous offset estimate is stale.
46+
if (_valid && cand_offset < _offset_us - kUtcDiscontinuityResetUs) {
47+
_valid = false;
48+
_last_emitted_us = 0;
49+
}
50+
51+
if (!_valid) {
52+
_offset_us = cand_offset;
53+
_valid = true;
54+
55+
} else {
56+
const hrt_abstime dt_us = (hrt_recv_us > _last_update_us) ? (hrt_recv_us - _last_update_us) : 0;
57+
const int64_t decay_us = static_cast<int64_t>(dt_us) * kMaxClockDriftPpm / 1'000'000;
58+
_offset_us -= decay_us;
59+
60+
if (cand_offset > _offset_us) {
61+
_offset_us = cand_offset;
62+
}
63+
}
64+
65+
_last_update_us = hrt_recv_us;
66+
67+
int64_t pvt_hrt_us = static_cast<int64_t>(utc_us) - _offset_us;
68+
69+
// Strict-less-than hrt_recv_us: equality is misread upstream as
70+
// "driver didn't set timestamp_sample" and replaced with SENS_GPS_DELAY.
71+
if (pvt_hrt_us >= static_cast<int64_t>(hrt_recv_us)) {
72+
pvt_hrt_us = static_cast<int64_t>(hrt_recv_us) - 1;
73+
}
74+
75+
// Strict-greater-than _last_emitted_us: snap-ups during the convergence
76+
// transient can produce a backward step that the EKF GPS buffer rejects.
77+
if (_last_emitted_us > 0 && pvt_hrt_us <= static_cast<int64_t>(_last_emitted_us)) {
78+
pvt_hrt_us = static_cast<int64_t>(_last_emitted_us) + 1;
79+
}
80+
81+
// Both clamps applied; bail out if the window (_last_emitted_us, hrt_recv_us)
82+
// was too narrow to satisfy them.
83+
if (pvt_hrt_us <= 0 || pvt_hrt_us >= static_cast<int64_t>(hrt_recv_us)) {
84+
return 0;
85+
}
86+
87+
_last_emitted_us = static_cast<hrt_abstime>(pvt_hrt_us);
88+
return _last_emitted_us;
89+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/****************************************************************************
2+
*
3+
* Copyright (c) 2026 PX4 Development Team. All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions
7+
* are met:
8+
*
9+
* 1. Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
* 2. Redistributions in binary form must reproduce the above copyright
12+
* notice, this list of conditions and the following disclaimer in
13+
* the documentation and/or other materials provided with the
14+
* distribution.
15+
* 3. Neither the name PX4 nor the names of its contributors may be
16+
* used to endorse or promote products derived from this software
17+
* without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
22+
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
23+
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
24+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
25+
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
26+
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
27+
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
28+
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29+
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30+
* POSSIBILITY OF SUCH DAMAGE.
31+
*
32+
****************************************************************************/
33+
34+
#pragma once
35+
36+
#include <stdint.h>
37+
#include <drivers/drv_hrt.h>
38+
39+
/**
40+
* Maps a GNSS PVT UTC timestamp to the FC's HRT timebase so that the EKF can
41+
* align GPS samples with IMU samples without relying on the SENS_GPS*_DELAY
42+
* constant-latency guess.
43+
*
44+
* Each observation gives cand = UTC_PVT - HRT_recv = (UTC - HRT) - latency,
45+
* where latency >= 0. The true offset (UTC - HRT) is therefore the supremum
46+
* of cand over recent observations. Estimated by an asymmetric leaky-max:
47+
*
48+
* - snap up whenever cand > current_offset (tracks lower latency and any
49+
* UTC-faster-than-HRT drift),
50+
* - decay slowly between observations at the worst-case FC crystal drift
51+
* rate so the offset can also follow UTC-slower-than-HRT drift without
52+
* accumulating bias over long flights.
53+
*
54+
* Residual bias of the mapped timestamp is approximately min(latency) over
55+
* the recent window -- a few ms at most on typical CAN/serial GPS, three
56+
* orders of magnitude better than the 110 ms SENS_GPS_DELAY fallback.
57+
*
58+
* Output is clamped to be strictly less than hrt_recv_us (so downstream
59+
* "did the driver set it" checks don't misfire) and strictly greater than
60+
* the previously emitted value (so the EKF GPS-buffer push doesn't reject
61+
* non-monotonic samples during the convergence transient).
62+
*/
63+
class UtcToHrtMapper
64+
{
65+
public:
66+
UtcToHrtMapper() = default;
67+
~UtcToHrtMapper() = default;
68+
69+
/**
70+
* @param utc_us PVT UTC timestamp [us since Unix epoch]; 0 if unavailable.
71+
* @param hrt_recv_us HRT timestamp captured when the GNSS message was received.
72+
* @return mapped HRT timestamp, or 0 if mapping not available for this sample.
73+
*/
74+
hrt_abstime map(uint64_t utc_us, hrt_abstime hrt_recv_us);
75+
76+
private:
77+
// Bound on FC HSE crystal drift vs GNSS atomic time. 100 ppm safely envelopes
78+
// raw crystals over temperature; TCXOs are at most a few ppm.
79+
static constexpr int64_t kMaxClockDriftPpm{100};
80+
81+
// A negative jump in cand larger than this can't be FC drift -- treat as a
82+
// UTC discontinuity (leap-second insertion, receiver recovery) and reset.
83+
static constexpr int64_t kUtcDiscontinuityResetUs{100'000};
84+
85+
int64_t _offset_us{0}; // estimate of UTC - HRT
86+
hrt_abstime _last_update_us{0};
87+
hrt_abstime _last_emitted_us{0};
88+
bool _valid{false};
89+
};

src/modules/sensors/vehicle_gps_position/VehicleGPSPosition.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,18 @@ void VehicleGPSPosition::Run()
163163
delay_us = _gps_param_slots[i].delay_us;
164164
}
165165

166-
// Apply delay to timestamp_sample if the driver didn't set one
166+
// Prefer PVT-UTC-to-HRT mapping when the driver didn't set timestamp_sample but
167+
// did provide a valid UTC PVT time. Residual bias is ~min(latency) (a few ms),
168+
// vs the ~110 ms SENS_GPS_DELAY fallback below.
169+
if (gps_data.timestamp_sample == 0 || gps_data.timestamp_sample == gps_data.timestamp) {
170+
const hrt_abstime mapped = _utc_to_hrt_mapper[i].map(gps_data.time_utc_usec, gps_data.timestamp);
171+
172+
if (mapped > 0) {
173+
gps_data.timestamp_sample = mapped;
174+
}
175+
}
176+
177+
// Fallback for drivers that don't provide UTC (e.g., no fix yet).
167178
if (gps_data.timestamp_sample == 0 || gps_data.timestamp_sample == gps_data.timestamp) {
168179
if (delay_us > 0 && gps_data.timestamp > delay_us) {
169180
gps_data.timestamp_sample = gps_data.timestamp - delay_us;

src/modules/sensors/vehicle_gps_position/VehicleGPSPosition.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949

5050
#include "gps_blending.hpp"
5151
#include "PpsTimeSync.hpp"
52+
#include "UtcToHrtMapper.hpp"
5253

5354
using namespace time_literals;
5455

@@ -96,6 +97,7 @@ class VehicleGPSPosition : public ModuleParams, public px4::ScheduledWorkItem
9697

9798
GpsBlending _gps_blending;
9899
PpsTimeSync _pps_time_sync;
100+
UtcToHrtMapper _utc_to_hrt_mapper[GPS_MAX_RECEIVERS];
99101

100102
struct GpsParamSlot {
101103
uint32_t device_id{0};

0 commit comments

Comments
 (0)