-
Notifications
You must be signed in to change notification settings - Fork 2
WAGO 750
WAGO 750 modules work differently from Beckhoff EL terminals. Only the 750-354 EtherCAT fieldbus coupler is an EtherCAT subdevice. The I/O modules plugged in after it sit on WAGO's internal local bus: they have no EtherCAT address, don't appear in the subdevice list, and have no identity tuple. Their process data is packed into the coupler's single PDO.
In ethercat_hal, the coupler is therefore a container device:
- It discovers its modules over CoE.
- It works out where each module's bits are inside the coupler's PDO.
- It creates one driver per module.
- It passes the coupler's whole process-image slice to every module driver. Each driver reads or writes its own bits at its own offset.
This page uses three modules as examples:
- 750-455: 4 × analog input, 4–20 mA
- 750-531: 4 × digital output
- 750-554: 2 × analog output, 4–20 mA
The coupler also supports the 750-402, 750-430, 750-501, 750-530, 750-1506 (digital I/O), the 750-652 (serial) and the 750-671 / 750-672 (stepper). They plug into the coupler the same way, but this page doesn't cover them.
The matching examples are 750_455_minimal.rs,
750_531_minimal.rs and
750_554_minimal.rs.
flowchart LR
subgraph Bus["EtherCAT bus"]
C["750-354 coupler<br/>subdevice, has device_address,<br/>start_tx..end_tx / start_rx..end_rx"]
end
subgraph K["WAGO local bus (behind the coupler)"]
M0["slot 0: 750-531"]
M1["slot 1: 750-455"]
M2["slot 2: 750-554"]
end
C --- M0 --- M1 --- M2
subgraph SW["Wago750_354 (driver)"]
S["slots: [Option<Module>; 64]<br/>slot_devices: [Option<Box<dyn DynamicEthercatDevice>>; 64]"]
end
| Type | Role |
|---|---|
Wago750_354 |
The coupler driver. It implements EthercatDevice, and its input() / output() loop over slot_devices. |
Module (devices/mod.rs) |
Metadata for one slot: slot, belongs_to_addr, vendor_id, product_id (the module's ident), has_tx / has_rx, and tx_offset / rx_offset (bit offsets within the coupler's PDO). |
DynamicEthercatDevice |
EthercatDevice + EthercatDynamicPDO (get_/set_tx_offset, get_/set_rx_offset). Every slot-module driver implements it. |
SubDeviceProductTuple |
(vendor_id, product_id). Modules are matched without a revision, for example WAGO_750_531_MODULE_IDENT. |
Everything up to and including init_slot_modules uses SDO reads, so it has to run in PreOp:
sequenceDiagram
participant App
participant Coupler as Wago750_354
participant Ch as EtherCATThreadChannel
App->>Ch: request_state_change(PreOp), wait
App->>Coupler: Wago750_354::initialize_modules(channel, addr)
Coupler->>Ch: sdo_read 0xF050:00 (module count)
Coupler->>Ch: sdo_read 0x9000+0x10·i :0A (module ident), for each slot
Coupler-->>App: Vec<Module>
App->>Coupler: set_module(m), for each module
App->>Coupler: init_slot_modules(channel, addr)
Coupler->>Ch: sdo_read 0x1C12/0x1C13 + PDO mapping objects
Note over Coupler: compute bit offset per slot,<br/>instantiate one driver per slot
App->>Ch: request_state_change(Op), wait
App->>App: re-read subdevices (coupler's start/end offsets)
Wago750_354::initialize_modules(channel, device_address) works like this:
-
It reads the module count from
0xF050:00. -
For every slot
i, it reads the module ident from0x9000 + 0x10·i, sub-index0x0A. -
get_modules()turns each ident into aModule. It sets the name and thehas_tx/has_rxflags from a hard-codedmatch:- 750-455:
has_txonly - 750-531:
has_rxonly - 750-554:
has_rxonly
An ident it doesn't recognise still becomes a
Module(both flagsfalse), and a line is printed. - 750-455:
The function doesn't touch the coupler instance. You pass each returned Module into the coupler with
coupler.set_module(module).
coupler.init_slot_modules(channel, device_address) first runs get_pdo_offsets() for both
directions:
- It reads the sync-manager PDO assignment:
0x1C13for inputs,0x1C12for outputs. - The first assigned PDO (sub-index 1) is the coupler's own. Its entry bit lengths are added up to give the starting offset, so the coupler's own status and control bits are skipped.
- It collects the mapping entries of every other assigned PDO (
u32= index << 16 | sub << 8 | bit length) and sorts them numerically. - It walks the sorted entries, adding up the bit lengths. Each entry's slot comes from its object
index:
(index − 0x6000) / 16for inputs and(index − 0x7000) / 16for outputs. Anything below 16 counts as slot 0. The first offset seen for each slot becomes that slot's start offset.
This relies on the coupler mapping slot n to objects 0x6000 + 0x10·n / 0x7000 + 0x10·n, and on
the process image being laid out in the same order as the sorted entries.
It then fills in tx_offset / rx_offset for every module (only in the directions where has_tx /
has_rx is set), and creates a driver for each slot from a second hard-coded match on
(vendor_id, product_id), for example:
WAGO_750_455_MODULE_IDENT => Box::new(wago_750_455::Wago750_455::new()),Each driver gets its offsets through set_tx_offset / set_rx_offset and goes into
slot_devices[dev_count].
init_slot_modules only runs once. If dev_count != 0, it returns straight away.
After Op, read the subdevice list again so that the coupler's start_tx..end_tx and
start_rx..end_rx are valid. Then give the coupler its slice:
coupler.input(BitSlice::<u8, Lsb0>::from_slice(&inputs[c.start_tx..c.end_tx]))?;
// … use slot devices …
coupler.output(BitSlice::<u8, Lsb0>::from_slice_mut(&mut outputs[c.start_rx..c.end_rx]))?;Wago750_354::input() and output() call input() / output() on every slot_devices entry, in
order, and stop at the first None. Every module gets the whole coupler slice, not a sub-slice, and
indexes into it with its own bit offset. Errors from the modules are thrown away (let _ = …).
The slot devices are Box<dyn DynamicEthercatDevice>, so you downcast them to the concrete driver.
Look the slot up once, after init_slot_modules:
use ethercat_hal::devices::wago_modules::wago_750_554::Wago750_554;
let ao_slot = coupler.slot_devices.iter()
.position(|s| s.as_ref().is_some_and(|d| d.as_any().downcast_ref::<Wago750_554>().is_some()))
.expect("no 750-554");
// every cycle:
let ao = coupler.slot_devices[ao_slot].as_mut().unwrap()
.as_any_mut().downcast_mut::<Wago750_554>().unwrap();
ao.set_current(0, ElectricCurrent::new::<milliampere>(12.0));slot_devices[i] is the module in physical slot i, counting from the coupler. This only holds as
long as every module before it is supported (see Limitations).
All three drivers have the same structure:
-
tx_bit_offset/rx_bit_offset, set by the coupler - an in-memory PDO struct (
tx_pdo/rx_pdo) -
EthercatDeviceimplemented by hand, not derived, because their positions depend on the offsets - empty
EthercatDeviceProcessinghooks, so none of them needinput_post_process/output_pre_process - a
*_MODULE_IDENTconstant
| Ident |
WAGO_750_531_MODULE_IDENT = (0x21, 2147483714) |
| Process data | RxPDO, 4 bits: bit rx_bit_offset + n = output n (Wago750_531OutputPort::DO1…DO4) |
| API |
set_output(port 0..=3, bool). Out-of-range ports are ignored. |
let dout = slot.as_any_mut().downcast_mut::<Wago750_531>().unwrap();
dout.set_output(0, true);| Ident |
WAGO_750_455_MODULE_IDENT = (0x21, 0x045541b3) |
| Process data | TxPDO, 4 × 16 bits starting at tx_bit_offset (channel n at +16·n) |
| Decoding |
value = (raw >> 4) / 0x7FF. Error when the two lowest bits are both set. |
| API |
get_current(port) returns Option<ElectricCurrent> in the range 4–20 mA. get_current_relative(port) returns Option<f64>. None = wiring or range error.
|
let ai = slot.as_any().downcast_ref::<Wago750_455>().unwrap();
match ai.get_current(0) {
Some(i) => println!("{:.2} mA", i.get::<milliampere>()),
None => println!("wiring error"),
}get_current_relative panics for ports above 3. The result isn't clamped, so a raw value above
0x7FF0 comes out as more than 20 mA.
| Ident |
WAGO_750_554_MODULE_IDENT = (0x21, 0x055442cd) |
| Process data | RxPDO, 2 × 16 bits starting at rx_bit_offset
|
| Encoding |
raw = round(relative × 0x7FFF) & 0x7FF8: 0x0000 = 4 mA, 0x7FFF = 20 mA. 12-bit resolution; the module ignores the lowest 3 bits. |
| API |
set_current(port, ElectricCurrent) or set_current_relative(port, 0.0..=1.0). Values outside the range are clamped. Ports other than 0 and 1 are ignored. |
This is a condensed version of the three examples:
use bitvec::{order::Lsb0, slice::BitSlice};
use ethercat_hal::MetaSubdevice;
use ethercat_hal::devices::{EthercatDevice, NewEthercatDevice};
use ethercat_hal::devices::wago_modules::{
wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354},
wago_750_455::Wago750_455,
wago_750_531::Wago750_531,
wago_750_554::Wago750_554,
};
use ethercat_hal::io::{
analog_input::AnalogCurrentInputDevice, analog_output::AnalogCurrentOutputDevice,
digital_output::DigitalOutputDevice,
};
use units::{electric_current::milliampere, f64::ElectricCurrent};
// --- PreOp ---
let is_coupler = |s: &&MetaSubdevice| (s.vendor, s.product_id) == (WAGO_750_354_IDENTITY_A.0, WAGO_750_354_IDENTITY_A.1);
let c = *handle.try_get_subdevices_vec_sync()?.iter().find(is_coupler).expect("no 750-354");
let mut coupler = Wago750_354::new();
for module in Wago750_354::initialize_modules(channel.clone(), c.device_address)? {
println!("slot {}: {} (ident 0x{:08x})", module.slot, module.name, module.product_id);
coupler.set_module(module);
}
coupler.init_slot_modules(channel.clone(), c.device_address);
fn find<T: 'static>(coupler: &Wago750_354) -> Option<usize> {
coupler.slot_devices.iter()
.position(|s| s.as_ref().is_some_and(|d| d.as_any().downcast_ref::<T>().is_some()))
}
let (ai, dout, ao) = (find::<Wago750_455>(&coupler), find::<Wago750_531>(&coupler), find::<Wago750_554>(&coupler));
// --- Op (then re-read the subdevice list for the coupler's offsets) ---
let c = *handle.try_get_subdevices_vec_sync()?.iter().find(is_coupler).unwrap();
loop {
if let Some(inputs) = handle.get_inputs() {
coupler.input(BitSlice::<u8, Lsb0>::from_slice(&inputs[c.start_tx..c.end_tx]))?;
}
let measured = ai
.and_then(|i| coupler.slot_devices[i].as_ref())
.and_then(|d| d.as_any().downcast_ref::<Wago750_455>())
.and_then(|d| d.get_current(0));
if let Some(i) = dout {
let d = coupler.slot_devices[i].as_mut().unwrap().as_any_mut().downcast_mut::<Wago750_531>().unwrap();
d.set_output(0, measured.is_some()); // DO1 = "sensor connected"
}
if let Some(i) = ao {
let d = coupler.slot_devices[i].as_mut().unwrap().as_any_mut().downcast_mut::<Wago750_554>().unwrap();
d.set_current(0, measured.unwrap_or(ElectricCurrent::new::<milliampere>(4.0))); // mirror AI1 → AO1
}
if let Some(outputs) = handle.write_outputs() {
coupler.output(BitSlice::<u8, Lsb0>::from_slice_mut(&mut outputs[c.start_rx..c.end_rx]))?;
handle.send_outputs();
}
}BOILERPLATE.rs has a template with sections A–E:
digital in, digital out, digital in+out, analog in and analog out. The steps are:
-
Create the driver. Copy the boilerplate to
wago_750_XXX.rs, keep the section that fits, and addpub mod wago_750_XXX;towago_modules/mod.rs. Base it on the closest reference module: 750-531 for bit I/O, 750-455 for 16-bit inputs, 750-554 for 16-bit outputs. -
Index from your own offset. Always read and write at
tx_bit_offset + …/rx_bit_offset + …. The slice you receive is the coupler's entire PDO. -
Set the ident. Set
WAGO_750_XXX_PRODUCT_IDto the value the coupler reports in0x9000+0x10·i :0A.initialize_modulesprints it for unknown modules, and the examples print every module's ident. -
Register the module in
wago_750_354.rsin both places:-
get_modules(): an arm that setsname,has_txandhas_rx. If you get the flags wrong, the offset for that direction stays 0. -
init_slot_modules(): an arm that creates the driver fromWAGO_750_XXX_MODULE_IDENT.
-
-
Don't add it to
device_from_subdevice_identity. Slot modules aren't EtherCAT subdevices.
These are true of the current code, and worth knowing before you rely on it:
-
One unsupported module breaks every module after it. When
init_slot_modulesmeets an ident it has no driver for, itreturns. Every module in a later slot gets no driver. -
The 750-460 isn't registered.
wago_750_460.rsexists, but neitherget_modules()norinit_slot_modules()knows about it, so a 750-460 counts as unsupported (see the point above). -
Errors are swallowed.
- SDO failures inside
init_slot_modulesare ignored, and the affected offsets stay 0. - Errors from module
input()/output()are thrown away by the coupler. - Some modules (for example the 750-1506)
expect()on out-of-range bits and panic instead.
- SDO failures inside
-
Wago750_354::input_len()/output_len()are always 0.set_moduleadds each module'stx_offset/rx_offsetto the totals before those offsets have been worked out. - No config or reset. Modules are neither configured nor re-scanned. To change the module layout, restart the application.
EtherCAT HAL
EtherCAT Devices
XTREM
Other crates