pub mod created_frame;
mod frame_box;
pub mod received_frame;
pub mod receiving_frame;
pub mod sendable_frame;
use crate::{
error::PduError, ethernet::EthernetFrame, fmt, pdu_loop::frame_header::EthercatFrameHeader,
};
use atomic_waker::AtomicWaker;
use core::{
ptr::{NonNull, addr_of, addr_of_mut},
sync::atomic::{AtomicU16, Ordering},
};
use frame_box::FrameBox;
/// A marker value for empty frames with no pushed PDUs.
///
/// The upper value must be non-zero for sentinel comparisons to work.
pub const FIRST_PDU_EMPTY: u16 = 0xff00;
/// Frame state.
#[atomic_enum::atomic_enum]
#[derive(PartialEq, Default)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum FrameState {
// SAFETY: Because we create a bunch of `Frame`s with `MaybeUninit::zeroed`, the `None` state
// MUST be equal to zero. All other fields in `Frame` are overridden in `replace`, so there
// should be no UB there.
/// The frame is available ready to be claimed.
#[default]
None = 0,
/// The frame is claimed with a zeroed data buffer and can be filled with command, data, etc
/// ready for sending.
Created = 1,
/// The frame has been populated with data and is ready to send when the TX loop next runs.
Sendable = 2,
/// The frame is being sent over the network interface.
Sending = 3,
/// The frame was successfully sent, and is now waiting for a response from the network.
Sent = 4,
/// A frame response has been received and validation/parsing is in progress.
RxBusy = 5,
/// Frame response parsing is complete and the returned data is now stored in the frame. The
/// frame and its data is ready to be returned in `Poll::Ready` of [`ReceiveFrameFut`].
RxDone = 6,
/// The frame TX/RX is complete, but the frame memory is still held by calling code.
RxProcessing = 7,
}
/// An individual frame state, PDU header config, and data buffer.
///
/// # A frame's journey
///
// TODO: Update this journey! The current docs are out of date!
// The following flowchart describes a `FrameElement`'s state changes during its use:
//
// <img alt="A flowchart showing the different state transitions of FrameElement" src="https://mermaid.ink/svg/pako:eNqdUztv2zAQ_isHTgngtLuGDLVadGkQ2E7bQYBxEc82YYoU-LBsJPnvPVLMy_JULaR05PfS3ZNorSRRiY22Q7tDF2BVNwb4-eGwo2XAQFV1Zw3Bzc3tcyNQa9uuN6l4dd00Jh8D5cHYAejY6ujVgfQJrrYRHZpAJOHxBBhsp1rwCfAa8IBK46MmCBZaxlRmC0lKI54_Mc8d8Sqnkkohq-ptHzW_wX39wChdh0bOQGLA_wBrRDb3pUO3X3syMsnMVlc_v9_x8gf3BKu_oK3tz-Uuy_kpxWslc5TbkOA92AM5MBQG6_ZTuJTMZbhUGRUvCp6jljh9D9nCLCfroZdx7Y7Zwlyj6koZ0JcLDHRuZHH8Fv1pyjt-L7S_UStOWVnztXe2Je_H39j1mgIx3eIVPkNUVc60iJRZUA5zlDPw1k111Nx7l3TU7z05gsQQXSKdf2gnTsDkzoye2KzvreFN6owp0f2bhUt079VC-omG-18mPYMKu9HOm3uSxbx0tmfPZ7xptMRMdOQ6VJIn8SmxNyLsiEFExVvJqTWiMS98DmOwy5NpRRVcpJmIPZuhWuGWMUW1Qe35K0kVrPs1jnae8Jd_545fZQ" style="background: white; max-height: 800px" />
//
// Source (MermaidJS):
//
// ```mermaid
// flowchart TD
// FrameState::None -->|"alloc_frame()\nFrame is now exclusively (guaranteed by atomic state) available to calling code"| FrameState::Created
// FrameState::Created -->|populate PDU command, data| FrameState::Created
// FrameState::Created -->|"frame.mark_sendable()\nTHEN\nWake TX loop"| FrameState::Sendable
// FrameState::Sendable -->|TX loop sends over network| FrameState::Sending
// FrameState::Sending -->|"RX loop receives frame, calls pdu_rx()\nClaims frame as receiving"| FrameState::RxBusy
// FrameState::RxBusy -->|"Validation/processing complete\nReceivingFrame::mark_received()\nWake frame waker"| FrameState::RxDone
// FrameState::RxDone -->|"Wake future\nCalling code can now use response data"| FrameState::RxProcessing
// FrameState::RxProcessing -->|"Calling code is done with frame\nReceivedFrame::drop()"| FrameState::None
// ```
#[derive(Debug)]
#[repr(C)]
pub struct FrameElement<const N: usize> {
/// Ethernet frame index in storage. Has nothing to do with PDU header index field.
storage_slot_index: u8,
status: AtomicFrameState,
waker: AtomicWaker,
/// Keeps track of how much of the PDU data buffer has been consumed.
pdu_payload_len: usize,
// Atomic as we iterate over all `FrameElement`s and read this field when receiving a frame.
/// Stores the PDU index of the first PDU written into this frame (if any).
///
/// Used by the network RX code to do a linear search in the frame storage to find the storage
/// behind the received frame.
///
/// The lower byte stores the PDU index, the upper byte stores a sentinel used to signify
/// whether the PDU has been set or not.
first_pdu: AtomicU16,
// MUST be the last element otherwise pointer arithmetic doesn't work for
// `NonNull<FrameElement<0>>`.
ethernet_frame: [u8; N],
}
impl<const N: usize> Default for FrameElement<N> {
fn default() -> Self {
Self {
status: AtomicFrameState::new(FrameState::None),
ethernet_frame: [0; N],
storage_slot_index: 0,
pdu_payload_len: 0,
first_pdu: AtomicU16::new(FIRST_PDU_EMPTY),
waker: AtomicWaker::default(),
}
}
}
impl<const N: usize> FrameElement<N> {
/// Get pointer to entire data: the Ethernet frame including header and all subsequent EtherCAT
/// payload.
unsafe fn ptr(this: NonNull<FrameElement<N>>) -> NonNull<u8> {
let buf_ptr: *mut [u8; N] = unsafe { addr_of_mut!((*this.as_ptr()).ethernet_frame) };
let buf_ptr: *mut u8 = buf_ptr.cast();
unsafe { NonNull::new_unchecked(buf_ptr) }
}
/// Get pointer to EtherCAT frame payload. i.e. the buffer after the end of the EtherCAT frame
/// header where all the PDUs (header and data) go.
unsafe fn ethercat_payload_ptr(this: NonNull<FrameElement<N>>) -> NonNull<u8> {
unsafe {
Self::ptr(this)
.byte_add(EthernetFrame::<&[u8]>::header_len())
.byte_add(EthercatFrameHeader::header_len())
.cast()
}
}
/// Set the frame's state without checking its current state.
pub(in crate::pdu_loop) unsafe fn set_state(this: NonNull<FrameElement<N>>, state: FrameState) {
let fptr = this.as_ptr();
unsafe { (*addr_of_mut!((*fptr).status)).store(state, Ordering::Release) };
}
/// Atomically swap the frame state from `from` to `to`.
///
/// If the frame is not currently in the given `from` state, this method will return an error
/// with the actual current frame state.
unsafe fn swap_state(
this: NonNull<FrameElement<N>>,
from: FrameState,
to: FrameState,
) -> Result<NonNull<FrameElement<N>>, FrameState> {
let fptr = this.as_ptr();
unsafe {
(*addr_of_mut!((*fptr).status)).compare_exchange(
from,
to,
Ordering::AcqRel,
Ordering::Relaxed,
)
}?;
Ok(this)
}
/// Attempt to clame a frame element as CREATED. Succeeds if the selected FrameElement is
/// currently in the NONE state.
unsafe fn claim_created(
this: NonNull<FrameElement<N>>,
frame_index: u8,
) -> Result<NonNull<FrameElement<N>>, PduError> {
// SAFETY: We atomically ensure the frame is currently available to use which guarantees no
// other thread could take it from under our feet.
//
// It is imperative that we check the existing state when claiming a frame as created. It
// matters slightly less for all other state transitions because once we have a created
// frame nothing else is able to take it unless it is put back into the `None` state.
let this = unsafe { Self::swap_state(this, FrameState::None, FrameState::Created) }
.map_err(|e| {
fmt::trace!(
"Failed to claim frame {}: status is {:?}, expected {:?}",
frame_index,
e,
FrameState::None
);
PduError::SwapState
})?;
unsafe {
(*addr_of_mut!((*this.as_ptr()).storage_slot_index)) = frame_index;
(*addr_of_mut!((*this.as_ptr()).pdu_payload_len)) = 0;
}
Ok(this)
}
unsafe fn claim_sending(this: NonNull<FrameElement<N>>) -> Option<NonNull<FrameElement<N>>> {
unsafe { Self::swap_state(this, FrameState::Sendable, FrameState::Sending) }.ok()
}
unsafe fn claim_receiving(this: NonNull<FrameElement<N>>) -> Option<NonNull<FrameElement<N>>> {
unsafe { Self::swap_state(this, FrameState::Sent, FrameState::RxBusy) }
.map_err(|actual_state| {
fmt::error!(
"Failed to claim receiving frame {}: expected state {:?}, but got {:?}",
unsafe { *addr_of_mut!((*this.as_ptr()).storage_slot_index) },
FrameState::Sent,
actual_state
);
})
.ok()
}
unsafe fn storage_slot_index(this: NonNull<FrameElement<0>>) -> u8 {
unsafe { *addr_of!((*this.as_ptr()).storage_slot_index) }
}
pub(in crate::pdu_loop) unsafe fn first_pdu_is(
this: NonNull<FrameElement<0>>,
search: u8,
) -> bool {
let raw = unsafe { (*addr_of!((*this.as_ptr()).first_pdu)).load(Ordering::Acquire) };
// Unused sentinel value occupies upper byte, so this equality will never hold for empty
// frames
u16::from(search) == raw
}
/// If no PDUs are present in the frame, set the first PDU index to the given value.
unsafe fn set_first_pdu(this: NonNull<FrameElement<0>>, value: u8) {
let first_pdu = unsafe { &mut *addr_of_mut!((*this.as_ptr()).first_pdu) };
// Only set first PDU index if the frame is empty, as denoted by the `FIRST_PDU_EMPTY`
// sentinel. Failures are ignored as we want a noop when the first PDU value was already
// set.
let _ = first_pdu.compare_exchange(
FIRST_PDU_EMPTY,
u16::from(value),
Ordering::Release,
Ordering::Relaxed,
);
}
/// Clear first PDU.
unsafe fn clear_first_pdu(this: NonNull<FrameElement<0>>) {
let first_pdu = unsafe { &*addr_of!((*this.as_ptr()).first_pdu) };
first_pdu.store(FIRST_PDU_EMPTY, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pdu_loop::frame_element::{AtomicFrameState, FIRST_PDU_EMPTY, FrameElement};
use atomic_waker::AtomicWaker;
use core::{ptr::NonNull, sync::atomic::AtomicU16};
#[test]
fn set_first_pdu_only_once() {
crate::test_logger();
const BUF_LEN: usize = 16;
let frame = FrameElement {
storage_slot_index: 0xab,
status: AtomicFrameState::new(FrameState::None),
waker: AtomicWaker::default(),
ethernet_frame: [0u8; BUF_LEN],
pdu_payload_len: 0,
first_pdu: AtomicU16::new(FIRST_PDU_EMPTY),
};
let frame_ptr = NonNull::from(&frame);
unsafe { FrameElement::<0>::set_first_pdu(frame_ptr.cast(), 0xab) };
unsafe { FrameElement::<0>::set_first_pdu(frame_ptr.cast(), 0xcd) };
assert_eq!(frame.first_pdu.load(Ordering::Relaxed), 0xab);
}
#[test]
fn find_empty_frame() {
crate::test_logger();
const BUF_LEN: usize = 16;
let frame = FrameElement {
storage_slot_index: 0xab,
status: AtomicFrameState::new(FrameState::None),
waker: AtomicWaker::default(),
ethernet_frame: [0u8; BUF_LEN],
pdu_payload_len: 0,
first_pdu: AtomicU16::new(FIRST_PDU_EMPTY),
};
let frame_ptr = NonNull::from(&frame);
assert!(!unsafe { FrameElement::<0>::first_pdu_is(frame_ptr.cast(), 0) });
}
#[test]
fn find_frame_zero() {
crate::test_logger();
const BUF_LEN: usize = 16;
let frame = FrameElement {
storage_slot_index: 0xab,
status: AtomicFrameState::new(FrameState::None),
waker: AtomicWaker::default(),
ethernet_frame: [0u8; BUF_LEN],
pdu_payload_len: 0,
first_pdu: AtomicU16::new(FIRST_PDU_EMPTY),
};
let frame_ptr = NonNull::from(&frame);
unsafe { FrameElement::<0>::set_first_pdu(frame_ptr.cast(), 0) }
assert!(unsafe { FrameElement::<0>::first_pdu_is(frame_ptr.cast(), 0) });
}
#[test]
fn find_frame_1() {
crate::test_logger();
const BUF_LEN: usize = 16;
let frame_0 = FrameElement {
storage_slot_index: 0xab,
status: AtomicFrameState::new(FrameState::None),
waker: AtomicWaker::default(),
ethernet_frame: [0u8; BUF_LEN],
pdu_payload_len: 0,
first_pdu: AtomicU16::new(FIRST_PDU_EMPTY),
};
let frame_ptr_0 = NonNull::from(&frame_0);
unsafe { FrameElement::<0>::set_first_pdu(frame_ptr_0.cast(), 123) }
// ---
let frame_1 = FrameElement {
storage_slot_index: 0xab,
status: AtomicFrameState::new(FrameState::None),
waker: AtomicWaker::default(),
ethernet_frame: [0u8; BUF_LEN],
pdu_payload_len: 0,
first_pdu: AtomicU16::new(FIRST_PDU_EMPTY),
};
let frame_ptr_1 = NonNull::from(&frame_1);
unsafe { FrameElement::<0>::set_first_pdu(frame_ptr_1.cast(), 0xff) }
// ---
assert!(!unsafe { FrameElement::<0>::first_pdu_is(frame_ptr_0.cast(), 0) });
assert!(unsafe { FrameElement::<0>::first_pdu_is(frame_ptr_0.cast(), 123) });
assert!(!unsafe { FrameElement::<0>::first_pdu_is(frame_ptr_0.cast(), 0xff) });
assert!(!unsafe { FrameElement::<0>::first_pdu_is(frame_ptr_1.cast(), 0) });
assert!(!unsafe { FrameElement::<0>::first_pdu_is(frame_ptr_1.cast(), 123) });
assert!(unsafe { FrameElement::<0>::first_pdu_is(frame_ptr_1.cast(), 0xff) });
}
// A sanity check to make sure we get hold of a pointer to the start of the ethernet frame array
// and not the start of the struct. This test is added due to a regression caused by refactoring
// the `ethercat_payload_ptr` method.
#[test]
fn payload_offset() {
const N: usize = 32;
// Minus ethernet header and EtherCAT header
const ETHERCAT_PAYLOAD: usize = N - 14 - 2;
let frame = FrameElement {
storage_slot_index: 0xaa,
// 5
status: AtomicFrameState::new(FrameState::RxBusy),
waker: AtomicWaker::default(),
// Should be zero but we'll set it to a random value for debugging
pdu_payload_len: 0xbb,
first_pdu: AtomicU16::new(0xcc),
// Fill with a canary value
ethernet_frame: [0xabu8; N],
};
let ptr = NonNull::from(&frame);
let payload = unsafe { FrameElement::<N>::ethercat_payload_ptr(ptr) };
let raw =
unsafe { core::slice::from_raw_parts(payload.as_ptr() as *const u8, ETHERCAT_PAYLOAD) };
assert_eq!(raw, &[0xabu8; ETHERCAT_PAYLOAD]);
}
}