Skip to main content

autocore_std/motion/
cia402.rs

1//! CiA 402 (CANopen drive profile) base types and traits.
2//!
3//! This module provides the standard CiA 402 state machine definitions,
4//! mode-of-operation enum, and trait-based abstractions for control word
5//! and status word bit manipulation.
6//!
7//! Layers:
8//! 1. **Raw newtypes** — [`RawControlWord`], [`RawStatusWord`]
9//! 2. **Base traits** — [`Cia402Control`], [`Cia402Status`], [`Cia402State`]
10//! 3. **Mode-specific traits** — [`PpControl`]/[`PpStatus`],
11//!    [`PvControl`]/[`PvStatus`], [`HomingControl`]/[`HomingStatus`]
12//!
13//! Vendor-specific extensions (e.g. Teknic, Yaskawa) build on top of these
14//! traits in their own crates.
15
16/// Raw u16 newtype — this IS the PDO data, no conversion needed
17#[derive(Clone, Copy, Debug, Default)]
18pub struct RawControlWord(pub u16);
19
20/// Raw u16 newtype for the status word.
21#[derive(Clone, Copy, Debug, Default)]
22pub struct RawStatusWord(pub u16);
23
24// ──────────────────────────────────────────────
25// Layer 1: CiA 402 State Machine (all modes)
26// ──────────────────────────────────────────────
27
28/// State machine commands common to every CiA 402 mode.
29///
30/// Bits 0–3 and 7 are the state-machine control bits.
31/// Bits 4–6, 8 are mode-specific (see `PpControl`, `PvControl`, etc.).
32/// Bits 9–15 are manufacturer-specific.
33pub trait Cia402Control {
34    /// Read the raw control word.
35    fn raw(&self) -> u16;
36    /// Get a mutable reference to the raw control word.
37    fn raw_mut(&mut self) -> &mut u16;
38
39    // ── Individual bit setters ──
40
41    /// Set bit 0 — Switch On.
42    fn set_switch_on(&mut self, v: bool) {
43        self.set_bit(0, v);
44    }
45    /// Set bit 1 — Enable Voltage.
46    fn set_enable_voltage(&mut self, v: bool) {
47        self.set_bit(1, v);
48    }
49    /// Set bit 2 — Quick Stop.
50    fn set_quick_stop(&mut self, v: bool) {
51        self.set_bit(2, v);
52    }
53    /// Set bit 3 — Enable Operation.
54    fn set_enable_operation(&mut self, v: bool) {
55        self.set_bit(3, v);
56    }
57    /// Set bit 7 — Fault Reset.
58    fn set_fault_reset(&mut self, v: bool) {
59        self.set_bit(7, v);
60    }
61
62    // ── State-machine transition commands ──
63    //
64    // Each command sets bits 0–3,7 to the pattern required by CiA 402
65    // while preserving mode-specific and vendor bits (4–6, 8–15).
66    //
67    // The mask 0x008F covers bits 0,1,2,3,7.
68
69    /// Shutdown command (transitions 2, 6, 8).
70    /// Target state: Ready to Switch On.
71    fn cmd_shutdown(&mut self) {
72        let w = self.raw_mut();
73        *w = (*w & !0x008F) | 0x0006; // bits 1,2 set; 0,3,7 clear
74    }
75
76    /// Switch On command (transition 3).
77    /// Target state: Switched On.
78    fn cmd_switch_on(&mut self) {
79        let w = self.raw_mut();
80        *w = (*w & !0x008F) | 0x0007; // bits 0,1,2 set; 3,7 clear
81    }
82
83    /// Enable Operation command (transition 4, or combined 2+3+4).
84    /// Target state: Operation Enabled.
85    fn cmd_enable_operation(&mut self) {
86        let w = self.raw_mut();
87        *w = (*w & !0x008F) | 0x000F; // bits 0-3 set; 7 clear
88    }
89
90    /// Disable Operation command (transition 5).
91    /// Target state: Switched On.
92    fn cmd_disable_operation(&mut self) {
93        let w = self.raw_mut();
94        *w = (*w & !0x008F) | 0x0007; // bits 0,1,2 set; 3,7 clear
95    }
96
97    /// Disable Voltage command (transitions 7, 9, 10, 12).
98    /// Target state: Switch On Disabled.
99    fn cmd_disable_voltage(&mut self) {
100        let w = self.raw_mut();
101        *w &= !0x0082; // clear bits 1 and 7
102    }
103
104    /// Quick Stop command (transition 11).
105    /// Target state: Quick Stop Active.
106    fn cmd_quick_stop(&mut self) {
107        let w = self.raw_mut();
108        *w = (*w & !0x0086) | 0x0002; // bit 1 set; bits 2,7 clear
109    }
110
111    /// Fault Reset command (transition 15, rising edge on bit 7).
112    /// Drive must be in Fault state. Transitions to Switch On Disabled.
113    fn cmd_fault_reset(&mut self) {
114        let w = self.raw_mut();
115        *w |= 0x0080; // set bit 7
116    }
117
118
119    /// Clear the Fault Reset command (transition 15, rising edge on bit 7).
120    fn cmd_clear_fault_reset(&mut self) {
121        self.set_bit(7, false);
122    }
123
124    /// Set or clear a single bit in the control word.
125    fn set_bit(&mut self, bit: u8, v: bool) {
126        let w = self.raw_mut();
127        if v {
128            *w |= 1 << bit;
129        } else {
130            *w &= !(1 << bit);
131        }
132    }
133}
134
135/// CiA 402 status word decoding, common to every mode.
136///
137/// Bits 0–6: state machine state.
138/// Bit 7:    warning.
139/// Bit 9:    remote (drive is controlled via fieldbus).
140/// Bit 10:   target reached (mode-dependent meaning).
141/// Bits 8, 11–15: mode-specific or manufacturer-specific.
142pub trait Cia402Status {
143    /// Read the raw status word.
144    fn raw(&self) -> u16;
145
146    /// Bit 0 — Ready to Switch On.
147    fn ready_to_switch_on(&self) -> bool {
148        self.raw() & (1 << 0) != 0
149    }
150    /// Bit 1 — Switched On.
151    fn switched_on(&self) -> bool {
152        self.raw() & (1 << 1) != 0
153    }
154    /// Bit 2 — Operation Enabled.
155    fn operation_enabled(&self) -> bool {
156        self.raw() & (1 << 2) != 0
157    }
158    /// Bit 3 — Fault.
159    fn fault(&self) -> bool {
160        self.raw() & (1 << 3) != 0
161    }
162    /// Bit 4 — Voltage Enabled.
163    fn voltage_enabled(&self) -> bool {
164        self.raw() & (1 << 4) != 0
165    }
166    /// Bit 5 — Quick Stop Active.
167    fn quick_stop_active(&self) -> bool {
168        self.raw() & (1 << 5) != 0
169    }
170    /// Bit 6 — Switch On Disabled.
171    fn switch_on_disabled(&self) -> bool {
172        self.raw() & (1 << 6) != 0
173    }
174    /// Bit 7 — Warning.
175    fn warning(&self) -> bool {
176        self.raw() & (1 << 7) != 0
177    }
178    /// Bit 9 — Remote.
179    fn remote(&self) -> bool {
180        self.raw() & (1 << 9) != 0
181    }
182    /// Bit 10 — Target Reached.
183    fn target_reached(&self) -> bool {
184        self.raw() & (1 << 10) != 0
185    }
186
187    /// Decode the CiA 402 state machine state from status word bits.
188    ///
189    /// The state is encoded in bits 0–3, 5, 6. Some states have bit 5
190    /// as "don't care", so we use different masks:
191    ///  - 0x006F (bits 0,1,2,3,5,6) for states where bit 5 is defined
192    ///  - 0x004F (bits 0,1,2,3,6)   for states where bit 5 is "x"
193    fn state(&self) -> Cia402State {
194        let w = self.raw();
195        // Check most-specific patterns first (bit 5 defined → mask 0x006F)
196        if w & 0x006F == 0x0027 { return Cia402State::OperationEnabled; }
197        if w & 0x006F == 0x0023 { return Cia402State::SwitchedOn; }
198        if w & 0x006F == 0x0021 { return Cia402State::ReadyToSwitchOn; }
199        if w & 0x006F == 0x0007 { return Cia402State::QuickStopActive; }
200        // Less-specific patterns (bit 5 is don't-care → mask 0x004F)
201        if w & 0x004F == 0x000F { return Cia402State::FaultReactionActive; }
202        if w & 0x004F == 0x0008 { return Cia402State::Fault; }
203        if w & 0x004F == 0x0040 { return Cia402State::SwitchOnDisabled; }
204        if w & 0x004F == 0x0000 { return Cia402State::NotReadyToSwitchOn; }
205        Cia402State::Unknown
206    }
207}
208
209/// CiA 402 drive state machine states.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum Cia402State {
212    /// Not Ready to Switch On — drive is initializing.
213    NotReadyToSwitchOn,
214    /// Switch On Disabled — drive power stage is disabled.
215    SwitchOnDisabled,
216    /// Ready to Switch On — waiting for Switch On command.
217    ReadyToSwitchOn,
218    /// Switched On — power stage is energized but not enabled.
219    SwitchedOn,
220    /// Operation Enabled — drive is active and accepting motion commands.
221    OperationEnabled,
222    /// Quick Stop Active — drive is decelerating to stop.
223    QuickStopActive,
224    /// Fault Reaction Active — drive is executing fault reaction.
225    FaultReactionActive,
226    /// Fault — drive has faulted and requires a fault reset.
227    Fault,
228    /// Unknown — status word pattern did not match any defined state.
229    Unknown,
230}
231
232impl std::fmt::Display for Cia402State {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        match self {
235            Self::NotReadyToSwitchOn => write!(f, "Not Ready to Switch On"),
236            Self::SwitchOnDisabled   => write!(f, "Switch On Disabled"),
237            Self::ReadyToSwitchOn    => write!(f, "Ready to Switch On"),
238            Self::SwitchedOn         => write!(f, "Switched On"),
239            Self::OperationEnabled   => write!(f, "Operation Enabled"),
240            Self::QuickStopActive    => write!(f, "Quick Stop Active"),
241            Self::FaultReactionActive => write!(f, "Fault Reaction Active"),
242            Self::Fault              => write!(f, "Fault"),
243            Self::Unknown            => write!(f, "Unknown"),
244        }
245    }
246}
247
248// ── Modes of Operation (0x6060 / 0x6061) ──
249
250/// CiA 402 Modes of Operation.
251///
252/// Written to RxPDO object 0x6060; read back from TxPDO object 0x6061.
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254#[repr(i8)]
255pub enum ModesOfOperation {
256    /// Profile Position mode (PP).
257    ProfilePosition          =  1,
258    /// Profile Velocity mode (PV).
259    ProfileVelocity          =  3,
260    /// Homing mode.
261    Homing                   =  6,
262    /// Interpolated Position mode (IP).
263    InterpolatedPosition     =  7,
264    /// Cyclic Synchronous Position mode (CSP).
265    CyclicSynchronousPosition = 8,
266    /// Cyclic Synchronous Velocity mode (CSV).
267    CyclicSynchronousVelocity = 9,
268    /// Cyclic Synchronous Torque mode (CST).
269    CyclicSynchronousTorque  = 10,
270}
271
272impl ModesOfOperation {
273    /// Convert an i8 to a `ModesOfOperation` variant, if valid.
274    pub fn from_i8(v: i8) -> Option<Self> {
275        match v {
276            1  => Some(Self::ProfilePosition),
277            3  => Some(Self::ProfileVelocity),
278            6  => Some(Self::Homing),
279            7  => Some(Self::InterpolatedPosition),
280            8  => Some(Self::CyclicSynchronousPosition),
281            9  => Some(Self::CyclicSynchronousVelocity),
282            10 => Some(Self::CyclicSynchronousTorque),
283            _  => None,
284        }
285    }
286
287    /// Convert to the underlying i8 value.
288    pub fn as_i8(self) -> i8 {
289        self as i8
290    }
291}
292
293impl std::fmt::Display for ModesOfOperation {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        match self {
296            Self::ProfilePosition           => write!(f, "Profile Position (PP)"),
297            Self::ProfileVelocity           => write!(f, "Profile Velocity (PV)"),
298            Self::Homing                    => write!(f, "Homing"),
299            Self::InterpolatedPosition      => write!(f, "Interpolated Position (IP)"),
300            Self::CyclicSynchronousPosition => write!(f, "Cyclic Synchronous Position (CSP)"),
301            Self::CyclicSynchronousVelocity => write!(f, "Cyclic Synchronous Velocity (CSV)"),
302            Self::CyclicSynchronousTorque   => write!(f, "Cyclic Synchronous Torque (CST)"),
303        }
304    }
305}
306
307// Implement base traits for the raw newtypes
308impl Cia402Control for RawControlWord {
309    fn raw(&self) -> u16 {
310        self.0
311    }
312    fn raw_mut(&mut self) -> &mut u16 {
313        &mut self.0
314    }
315}
316impl Cia402Status for RawStatusWord {
317    fn raw(&self) -> u16 {
318        self.0
319    }
320}
321
322// ──────────────────────────────────────────────
323// Layer 2: Profile Position mode bits (PP)
324// ──────────────────────────────────────────────
325
326/// Profile Position (PP) mode control word bits.
327pub trait PpControl: Cia402Control {
328    /// Bit 4 — New Set-Point: rising edge starts a new positioning move.
329    fn set_new_set_point(&mut self, v: bool) {
330        self.set_bit(4, v);
331    }
332    /// Bit 5 — Change Set Immediately: if true, interrupt current move.
333    fn set_change_set_immediately(&mut self, v: bool) {
334        self.set_bit(5, v);
335    }
336    /// Bit 6 — Relative: target position is relative to current.
337    fn set_relative(&mut self, v: bool) {
338        self.set_bit(6, v);
339    }
340    /// Bit 8 — Halt: decelerate to stop.
341    fn set_halt(&mut self, v: bool) {
342        self.set_bit(8, v);
343    }
344}
345
346/// Profile Position (PP) mode status word bits.
347pub trait PpStatus: Cia402Status {
348    /// Bit 10 — Target Reached: positioning move completed.
349    fn pp_target_reached(&self) -> bool {
350        self.raw() & (1 << 10) != 0
351    }
352    /// Bit 11 — Internal Limit Active.
353    fn internal_limit(&self) -> bool {
354        self.raw() & (1 << 11) != 0
355    }
356    /// Bit 12 — Set-Point Acknowledge: drive accepted the new set-point.
357    fn set_point_acknowledge(&self) -> bool {
358        self.raw() & (1 << 12) != 0
359    }
360    /// Bit 13 — Following Error: position tracking error exceeded limit.
361    fn following_error(&self) -> bool {
362        self.raw() & (1 << 13) != 0
363    }
364}
365
366// ──────────────────────────────────────────────
367// Layer 2: Profile Velocity mode bits (PV)
368// ──────────────────────────────────────────────
369
370/// Profile Velocity (PV) mode control word bits.
371pub trait PvControl: Cia402Control {
372    /// Bit 8 — Halt: decelerate to zero velocity.
373    fn set_halt(&mut self, v: bool) {
374        self.set_bit(8, v);
375    }
376}
377
378/// Profile Velocity (PV) mode status word bits.
379pub trait PvStatus: Cia402Status {
380    /// Bit 10 — Target Reached: actual velocity equals target velocity.
381    fn pv_target_reached(&self) -> bool {
382        self.raw() & (1 << 10) != 0
383    }
384    /// Bit 11 — Internal Limit Active.
385    fn pv_internal_limit(&self) -> bool {
386        self.raw() & (1 << 11) != 0
387    }
388    /// Bit 12 — Speed: 0 = velocity != 0, 1 = velocity = 0.
389    fn speed_is_zero(&self) -> bool {
390        self.raw() & (1 << 12) != 0
391    }
392    /// Bit 13 — Max Slippage Error (AC motors; not used by ClearPath-EC).
393    fn max_slippage_error(&self) -> bool {
394        self.raw() & (1 << 13) != 0
395    }
396}
397
398// ──────────────────────────────────────────────
399// Layer 2: Homing mode bits
400// ──────────────────────────────────────────────
401
402/// Homing mode control word bits.
403pub trait HomingControl: Cia402Control {
404    /// Bit 4 — Homing Operation Start: rising edge starts homing.
405    fn set_homing_start(&mut self, v: bool) {
406        self.set_bit(4, v);
407    }
408    /// Bit 8 — Halt: interrupt homing and decelerate.
409    fn set_halt(&mut self, v: bool) {
410        self.set_bit(8, v);
411    }
412}
413
414/// Homing mode status word bits.
415pub trait HomingStatus: Cia402Status {
416    /// Bit 10 — Target Reached: homing target position reached.
417    fn homing_target_reached(&self) -> bool {
418        self.raw() & (1 << 10) != 0
419    }
420    /// Bit 12 — Homing Attained: homing procedure completed successfully.
421    fn homing_attained(&self) -> bool {
422        self.raw() & (1 << 12) != 0
423    }
424    /// Bit 13 — Homing Error: homing procedure failed.
425    fn homing_error(&self) -> bool {
426        self.raw() & (1 << 13) != 0
427    }
428}
429
430// Implement PP traits for RawControlWord / RawStatusWord
431impl PpControl for RawControlWord {}
432impl PpStatus for RawStatusWord {}
433impl PvControl for RawControlWord {}
434impl PvStatus for RawStatusWord {}
435impl HomingControl for RawControlWord {}
436impl HomingStatus for RawStatusWord {}
437
438// ──────────────────────────────────────────────
439// Tests
440// ──────────────────────────────────────────────
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn test_state_decoding() {
448        // Standard patterns
449        assert_eq!(RawStatusWord(0x0000).state(), Cia402State::NotReadyToSwitchOn);
450        assert_eq!(RawStatusWord(0x0040).state(), Cia402State::SwitchOnDisabled);
451        assert_eq!(RawStatusWord(0x0021).state(), Cia402State::ReadyToSwitchOn);
452        assert_eq!(RawStatusWord(0x0023).state(), Cia402State::SwitchedOn);
453        assert_eq!(RawStatusWord(0x0027).state(), Cia402State::OperationEnabled);
454        assert_eq!(RawStatusWord(0x0007).state(), Cia402State::QuickStopActive);
455        assert_eq!(RawStatusWord(0x000F).state(), Cia402State::FaultReactionActive);
456        assert_eq!(RawStatusWord(0x0008).state(), Cia402State::Fault);
457    }
458
459    #[test]
460    fn test_state_decoding_bit5_dont_care() {
461        // States where bit 5 is "don't care" must decode correctly
462        // regardless of whether bit 5 is 0 or 1.
463
464        // Not Ready to Switch On: bit 5 = 1 → still NRTSO
465        assert_eq!(RawStatusWord(0x0020).state(), Cia402State::NotReadyToSwitchOn);
466        // Switch On Disabled: bit 5 = 1
467        assert_eq!(RawStatusWord(0x0060).state(), Cia402State::SwitchOnDisabled);
468        // Fault Reaction Active: bit 5 = 1
469        assert_eq!(RawStatusWord(0x002F).state(), Cia402State::FaultReactionActive);
470        // Fault: bit 5 = 1
471        assert_eq!(RawStatusWord(0x0028).state(), Cia402State::Fault);
472    }
473
474    #[test]
475    fn test_state_decoding_ignores_high_bits() {
476        // Bits 8+ should not affect state decoding
477        assert_eq!(RawStatusWord(0xFF27).state(), Cia402State::OperationEnabled);
478        assert_eq!(RawStatusWord(0x8040).state(), Cia402State::SwitchOnDisabled);
479    }
480
481    #[test]
482    fn test_cmd_shutdown() {
483        let mut cw = RawControlWord(0xFF00);
484        cw.cmd_shutdown();
485        // Bits 1,2 set; bits 0,3,7 clear; bits 4-6,8-15 preserved
486        assert_eq!(cw.0, 0xFF06);
487    }
488
489    #[test]
490    fn test_cmd_enable_operation() {
491        let mut cw = RawControlWord(0x0000);
492        cw.cmd_enable_operation();
493        assert_eq!(cw.0, 0x000F);
494    }
495
496    #[test]
497    fn test_cmd_fault_reset() {
498        let mut cw = RawControlWord(0x0000);
499        cw.cmd_fault_reset();
500        assert!(cw.0 & 0x0080 != 0); // bit 7 set
501    }
502
503    #[test]
504    fn test_modes_of_operation_roundtrip() {
505        for mode in [
506            ModesOfOperation::ProfilePosition,
507            ModesOfOperation::ProfileVelocity,
508            ModesOfOperation::Homing,
509        ] {
510            assert_eq!(ModesOfOperation::from_i8(mode.as_i8()), Some(mode));
511        }
512        assert_eq!(ModesOfOperation::from_i8(99), None);
513    }
514}