Step 3.3a: PTP servo, clock locks to the GM

- ptp_hw: ptp_hw_adj_freq() (ETH_MAC_ESP_CMD_ADJ_PTP_TIME, absolute ppb
  vs nominal, clamped +-500 ppm, retried while the addend update is
  busy) and ptp_hw_step() (read + write time).
- ptp_clock: first Sync after a GM is selected seeds the integral with
  the measured rate and steps the clock; then linuxptp-style PI (kp/ki
  from the Sync interval: 0.7/0.3 at 1 s). Re-step above 1 ms. Locked
  after 8 Syncs with |offset| < 1 us, unlocked after 3 above; GM change
  or loss unlocks and keeps the frequency (holdover).
- Verified vs ptp4l: stepped to GM time, locked after ~19 s; locked
  offset within +-510 ns (mostly < 300 ns), correction +39.9 ppm
  (crystal -39.8 ppm), path delay ~10.2 us.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 06:15:26 +10:00
parent e2f4d81326
commit 685060e12c
3 changed files with 119 additions and 3 deletions
+83 -3
View File
@@ -1,5 +1,7 @@
#include "ptp_clock.h" #include "ptp_clock.h"
#include <math.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "aes67_cfg.h" #include "aes67_cfg.h"
@@ -16,6 +18,11 @@
#define PTP_GENERAL_PORT 320 #define PTP_GENERAL_PORT 320
#define HDR_LEN 34 #define HDR_LEN 34
#define FLAG_TWO_STEP 0x0200 #define FLAG_TWO_STEP 0x0200
#define STEP_NS 1000000 // re-step instead of slewing above 1 ms
#define LOCK_NS 1000 // |offset| below this counts as good
#define LOCK_GOOD 8 // consecutive good Syncs to lock
#define LOCK_BAD 3 // consecutive bad Syncs to unlock
#define MAX_DRIFT_PPB 500000.0
static const char *TAG = "ptp"; static const char *TAG = "ptp";
static const uint8_t PTP_MCAST_MAC[6] = { 0x01, 0x00, 0x5e, 0x00, 0x01, 0x81 }; static const uint8_t PTP_MCAST_MAC[6] = { 0x01, 0x00, 0x5e, 0x00, 0x01, 0x81 };
@@ -53,8 +60,18 @@ static struct {
// while the local clock is not yet syntonised. // while the local clock is not yet syntonised.
int64_t raw, prev_raw, prev_t2; int64_t raw, prev_raw, prev_t2;
double rate; double rate;
int8_t log_sync; // GM's Sync interval (from Sync logMessageInterval)
// Servo (PI, linuxptp-style gains)
bool stepped; // clock set to GM time since this GM was selected
double drift_ppb; // integral term
double freq_ppb; // correction currently applied (positive = faster)
int64_t offset_ns;
int good, bad;
bool locked;
} s; } s;
static void set_locked(bool locked);
/* ----- helpers ----- */ /* ----- helpers ----- */
static uint16_t rd16(const uint8_t *p) { return (p[0] << 8) | p[1]; } static uint16_t rd16(const uint8_t *p) { return (p[0] << 8) | p[1]; }
@@ -146,11 +163,71 @@ static void on_announce(const uint8_t *b, int len, uint32_t src_ip)
s.gm = m; s.gm = m;
s.sync_pending = s.dreq_pending = false; s.sync_pending = s.dreq_pending = false;
s.delay_ns = s.prev_t2 = 0; s.delay_ns = s.prev_t2 = 0;
s.stepped = false;
set_locked(false);
s.log_dreq = 0; s.log_dreq = 0;
s.next_dreq_us = 0; s.next_dreq_us = 0;
} }
} }
static void set_locked(bool locked)
{
if (locked != s.locked) {
s.locked = locked;
if (locked) {
ESP_LOGI(TAG, "locked: offset %+lld ns, frequency %+.3f ppm, path delay %lld ns",
s.offset_ns, s.freq_ppb / 1000, s.delay_ns);
} else {
ESP_LOGW(TAG, "unlocked");
}
}
if (!locked) {
s.good = 0;
}
}
// offset = local - GM (ns)
static void servo(int64_t offset)
{
s.offset_ns = offset;
if (!s.stepped || llabs(offset) > STEP_NS) {
if (!s.stepped) {
// Seed the integral with the measured rate error (measured with the current correction applied).
s.drift_ppb += s.rate * 1e9;
}
s.freq_ppb = -s.drift_ppb;
ptp_hw_adj_freq(s.freq_ppb);
esp_err_t err = ptp_hw_step(offset);
ESP_LOGI(TAG, "clock stepped by %+lld ns (%s), frequency %+.3f ppm", -offset, esp_err_to_name(err),
s.freq_ppb / 1000);
s.stepped = true;
s.prev_t2 = 0; // rate across the step is meaningless
s.bad = 0;
set_locked(false);
return;
}
// linuxptp PI gains for hardware timestamps, scaled by the Sync interval
double iv = ldexp(1.0, s.log_sync);
double kp = fmin(0.7 * pow(iv, -0.3), 0.7 / iv);
double ki = fmin(0.3 * pow(iv, 0.4), 0.3 / iv);
double ppb = kp * offset + s.drift_ppb;
s.drift_ppb = fmax(-MAX_DRIFT_PPB, fmin(MAX_DRIFT_PPB, s.drift_ppb + ki * offset));
s.freq_ppb = -ppb;
ptp_hw_adj_freq(s.freq_ppb);
if (llabs(offset) < LOCK_NS) {
s.bad = 0;
if (++s.good >= LOCK_GOOD) {
set_locked(true);
}
} else {
s.good = 0;
if (s.locked && ++s.bad >= LOCK_BAD) {
set_locked(false);
}
}
}
static void sync_complete(void) static void sync_complete(void)
{ {
s.sync_pending = false; s.sync_pending = false;
@@ -164,9 +241,9 @@ static void sync_complete(void)
return; return;
} }
// offset = t2 - t1 - corrections - mean path delay // offset = t2 - t1 - corrections - mean path delay
int64_t offset = s.raw - s.delay_ns; servo(s.raw - s.delay_ns);
ESP_LOGI(TAG, "seq %u: offset %+lld ns, path delay %lld ns, rate %+.3f ppm", s.sync_seq, offset, s.delay_ns, ESP_LOGI(TAG, "seq %u: offset %+lld ns, freq %+.3f ppm, path delay %lld ns%s", s.sync_seq, s.offset_ns,
s.rate * 1e6); s.freq_ppb / 1000, s.delay_ns, s.locked ? ", locked" : "");
} }
static void on_sync(const uint8_t *b, int len) static void on_sync(const uint8_t *b, int len)
@@ -181,6 +258,7 @@ static void on_sync(const uint8_t *b, int len)
return; return;
} }
s.sync_seq = seq; s.sync_seq = seq;
s.log_sync = (int8_t)b[33];
s.t2 = mac_ns(&t2); s.t2 = mac_ns(&t2);
s.sync_corr = rd_corr_ns(b + 8); s.sync_corr = rd_corr_ns(b + 8);
if (rd16(b + 6) & FLAG_TWO_STEP) { if (rd16(b + 6) & FLAG_TWO_STEP) {
@@ -309,6 +387,8 @@ static void ptp_task(void *arg)
ESP_LOGW(TAG, "TimeTransmitter lost (no Announce for %lld ms), listening", window / 1000); ESP_LOGW(TAG, "TimeTransmitter lost (no Announce for %lld ms), listening", window / 1000);
memset(&s.gm, 0, sizeof(s.gm)); memset(&s.gm, 0, sizeof(s.gm));
s.delay_ns = s.prev_t2 = 0; s.delay_ns = s.prev_t2 = 0;
s.stepped = false;
set_locked(false); // frequency correction stays (holdover)
} else if (now >= s.next_dreq_us && s.prev_t2) { } else if (now >= s.next_dreq_us && s.prev_t2) {
send_delay_req(); send_delay_req();
// Delay_Req interval from the GM's Delay_Resp; randomised 0.5..1.5x // Delay_Req interval from the GM's Delay_Resp; randomised 0.5..1.5x
+32
View File
@@ -77,6 +77,38 @@ esp_err_t ptp_hw_get_time(eth_mac_time_t *t)
return esp_eth_ioctl(s_eth, ETH_MAC_ESP_CMD_G_PTP_TIME, t); return esp_eth_ioctl(s_eth, ETH_MAC_ESP_CMD_G_PTP_TIME, t);
} }
esp_err_t ptp_hw_adj_freq(double ppb)
{
if (ppb > 500000) {
ppb = 500000; // +-500 ppm is far outside any sane crystal
} else if (ppb < -500000) {
ppb = -500000;
}
int32_t v = (int32_t)(ppb >= 0 ? ppb + 0.5 : ppb - 0.5);
esp_err_t err = ESP_ERR_INVALID_STATE;
// The previous addend update may still be in progress: retry briefly.
for (int i = 0; i < 10 && err == ESP_ERR_INVALID_STATE; i++) {
err = esp_eth_ioctl(s_eth, ETH_MAC_ESP_CMD_ADJ_PTP_TIME, &v);
}
return err;
}
esp_err_t ptp_hw_step(int64_t offset_ns)
{
eth_mac_time_t t;
esp_err_t err = ptp_hw_get_time(&t);
if (err != ESP_OK) {
return err;
}
int64_t ns = (int64_t)t.seconds * 1000000000LL + t.nanoseconds - offset_ns;
if (ns < 0) {
return ESP_ERR_INVALID_ARG;
}
t.seconds = ns / 1000000000LL;
t.nanoseconds = ns % 1000000000LL;
return esp_eth_ioctl(s_eth, ETH_MAC_ESP_CMD_S_PTP_TIME, &t);
}
static uint16_t ip_checksum(const uint8_t *h, size_t len) static uint16_t ip_checksum(const uint8_t *h, size_t len)
{ {
uint32_t sum = 0; uint32_t sum = 0;
+4
View File
@@ -20,6 +20,10 @@ esp_err_t ptp_hw_init(esp_eth_handle_t eth, esp_netif_t *netif);
// sequence ID and sourcePortIdentity (10 bytes). Each record is returned once. // sequence ID and sourcePortIdentity (10 bytes). Each record is returned once.
bool ptp_hw_rx_ts(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, eth_mac_time_t *ts); bool ptp_hw_rx_ts(uint8_t msg_type, uint16_t seq, const uint8_t *src_port_id, eth_mac_time_t *ts);
esp_err_t ptp_hw_get_time(eth_mac_time_t *t); esp_err_t ptp_hw_get_time(eth_mac_time_t *t);
// Frequency correction relative to the nominal rate, in ppb (positive = faster).
esp_err_t ptp_hw_adj_freq(double ppb);
// Step the clock back by offset_ns (offset = local - GM), via read + write.
esp_err_t ptp_hw_step(int64_t offset_ns);
// Send a PTP event message as a raw Ethernet/IPv4/UDP frame (port 319 -> 319) and return its // Send a PTP event message as a raw Ethernet/IPv4/UDP frame (port 319 -> 319) and return its
// hardware TX timestamp. dst_ip/dst_mac: 224.0.1.129 / 01:00:5e:00:01:81 or the GM unicast. // hardware TX timestamp. dst_ip/dst_mac: 224.0.1.129 / 01:00:5e:00:01:81 or the GM unicast.
esp_err_t ptp_hw_send_event(const uint8_t dst_mac[6], uint32_t dst_ip, uint8_t dscp, esp_err_t ptp_hw_send_event(const uint8_t dst_mac[6], uint32_t dst_ip, uint8_t dscp,