YARP CiA-402 EtherCAT Device 0.6.0
YARP device plugin for EtherCAT CiA-402 drives
Loading...
Searching...
No Matches
EthercatManager.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Fondazione Istituto Italiano di Tecnologia (IIT)
2// SPDX-License-Identifier: BSD-3-Clause
3
4#ifndef YARP_DEV_CIA402_ETHERCAT_MANAGER_H
5#define YARP_DEV_CIA402_ETHERCAT_MANAGER_H
6
7#include <atomic>
8#include <cstdint>
9#include <cstring>
10#include <mutex>
11#include <string>
12#include <thread>
13#include <type_traits>
14#include <unordered_map>
15#include <vector>
16
17// SOEM
18#include <soem/soem.h>
19
20namespace CiA402
21{
22
23#pragma pack(push, 1)
24
25/**
26 * @brief Process Data Object written from master to slave (RxPDO).
27 *
28 * This struct mirrors the layout of the outputs sent cyclically to each drive.
29 * The exact mapping is configured at startup (see configurePDOMapping), but
30 * fields correspond to standard CiA-402 objects:
31 * - Controlword (0x6040)
32 * - OpMode (0x6060)
33 * - TargetTorque (0x6071)
34 * - TargetPosition (0x607A)
35 * - TargetVelocity (0x60FF)
36 */
37struct RxPDO // (= master → slave, also called *Rx*PDO)
38{
39 uint16_t Controlword; ///< 0x6040: Controlword
40 int8_t OpMode; ///< 0x6060: Modes of operation
41 int16_t TargetTorque; ///< 0x6071: Target torque
42 int32_t TargetPosition; ///< 0x607A: Target position
43 int32_t TargetVelocity; ///< 0x60FF: Target velocity
44};
45#pragma pack(pop)
46
47/**
48 * @brief Identifiers for fields read from a slave (TxPDO → master).
49 *
50 * Each value corresponds to a standard or vendor-specific object/subindex
51 * that can be mapped into the TxPDO image and accessed via TxView.
52 */
53enum class TxField : uint8_t
54{
55 Statusword, ///< 0x6041: Statusword
56 OpModeDisplay, ///< 0x6061: Modes of operation display
57 Position6064, ///< 0x6064: Position actual value
58 Velocity606C, ///< 0x606C: Velocity actual value
59 Torque6077, ///< 0x6077: Torque actual value
60 PositionError6065, ///< 0x6065: Position demand value / error
61 Timestamp20F0, ///< 0x20F0: Timestamp (if supported)
62 STO_6621_01, ///< 0x6621:01 Safe Torque Off status (exposed via PDO)
63 SBC_6621_02, ///< 0x6621:02 Safe Brake Control status (exposed via PDO)
64 Enc1Pos2111_02, ///< 0x2111:02 Vendor enc1 position
65 Enc1Vel2111_03, ///< 0x2111:03 Vendor enc1 velocity
66 Enc2Pos2113_02, ///< 0x2113:02 Vendor enc2 position
67 Enc2Vel2113_03, ///< 0x2113:03 Vendor enc2 velocity
68 TemperatureDrive ///< 0x2031:01 Temperature (if supported)
69};
70
71/**
72 * @brief Metadata describing a field inside the raw TxPDO image.
73 */
75{
76 TxField id; ///< Logical field id.
77 uint32_t byteOffset; ///< Byte offset from the base of the Tx image.
78 uint8_t bitSize; ///< Size in bits (8/16/32).
79};
80
81/**
82 * @brief Lightweight view over a slave's TxPDO image.
83 *
84 * Provides presence checks and typed accessors to values mapped into the
85 * device TxPDO. Lifetime note: this is a non-owning view, valid as long as
86 * the underlying process image buffer remains alive and unmoved.
87 */
88class TxView
89{
90public:
91 /**
92 * @brief Construct a view.
93 * @param base Pointer to the beginning of the raw TxPDO buffer for a slave.
94 * @param map Pointer to the field map describing where each entry is.
95 */
96 TxView(const uint8_t* base, const std::unordered_map<TxField, FieldInfo>* map)
97 : m_base(base)
98 , m_map(map)
99 {
100 }
101
102 /**
103 * @brief Check if a given field is available in the current mapping.
104 */
105 bool has(TxField f) const
106 {
107 return m_map && (m_map->find(f) != m_map->end());
108 }
109
110 /**
111 * @brief Read a field from the TxPDO image with a typed accessor.
112 *
113 * Supports any trivially copyable type whose size matches the mapped field
114 * (commonly 8/16/32-bit integers or IEEE-754 REALs coming from the drive).
115 * If the field is not present or the size does not match, the fallback is returned.
116 */
117 template <typename T> T get(TxField f, T fallback = {}) const
118 {
119 static_assert(std::is_trivially_copyable_v<T>, "TxView::get requires trivially copyable T");
120
121 auto it = m_map->find(f);
122 if (it == m_map->end())
123 {
124 return fallback;
125 }
126
127 const auto& fi = it->second;
128 if (fi.bitSize % 8 != 0 || fi.bitSize == 0)
129 {
130 return fallback;
131 }
132 const std::size_t byteSize = fi.bitSize / 8;
133 if (byteSize != sizeof(T) || byteSize == 0)
134 {
135 return fallback;
136 }
137
138 T value{};
139 std::memcpy(&value, m_base + fi.byteOffset, byteSize);
140 return value;
141 }
142
143private:
144 const uint8_t* m_base{nullptr}; ///< Raw TxPDO base pointer.
145 const std::unordered_map<TxField, FieldInfo>* m_map{nullptr}; ///< Mapping of available fields.
146};
147
148/**
149 * @brief Minimal EtherCAT master built on SOEM for CiA-402 devices.
150 *
151 * Responsibilities:
152 * - Discover and initialize the bus on a given network interface.
153 * - Configure PDO mappings (including safety/status signals such as STO/SBC).
154 * - Maintain the cyclic exchange (send/receive) of process data.
155 * - Expose convenient accessors for RxPDO (outputs) and TxView (inputs).
156 * - Provide helpers for SDO reads and diagnostic information (WKC).
157 * - Monitor errors in a background thread.
158 *
159 * Notes:
160 * - SOEM uses 1-based slave indices; all methods follow that convention.
161 * - Pointers/views returned remain valid until re-initialization or shutdown.
162 */
164{
165public:
166 /**
167 * @brief Error codes returned by EthercatManager APIs.
168 */
169 enum class Error : int
170 {
171 NoError = 0, ///< Operation completed successfully.
172 InitFailed = -1, ///< Bus/stack initialization failed.
173 NoSlavesFound = -2, ///< No EtherCAT slaves were discovered.
174 ConfigFailed = -3, ///< PDO/SDO configuration failed.
175 SlavesNotOp = -4, ///< Slaves did not reach OP state.
176 AlreadyInitialized = -5, ///< init() called more than once.
177 NotInitialized = -6, ///< Operation requires a prior successful init().
178 InvalidSlaveIndex = -7, ///< Provided slave index is out of range (1..ec_slavecount).
179 PdoExchangeFailed = -8, ///< Failed to exchange PDOs or read SDO.
180 };
181
182 /** @brief Construct the manager (no I/O started). */
184 /** @brief Destructor, stops background monitoring and releases resources. */
186
189
190 /**
191 * @brief Initialize the EtherCAT stack and configure slaves.
192 * @param ifname Network interface name (e.g., "eth0").
193 * @return Error::NoError on success, specific error otherwise.
194 */
195 Error init(const std::string& ifname) noexcept;
196
197 /** @brief Whether the manager has been successfully initialized. */
198 bool isInitialized() const noexcept
199 {
200 return m_initialized;
201 }
202
203 /** @brief Whether the ring is in OPERATIONAL state. */
204 bool isOperational() const noexcept
205 {
206 return m_isOperational;
207 }
208
209 /**
210 * @brief Perform one cyclic send/receive exchange over the bus.
211 * @return Error::NoError on success; PdoExchangeFailed otherwise.
212 */
213 Error sendReceive() noexcept;
214
215 /**
216 * @brief Last Working Counter (WKC) observed from the previous cycle.
217 */
218 int getWorkingCounter() const noexcept
219 {
220 return m_lastWkc;
221 }
222
223 int getExpectedWorkingCounter() const noexcept
224 {
225 return m_expectedWkc;
226 }
227
228 /**
229 * @brief Set receive timeout for PDO exchange (microseconds).
230 * Default is EC_TIMEOUTRET from SOEM. Larger values can help on busy systems.
231 */
232 void setPdoTimeoutUs(int usec) noexcept
233 {
234 m_pdoTimeoutUs = (usec > 0 ? usec : m_pdoTimeoutUs);
235 }
236
237 /**
238 * @brief Get current receive timeout for PDO exchange (microseconds).
239 */
240 int getPdoTimeoutUs() const noexcept
241 {
242 return m_pdoTimeoutUs;
243 }
244
245 /**
246 * @brief Access the RxPDO (master→slave) buffer for a given slave.
247 * @note Pointer remains valid until re-initialization.
248 */
249 const RxPDO* getRxPDO(int slaveIndex) const noexcept;
250 /** @overload */
251 RxPDO* getRxPDO(int slaveIndex) noexcept;
252
253 /**
254 * @brief Obtain a typed, bounds-checked view over the TxPDO (slave→master).
255 */
256 TxView getTxView(int slaveIndex) const noexcept;
257
258 /**
259 * @brief Read an SDO value from a slave (blocking call).
260 * @tparam T Trivially copyable integral type matching the object size.
261 * @param slaveIndex 1-based slave index.
262 * @param idx Object index (e.g., 0x6041).
263 * @param subIdx Object subindex.
264 * @param out Destination variable populated on success.
265 * @return Error::NoError on success; an error code otherwise.
266 */
267 template <typename T>
268 Error readSDO(int slaveIndex, uint16_t idx, uint8_t subIdx, T& out) noexcept;
269
270 /**
271 * @brief Write an SDO value to a slave (blocking call).
272 * @tparam T Trivially copyable integral type matching the object size.
273 * @param slaveIndex 1-based slave index.
274 * @param idx Object index (e.g., 0x6040).
275 * @param subIdx Object subindex.
276 * @param value Value to write.
277 * @return Error::NoError on success; an error code otherwise.
278 */
279 template <typename T>
280 Error writeSDO(int slaveIndex, uint16_t idx, uint8_t subIdx, const T& value) noexcept;
281
282 /**
283 * @brief Get the name of a slave device.
284 * @param slaveIndex 1-based slave index.
285 * @return Slave name as a string.
286 */
287 std::string getName(int slaveIndex) const noexcept;
288
289 /**
290 * @brief Enable DC Sync0 for a slave.
291 * @param cycleNs Sync cycle time in nanoseconds.
292 * @param shiftNs Sync phase shift in nanoseconds.
293 * @return Error::NoError on success; an error code otherwise.
294 */
295 Error enableDCSync0(uint32_t cycleNs, int32_t shiftNs = 0) noexcept;
296
297 /**
298 * @brief Disable DC Sync0 for a slave.
299 * @return Error::NoError on success; an error code otherwise.
300 */
301 Error disableDCSync0() noexcept;
302
303 /**
304 * @brief Print quick diagnostics about bus/slave state and WKC.
305 * Call this when a PDO exchange fails to get immediate hints.
306 */
307 void dumpDiagnostics() noexcept;
308
309 /**
310 * @brief Transition all slaves to OPERATIONAL and start monitoring.
311 *
312 * Call this after init() and any SDO configuration reads, right before
313 * starting the cyclic control loop. It computes WKC expectations, caches
314 * pointers if needed, and launches the background state monitor.
315 */
316 Error goOperational() noexcept;
317
318 /**
319 * @brief Transition all slaves back to PRE-OP and stop monitoring.
320 *
321 * Use this to temporarily drop out of OP for tests or reconfiguration.
322 * Leaves the manager initialized (SAFE-OP/PRE-OP), but not operational.
323 */
324 Error goPreOp() noexcept;
325
326private:
327 /** @brief Background error/AL status monitor loop. */
328 void errorMonitorLoop() noexcept;
329
330 /**
331 * @brief Validate a SOEM 1-based slave index.
332 * @param slaveIndex 1-based index to validate.
333 * @return True if valid (1 <= slaveIndex <= slavecount); false otherwise.
334 */
335 bool indexValid(int slaveIndex) const noexcept;
336
337 /**
338 * @brief Program the device PDO mapping and build Tx field tables.
339 * @param s 1-based slave index.
340 */
341 Error configurePDOMapping(int s);
342
343 ecx_contextt m_ctx{}; ///< SOEM context structure.
344 bool m_portOpen{false}; ///< True if the network port was opened.
345
346 std::atomic<bool> m_initialized{false}; ///< True after successful init().
347 std::atomic<bool> m_runWatch{false}; ///< Controls error monitor thread.
348 std::thread m_watchThread; ///< Error monitor thread.
349 std::atomic<bool> m_isOperational{false}; ///< True once slaves reached OP.
350
351 std::vector<RxPDO*> m_rxPtr; ///< Per-slave RxPDO pointers.
352 std::vector<uint8_t*> m_txRaw; ///< Per-slave raw Tx image base pointers.
353 std::vector<std::unordered_map<TxField, FieldInfo>> m_txMap; ///< Per-slave Tx field maps.
354
355 int m_lastWkc{0}; ///< Last working counter.
356 int m_expectedWkc{0}; ///< Expected working counter.
357 int m_consecutivePdoErrors{0}; ///< Number of consecutive PDO failures.
358 char m_ioMap[4096]{}; ///< SOEM IO map buffer (shared).
359 int m_pdoTimeoutUs{EC_TIMEOUTRET}; ///< Receive timeout for process data.
360
361 mutable std::mutex m_ioMtx; ///< Protects IO/SDO accesses.
362
363 static constexpr std::string_view m_kClassName = "EthercatManager"; // Class name for logging
364};
365
366template <typename T>
368EthercatManager::readSDO(int slaveIndex, uint16_t idx, uint8_t subIdx, T& out) noexcept
369{
370 // Validate preconditions before attempting EtherCAT operation
371 if (!m_initialized.load())
373 if (!indexValid(slaveIndex))
375
376 int size = sizeof(T); // Expected data size for type T
377 int rc = 0; // SOEM return code (>0 = success, <=0 = error)
378
379 // SDO operations must be serialized to avoid SOEM conflicts
380 {
381 std::lock_guard<std::mutex> lock(m_ioMtx);
382 // ec_SDOread: slaveIndex, index, subindex, complete_access, size_ptr, data_ptr, timeout
383 rc = ecx_SDOread(&m_ctx, slaveIndex, idx, subIdx, false, &size, &out, EC_TIMEOUTRXM);
384 }
385
386 // Convert SOEM result to our error enum
387 return (rc > 0) ? Error::NoError : Error::PdoExchangeFailed;
388}
389
390template <typename T>
392EthercatManager::writeSDO(int slaveIndex, uint16_t idx, uint8_t subIdx, const T& value) noexcept
393{
394 // Validate preconditions before attempting EtherCAT operation
395 if (!m_initialized.load())
397 if (!indexValid(slaveIndex))
399
400 int size = sizeof(T); // Expected data size for type T
401 int rc = 0; // SOEM return code (>0 = success, <=0 = error)
402 {
403 std::lock_guard<std::mutex> lock(m_ioMtx);
404
405 // ec_SDOwrite: slaveIndex, index, subindex, complete_access, size, data_ptr, timeout
406 rc = ecx_SDOwrite(&m_ctx,
407 slaveIndex,
408 idx,
409 subIdx,
410 false,
411 size,
412 const_cast<T*>(&value),
413 EC_TIMEOUTRXM);
414 }
415
416 // Convert SOEM result to our error enum
417 return (rc > 0) ? Error::NoError : Error::PdoExchangeFailed;
418}
419
420} // namespace CiA402
421
422#endif // YARP_DEV_CIA402_ETHERCAT_MANAGER_H
TxView getTxView(int slaveIndex) const noexcept
Obtain a typed, bounds-checked view over the TxPDO (slave→master).
EthercatManager(const EthercatManager &)=delete
bool isInitialized() const noexcept
Whether the manager has been successfully initialized.
int getWorkingCounter() const noexcept
Last Working Counter (WKC) observed from the previous cycle.
~EthercatManager()
Destructor, stops background monitoring and releases resources.
Error goPreOp() noexcept
Transition all slaves back to PRE-OP and stop monitoring.
void setPdoTimeoutUs(int usec) noexcept
Set receive timeout for PDO exchange (microseconds). Default is EC_TIMEOUTRET from SOEM....
EthercatManager()
Construct the manager (no I/O started).
int getExpectedWorkingCounter() const noexcept
Error enableDCSync0(uint32_t cycleNs, int32_t shiftNs=0) noexcept
Enable DC Sync0 for a slave.
Error init(const std::string &ifname) noexcept
Initialize the EtherCAT stack and configure slaves.
std::string getName(int slaveIndex) const noexcept
Get the name of a slave device.
int getPdoTimeoutUs() const noexcept
Get current receive timeout for PDO exchange (microseconds).
EthercatManager & operator=(const EthercatManager &)=delete
bool isOperational() const noexcept
Whether the ring is in OPERATIONAL state.
Error sendReceive() noexcept
Perform one cyclic send/receive exchange over the bus.
Error goOperational() noexcept
Transition all slaves to OPERATIONAL and start monitoring.
void dumpDiagnostics() noexcept
Print quick diagnostics about bus/slave state and WKC. Call this when a PDO exchange fails to get imm...
const RxPDO * getRxPDO(int slaveIndex) const noexcept
Access the RxPDO (master→slave) buffer for a given slave.
Error readSDO(int slaveIndex, uint16_t idx, uint8_t subIdx, T &out) noexcept
Read an SDO value from a slave (blocking call).
Error disableDCSync0() noexcept
Disable DC Sync0 for a slave.
Error
Error codes returned by EthercatManager APIs.
@ PdoExchangeFailed
Failed to exchange PDOs or read SDO.
@ SlavesNotOp
Slaves did not reach OP state.
@ ConfigFailed
PDO/SDO configuration failed.
@ InitFailed
Bus/stack initialization failed.
@ NoError
Operation completed successfully.
@ NotInitialized
Operation requires a prior successful init().
@ AlreadyInitialized
init() called more than once.
@ NoSlavesFound
No EtherCAT slaves were discovered.
@ InvalidSlaveIndex
Provided slave index is out of range (1..ec_slavecount).
Error writeSDO(int slaveIndex, uint16_t idx, uint8_t subIdx, const T &value) noexcept
Write an SDO value to a slave (blocking call).
Lightweight view over a slave's TxPDO image.
bool has(TxField f) const
Check if a given field is available in the current mapping.
TxView(const uint8_t *base, const std::unordered_map< TxField, FieldInfo > *map)
Construct a view.
T get(TxField f, T fallback={}) const
Read a field from the TxPDO image with a typed accessor.
T memcpy(T... args)
TxField
Identifiers for fields read from a slave (TxPDO → master).
@ Velocity606C
0x606C: Velocity actual value
@ Enc2Pos2113_02
0x2113:02 Vendor enc2 position
@ SBC_6621_02
0x6621:02 Safe Brake Control status (exposed via PDO)
@ Enc2Vel2113_03
0x2113:03 Vendor enc2 velocity
@ OpModeDisplay
0x6061: Modes of operation display
@ Timestamp20F0
0x20F0: Timestamp (if supported)
@ PositionError6065
0x6065: Position demand value / error
@ Statusword
0x6041: Statusword
@ TemperatureDrive
0x2031:01 Temperature (if supported)
@ Position6064
0x6064: Position actual value
@ Torque6077
0x6077: Torque actual value
@ Enc1Pos2111_02
0x2111:02 Vendor enc1 position
@ Enc1Vel2111_03
0x2111:03 Vendor enc1 velocity
@ STO_6621_01
0x6621:01 Safe Torque Off status (exposed via PDO)
Metadata describing a field inside the raw TxPDO image.
TxField id
Logical field id.
uint32_t byteOffset
Byte offset from the base of the Tx image.
uint8_t bitSize
Size in bits (8/16/32).
Process Data Object written from master to slave (RxPDO).
int8_t OpMode
0x6060: Modes of operation
uint16_t Controlword
0x6040: Controlword
int32_t TargetPosition
0x607A: Target position
int16_t TargetTorque
0x6071: Target torque
int32_t TargetVelocity
0x60FF: Target velocity