PLC context
PLC context is the single common variable space where PLC program threads and I/O exchange data.
The context is automatically generated and placed under a read-write lock.
Defining
The context is defined in the PLC configuration (e.g. plc.yml) as
context:
fields:
var1: REAL
var2: DINT
var3: BOOL
Data types
The context may contain the following IEC-61131 data types, which are automatically translated into Rust data types:
IEC |
Rust |
Description |
---|---|---|
BOOL |
bool |
Boolean |
BYTE, USINT |
u8 |
8-bit unsigned integer |
WORD, UINT |
u16 |
16-bit unsigned integer |
DWORD, UDINT |
u32 |
32-bit unsigned integer |
LWORD, ULINT |
u64 |
64-bit unsigned integer |
SINT |
i8 |
8-bit signed integer |
INT |
i16 |
16-bit signed integer |
DINT |
i32 |
32-bit signed integer |
LINT |
i64 |
64-bit signed integer |
REAL |
f32 |
32-bit float |
LREAL |
f64 |
64-bit float |
Rust data types can be used directly as well with no limitations.
Arrays
Arrays can be defined in both IEC-61131-way:
context:
fields:
vars: REAL[4]
or in a Rust way:
context:
fields:
vars: "[f32; 4]"
Structures
If a context field as got sub-fields, they are automatically translated into a structure:
context:
fields:
struct1:
var1: BOOL
var2: UINT
Arrays of structures
Arrays of structures can be defined as:
context:
fields:
"struct1[10]":
var1: BOOL
var2: UINT
Boxed arrays
By default arrays are defined in stack memory which provides faster access to variables but may lead to stack overflow for large ones.
If an array is specified with “!” symbol after its size:
context:
fields:
vars: REAL[4000!]
it is created as boxed (heap-allocated). The same option can be applied for arrays of structures.
Custom types
If custom types are required in the context, they must be placed into plc_types crate module. If found, the module is automatically imported into the context.
context:
fields:
timer1: Duration # another way is use the full path: std::time::Duration
data: MyStruct
main.rs:
mod plc_types;
plc_types.rs:
pub use std::time::Duration; // external data type re-export
#[derive(Default)]
pub(crate) struct MyStruct {
var1: bool,
var2: f32
}
Note
All custom types MUST implement the Default trait.
Accessing
As already mentioned, the context is placed under a read-write lock (parking_lot::RwLock). To prevent other threads, including I/O ones, getting stuck, the context should always be unlocked for a minimal period of time, especially if heavy calculations are planned.
With macros
use rplc::prelude::*;
mod plc;
#[plc_program(loop = "500ms")]
fn p1() {
let mut var1 = {
let ctx = plc_context!(); // context is read-locked
ctx.var1
}; // context is unlocked
// perform some heavy calculations
{
let mut ctx = plc_context_mut!(); // context is read-write-locked
ctx.var1 = var1;
} // context is unlocked
}
Directly
The context can be accessed directly as:
use rplc::prelude::*;
mod plc;
use plc::context::CONTEXT;
#[plc_program(loop = "500ms")]
fn p1() {
let var1 = { // context is read-locked
let ctx = CONTEXT.read();
ctx.var1
}; // context is unlocked
// ....
}
Serialization
The context structures are created in C-representation (repr(C)) which allows to send them to externally linked C or Structured Text methods as-is.
Additionally, the context can be declared as Serde (de)serializable:
context:
serialize: true
fields:
var1: BOOL
var2: REAL
After, the context or its part can be e.g. loaded and saved using e.g. MessagePack, JSON or any other data packer:
use std::fs;
fn main() {
init_plc!();
if let Ok(data) = fs::read("plc.dat") {
info!("loading context");
// pointer dereference is not required if a part is loaded
*plc_context_mut!() = rmp_serde::from_slice(&data).unwrap();
}
run_plc!();
fs::write(
"plc.dat",
// pointer reference-dereference is not required if a part is saved
rmp_serde::to_vec_named(&*plc_context!()).unwrap(),
)
.unwrap();
}
Note
If custom types are used, all of them MUST implement serde::Serialize and serde::Deserialize.