Real-Time Dual Execution Domain
EnerOS adopts a dual execution domain architecture: the General Domain carries Agent orchestration, AI inference, and business logic; the Real-time Domain carries protection logic, switching operations, and closed-loop control, guaranteeing P99 < 1ms deterministic latency. The two domains communicate through kernel-space shared memory, avoiding process switching overhead.
The power system has stringent latency requirements: relay protection must act within 4 cycles (80ms @ 50Hz), PMU phasor calculation requires output above 30 Hz, and closed-loop AVR control cycles are at the 100 ms level. The millisecond-level jitter of a general-purpose operating system (Linux CFS scheduling) cannot meet these scenarios. EnerOS implements “fast-slow separation” at the kernel level through the dual execution domain.
Architecture Overview
┌─────────────────────────────────────────────────┐
│ General Domain │
│ - Agent orchestration │
│ - LLM inference │
│ - Business process │
│ - Time-series analysis │
│ - Scheduling: CFS (Completely Fair Scheduler) │
└──────────────────┬──────────────────────────────┘
│ Shared Memory / Lock-free Queue
│ Latency < 10μs
┌──────────────────┴──────────────────────────────┐
│ Real-time Domain │
│ - Protection logic (4ms) │
│ - Closed-loop control (1ms) │
│ - Switching operations (100μs) │
│ - Phasor calculation (250μs) │
│ - Scheduling: SCHED_FIFO / PREEMPT_RT / Xenomai │
└─────────────────────────────────────────────────┘
Domain Responsibility Division
| Task Type | General Domain | Real-time Domain |
|---|
| Agent orchestration | ✅ | - |
| LLM inference | ✅ | - |
| Load flow calculation | ✅ | - |
| State estimation | ✅ | 100ms cycle version |
| Digital twin What-If | ✅ | - |
| Protection logic (distance protection) | - | ✅ |
| Closed-loop control (AVR / AGC) | - | ✅ |
| Switching operations (GOOSE) | - | ✅ |
| Phasor calculation (PMU) | - | ✅ |
| Fault recording trigger | - | ✅ |
Real-time Domain Task Definition
Real-time tasks are defined via the RealtimeTask trait, following the periodic trigger model:
use eneros_realtime::{RealtimeTask, RtContext, RtResult};
use std::time::Duration;
struct ProtectionLogic {
period: Duration,
breaker_id: u32,
zone_reach: f64, // Ω
}
impl RealtimeTask for ProtectionLogic {
fn period(&self) -> Duration { self.period }
fn priority(&self) -> i32 { 99 } // SCHED_FIFO priority
fn cpu_affinity(&self) -> Option<Vec<u32>> {
Some(vec![0, 1]) // Pin to CPU 0,1
}
fn run(&mut self, ctx: &mut RtContext) -> RtResult<()> {
// Read PMU data (shared memory, zero-copy)
let pmu = ctx.pmu_snapshot();
// Distance protection algorithm
let z_seen = self.compute_impedance(&pmu);
if z_seen < self.zone_reach {
// Trip immediately (lock-free channel, < 100μs)
ctx.trip_breaker(self.breaker_id);
ctx.log_event(ProtectionEvent::Trip {
impedance: z_seen,
reach: self.zone_reach,
});
}
Ok(())
}
}
impl ProtectionLogic {
fn compute_impedance(&self, pmu: &PmuSnapshot) -> f64 {
let v = pmu.voltage_complex();
let i = pmu.current_complex();
(v / i).norm()
}
}
| Method | Type | Required | Description |
|---|
| period() | Duration | Yes | Task period |
| priority() | i32 | Yes | SCHED_FIFO priority (1-99) |
| cpu_affinity() | Option<Vec> | No | CPU affinity |
| run(&mut ctx) | RtResult | Yes | Task body |
| init(&mut ctx) | RtResult | No | Initialization |
| on_overrun(dur) | RtResult | No | Timeout callback |
| shutdown(&mut ctx) | RtResult | No | Cleanup on exit |
Task Scheduling
The real-time domain adopts priority-based preemptive scheduling, combined with Linux PREEMPT_RT or Xenomai:
use eneros_realtime::{RtScheduler, SchedPolicy, IsolationConfig};
let mut sched = RtScheduler::new()
.policy(SchedPolicy::Fifo)
.cpu_affinity(0..2) // Isolate CPU 0,1
.isolate_from_general(true)
.isolation(IsolationConfig {
isolcpus: true, // Kernel boot parameter isolcpus
nohz_full: true, // NO_HZ_FULL
rcu_nocbs: true, // RCU offloading
huge_pages: true, // Huge pages
disable_irq: true, // Disable IRQ
});
sched.spawn(ProtectionLogic::new(Duration::from_micros(4000), brk_id, 5.0));
sched.spawn(ClosedLoopControl::new(Duration::from_millis(1)));
sched.spawn(BreakerOperation::new(Duration::from_micros(100)));
sched.spawn(PhasorCalculator::new(Duration::from_micros(250)));
sched.start()?;
SchedPolicy Variants
| Policy | Description | Applicable Scenario |
|---|
| Fifo | SCHED_FIFO first-in-first-out | Most real-time tasks |
| RoundRobin | SCHED_RR time-sliced rotation | Multiple tasks at the same priority |
| Deadline | SCHED_DEADLINE EDF | Periodic tasks |
| Xenomai | Xenomai Cobalt | Ultimate real-time performance |
Cross-Domain Communication
The two domains communicate through lock-free ring queues, avoiding lock contention and cache jitter:
use eneros_realtime::cross::{Channel, Message, ChannelConfig};
let (tx, rx) = Channel::unbounded::<ControlCmd>(
ChannelConfig {
capacity: 1024,
spin_wait_us: 5,
cache_align: true,
},
);
// General domain → Real-time domain
tx.send(ControlCmd::SetVoltage(1.02))?;
// Real-time domain receives (non-blocking)
if let Some(cmd) = rx.try_recv() {
match cmd {
ControlCmd::SetVoltage(v) => self.target_voltage = v,
ControlCmd::TripBreaker(id) => ctx.trip_breaker(id),
}
}
// Real-time domain → General domain (event reporting)
let (evt_tx, evt_rx) = Channel::bounded::<RtEvent>(256);
evt_tx.send(RtEvent::BreakerTripped { id: 5, reason: "overcurrent" });
Channel Types
| Type | Capacity | Blocking Behavior | Applicable Scenario |
|---|
| unbounded | Unlimited | Send does not block | General → Real-time |
| bounded(n) | n | Blocks when full | Real-time → General |
| spsc | 1 | Single-producer single-consumer | High-speed data stream |
| mpmc | n | Multi-producer multi-consumer | General scenario |
Cross-Domain Message Latency
| Path | Latency (P99) |
|---|
| General → Real-time (lock-free queue) | < 5μs |
| Real-time → General (event queue) | < 10μs |
| Shared memory read | < 1μs |
| Shared memory write | < 1μs |
Determinism Guarantees
| Measure | Description | Effect |
|---|
| CPU isolation | isolcpus=0,1 isolates real-time cores | Avoid being preempted by other processes |
| Memory pre-allocation | Allocated at startup, no GC at runtime | Avoid allocation jitter |
| Lock-free data structures | SPSC/MPMC lock-free queues | Avoid lock contention |
| Huge pages | 2MB / 1GB hugepages | Reduce TLB misses |
| IRQ affinity | Network card interrupts bound to non-real-time cores | Avoid IRQ interruptions |
| Watchdog | Auto-reset on task timeout | Fault self-healing |
| NO_HZ_FULL | Reduce timer ticks | Reduce context switches |
| RCU nocbs | RCU callback offloading | Reduce jitter |
Kernel Boot Parameter Example
# /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=0,1 nohz_full=0,1 rcu_nocbs=0,1 \
hugepagesz=2M hugepages=512 iommu=pt intel_idle.max_cstate=0 \
processor.max_cstate=0 idle=poll"
Task Types and Periods
| Task | Period | Priority | Jitter | CPU |
|---|
| Switching operation (GOOSE) | 100μs | 99 | < 5μs | 0 |
| Phasor calculation (PMU) | 250μs | 98 | < 10μs | 0 |
| Closed-loop control (AVR) | 1ms | 95 | < 50μs | 1 |
| Closed-loop control (AGC) | 4ms | 94 | < 100μs | 1 |
| Protection logic (distance) | 4ms | 90 | < 100μs | 0 |
| Protection logic (overcurrent) | 4ms | 90 | < 100μs | 0 |
| State estimation (fast) | 100ms | 50 | < 1ms | 1 |
| Communication (IEC 61850) | 1ms | 80 | < 50μs | 1 |
RtContext API
APIs provided by the real-time task context:
use eneros_realtime::RtContext;
impl RtContext {
// Data access (shared memory, zero-copy)
pub fn pmu_snapshot(&self) -> PmuSnapshot;
pub fn rtu_snapshot(&self) -> RtuSnapshot;
pub fn network_state(&self) -> NetworkState;
// Control output
pub fn trip_breaker(&self, id: u32);
pub fn close_breaker(&self, id: u32);
pub fn set_excitation(&self, gen_id: u32, v: f64);
pub fn set_tap(&self, xfmr_id: u32, tap: i16);
// Communication
pub fn send_event(&self, evt: RtEvent);
pub fn try_recv_cmd(&self) -> Option<ControlCmd>;
// Logging (lock-free)
pub fn log_event(&self, evt: impl Event);
pub fn waveform_record(&self, data: &WaveformBuffer);
// Time
pub fn now(&self) -> Timestamp;
pub fn elapsed(&self) -> Duration;
}
Fault Switching
A General domain failure does not affect the Real-time domain: when the Real-time domain detects a General domain heartbeat timeout, it automatically switches to “Safe Mode” and runs according to a preset strategy.
use eneros_realtime::SafetyMode;
sched.on_general_domain_timeout(|| {
// Switch to safe mode
SafetyMode::island_operation()
.hold_frequency(50.0)
.hold_voltage(1.0)
.disable_remote_control()
.enable_local_protection()
.activate();
});
// Heartbeat configuration
sched.heartbeat_config(HeartbeatConfig {
interval: Duration::from_millis(100),
timeout: Duration::from_millis(500),
on_missed: HeartbeatAction::Warn,
on_timeout: HeartbeatAction::EnterSafetyMode,
});
SafetyMode Behavior
| Behavior | Meaning |
|---|
| hold_frequency | Maintain frequency at the set value |
| hold_voltage | Maintain voltage at the set value |
| disable_remote_control | Reject remote control commands |
| enable_local_protection | Enable local protection |
| activate | Activate safe mode |
| Metric | Value | Test Method |
|---|
| Task scheduling jitter (P99) | < 50μs | 10,000 periodic tasks |
| Task scheduling jitter (P99.9) | < 100μs | 10,000 periodic tasks |
| Cross-domain message latency | < 10μs | Lock-free queue |
| GPIO response (switching operation) | < 100μs | Oscilloscope measurement |
| End-to-end protection action (P99) | < 1ms | Fault injection |
| End-to-end protection action (worst case) | < 4ms | Fault injection |
| Max continuous jitter-free operation | > 30 days | 7×24 test |
| CPU utilization (real-time core) | < 60% | Full load |
| Memory usage (real-time domain) | < 100MB | Pre-allocated |
Test environment: Intel Xeon 4 cores / 8GB / Ubuntu 22.04 + PREEMPT_RT.
Configuration Parameters
Real-time domain-related configuration in eneros.toml:
[realtime]
# Scheduling policy: fifo / round_robin / deadline / xenomai
policy = "fifo"
# Real-time CPU cores (consistent with isolcpus)
cpus = [0, 1]
# Huge pages (MB)
huge_pages_mb = 1024
# Whether to enable the watchdog
watchdog = true
# Watchdog timeout (ms)
watchdog_timeout_ms = 100
# Heartbeat interval (ms)
heartbeat_interval_ms = 100
# Heartbeat timeout (ms)
heartbeat_timeout_ms = 500
# Failover to safe mode
safety_mode_on_timeout = true
# Maximum number of real-time tasks
max_tasks = 256
| Parameter | Type | Default | Description |
|---|
| policy | enum | fifo | Scheduling policy |
| cpus | Vec | [0,1] | Real-time CPU cores |
| huge_pages_mb | u32 | 1024 | Huge pages size |
| watchdog | bool | true | Enable watchdog |
| watchdog_timeout_ms | u32 | 100 | Watchdog timeout |
| heartbeat_interval_ms | u32 | 100 | Heartbeat interval |
| heartbeat_timeout_ms | u32 | 500 | Heartbeat timeout |
| safety_mode_on_timeout | bool | true | Switch to safe mode on timeout |
| max_tasks | u32 | 256 | Maximum number of real-time tasks |
Relationship with Other Capabilities
| Related Capability | Interaction |
|---|
| Safety Guard | Real-time domain commands also pass through the gateway (simplified version) |
| Digital Twin Engine | Syncs mirror with the real-time domain |
| Equipment Model Library | Real-time domain uses simplified equipment models |
| Multi-Agent Collaboration | Protection-class Agents run in the real-time domain |
| Time-Series Native Operations | PMU data is written in streaming fashion |
| Physical Constraint Decision | Real-time domain constraint validation completes in microseconds |
| Grid Topology First-Class Citizen | Topology changes sync to the real-time domain in real time |