///|
/// Zero-copy PDO exchange functions using ZeroCopyNic and FramePool.
/// These eliminate all per-cycle heap allocation in the real-time hot path.
///|
/// MutablePdoContext combines a MutableProcessImage with PDO mappings
/// for zero-copy exchange. Analogous to PdoContext but uses FixedArray[Byte].
pub(all) struct MutablePdoContext {
image : @hal.MutableProcessImage
mappings : Array[PdoMapping]
logical_base : UInt
logical_size : Int
} derive(Debug)
///|
/// Build a MutablePdoContext from slave mappings, using pre-allocated buffers.
pub fn MutablePdoContext::build(
slave_mappings : Array[(@mailbox.SlaveMapping, UInt)],
logical_base : UInt,
) -> MutablePdoContext {
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: @hal.MutableProcessImage::new(out_offset, in_offset),
mappings,
logical_base,
logical_size: total_logical,
}
}
///|
/// Set output data for a specific slave in the mutable process image.
pub fn MutablePdoContext::set_slave_outputs(
self : MutablePdoContext,
station : UInt,
data : FixedArray[Byte],
) -> 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(),
)
}
for j = 0; j < m.output_length; j = j + 1 {
self.image.outputs[m.output_offset + j] = data[j]
}
return
}
}
raise @hal.EcError::ConfigMismatch(
"slave station " + station.to_string() + " not found in mappings",
)
}
///|
/// Get input data for a specific slave from the mutable process image.
/// Returns a copy as FixedArray[Byte] (one allocation, but only on user request,
/// not on the hot path).
pub fn MutablePdoContext::get_slave_inputs(
self : MutablePdoContext,
station : UInt,
) -> FixedArray[Byte] raise @hal.EcError {
for i = 0; i < self.mappings.length(); i = i + 1 {
let m = self.mappings[i]
if m.station == station {
return FixedArray::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",
)
}
///|
/// Zero-copy PDO exchange using ZeroCopyNic.
/// The complete hot-path cycle with zero heap allocation:
/// 1. Acquire TX slot from pool (no alloc)
/// 2. Encode LRW frame directly into TX buffer (no alloc)
/// 3. Submit frame to NIC (no alloc)
/// 4. Receive response into RX slot (no alloc)
/// 5. Decode response data in-place into MutableProcessImage (no alloc)
/// 6. Release RX slot (no alloc)
pub fn[N : @hal.ZeroCopyNic] pdo_exchange_zero_copy(
nic : N,
ctx : MutablePdoContext,
timeout : @hal.Duration,
) -> PdoExchangeResult raise @hal.EcError {
if ctx.logical_size <= 0 {
return { wkc: 0U, irq: 0U }
}
// 1. Acquire TX slot
let tx_ref = nic.acquire_tx()
let tx_buf = nic.tx_buffer(tx_ref)
// 2. Encode LRW frame: outputs in data area, zeros for input area
let frame_len = encode_lrw_pdo_frame_into(
tx_buf,
broadcast_mac,
default_master_mac,
b'\x01', // index
logical_address(ctx.logical_base),
ctx.image.outputs,
ctx.image.output_size,
ctx.image.input_size,
)
// 3. Submit
nic.submit(tx_ref, length=frame_len)
// 4. Receive response
let rx_ref = nic.recv(timeout~)
let rx_buf = nic.rx_buffer(rx_ref)
let rx_len = nic.rx_length(rx_ref)
// 5. Decode response: skip output echo, copy inputs directly into image
let result = Result::Ok(
decode_pdu_response_slice(
rx_buf,
rx_len,
ctx.image.output_size, // skip output echo bytes
ctx.image.inputs,
0,
ctx.image.input_size,
),
) catch {
err => Result::Err(err)
}
// 6. Release RX slot
nic.release(rx_ref)
match result {
Ok((wkc, irq)) => { wkc, irq }
Err(e) => raise e
}
}
///|
/// Encode an LRW PDO frame with split output data and zero-filled input area.
/// This is specialized for the PDO exchange pattern where:
/// - First `output_size` bytes come from the outputs array
/// - Next `input_size` bytes are zeros (to be filled by slaves)
/// Returns the total frame length.
fn encode_lrw_pdo_frame_into(
buf : FixedArray[Byte],
dst_mac : FixedArray[Byte],
src_mac : FixedArray[Byte],
index : Byte,
logical_addr : UInt,
outputs : FixedArray[Byte],
output_size : Int,
input_size : Int,
) -> Int {
let data_len = output_size + input_size
let mut pos = 0
// Ethernet II header
for i in 0..<6 {
buf[pos + i] = dst_mac[i]
}
pos = pos + 6
for i in 0..<6 {
buf[pos + i] = src_mac[i]
}
pos = pos + 6
buf[pos] = b'\x88'
buf[pos + 1] = b'\xA4'
pos = pos + 2
// EtherCAT header
let pdu_payload_len = pdu_header_len + data_len + wkc_len
let ecat_hdr = (pdu_payload_len.reinterpret_as_uint() & 0x7FFU) |
(ecat_frame_type.reinterpret_as_uint() << 12)
write_u16_le_fixed(buf, pos, ecat_hdr)
pos = pos + 2
// PDU header
buf[pos] = EcCommand::LRW.to_byte()
buf[pos + 1] = index
write_u32_le_fixed(buf, pos + 2, logical_addr)
let len_flags = data_len.reinterpret_as_uint() & 0x7FFU
write_u16_le_fixed(buf, pos + 6, len_flags)
write_u16_le_fixed(buf, pos + 8, 0U) // IRQ
pos = pos + pdu_header_len
// Data: outputs then zeros for inputs
for i = 0; i < output_size; i = i + 1 {
buf[pos + i] = outputs[i]
}
pos = pos + output_size
for i = 0; i < input_size; i = i + 1 {
buf[pos + i] = b'\x00'
}
pos = pos + input_size
// WKC = 0
write_u16_le_fixed(buf, pos, 0U)
pos = pos + wkc_len
pos
}
///|
/// Zero-copy read-only PDO cycle (LRD).
pub fn[N : @hal.ZeroCopyNic] pdo_read_zero_copy(
nic : N,
ctx : MutablePdoContext,
timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
if ctx.image.input_size <= 0 {
return 0U
}
let tx_ref = nic.acquire_tx()
let tx_buf = nic.tx_buffer(tx_ref)
// For LRD, data payload is all zeros. Use encode_zero_data_frame_into
// to avoid allocating a zero buffer.
let frame_len = encode_zero_data_frame_into(
tx_buf,
broadcast_mac,
default_master_mac,
EcCommand::LRD,
b'\x01',
logical_address(
ctx.logical_base + ctx.image.output_size.reinterpret_as_uint(),
),
ctx.image.input_size,
)
nic.submit(tx_ref, length=frame_len)
let rx_ref = nic.recv(timeout~)
let rx_buf = nic.rx_buffer(rx_ref)
let rx_len = nic.rx_length(rx_ref)
let result = Result::Ok(
decode_pdu_response_from(
rx_buf,
rx_len,
ctx.image.inputs,
0,
ctx.image.input_size,
),
) catch {
err => Result::Err(err)
}
nic.release(rx_ref)
match result {
Ok((wkc, _irq)) => wkc
Err(e) => raise e
}
}
///|
/// Zero-copy write-only PDO cycle (LWR).
pub fn[N : @hal.ZeroCopyNic] pdo_write_zero_copy(
nic : N,
ctx : MutablePdoContext,
timeout : @hal.Duration,
) -> UInt raise @hal.EcError {
if ctx.image.output_size <= 0 {
return 0U
}
let tx_ref = nic.acquire_tx()
let tx_buf = nic.tx_buffer(tx_ref)
let frame_len = encode_pdu_frame_into(
tx_buf,
broadcast_mac,
default_master_mac,
EcCommand::LWR,
b'\x01',
logical_address(ctx.logical_base),
ctx.image.outputs,
0,
ctx.image.output_size,
)
nic.submit(tx_ref, length=frame_len)
let rx_ref = nic.recv(timeout~)
let rx_buf = nic.rx_buffer(rx_ref)
let rx_len = nic.rx_length(rx_ref)
// For LWR we only need WKC, discard response data in-place
let result = Result::Ok(decode_pdu_wkc_only(rx_buf, rx_len)) catch {
err => Result::Err(err)
}
nic.release(rx_ref)
match result {
Ok(wkc) => wkc
Err(e) => raise e
}
}