Skip to main content
Version: 1.1.1

Real-time behavior and timing

Goal: understand how the LXMASTER cyclic thread is scheduled, how the bus stays time-synchronized, and how to measure timing quality from inside your application using the built-in diagnostics.

Up to now you have trusted start() to "make it real-time." This tutorial opens that box: what the real-time thread is, the knobs that govern it, and the statistics the library collects so you can prove your machine holds timing.

EtherCAT background: why timing is everything

Real-time EtherCAT is built on two forms of timing:

  • Host cycle timing (jitter). The master runs a cyclic thread that must wake up at a fixed period (the ENI's cycle time) and send a frame. If the OS wakes it late, the frame is late. Jitter is the signed error between when the thread should have woken and when it actually did.
  • Distributed Clocks (DC). Every DC-capable slave has a local clock. The bus designates one slave's clock as the reference and disciplines all the others to it, then fires a SYNC0 pulse at the top of each cycle so all devices act on data at the same instant. The master must also align its cyclic wake to that DC reference — the residual error is the DC-sync alignment error.

Good motion needs both small: low jitter so frames leave on time, and tight DC alignment so distributed drives act in lockstep. See Clocks and Synchronization for the full picture.

The real-time thread and its knobs

start() applies real-time scheduling to the cyclic thread before it brings the bus up. The settings live in cfg.rt:

FieldDefaultMeaning
enable_rt_schedulingtruePut the cyclic thread on SCHED_FIFO. Set false on stock kernels / dev machines.
rt_priority49SCHED_FIFO priority (1–99). 49 sits above IRQ threads but below the watchdog.
cpu_affinity-1Pin the thread to this core (0-based). -1 = no affinity. Pin to an isolated core for best jitter.

You normally do not set these in code. NetworkConfig::defaults() reads them from the per-host environment published by lxmaster host setup (via LXMASTER_RT_ENABLE, LXMASTER_RT_PRIORITY, LXMASTER_RT_CPU, …), so the host profile is the single place RT identity is configured. Overriding in code is for special cases:

lxmaster::NetworkConfig cfg = lxmaster::NetworkConfig::defaults();
cfg.rt.enable_rt_scheduling = false; // e.g. a laptop with no RT kernel
The kernel still matters

Configuration only asks for real-time scheduling; the quality you get depends on the kernel. A PREEMPT_RT kernel with an isolated core gives the tightest jitter. See the host tutorial for setting up an RT host, and the diagnostics tutorial for measuring the host itself.

Sync mode is decided by the ENI

You do not choose DC vs free-run — the ENI does. If the bus carries a SYNC0-capable device, LXMASTER runs in DcSync0 mode (cyclic wake disciplined to the DC reference); otherwise it falls back to SmEvent (sync-manager event) mode. Read it back after bring-up:

switch (net.syncMode()) {
case lxmaster::SyncMode::DcSync0: std::cout << "DC-synchronized\n"; break;
case lxmaster::SyncMode::SmEvent: std::cout << "SM-event (no DC)\n"; break;
}

DC tuning parameters (PI gains, the OP-entry alignment gate) also come from the host environment and are inert in SmEvent mode.

Measuring jitter

After a run, jitterStats() summarizes the host cycle timing (all values in nanoseconds):

net.stop();
const auto j = net.jitterStats();
std::cout << "jitter min=" << j.min_ns << " max=" << j.max_ns
<< " mean=" << j.mean_ns << " mean|abs|=" << j.mean_abs_ns
<< " over " << j.samples << " cycles\n";
FieldWhat it tells you
min_ns / max_nsWorst early / late wake. The spread is your timing envelope.
mean_nsSystematic offset — should sit near zero.
mean_abs_nsAverage timing error magnitude regardless of sign.
samplesCycles included.

jitterStats() is also live during the run, so you can print a rolling summary.

Measuring DC-sync alignment

In DcSync0 mode, dcSyncStats() reports how tightly the master's wake tracks the DC reference — the error before the PI controller corrects it:

const auto dc = net.dcSyncStats();
if (dc.samples > 0) { // 0 in SmEvent mode
std::cout << "dc-sync mean=" << dc.mean_ns << " max|abs|=" << dc.mean_abs_ns
<< " converged offset=" << dc.final_integral << " ns\n";
}

final_integral is the steady-state offset the PI loop converged to — useful for spotting a systematic host↔bus clock skew. samples == 0 means you were not in a DC-aligned mode.

Per-cycle tracing

Aggregate stats hide transients. To capture every cycle, enable the sync trace ring buffer before start():

cfg.debug.sync_trace_capacity = 16384; // power-of-two slots; 0 = disabled
cfg.debug.sync_trace_window_ns = 10000; // count cycles where |dc_delta| > 10 us

While running, syncTraceViolationCount() gives a live tally of cycles that breached the window. After stop(), pull the full history:

const auto report = net.syncTraceReport();
std::cout << report.samples.size() << " cycles captured, "
<< report.violation_count << " violations, "
<< report.total_writes << " total cycles\n";

for (const auto& s : report.samples) {
// s.rt_cycle, s.dc_delta_ns, s.jitter_err_ns, s.integral_ns, s.phase ...
}

Each SyncTraceSample carries the cycle number, the DC delta, the host jitter, the PI integrator state, and the execution phase (Warmup, DcGate, Cooldown, Operational, Shutdown). Dumping these to CSV and plotting them is the best way to diagnose an intermittent timing glitch — you can see exactly which cycle and phase it happened in.

Tracing has a cost

The ring is lock-free and cheap, but not free. Enable it while investigating, size it to the window you care about, and leave sync_trace_capacity = 0 in production unless you want a permanent flight recorder.

Application vs. host diagnostics

These in-process statistics measure your running application. To validate the host before you deploy — raw scheduling latency, behavior under load — use the CLI: lxmaster diag runs cyclictest-style jitter and stress soak tests on the isolated RT core. Use the CLI to qualify the machine; use the APIs here to monitor the real workload.

For error strings and the step-by-step runtime log (including the DcSync and Cyclic log categories that complement these timing stats), see Errors, logging, and debugging.

Recap

  • The cyclic thread runs on SCHED_FIFO; its priority and core come from the host environment (cfg.rt, populated by NetworkConfig::defaults()).
  • Sync mode (DcSync0 vs SmEvent) is decided by the ENI, not the app.
  • jitterStats() measures host cycle timing; dcSyncStats() measures DC alignment (only in DcSync0).
  • Enable sync_trace_capacity for per-cycle forensics via syncTraceReport().
  • Qualify the host with lxmaster diag; monitor the workload with these APIs.

Next

Timing tells you the bus is healthy. Next: what happens — and what you do — when it is not, in Detecting and handling bus faults.