Repository navigation
Derive Macros
This crate holds the procedural macros that generate the repetitive parts of ethercat_hal device
drivers: the bit-level PDO (de)serialization, the CoE PDO assignment, and the EthercatDevice glue.
| Derive | Put it on | What it generates |
|---|---|---|
PdoObject |
one PDO entry type (for example BoolPdoObject) |
pdo::PdoObject::size() |
TxPdo |
a struct listing a terminal's input PDOs |
pdo::TxPdo + coe::Configuration (writes 0x1C13) |
RxPdo |
a struct listing a terminal's output PDOs |
pdo::RxPdo + coe::Configuration (writes 0x1C12) |
EthercatDevice |
the driver struct |
devices::EthercatDevice + devices::EthercatDeviceUsed
|
Writing device drivers shows how these fit into a complete driver. This page describes exactly what each macro requires and what it expands to.
The generated code uses crate:: paths (crate::pdo::RxPdo, crate::coe::Configuration,
crate::EtherCATThreadChannel, crate::devices::…), plus bare anyhow:: and bitvec:: paths. So
the macros only compile in the ethercat_hal crate. If you use them anywhere else, you get
cannot find 'pdo' in 'crate' and similar errors.
To write a driver in another crate, implement the traits by hand.
#[derive(Debug, Clone, Default, PdoObject)]
#[pdo_object(bits = 1)]
pub struct BoolPdoObject {
pub value: bool,
}This expands to:
impl crate::pdo::PdoObject for BoolPdoObject {
fn size(&self) -> usize { 1 }
}-
#[pdo_object(bits = N)]is required.Nis the object's size on the wire, in bits. - It only generates the size. You still write the encoding and decoding yourself:
impl TxPdoObject(aread(&mut self, &BitSlice)) and/orimpl RxPdoObject(awrite(&self, &mut BitSlice)). The slice you get is exactlyNbits long, starting at bit 0. - Generics are supported.
These go on the struct that lists every PDO a terminal can map in one direction:
#[derive(Debug, Clone, RxPdo)]
pub struct EL2002RxPdo {
#[pdo_object_index(0x1600)]
pub channel1: Option<BoolPdoObject>,
#[pdo_object_index(0x1601)]
pub channel2: Option<BoolPdoObject>,
}The requirements:
- Named fields only. Tuple structs and enums aren't supported.
-
Every field must have
#[pdo_object_index(0x....)], au16CoE index. If one is missing, the macro panics at compile time withmissing required field 0 on #[pdo_object_index]. -
Every field must be
Option<T>:- for
TxPdo,T: TxPdoObject; - for
RxPdo,T: RxPdoObject.
- for
- Field order is significant. It sets both the bit order in the process image and the order in the CoE assignment list.
RxPdo expands to this (TxPdo is the same, with 0x1C13, TxPdoObject, and an extra
get_objects_mut):
impl crate::coe::Configuration for EL2002RxPdo {
fn write_config(&self, channel: crate::EtherCATThreadChannel, device_address: u16)
-> Result<(), anyhow::Error>
{
channel.sdo_write(device_address, 0x1C12, 0, 0u8)?; // clear assignment
let mut len = 0;
if let Some(_) = &self.channel1 { len += 1; channel.sdo_write(device_address, 0x1C12, len, 0x1600u16)?; }
if let Some(_) = &self.channel2 { len += 1; channel.sdo_write(device_address, 0x1C12, len, 0x1601u16)?; }
channel.sdo_write(device_address, 0x1C12, 0, len)?; // commit count
Ok(())
}
}
impl crate::pdo::RxPdo for EL2002RxPdo {
fn get_objects(&self) -> Box<[Option<&dyn crate::pdo::RxPdoObject>]> {
Box::new([
self.channel1.as_ref().map(|o| o as &dyn crate::pdo::RxPdoObject),
self.channel2.as_ref().map(|o| o as &dyn crate::pdo::RxPdoObject),
])
}
}The provided methods of RxPdo / TxPdo (size(), write(), read() in
pdo/mod.rs) build on get_objects():
- They walk the fields in order.
- A
Nonefield is skipped and takes up no bits. - Each
Someobject gets the nextobject.size()bits. - The total size is rounded up to a whole byte.
Two consequences:
-
write_configchanges the assignment only (0x1C12/0x1C13), not the contents of the mapping objects (0x16xx/0x1Axx). It has to be called in PreOp, like every SDO write. -
Whether a field is
SomeorNonedecides what gets assigned. The same struct therefore describes the terminal's configuration and its process-image layout. Keep the driver's PDO struct identical to the one you wrote to the terminal.ConfigurableDevice::write_configimplementations do this by rebuildingtxpdo/rxpdofrom the chosenPredefinedPdoAssignment.
This goes on the driver struct:
#[derive(EthercatDevice)]
pub struct EL2002 {
pub rxpdo: EL2002RxPdo,
is_used: bool,
}The macro looks for fields by name:
| Field | Generated |
|---|---|
txpdo present |
input(bits) → self.txpdo.read(bits), input_len() → self.txpdo.size()
|
txpdo absent |
input → Ok(()), input_len → 0
|
rxpdo present |
output(bits) → self.rxpdo.write(bits), output_len() → self.rxpdo.size()
|
rxpdo absent |
output → Ok(()), output_len → 0
|
is_used: bool |
required, used by the generated EthercatDeviceUsed::{is_used, set_used}
|
It always generates:
-
as_any,as_any_mutandinto_any_boxed, which returnself; -
is_module() → false; -
get_module() → None; -
set_module(_), which does nothing.
Requirements and pitfalls:
-
Bring the PDO traits into scope. The generated
self.rxpdo.write(…)and.size()are trait method calls, so the driver's module mustuse crate::pdo::RxPdoand/orcrate::pdo::TxPdo, or you get "no method namedwrite". The trait and the derive macro have the same name, and you can import both, as EL2002 does:use crate::pdo::RxPdo; use ethercat_hal_derive::RxPdo;. -
Supertraits are your job.
EthercatDevicealso requiresNewEthercatDevice,EthercatDeviceProcessing,Debug,Send,SyncandAny. Implement or derive these yourself. -
The processing hooks aren't called. The generated
input/outputdon't callinput_post_process/output_pre_process. The application has to call them. -
No generics. The impl is written as
impl … for #namewithout generic parameters, so a generic driver struct won't compile. -
No offsets. It always reads and writes from bit 0 of the slice it's given. That is why WAGO
750 slot modules (which need
tx_bit_offset/rx_bit_offsetand a realset_module) implementEthercatDeviceby hand. See WAGO 750.
Every macro calls .unwrap() on its parse result. A mistake in an attribute (missing, wrong type,
missing bits = …) therefore shows up as proc-macro derive panicked, with the underlying deluxe
message in the help: line, not as an error pointing at the field.
The EthercatDevice derive doesn't parse any attributes. Missing fields show up as ordinary type
errors in the generated code, for example "no field is_used".
-
Dependencies:
syn(full),quote,proc-macro2, anddeluxefor attribute parsing. -
Tests: the crate has none. Build
ethercat_hal, which uses the derives in more than 40 files, and run its tests (cd ethercat_hal && cargo test). -
Inspecting output: to see what a derive generates, run
cargo expand(thecargo-expandtool) on a driver module, for example:cd ethercat_hal && cargo expand devices::beckhoff_modules::el2002
EtherCAT HAL
EtherCAT Devices
XTREM
Other crates