Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions embedded/esp32p4_smv_injector/main/app_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ struct SampleClockContext final {
std::atomic<std::int32_t> cumulative_slew_ticks{0};
std::atomic<std::uint32_t> publisher_fault_generation{0U};
std::atomic<bool> phase_authority_valid{false};
// Last successfully observed sample epoch (published only by the 1Hz
// control-plane observer). The 4kHz producer checks freshness without
// waiting for that observer or consulting blocking MAC/PTP APIs.
std::atomic<std::uint32_t> last_phase_observed_epoch{0U};
// Immutable for one monitored timer session; read under phase mutex.
SvPtpClockLease armed_ptp_clock{};
SvBoundedSlewCursor phase_slew{};
Expand Down Expand Up @@ -617,6 +621,7 @@ gptimer_handle_t start_sample_clock(
g_sample_clock_context.cumulative_slew_ticks.store(0, std::memory_order_relaxed);
g_sample_clock_context.publisher_fault_generation.store(0U, std::memory_order_relaxed);
g_sample_clock_context.phase_authority_valid.store(false, std::memory_order_relaxed);
g_sample_clock_context.last_phase_observed_epoch.store(0U, std::memory_order_relaxed);
g_sample_clock_context.armed_ptp_clock = {};
g_sample_clock_context.phase_index = 0U;
g_sample_clock_context.previous_phase_healthy = false;
Expand Down Expand Up @@ -708,6 +713,8 @@ void stop_sample_clock(gptimer_handle_t& timer) noexcept {
0, std::memory_order_relaxed);
g_sample_clock_context.phase_authority_valid.store(
false, std::memory_order_release);
g_sample_clock_context.last_phase_observed_epoch.store(
0U, std::memory_order_release);
g_sample_clock_context.armed_ptp_clock = {};
#endif
static_cast<void>(gptimer_stop(timer));
Expand Down Expand Up @@ -1000,6 +1007,11 @@ void publisher_task(void* argument) {
if (smp_synch_source_local_qualified() &&
(!g_sample_clock_context.phase_authority_valid.load(
std::memory_order_acquire) ||
!ar::iec61850::time_sync::SvPhaseObservationFreshness::fresh(
g_sample_clock_context.last_phase_observed_epoch.load(
std::memory_order_acquire),
g_sample_clock_context.epoch_generation.load(
std::memory_order_acquire)) ||
ar_ptp_lab_source_generation() !=
g_sample_clock_context.armed_ptp_clock.generation)) {
smp_synch_set_source_local_qualified(false);
Expand Down Expand Up @@ -1344,6 +1356,10 @@ void sv_ptp_phase_observer_task(void* argument) {
// The PRE-START clock must truly steer while SV TX is OFF.
// The old live_tx_running() gate invalidated its servo on every
// 1Hz observation; capture verified 120s of smpSynch=0.
g_sample_clock_context.last_phase_observed_epoch.store(
decision.epoch_second_continuous
? observation.last_sv_epoch_generation : 0U,
std::memory_order_release);
g_sample_clock_context.phase_authority_valid.store(
decision.epoch_second_continuous, std::memory_order_release);
g_sample_clock_context.requested_slew_ticks.store(
Expand Down
16 changes: 16 additions & 0 deletions include/ariec61850/time_sync/sv_ptp_phase_authority.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,22 @@ class SvPtpPhaseAuthority final {
SvPtpPhaseState state_{SvPtpPhaseState::acquiring};
};

// A stalled/late 1Hz phase observer must not leave smpSynch=1 on the 4kHz
// wire indefinitely. Called by the publisher as an O(1) atomic-read-only
// LAST-RESORT lease check; known source/fault/phase failures still revoke
// immediately. This is not a new clock or qualification authority.
struct SvPhaseObservationFreshness final {
static constexpr std::uint32_t kMaxUnobservedEpochs = 1U;
[[nodiscard]] static constexpr bool fresh(
const std::uint32_t observed_epoch,
const std::uint32_t current_epoch) noexcept {
// A zero generation is never a valid sampled PTP second. Unsigned
// subtraction naturally handles the 32-bit epoch counter rollover.
return observed_epoch != 0U && current_epoch != 0U &&
current_epoch - observed_epoch <= kMaxUnobservedEpochs;
}
};

// A control-plane gate for evidence that a canonical SV publisher has not
// skipped sample slots or failed Ethernet TX since the previous observation.
// A fresh session and any fault require re-acquisition; neither can inherit
Expand Down
22 changes: 22 additions & 0 deletions tests/test_sv_ptp_phase_authority.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ void check_auto_preflight_does_not_invent_sync_or_start() {
}
}

void check_phase_observer_stall_revokes_source_wire_claim() {
using Fresh = SvPhaseObservationFreshness;
static_assert(Fresh::kMaxUnobservedEpochs == 1U);
// After a qualified observation at epoch 25, the producer may continue
// into the next second while the observer runs at t+250ms. An observer
// stall cannot advertise LOCAL through the following epoch.
CHECK(!Fresh::fresh(0U, 25U));
CHECK(!Fresh::fresh(25U, 0U));
CHECK(Fresh::fresh(25U, 25U));
CHECK(Fresh::fresh(25U, 26U));
CHECK(!Fresh::fresh(25U, 27U));
CHECK(!Fresh::fresh(25U, 100U));
CHECK(!Fresh::fresh(26U, 25U)); // future/stale generation
// No retained qualification across a 32-bit generation wrap; only a
// genuinely adjacent observation may pass.
CHECK(Fresh::fresh(0xfffffffeU, 0xffffffffU));
CHECK(!Fresh::fresh(0xfffffffeU, 1U));
CHECK(!Fresh::fresh(0xffffffffU, 0U)); // zero remains invalid
CHECK(Fresh::fresh(0xffffffffU, 1U) == false); // two skipped epochs
}

void check_phase_sign_and_bounded_corrections() {
SvPtpPhaseAuthority controller;
const auto warm = controller.observe(snapshot(1U, 220));
Expand Down Expand Up @@ -397,6 +418,7 @@ void check_continuous_ptp_second_binding() {
int main() {
try {
check_auto_preflight_does_not_invent_sync_or_start();
check_phase_observer_stall_revokes_source_wire_claim();
check_phase_sign_and_bounded_corrections();
check_uncertain_brackets_never_claim_software_tracking();
check_per_second_slew_distribution();
Expand Down
Loading