feat(usrp): Sync to CLOCK_REALTIME without GPS

Add a method to sync to CLOCK_REALTIME for USRPs without GPS sync.
The main idea centers around the round-trip time of USRP timing API.
As long as the API RTT is low enough (witihn a few symbols) it is
possible to achieve partial synchronization and maintain it until the
clocks drift.

Signed-off-by: Bartosz Podrygajlo <bartosz.podrygajlo@openairinterface.org>
This commit is contained in:
Bartosz Podrygajlo
2026-05-12 12:21:53 +02:00
parent af4180e47f
commit 3d443735d3
2 changed files with 109 additions and 1 deletions

View File

@@ -151,3 +151,53 @@ There are two workarounds:
You can also find more information on this in the [5G/NR gNB with COTS UE
tutorial](./NR_SA_Tutorial_COTS_UE.md).
### Map UTC timestamp to sample index
#### Without GPS sync
The current software attempts to generate a valid sample_index from UTC timestamp
in `get_timestamp`. This is done by first continuously polling `get_time_last_pps`
setting up the clock reference using `set_time_next_pps`.
This works only for devices with short and stable round-trip time of `get_time_last_pps`
The following graph describes the delay:
```mermaid
sequenceDiagram
participant host
participant usrp
participant last_pps
participant usrp_clock
loop get_last_pps
host ->>+ usrp: get_last_pps query
usrp ->> last_pps: read
alt delay start
usrp_clock -->> last_pps: write ?
end
usrp ->>- host: get_last_pps response
end
host ->>+ usrp: get_last_pps query
alt delay start
usrp_clock -->> last_pps: write ?
end
usrp ->> last_pps: read
usrp ->>- host: get_last_pps response
note over host: read CLOCK_REALTIME
host ->> usrp: set_time_next_pps
```
The total delay between the PPS pulse on the USRP and the host reading CLOCK_REALTIME can
be estimated to between 1.5 RTT (if last_pps was written after USRP read it in the
`get_last_pps` loop above) to 0.5 RTT (if last_pps was written right before the final
`get_last_pps` call).
This mechanism makes it so that USRP clock is running ahead of the host CLOCK_REALTIME.
to circumvent that, an extra offset is added when calculating sample index in `get_timestamp`.
For 5G, if the RTT value is small (~few symbols) and stable (low stdev) it would allow for
temporary synchronization to CLOCK_REALTIME at least until the clocks drift.

View File

@@ -84,6 +84,8 @@ typedef struct {
//int first_rx;
//! timestamp of RX packet
openair0_timestamp_t rx_timestamp;
bool utc_sync;
double wall_to_hw_offset;
} usrp_state_t;
//void print_notes(void)
@@ -1039,6 +1041,23 @@ static void usrp_sync_pps(usrp_state_t *s)
LOG_I(HW, "USRP clock set to %f sec\n", tai_sec);
}
openair0_timestamp_t get_timestamp(openair0_device *device, struct timespec *utc_ts) {
usrp_state_t *s = (usrp_state_t *)device->priv;
if (!s->utc_sync) {
return 0;
}
// 1. CPU-domain UTC (The "Wall Clock")
double wall_secs = (double)utc_ts->tv_sec + (double)utc_ts->tv_nsec / 1e9;
// 2. Map to Hardware-domain UTC
double hw_secs = wall_secs + s->wall_to_hw_offset;
auto ts = uhd::time_spec_t(hw_secs);
// 3. Convert to Absolute Ticks
return (openair0_timestamp_t)(ts.to_ticks(s->sample_rate));
}
extern "C" {
int device_init(openair0_device_t *device, openair0_config_t *openair0_cfg)
{
@@ -1440,7 +1459,46 @@ extern "C" {
if (s->usrp->get_time_source(0) == "external") {
usrp_sync_pps(s);
} else {
s->usrp->set_time_next_pps(uhd::time_spec_t(0.0));
// No GPS sync and no external clock - best effort to sync to host clock
// estimate rtt of get_time_last_pps()
const int n_tries = 100;
const uint64_t ns_to_s = 1000000000;
std::vector<uint64_t> rtt_samples;
for (int i = 0; i < n_tries; i++) {
struct timespec start;
clock_gettime(CLOCK_MONOTONIC, &start);
s->usrp->get_time_last_pps();
struct timespec end;
clock_gettime(CLOCK_MONOTONIC, &end);
uint64_t rtt_sample = (end.tv_sec - start.tv_sec) * ns_to_s + (end.tv_nsec - start.tv_nsec);
rtt_samples.push_back(rtt_sample);
}
uint64_t rtt_sum = 0;
for (int i = 0; i < n_tries; i++) {
rtt_sum += rtt_samples[i];
}
double rtt_mean = rtt_sum / (double)n_tries;
double rtt_var = 0;
for (int i = 0; i < n_tries; i++) {
rtt_var += (rtt_mean - rtt_samples[i]) * (rtt_mean - rtt_samples[i]);
}
rtt_var /= n_tries;
LOG_A(HW, "RTT of get_time_last_pps() mean %f stdev %f uS\n", rtt_mean / 1e3, sqrt(rtt_var) / 1e3);
uhd::time_spec_t time_last_pps = s->usrp->get_time_last_pps();
while (time_last_pps == s->usrp->get_time_last_pps()) {
}
struct timespec now;
int ret = clock_gettime(CLOCK_REALTIME, &now);
if (ret != 0) {
LOG_E(HW, "Failed to get time from clock");
exit(EXIT_FAILURE);
}
double utc_timestamp = now.tv_sec + 1.0 + now.tv_nsec / 1e9;
s->usrp->set_time_next_pps(uhd::time_spec_t(utc_timestamp));
s->wall_to_hw_offset = rtt_mean * 2;
s->utc_sync = true;
}
if (s->usrp->get_clock_source(0) == "external") {