Re-implementation of rustbootd's ecat_el6695_* examples as a single binary with fixes found in review and on hardware: - DC-follow PLL hardened against period-2 hunting: slew-limited anchor (+/-50us/cycle), bistable-trap snap re-anchor, re-prime on stale deadline - drift-free absolute-grid ticker mode; probe mode for timestamp forensics - bounded-memory online stats (histograms), graceful SIGINT/SIGTERM shutdown with full report, error-streak abort - timestamp plausibility filter comparing against the previous raw sample (avoids the deadlock after a startup outlier) - XFC scope waveform options: --el2202 (with --el2202-dual), --el2262, --el1252 latch timestamp readback with per-channel edge statistics - register access unified in regs.rs: named bit constants everywhere, read-modify-write for enable/activation bytes - vendored patched ethercrab 0.7.1 (sdo_write_complete, send_raw_coe) Verified on J1900 (PREEMPT_RT 6.6.135): 600k cycles/600s exact 1 kHz, tx/rx zero errors, phase_err p50=164us std=5us; EL2202<->EL1252 loopback edge interval mean 2000.24us std=24.65us.
170 lines
5.3 KiB
Rust
170 lines
5.3 KiB
Rust
//! Linux real-time helpers: memory locking, SCHED_FIFO promotion, CPU
|
|
//! affinity, stack prefaulting and absolute-time periodic wakeups.
|
|
//!
|
|
//! Everything is thin `libc` wrappers; `sched_setscheduler` goes through a raw
|
|
//! syscall because statically-linked musl builds have been observed to return
|
|
//! ENOSYS through the libc wrapper.
|
|
|
|
use std::io::Error as IoError;
|
|
|
|
#[derive(Debug)]
|
|
pub struct RtError {
|
|
pub op: &'static str,
|
|
pub source: IoError,
|
|
}
|
|
|
|
impl std::fmt::Display for RtError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{} failed: {}", self.op, self.source)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for RtError {}
|
|
|
|
fn sys_err(op: &'static str) -> RtError {
|
|
RtError {
|
|
op,
|
|
source: IoError::last_os_error(),
|
|
}
|
|
}
|
|
|
|
/// CLOCK_MONOTONIC in nanoseconds.
|
|
pub fn now_ns() -> u64 {
|
|
// SAFETY: ts is fully written by clock_gettime on success.
|
|
unsafe {
|
|
let mut ts: libc::timespec = core::mem::zeroed();
|
|
libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts);
|
|
(ts.tv_sec as u64) * 1_000_000_000 + ts.tv_nsec as u64
|
|
}
|
|
}
|
|
|
|
/// Absolute-time sleep on CLOCK_MONOTONIC, retrying on EINTR.
|
|
pub fn sleep_until_ns(deadline_ns: u64) {
|
|
let mut ts: libc::timespec = unsafe { core::mem::zeroed() };
|
|
ts.tv_sec = (deadline_ns / 1_000_000_000) as _;
|
|
ts.tv_nsec = (deadline_ns % 1_000_000_000) as _;
|
|
loop {
|
|
// SAFETY: ts is a valid timespec; remainder pointer is null.
|
|
let rc = unsafe {
|
|
libc::clock_nanosleep(
|
|
libc::CLOCK_MONOTONIC,
|
|
libc::TIMER_ABSTIME,
|
|
&ts,
|
|
core::ptr::null_mut(),
|
|
)
|
|
};
|
|
if rc != libc::EINTR {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sleep until `deadline_ns`, but hand off to a spin loop for the final
|
|
/// `spin_ns` to minimise wakeup latency. Cheap when already past deadline.
|
|
pub fn hybrid_sleep_until(deadline_ns: u64, spin_ns: u64) {
|
|
loop {
|
|
let now = now_ns();
|
|
if now >= deadline_ns {
|
|
return;
|
|
}
|
|
let remain = deadline_ns - now;
|
|
if remain > spin_ns {
|
|
sleep_until_ns(deadline_ns - spin_ns);
|
|
} else {
|
|
std::hint::spin_loop();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Lock all current and future process memory (no page faults in the RT loop).
|
|
pub fn lock_all_memory() -> Result<(), RtError> {
|
|
// SAFETY: mlockall has no memory-safety preconditions.
|
|
let rc = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
|
|
if rc != 0 {
|
|
return Err(sys_err("mlockall"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Promote the current thread: pin to `core_id`, SCHED_FIFO `fifo_priority`,
|
|
/// and (optionally) lock memory.
|
|
pub fn promote_current_thread(
|
|
core_id: usize,
|
|
fifo_priority: u8,
|
|
lock_memory: bool,
|
|
) -> Result<(), RtError> {
|
|
// SAFETY: cpu_set is zero-initialised then filled via the macros.
|
|
unsafe {
|
|
let mut set: libc::cpu_set_t = core::mem::zeroed();
|
|
libc::CPU_ZERO(&mut set);
|
|
libc::CPU_SET(core_id, &mut set);
|
|
let rc = libc::sched_setaffinity(0, core::mem::size_of::<libc::cpu_set_t>(), &set);
|
|
if rc != 0 {
|
|
return Err(sys_err("sched_setaffinity"));
|
|
}
|
|
}
|
|
|
|
// Raw syscall avoids musl/glibc ABI differences (ENOSYS on static musl).
|
|
// SAFETY: param is a valid sched_param for the current thread.
|
|
unsafe {
|
|
let mut param: libc::sched_param = core::mem::zeroed();
|
|
param.sched_priority = fifo_priority as libc::c_int;
|
|
let rc = libc::syscall(libc::SYS_sched_setscheduler, 0 as libc::c_int, libc::SCHED_FIFO, ¶m);
|
|
if rc != 0 {
|
|
return Err(sys_err("sched_setscheduler"));
|
|
}
|
|
}
|
|
|
|
if lock_memory {
|
|
lock_all_memory()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Fault in `bytes` of stack now so the RT loop never takes a minor fault.
|
|
pub fn prefault_stack(bytes: usize) {
|
|
const CHUNK: usize = 4096;
|
|
let mut remaining = bytes;
|
|
while remaining > 0 {
|
|
let mut buf = [0u8; CHUNK];
|
|
for (i, b) in buf.iter_mut().enumerate() {
|
|
*b = (i & 0xff) as u8;
|
|
}
|
|
core::hint::black_box(&buf);
|
|
remaining = remaining.saturating_sub(CHUNK);
|
|
}
|
|
}
|
|
|
|
/// Absolute-time periodic ticker. Deadlines are anchored to an absolute
|
|
/// grid, so a single late cycle does not shift subsequent ones (no drift).
|
|
pub struct Ticker {
|
|
next_wakeup_ns: u64,
|
|
period_ns: u64,
|
|
}
|
|
|
|
impl Ticker {
|
|
/// First deadline is `now + period`.
|
|
pub fn new(period_ns: u64) -> Self {
|
|
Self {
|
|
next_wakeup_ns: now_ns().saturating_add(period_ns),
|
|
period_ns,
|
|
}
|
|
}
|
|
|
|
/// Sleep until the next grid point. Returns wake jitter in ns
|
|
/// (actual wake time minus planned deadline; positive = late).
|
|
pub fn wait_next_period(&mut self) -> i64 {
|
|
let deadline = self.next_wakeup_ns;
|
|
sleep_until_ns(deadline);
|
|
let actual = now_ns();
|
|
// Skip grid points already missed so we re-sync instead of
|
|
// hammering through a backlog after a long overrun.
|
|
self.next_wakeup_ns = self.next_wakeup_ns.saturating_add(self.period_ns);
|
|
if self.next_wakeup_ns <= actual {
|
|
let missed = (actual - self.next_wakeup_ns) / self.period_ns + 1;
|
|
self.next_wakeup_ns = self.next_wakeup_ns.saturating_add(missed * self.period_ns);
|
|
}
|
|
actual as i64 - deadline as i64
|
|
}
|
|
}
|