Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,14 @@ if(ARIEC61850_BUILD_TESTS)
add_test(NAME ariec61850_m4_multi_profile_staging_tests
COMMAND ariec61850_m4_multi_profile_staging_tests)

add_executable(ariec61850_m4_three_sv_grid_tests tests/test_m4_three_sv_grid.cpp)
target_link_libraries(ariec61850_m4_three_sv_grid_tests PRIVATE ARIEC61850::core)
target_compile_features(ariec61850_m4_three_sv_grid_tests PRIVATE cxx_std_20)
ariec61850_apply_warnings(ariec61850_m4_three_sv_grid_tests)
ariec61850_apply_sanitizers(ariec61850_m4_three_sv_grid_tests)
add_test(NAME ariec61850_m4_three_sv_grid_tests
COMMAND ariec61850_m4_three_sv_grid_tests)

add_executable(ariec61850_simulation_tests tests/test_simulation.cpp)
target_link_libraries(ariec61850_simulation_tests PRIVATE ARIEC61850::core)
target_compile_definitions(ariec61850_simulation_tests PRIVATE
Expand Down
100 changes: 100 additions & 0 deletions include/ariec61850/sampled_values/m4_three_sv_grid.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "ariec61850/sampled_values/m4_three_sv_roster.hpp"

#include <array>
#include <cstddef>
#include <cstdint>

namespace ar::iec61850::sampled_values {

// Deterministic shared-clock tick planning: this is NOT the PTP servo and
// NEVER independently fabricates lock. Firmware supplies the actual qualified
// lease and sample counter from its one GPTimer/PTP authority.
struct M4GridTick final {
std::uint64_t ptp_generation{};
std::uint64_t tick_sequence{}; // absolute 4000Hz grid slot, never TX attempts
std::uint8_t smp_synch{}; // caller's common PTP decision: 0,1,2
bool timing_qualified{};
};

struct M4GridFrame final {
std::uint8_t slot{}; // 0..2 maps to a distinct SCD MU
std::uint16_t smp_cnt{}; // one common epoch for every enabled MU
std::uint8_t smp_synch{}; // same PTP decision, never per-MU override
};

struct M4GridBatch final {
std::array<M4GridFrame, m4_three_sv_capacity> frames{};
std::uint8_t count{};
std::uint64_t tick_sequence{};
std::uint64_t ptp_generation{};
std::uint64_t missed_ticks{};
bool timing_discontinuity{}; // stale PTP lease must not emit stale sync
};

class M4ThreeSvGridPlanner final {
public:
// A new common reference epoch is accepted ONLY by the existing external
// PTP-to-GPTimer owner. No stream-specific reset of the sample clock.
void reset(const std::uint64_t ptp_generation) noexcept {
generation_ = ptp_generation;
initialized_ = ptp_generation != 0U;
observed_tick_ = false;
last_tick_ = 0U;
}

void set_enabled(const std::size_t slot, const bool enabled) noexcept {
if (slot < enabled_.size()) enabled_[slot] = enabled;
}

[[nodiscard]] bool enabled(const std::size_t slot) const noexcept {
return slot < enabled_.size() && enabled_[slot];
}

[[nodiscard]] M4GridBatch plan(const M4GridTick tick) noexcept {
M4GridBatch batch{};
batch.tick_sequence = tick.tick_sequence;
batch.ptp_generation = tick.ptp_generation;

if (!initialized_ || tick.ptp_generation == 0U ||
tick.ptp_generation != generation_ || !tick.timing_qualified ||
tick.smp_synch > 2U) {
batch.timing_discontinuity = true;
// Revocation is group-wide: callers must explicitly re-arm on
// the newly qualified PTP epoch, never auto-promote synchrony.
initialized_ = false;
return batch;
}

if (observed_tick_ && tick.tick_sequence <= last_tick_) {
batch.timing_discontinuity = true;
initialized_ = false;
return batch;
}
if (observed_tick_) {
batch.missed_ticks = tick.tick_sequence - last_tick_ - 1U;
}
observed_tick_ = true;
last_tick_ = tick.tick_sequence;

const auto counter = static_cast<std::uint16_t>(
tick.tick_sequence % m4_three_sv_rate_hz);
for (std::size_t slot = 0U; slot < enabled_.size(); ++slot) {
if (!enabled_[slot]) continue;
batch.frames[batch.count++] = M4GridFrame{
static_cast<std::uint8_t>(slot), counter, tick.smp_synch};
}
return batch;
}

private:
std::array<bool, m4_three_sv_capacity> enabled_{{true, true, true}};
std::uint64_t generation_{};
std::uint64_t last_tick_{};
bool initialized_{};
bool observed_tick_{};
};

} // namespace ar::iec61850::sampled_values
88 changes: 88 additions & 0 deletions tests/test_m4_three_sv_grid.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ariec61850/sampled_values/m4_three_sv_grid.hpp"

#include <cstddef>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>

namespace {
#define CHECK(condition) do { if (!(condition)) { \
throw std::runtime_error(std::string{"M4 shared grid: "} + #condition + \
" line " + std::to_string(__LINE__)); \
} } while (false)

using namespace ar::iec61850::sampled_values;

void verify_synchronous_three_stream_planning() {
M4ThreeSvGridPlanner scheduler;
CHECK(scheduler.enabled(0U) && scheduler.enabled(1U) && scheduler.enabled(2U));
scheduler.reset(123U);
std::uint32_t generated = 0U;
for (std::uint64_t tick = 0U; tick < 8000U; ++tick) {
const auto batch = scheduler.plan({123U, tick, 2U, true});
CHECK(!batch.timing_discontinuity);
CHECK(batch.count == 3U);
CHECK(batch.missed_ticks == 0U);
for (std::size_t slot = 0U; slot < 3U; ++slot) {
CHECK(batch.frames[slot].slot == slot);
CHECK(batch.frames[slot].smp_cnt == (tick % 4000U));
CHECK(batch.frames[slot].smp_synch == 2U);
}
generated += batch.count;
}
CHECK(generated == 24000U); // exactly 12000/s across 2 simulated seconds
}

void verify_per_stream_isolation_and_group_revocation() {
M4ThreeSvGridPlanner planner;
planner.reset(41U);
auto batch = planner.plan({41U, 3998U, 1U, true});
CHECK(batch.count == 3U && batch.frames[0].smp_cnt == 3998U);
planner.set_enabled(1U, false);
batch = planner.plan({41U, 3999U, 1U, true});
CHECK(batch.count == 2U);
CHECK(batch.frames[0].slot == 0U && batch.frames[1].slot == 2U);
CHECK(batch.frames[0].smp_cnt == 3999U && batch.frames[1].smp_cnt == 3999U);

planner.set_enabled(1U, true);
batch = planner.plan({41U, 4000U, 1U, true});
CHECK(batch.count == 3U);
CHECK(batch.frames[1].slot == 1U && batch.frames[1].smp_cnt == 0U);
CHECK(batch.frames[0].smp_cnt == batch.frames[2].smp_cnt);

batch = planner.plan({41U, 4004U, 1U, true});
CHECK(batch.count == 3U && batch.missed_ticks == 3U);
CHECK(batch.frames[0].smp_cnt == 4U); // skip late samples, no stale replay

batch = planner.plan({42U, 4005U, 2U, true});
CHECK(batch.count == 0U && batch.timing_discontinuity);
batch = planner.plan({42U, 4006U, 2U, true});
CHECK(batch.count == 0U); // no unqualified automatic re-arm
planner.reset(42U);
batch = planner.plan({42U, 4007U, 2U, true});
CHECK(batch.count == 3U && !batch.timing_discontinuity);

batch = planner.plan({42U, 4008U, 2U, false});
CHECK(batch.count == 0U && batch.timing_discontinuity);
planner.reset(42U);
batch = planner.plan({42U, 4009U, 0U, true});
CHECK(batch.count == 3U && batch.frames[0].smp_synch == 0U);
batch = planner.plan({42U, 4009U, 0U, true});
CHECK(batch.count == 0U && batch.timing_discontinuity);
}
}

int main() {
try {
verify_synchronous_three_stream_planning();
verify_per_stream_isolation_and_group_revocation();
std::cout << "PASS M4 shared 4kHz sample grid: three independent slots, "
"one epoch/smpCnt/smpSynch, skipped slots and PTP revocation\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
Loading