///|
/// PDO (Process Data Object) runtime loop.
/// Manages the cyclic exchange of process data between master and slaves
/// using logical addressing (LRW/LRD/LWR).
///|
/// Process data image: a contiguous byte buffer backing all slave I/O.
pub(all) struct ProcessImage {
/// Output data (master → slaves).
mut outputs : Bytes
/// Input data (slaves → master).
mut inputs : Bytes
/// Total output size in bytes.
output_size : Int
/// Total input size in bytes.
input_size : Int
} derive(Debug)
///|
pub fn ProcessImage::new(output_size : Int, input_size : Int) -> ProcessImage {
{
outputs: Bytes::new(output_size),
inputs: Bytes::new(input_size),
output_size,
input_size,
}
}
///|
/// Per-slave PDO mapping entry, describing where in the process image
/// this slave's data lives.
pub(all) struct PdoMapping {
/// Slave configured address.
station : UInt
/// Logical start address for this slave's output data.
output_logical : UInt
/// Byte offset into ProcessImage.outputs.
output_offset : Int
/// Number of output bytes.
output_length : Int
/// Logical start address for this slave's input data.
input_logical : UInt
/// Byte offset into ProcessImage.inputs.
input_offset : Int
/// Number of input bytes.
input_length : Int
} derive(Eq, Debug)
///|
/// PDO exchange context: holds the process image and all mappings.
pub(all) struct PdoContext {
image : ProcessImage
mappings : Array[PdoMapping]
/// Logical base address for the entire process data domain.
logical_base : UInt
/// Total logical range in bytes (output + input).
logical_size : Int
/// When true, use separate LRD + LWR instead of LRW.
/// Set when any slave in the group sets the SII PreferNotLRW flag.
mut block_lrw : Bool
} derive(Debug)
///|
/// Result of one PDO exchange, including WKC and datagram IRQ.
pub(all) struct PdoExchangeResult {
wkc : UInt
irq : UInt
} derive(Eq, Debug)
///|
/// Build a PdoContext from a list of slave mappings.
/// Each SlaveMapping provides output/input byte counts from FMMU calculation.
pub fn PdoContext::build(
slave_mappings : Array[(@mailbox.SlaveMapping, UInt)],
logical_base : UInt,
) -> PdoContext {
let mappings : Array[PdoMapping] = []
let mut out_offset = 0
let mut in_offset = 0
let mut current_logical = logical_base
for i = 0; i < slave_mappings.length(); i = i + 1 {
let (sm, station) = slave_mappings[i]
let out_len = sm.output_bytes
let in_len = sm.input_bytes
let out_logical = current_logical
current_logical = current_logical + out_len.reinterpret_as_uint()
let in_logical = current_logical
current_logical = current_logical + in_len.reinterpret_as_uint()
mappings.push({
station,
output_logical: out_logical,
output_offset: out_offset,
output_length: out_len,
input_logical: in_logical,
input_offset: in_offset,
input_length: in_len,
})
out_offset = out_offset + out_len
in_offset = in_offset + in_len
}
let total_logical = (current_logical - logical_base).reinterpret_as_int()
{
image: ProcessImage::new(out_offset, in_offset),
mappings,
logical_base,
logical_size: total_logical,
block_lrw: false,
}
}
///|
/// Execute one PDO cycle: send outputs via LRW, receive inputs.
/// Returns the working counter from the response.
pub fn[N : @hal.Nic] pdo_exchange(
nic : N,
ctx : PdoContext,
timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
pdo_exchange_result(nic, ctx, timeout).wkc
}
///|
/// Execute one PDO cycle and return both WKC and IRQ from the response datagram.
pub fn[N : @hal.Nic] pdo_exchange_result(
nic : N,
ctx : PdoContext,
timeout : @hal.Duration,
) -> PdoExchangeResult raise @hal.EcError {
if ctx.logical_size <= 0 {
return { wkc: 0U, irq: 0U }
}
if ctx.block_lrw {
return pdo_exchange_split_result(nic, ctx, timeout)
}
// Build the outgoing data: combine outputs + zero-filled input area
let total = ctx.image.output_size + ctx.image.input_size
let out_data = Bytes::makei(total, fn(i) {
if i < ctx.image.output_size {
ctx.image.outputs[i]
} else {
b'\x00'
}
})
let pdu = transact_pdu(
nic,
EcCommand::LRW,
logical_address(ctx.logical_base),
out_data,
timeout,
)
// Extract input data from the response
if pdu.data.length() >= total {
ctx.image.inputs = Bytes::makei(ctx.image.input_size, fn(i) {
pdu.data[ctx.image.output_size + i]
})
}
{ wkc: pdu.wkc, irq: pdu.header.irq }
}
///|
/// Execute one PDO cycle using separate LRD + LWR instead of LRW.
/// Used when any slave in the group sets the PreferNotLRW SII flag.
/// WKC from LWR is doubled to normalise against LRW semantics (per SOEM convention).
fn[N : @hal.Nic] pdo_exchange_split_result(
nic : N,
ctx : PdoContext,
timeout : @hal.Duration,
) -> PdoExchangeResult raise @hal.EcError {
let mut total_wkc = 0U
let mut irq = 0U
// LRD: read inputs
if ctx.image.input_size > 0 {
let pdu = transact_pdu(
nic,
EcCommand::LRD,
logical_address(
ctx.logical_base + ctx.image.output_size.reinterpret_as_uint(),
),
Bytes::new(ctx.image.input_size),
timeout,
)
if pdu.data.length() >= ctx.image.input_size {
ctx.image.inputs = Bytes::makei(ctx.image.input_size, fn(i) {
pdu.data[i]
})
}
total_wkc = total_wkc + pdu.wkc
irq = pdu.header.irq
}
// LWR: write outputs
if ctx.image.output_size > 0 {
let pdu = transact_pdu(
nic,
EcCommand::LWR,
logical_address(ctx.logical_base),
ctx.image.outputs,
timeout,
)
// Double LWR WKC to normalise against LRW: LRW increments WKC for
// both read and write, while LWR increments only for write.
total_wkc = total_wkc + pdu.wkc * 2U
if irq == 0U {
irq = pdu.header.irq
}
}
{ wkc: total_wkc, irq }
}
///|
/// Execute a read-only PDO cycle (LRD): fetch inputs without writing outputs.
pub fn[N : @hal.Nic] pdo_read(
nic : N,
ctx : PdoContext,
timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
if ctx.image.input_size <= 0 {
return 0U
}
let pdu = transact_pdu(
nic,
EcCommand::LRD,
logical_address(
ctx.logical_base + ctx.image.output_size.reinterpret_as_uint(),
),
Bytes::new(ctx.image.input_size),
timeout,
)
if pdu.data.length() >= ctx.image.input_size {
ctx.image.inputs = Bytes::makei(ctx.image.input_size, fn(i) { pdu.data[i] })
}
pdu.wkc
}
///|
/// Execute a write-only PDO cycle (LWR): send outputs without reading.
pub fn[N : @hal.Nic] pdo_write(
nic : N,
ctx : PdoContext,
timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
if ctx.image.output_size <= 0 {
return 0U
}
let pdu = transact_pdu(
nic,
EcCommand::LWR,
logical_address(ctx.logical_base),
ctx.image.outputs,
timeout,
)
pdu.wkc
}
///|
/// Set output data for a specific slave in the process image.
pub fn PdoContext::set_slave_outputs(
self : PdoContext,
station : UInt,
data : Bytes,
) -> Unit raise @hal.EcError {
for i = 0; i < self.mappings.length(); i = i + 1 {
let m = self.mappings[i]
if m.station == station {
if data.length() != m.output_length {
raise @hal.EcError::ConfigMismatch(
"output data length mismatch: expected " +
m.output_length.to_string() +
" got " +
data.length().to_string(),
)
}
// Copy data into the process image outputs
let buf = Buffer(size_hint=self.image.output_size)
for j = 0; j < self.image.output_size; j = j + 1 {
if j >= m.output_offset && j < m.output_offset + m.output_length {
buf.write_byte(data[j - m.output_offset])
} else {
buf.write_byte(self.image.outputs[j])
}
}
self.image.outputs = buf.to_bytes()
return
}
}
raise @hal.EcError::ConfigMismatch(
"slave station " + station.to_string() + " not found in mappings",
)
}
///|
/// Get input data for a specific slave from the process image.
pub fn PdoContext::get_slave_inputs(
self : PdoContext,
station : UInt,
) -> Bytes raise @hal.EcError {
for i = 0; i < self.mappings.length(); i = i + 1 {
let m = self.mappings[i]
if m.station == station {
return Bytes::makei(m.input_length, fn(j) {
self.image.inputs[m.input_offset + j]
})
}
}
raise @hal.EcError::ConfigMismatch(
"slave station " + station.to_string() + " not found in mappings",
)
}