undoPLC Released · v0.2.3
Open source multi-threaded PLC infrastructure designed for the undoRT ecosystem. Provides deterministic Master/Worker synchronization with real-time guarantees.
std::latch, added in v0.1.1 for the task
registration handshake, was the only C++20 construct in the library; it is now
UndoLatch, built on std::mutex and std::condition_variable.
v0.2.2 also corrected the project version, which the packaging had been reporting as
0.2.0.
Overview
undoPLC is a C++17 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) - One-shot registration gate (
undoLatch) - Aligned cycle time management
- SCHED_FIFO real-time scheduling
undoCoreintegration for 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
- 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 |
UndoLatch |
One-shot gate that opens once every task thread has registered. Header only,
built on std::mutex and std::condition_variable |
undoCore::ProcessImage |
Double-buffered IEC 61131-3 memory layout (%I, %Q, %M) |
undoCore::IoBus |
Abstract fieldbus master interface (implemented by undoBUS) |
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. It works in three steps:
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
Unit Tests
The real-time application above needs an RT kernel. The unit tests do not, and are
built with -DBUILD_TESTS=ON and run through ctest:
cmake -S . -B build -DBUILD_TESTS=ON
cmake --build build -j
cd build && ctest --output-on-failure
test_latch covers UndoLatch: that it opens only after the last
arrival, that it never opens early, that wait() is idempotent and safe from
several threads at once, the Master/Worker registration handshake itself, and
concurrent arrivals. It is the one test that runs without isolated cores.
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()
|
UndoLatch |
count_down(), wait(), try_wait()
|
undoPLC