undoPLC Released ยท v0.2.0
Open source multi-threaded PLC infrastructure designed for the undoRT ecosystem. Provides deterministic Master/Worker synchronization with real-time guarantees.
ProcessImage for I/O management and IoBus as the fieldbus contract.
Overview
undoPLC is a C++20 framework that implements a deterministic fork-join execution model for real-time control applications. It provides:
- Lock-free deferred logging (
undoLog) - CPU isolation and frequency management (
undoSystem) - Priority inheritance mutexes (
undoMutex) - Master/Worker task synchronization (
undoTasks) - Aligned cycle time management
- SCHED_FIFO real-time scheduling
- NEW: Integrated with
undoCorefor I/O management
Key Features
1. Master/Worker Fork-Join Model
The UndoMasterTaskBase orchestrates the execution cycle:
- Fork phase: Master wakes all workers simultaneously
- Execution phase: Workers run user logic in parallel
- Join phase: Master waits for all workers to complete
- Watchdog: Detects cycle overruns and triggers safe stop
- NEW: Uses
undoCore::ProcessImagefor I/O management
2. Cycle Time Management
The system maintains an aligned absolute cycle time that is:
- Independent of system jitter
- Incremented deterministically at each cycle
- Shared consistently between Master and all Workers
- Available in ns, ยตs, and ms units
// Master provides aligned cycle time to workers
uint64_t cycleTimeMs = master.getCurrentCycleTimeMs();
uint64_t cycleTimeNs = master.getCurrentCycleTimeNs();
3. Startup Delay
The Master task starts with a 5-cycle delay to ensure:
- All Workers are fully initialized
- The system has time to stabilize
- No race conditions at startup
Architecture
Core Components
| Component | Description |
|---|---|
UndoMasterTaskBase |
Orchestrates the execution cycle, reads/writes fieldbus I/O via undoCore::IoBus |
UndoWorkerTaskBase |
Executes user PRG logic in parallel on isolated cores |
UndoLog |
Lock-free deferred logging with thread-local queues |
UndoSys |
CPU isolation, frequency management, TSC utilities |
UndoMutex |
Pthread mutex with PTHREAD_PRIO_INHERIT |
undoCore::ProcessImage |
Double-buffered IEC 61131-3 memory layout (%I, %Q, %M) |
undoCore::IoBus |
Abstract fieldbus master interface (implemented by undoBUS) |
undoCore as a submodule,
ensuring seamless interoperability with undoBUS and other undoRT components.
Execution Flow
// 1. Master thread runs with SCHED_FIFO
while (running) {
// Wait for next aligned cycle
clock_nanosleep(...);
// Update absolute cycle time
_currentCycleTimeNs += _cycleTimeNs;
// Read fieldbus inputs (via undoCore::IoBus)
readInputBus(); // calls bus->waitCycle() -> processImage.copyIn()
// Fork: wake all workers
workerCv.notify_all();
// Join: wait for workers with timeout
masterCv.wait_for(...);
// Write fieldbus outputs (via undoCore::IoBus)
writeOutputBus(); // calls bus->notifyDone() -> processImage.copyOut()
}
// 2. Worker threads run with SCHED_FIFO (lower priority)
while (running) {
// Wait for master signal
workerCv.wait(...);
// Copy master's cycle time
_currentCycleTimeNs = master.getCurrentCycleTimeNs();
// Execute user logic
runWork();
// Notify master of completion
activeWorkers.fetch_sub(1);
}
Cycle Time Management
The aligned cycle time is a key feature that provides deterministic timing independent of system jitter. Here's how it works:
Initialization
// In UndoMasterTaskBase::run()
struct timespec baseTime;
clock_gettime(CLOCK_MONOTONIC, &baseTime);
uint64_t baseNs = baseTime.tv_sec * 1000000000ULL + baseTime.tv_nsec;
// Align to next multiple of cycle time
uint64_t alignNs = _cycleTimeNs - (baseNs % _cycleTimeNs);
if (alignNs == _cycleTimeNs) alignNs = 0;
baseNs += alignNs;
// Add 5-cycle startup delay for safety
baseNs += (_cycleTimeNs * _STARTUP_DELAY_CYCLES);
_currentCycleTimeNs.store(baseNs, std::memory_order_release);
Cycle Update
// Each cycle increment is deterministic
uint64_t currentAbsoluteNs = _currentCycleTimeNs.load(std::memory_order_acquire) + _cycleTimeNs;
_currentCycleTimeNs.store(currentAbsoluteNs, std::memory_order_release);
Worker Synchronization
// Worker copies master's cycle time
_cycleTimeNs = _master->getCycleNs();
_currentCycleTimeNs = _master->getCurrentCycleTimeNs();
Test Application
The main.cpp
demonstrates the complete infrastructure with a real-time test:
Demo Worker
Simulates a PRG block with deterministic busy-wait:
class DemoWorkerTask : public UndoWorkerTaskBase
{
protected:
bool runWork() override
{
// Simulate fixed computational workload
UndoSys::getInstance().busyWait(_workNs);
++_cycleCount;
return true;
}
};
Demo Master
Drives the execution cycle and logs diagnostics every second:
class DemoMasterTask : public UndoMasterTaskBase
{
protected:
void writeOutputBus() override
{
if (++_cycleCount % 1000 == 0) {
const DiagVars& d = getDiagVars();
UndoLog::getInstance().logRT(
LogDomain::PLC, LOG_INFO,
"cycle %llu (%lu mS)| exec[min=%u max=%u]us jitter[min=%u max=%u]us",
_cycleCount, getCurrentCycleTimeMs(),
d.execMin, d.execMax, d.jitterMin, d.jitterMax
);
}
}
};
Running the Test
# Build and run with PREEMPT_RT kernel
$ make
$ sudo ./undoPLC --log2console
# Expected output:
# [INFO] undoPLC: Starting with 5 cycles delay
# [INFO] undoPLC: Master on core 4 | 2 worker(s) spawned
# [INFO] undoPLC: cycle 1000 (1000 mS)| exec[min=98 max=102]us jitter[min=0 max=5]us
- PREEMPT_RT kernel
- At least 2 isolated CPU cores (
isolcpus=in GRUB) - Root privileges for CPU frequency management
Configuration Example
# GRUB configuration (/etc/default/grub)
GRUB_CMDLINE_LINUX="... isolcpus=4,5,6,7 nohz_full=4,5,6,7 rcu_nocbs=4,5,6,7"
# Update GRUB
$ sudo update-grub
# Reboot and verify
$ cat /sys/devices/system/cpu/isolated
4-7
API Reference
Full API documentation is generated with Doxygen: Browse API Reference
Key Classes
| Class | Key Methods |
|---|---|
UndoMasterTaskBase |
start(), stop(), getCurrentCycleTimeNs(),
getCurrentCycleTimeMs(), registerWorker(),
readInputBus(), writeOutputBus()
|
UndoWorkerTaskBase |
start(), stop(), runWork(),
getCurrentCycleTimeNs()
|
UndoLog |
logRT(), registerThread(), init()
|
UndoSys |
getIsolatedCpu(), getSharedCpu(),
setCpuNominalFrequency(), tsc2Ns()
|
undoPLC