etherage/
sii.rs

1/*!
2    SII (Slave Information Interface) allows to retreive from EEPROM declarative informations about a slave (like a manifest) like product code, vendor, etc as well as slave boot-up configs.
3
4    The EEPROM can also store configurations variables for the specific slaves purpose (like control loop coefficients, safety settings, etc) but these values are vendor-specific and often now meant for user inspection through the SII. They are still accessible using the tools provided here, but not structure is provided to interpret them.
5
6    ETG.1000.4.6.6.4, ETG.2010
7
8    This module features [Sii] and [SiiCursor] in order to read/write and parse the eeprom data
9
10    The EEPROM has 2 regions of data:
11
12    - EEPROM registers: fixed addresses, described in [eeprom] and accessed by [`Sii`]
13    - EEPROM categories: contiguous data blocks, described by [`Category`] and accessed by [`SiiCursor`]
14
15    Here is how to use registers:
16    ```ignore
17    sii.acquire().await?;
18    let vendor = sii.read(eeprom::device::vendor).await?;
19    let alias = sii.read(eeprom::address_alias).await?;
20    sii.release().await?;
21    ```
22
23    Here is how to parse the categories:
24    ```ignore
25    sii.acquire().await?;
26    let mut categories = sii.categories();
27    let general = loop {
28        let category = categories.unpack::<Category>().await?;
29        // we got our desired category, do something with it
30        if category.ty() == CategoryType::General {
31            // we can then read the category (or at least its header) as a register
32            break Ok(categories.unpack::<General>().await?)
33        }
34        // end of eeprom reached
35        else if category.ty() == CategoryType::End {
36            break Err(EthercatError::Master("no general category in EEPROM"))
37        }
38        // or squeeze the current category
39        else {
40            categories.advance(WORD*category.size());
41        }
42    };
43    sii.release().await?;
44    ```
45*/
46
47use crate::{
48    error::{EthercatError, EthercatResult},
49    data::{self, PduData, Storage, Field, Cursor},
50    rawmaster::{RawMaster, SlaveAddress},
51    registers,
52    eeprom,
53    };
54use std::sync::Arc;
55use bilge::prelude::*;
56
57
58pub const WORD: u16 = eeprom::WORD as _;
59
60
61/**
62    implementation of the Slave Information Interface (SII) to communicate with a slave's EEPROM memory
63
64    The EEPROM has 2 regions of data:
65
66    - EEPROM registers: at fixed addresses, described in [eeprom]
67    - EEPROM categories: as contiguous data blocks, starting from fixed address [eeprom::categories]
68
69    This struct is providing access to both, but only works with fixed addresses. To parse the categories, use a cursor returned by [Self::categories] then parse the structures iteratively.
70
71    A `Sii` instance is generally obtained from [Slave::sii](crate::Slave::sii)
72*/
73pub struct Sii {
74    master: Arc<RawMaster>,
75    slave: SlaveAddress,
76    /// address mask (part of the address actually used by the slave)
77    mask: u16,
78    /// whether the EEPROM is writable through the SII
79    writable: bool,
80    /// whether the master owns access to the SII (and EEPROM)
81    locked: bool,
82}
83impl Sii {
84    /**
85        create an instance of this struct to use the SII of the given slave
86
87        to stay protocol-safe, one instance of this struct only should exist per slave.
88
89        At contrary to the EEPROM addresses used at the ethercat communication level, this struct and its methods only use byte addresses, write requests should be word-aligned.
90    */
91    pub async fn new(master: Arc<RawMaster>, slave: SlaveAddress) -> EthercatResult<Sii, SiiError> {
92        let status = master.read(slave, registers::sii::control).await.one()?;
93        let mask = match status.address_unit() {
94            registers::SiiUnit::Byte => 0xff,
95            registers::SiiUnit::Word => 0xffff,
96        };
97        if status.checksum_error()
98            {return Err(EthercatError::Slave(slave, SiiError::Checksum))};
99        Ok(Self {
100            master,
101            slave,
102            mask,
103            writable: status.write_access(),
104            locked: false,
105            })
106    }
107    /// tells if the EEPROM is writable through the SII
108    pub fn writable(&self) -> bool {self.writable}
109
110    /**
111        acquire the SII so we can use it through the registers
112
113        The access to the EEPROM is always made through the SII, even internally for the slave, which mean for the slave to access its EEPROM, the master should not have it.
114
115        the access need to be acquired by the master before it to read or write to the EEPROM. Any attempt without acquiring it will fail
116    */
117    pub async fn acquire(&mut self) -> EthercatResult {
118        if ! self.locked {
119            self.locked = true;
120            self.master.write(self.slave, registers::sii::access, {
121                let mut config = registers::SiiAccess::default();
122                config.set_owner(registers::SiiOwner::EthercatDL);
123                config
124                }).await.one()?;
125        }
126        Ok(())
127    }
128    /**
129        release the SII to the device internal control
130
131        The access to the EEPROM usually needs to be released during slave initialization steps (meaning for state transitions). Most attempt to initialize without releasing will certainly fail.
132    */
133    pub async fn release(&mut self) -> EthercatResult {
134        if self.locked {
135            self.locked = false;
136            self.master.write(self.slave, registers::sii::access, {
137                let mut config = registers::SiiAccess::default();
138                config.set_owner(registers::SiiOwner::Pdi);
139                config
140                }).await.one()?;
141        }
142        Ok(())
143    }
144    
145    /// read data from the slave's EEPROM using the SII
146    pub async fn read<T: PduData>(&mut self, field: Field<T>) -> EthercatResult<T, SiiError> {
147        let mut buffer = T::Packed::uninit();
148        self.read_slice(field.byte as _, buffer.as_mut()).await?;
149        Ok(T::unpack(buffer.as_ref())?)
150    }
151    /// read a slice of the slave's EEPROM memory
152    pub async fn read_slice<'b>(&mut self, address: u16, value: &'b mut [u8]) -> EthercatResult<&'b [u8], SiiError> {
153        // some slaves use 2 byte addresses but declare they are using 1 only, so disable this check for now
154        if address & !self.mask != 0
155            {return Err(EthercatError::Master("wrong EEPROM address: address range is 1 byte only"))}
156
157        let mut start = (address % WORD) as usize;
158        let mut cursor = Cursor::new(value.as_mut());
159        while cursor.remain().len() != 0 {
160            // send request
161            self.master.write(self.slave, registers::sii::control_address, registers::SiiControlAddress {
162                control: {
163                    let mut control = registers::SiiControl::default();
164                    control.set_read_operation(true);
165                    control
166                },
167                address: (address + cursor.position() as u16) / WORD,
168            }).await.one()?;
169            // wait for interface to become available
170            let status = loop {
171                if let Ok(answer) = self.master.read(self.slave, registers::sii::control).await.one() {
172                    if ! answer.busy()  && ! answer.read_operation()
173                        {break answer}
174                }
175            };
176            // check for errors
177            if status.command_error()
178                {return Err(EthercatError::Slave(self.slave, SiiError::Command))}
179            if status.device_info_error()
180                {return Err(EthercatError::Slave(self.slave, SiiError::DeviceInfo))}
181
182            // buffer the result
183            let size = match status.read_size() {
184                registers::SiiTransaction::Bytes4 => 4,
185                registers::SiiTransaction::Bytes8 => 8,
186                }.min(start + cursor.remain().len());
187            let data = self.master.read(self.slave, registers::sii::data).await.one()?;
188            cursor.write(&data[start .. size]).unwrap();
189            start = 0;
190        }
191        Ok(value)
192    }
193
194    /**
195        write data to the slave's EEPROM using the SII
196
197        this will only work if [Self::writable], else will raise an error
198    */
199    pub async fn write<T: PduData>(&mut self, field: Field<T>, value: &T) -> EthercatResult<(), SiiError> {
200        let mut buffer = T::Packed::uninit();
201        value.pack(buffer.as_mut()).unwrap();
202        self.write_slice(field.byte as _, buffer.as_ref()).await
203    }
204    /**
205        write the given slice of data in the slave's EEPROM
206
207        this will only work if [Self::writable], else will raise an error
208    */
209    pub async fn write_slice(&mut self, address: u16, value: &[u8]) -> EthercatResult<(), SiiError> {
210        if address % WORD != 0
211            {return Err(EthercatError::Master("address must be word-aligned"))}
212        // some slaves use 2 byte addresses but declare they are using 1 only, so disable this check for now
213//         if address & !self.mask != 0
214//             {return Err(EthercatError::Master("wrong EEPROM address: address range is 1 byte only"))}
215
216        let mut cursor = Cursor::new(value.as_ref());
217        while cursor.remain().len() != 0 {
218            // write operation is forced to be 2 bytes (ETG.1000.4 6.4.5)
219            // send request
220            self.master.write(self.slave, registers::sii::control_address_data, registers::SiiControlAddressData {
221                control: {
222                    let mut control = registers::SiiControl::default();
223                    control.set_write_operation(true);
224                    control
225                },
226                address: (address + cursor.position() as u16) / WORD,
227                reserved: 0,
228                data: cursor.unpack().unwrap(),
229            }).await.one()?;
230
231            // wait for interface to become available
232            let status = loop {
233                if let Ok(answer) = self.master.read(self.slave, registers::sii::control).await.one() {
234                    if ! answer.busy()  && ! answer.write_operation()
235                        {break answer}
236                }
237            };
238            // check for errors
239            if status.command_error()
240                {return Err(EthercatError::Slave(self.slave, SiiError::Command))}
241            if status.write_error()
242                {return Err(EthercatError::Slave(self.slave, SiiError::Write))}
243        }
244        Ok(())
245    }
246
247    /// reload first 128 bits of data from the EEPROM
248    pub async fn reload(&mut self) -> EthercatResult<(), SiiError> {
249        self.master.write(self.slave, registers::sii::control, {
250            let mut control = registers::SiiControl::default();
251            control.set_reload_operation(true);
252            control
253        }).await.one()?;
254
255        // wait for interface to become available
256        let status = loop {
257            if let Ok(answer) = self.master.read(self.slave, registers::sii::control).await.one() {
258                if ! answer.busy() && ! answer.reload_operation()
259                    {break answer}
260            }
261        };
262        // check for errors
263        if status.command_error()
264                {return Err(EthercatError::Slave(self.slave, SiiError::Command))}
265        if status.checksum_error()
266                {return Err(EthercatError::Slave(self.slave, SiiError::Checksum))}
267        if status.device_info_error()
268                {return Err(EthercatError::Slave(self.slave, SiiError::DeviceInfo))}
269        Ok(())
270    }
271    
272    /// cursor pointing at the start of categories. See [Category]
273    pub fn categories(&mut self) -> SiiCursor<'_> {
274        SiiCursor::new(self, eeprom::categories)
275    }
276
277    /**
278        read the strings category of the EEPROM, and return its content as rust datatype
279
280        these strings are usually pointed to by sdo values or other fields in the EEPROM's categories
281    */
282    pub async fn strings(&mut self) -> EthercatResult<Vec<String>, SiiError> {
283        let mut categories = self.categories();
284        loop {
285            let category = categories.unpack::<Category>().await?;
286            if category.ty() == CategoryType::Strings {
287                let num = categories.unpack::<u8>().await?;
288                let mut strings = Vec::with_capacity(num as _);
289
290                for _ in 0 .. num {
291                    // string length in byte
292                    let len = categories.unpack::<u8>().await?;
293                    // read string
294                    let mut buffer = vec![0; len as _];
295                    categories.read(&mut buffer).await?;
296                    strings.push(String::from_utf8(buffer)
297                        .map_err(|_|  EthercatError::<SiiError>::Master("strings in EEPROM are not UTF8"))?
298                        );
299                }
300
301                return Ok(strings)
302            }
303            else if category.ty() == CategoryType::End {
304                return Err(EthercatError::Master("no strings category in EEPROM"))
305            }
306            else {
307                categories.advance(WORD*category.size());
308            }
309        }
310    }
311
312    /// readthe general informations category of the EEPROM and return its content
313    pub async fn generals(&mut self) -> EthercatResult<General, SiiError> {
314        let mut categories = self.categories();
315        loop {
316            let category = categories.unpack::<Category>().await?;
317            if category.ty() == CategoryType::General {
318                return categories.unpack::<General>().await
319            }
320            else if category.ty() == CategoryType::End {
321                return Err(EthercatError::Master("no general category in EEPROM"))
322            }
323            else {
324                categories.advance(WORD*category.size());
325            }
326        }
327    }
328}
329
330/**
331    Helper for parsing the category of the eeprom through the SII
332
333    It is inspired from [data::Cursor] but this one directly reads into the EEPROM rather than in a local buffer
334*/
335pub struct SiiCursor<'a> {
336    sii: &'a mut Sii,
337    position: u16,
338    end: u16,
339}
340impl<'a> SiiCursor<'a> {
341    /// initialize at the given byte position in the EEPROM
342    pub fn new(sii: &'a mut Sii, position: u16) -> Self
343        {Self {sii, position, end: u16::MAX}}
344    /// byte position in the EEPROM
345    pub fn position(&self) -> u16
346        {self.position}
347    /// number of bytes remaining before region end
348    pub fn remain(&self) -> u16
349        {self.end.max(self.position) - self.position}
350
351    /// create a new instance of cursor at the same location, it is only meant to ease practice of parsing multiple time the same region
352    pub fn shadow(&mut self) -> SiiCursor<'_> {
353        SiiCursor {
354            sii: self.sii,
355            position: self.position,
356            end: self.end,
357            }
358    }
359    /// create a new instance of cursor at the same location but ending after size, moving the the current cursor after size. it is only meant to ease preactice of isolating parsing a section from a parent section
360    pub fn sub(&mut self, size: u16) -> SiiCursor<'_> {
361        let position = self.position;
362        self.position += size;
363        SiiCursor {
364            sii: self.sii,
365            position,
366            end: self.position,
367            }
368    }
369    /// advance byte position of the given increment
370    pub fn advance(&mut self, increment: u16) {
371        self.position += increment;
372    }
373    /// read bytes filling the given slice and advance the position
374    pub async fn read(&mut self, dst: &mut [u8]) -> EthercatResult<(), SiiError> {
375        self.sii.read_slice(self.position, dst).await?;
376        self.position += dst.len() as u16;
377        Ok(())
378    }
379    /// read the given data and advance the position
380    pub async fn unpack<T: PduData>(&mut self) -> EthercatResult<T, SiiError> {
381        let mut buffer = T::Packed::uninit();
382        self.read(buffer.as_mut()).await?;
383        Ok(T::unpack(buffer.as_ref())?)
384    }
385    /// write the given bytes and advance the position
386    pub async fn write(&mut self, dst: &[u8]) -> EthercatResult<(), SiiError> {
387        self.sii.write_slice(self.position, dst).await?;
388        self.position += dst.len() as u16;
389        Ok(())
390    }
391    /// write the given data and advance the position
392    pub async fn pack<T: PduData>(&mut self, value: T) -> EthercatResult<(), SiiError> {
393        let mut buffer = T::Packed::uninit();
394        value.pack(buffer.as_mut()).unwrap();
395        self.write(buffer.as_ref()).await
396    }
397}
398
399/// error raised by the SII of a slave
400#[derive(Copy, Clone, Debug, Eq, PartialEq)]
401pub enum SiiError {
402    /// bad SII command
403    Command,
404    /// EEPROM data has been corrupted
405    Checksum,
406    /// bad data in device info section
407    DeviceInfo,
408    /// cannot write the requested location in EEPROM
409    Write,
410}
411
412impl From<EthercatError<()>> for EthercatError<SiiError> {
413    fn from(src: EthercatError<()>) -> Self {src.upgrade()}
414}
415
416
417/**
418    header for a SII category
419    
420    ETG.1000.6 table 17
421
422    A Category is an any-length data section starting by this struct as header.
423    The category can be squeezed to read the next one by using its [size](Self::size), its content is idnetified by its [type](Self::ty)
424*/
425#[bitsize(32)]
426#[derive(TryFromBits, DebugBits, Copy, Clone)]
427pub struct Category {
428    /// category type as defined in ETG.1000.6 Table 19
429    pub ty: CategoryType,
430    /// size in word of the category
431    pub size: u16,
432}
433data::bilge_pdudata!(Category, u32);
434
435/**
436    type of category in the SII
437
438    ETG.1000.6 table 19
439*/
440#[bitsize(16)]
441#[derive(FromBits, Debug, Copy, Clone, Eq, PartialEq)]
442pub enum CategoryType {
443    Nop = 0,
444    /**
445        String repository for other Categories structure of this category data
446        see ETG.1000.6 Table 20
447
448        **section content:**
449
450        ```ignore
451            [num of strings: u8] ([byte length][bytes]) ([byte length][bytes]) ...
452        ```
453    */
454    Strings = 10,
455    /// Data Types for future use
456    DataTypes = 20,
457    /**
458        General information structure of this category data. see ETG.1000.6 Table 21
459
460        **section content:**  [`General`]
461    */
462    General = 30,
463    /**
464        FMMUs to be used structure of this category data. see ETG.1000.6 Table 23
465
466        **section content:**  [`[FmmuUsage; _]`](FmmuUsage)
467    */
468    Fmmu = 40,
469    /**
470        second FMMU category added by ETG.2010 table 1
471
472        **section content:**  [`[FmmuExtension; _]`](FmmuExtension)
473    */
474    FmmuExtension = 42,
475    /**
476        Sync Manager Configuration structure of this category data. see ETG.1000.6 Table 24
477
478        **section content:**  [`[SyncManager; _]`](SyncManager)
479    */
480    SyncManager = 41,
481    /**
482        second sync manager category added by ETG.2010 table 1
483
484        **section content:**  [`[SyncUnit; _]`](SyncUnit)
485    */
486    SyncUnit = 43,
487    /**
488        TxPDO description structure of this category data. see ETG.1000.6 Table 25
489
490        **section content:**  [`[[Pdo][entries]; _]`](Pdo)
491    */
492    PdoWrite = 50,
493    /**
494        RxPDO description structure of this category data. see ETG.1000.6 Table 25
495
496        **section content:**  [`[[Pdo][entries]; _]`](Pdo)
497    */
498    PdoRead = 51,
499    /**
500        Distributed Clock for future use
501
502        **section content:**  [`DistributedClock`]
503    */
504    Dc = 60,
505
506    #[fallback]
507    Unsupported = 0x0800,
508
509    /// mark the end of SII categories
510    End = 0xffff,
511}
512
513/// ETG.1000.6 table 21
514#[repr(packed)]
515#[derive(Clone, Eq, PartialEq, Debug)]
516pub struct General {
517    /// Group Information (Vendor specific) - Index to STRINGS
518    pub group: u8,
519    /// Image Name (Vendor specific) - Index to STRINGS
520    pub image: u8,
521    /// Device Order Number (Vendor specific) - Index to STRINGS
522    pub order: u8,
523    /// Device Name Information (Vendor specific) - Index to STRINGS
524    pub name: u8,
525    _reserved0: u8,
526    /// supported CoE features
527    pub coe: CoeDetails,
528    /// supported FoE features
529    pub foe: FoeDetails,
530    /// supported EoE features
531    pub eoe: EoeDetails,
532    _reserved1: [u8;3],
533    pub flags: GeneralFlags,
534    /// EBus Current Consumption in mA, negative Values means feeding in current feed in sets the available current value to the given value
535    pub ebus_current: i16,
536    /// Index to Strings – duplicate for compatibility reasons
537    pub group2: u8,
538    _reserved2: u8,
539    /// Description of Physical Ports
540    pub ports: PhysicialPorts,
541    /// Element defines the ESC memory address where the Identification ID is saved if Identification Method = IdentPhyM
542    pub identification_address: u16,
543}
544data::packed_pdudata!(General);
545
546/// supported CoE features
547#[bitsize(8)]
548#[derive(FromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
549pub struct CoeDetails {
550    pub sdo: bool,
551    pub sdo_info: bool,
552    pub pdo_assign: bool,
553    pub pdo_config: bool,
554    pub startup_upload: bool,
555    pub sdo_complete: bool,
556    _reserved: u2,
557}
558#[bitsize(8)]
559#[derive(FromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
560pub struct FoeDetails {
561    // protocol supported
562    pub enable: bool,
563    reserved: u7,
564}
565#[bitsize(8)]
566#[derive(FromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
567pub struct EoeDetails {
568    // protocol supported
569    pub enable: bool,
570    reserved: u7,
571}
572#[bitsize(8)]
573#[derive(FromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
574pub struct GeneralFlags {
575    pub enable_safeop: bool,
576    pub enable_notlrw: bool,
577    pub mbox_dll: bool,
578    /// ID selector mirrored in AL Statud Code
579    pub ident_alsts: bool,
580    /// ID selector value mirrored in specific physical memory as deonted by the parameter “Physical Memory Address”
581    pub ident_phym: bool,
582    reserved: u3,
583}
584
585#[bitsize(16)]
586#[derive(FromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
587pub struct PhysicialPorts {
588    pub ports: [PhysicalPort; 4],
589}
590#[bitsize(4)]
591#[derive(FromBits, Debug, Copy, Clone, Eq, PartialEq)]
592pub enum PhysicalPort {
593    #[fallback]
594    Disabled = 0x0,
595    /// media independent interface
596    Mii = 0x1,
597    Reserved = 0x2,
598    Ebus = 0x3,
599    /// NOTE: Fast Hot Connect means a Port with Ethernet Physical Layer and Autonegotiation off (100Mbps fullduplex)
600    FastHotconnect = 0x4,
601}
602
603/// ETG.1000.6 table 23, ETG.2010 tablee 9
604#[bitsize(8)]
605#[derive(FromBits, Debug, Copy, Clone, Eq, PartialEq)]
606pub enum FmmuUsage {
607    #[fallback]
608    Disabled = 0,
609    Outputs = 1,
610    Inputs = 2,
611    SyncManagerStatus = 3,
612}
613data::bilge_pdudata!(FmmuUsage, u8);
614
615/// ETG.2010 table 10
616#[bitsize(24)]
617#[derive(TryFromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
618pub struct FmmuExtension {
619    /**
620        As SM.OpOnly obsolete
621
622        Esi:DeviceType:Fmmu:OpOnly
623    */
624    pub op_only: bool,
625    /**
626        Mandatory if more than one FMMU for the same direction is used to map data to non-consecutive memory areas
627
628        Assigns this FMMU to a SyncManager
629
630        Esi: DeviceType:Fmmu:Sm
631    */
632    pub sm_defined: bool,
633    /// Esi: DeviceType:Fmmu:Su
634    pub su_defined: bool,
635    reserved: u5,
636    /**
637        Assigns this FMMU to a SyncManager
638
639        Esi: DeviceType:Fmmu:Sm
640    */
641    pub sm: u8,
642    /// Esi: DeviceType:Fmmu:Su
643    pub su: u8,
644}
645data::bilge_pdudata!(FmmuExtension, u24);
646
647/// ETG.1000.6 table 24
648#[bitsize(64)]
649#[derive(TryFromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
650pub struct SyncManager {
651    /// Origin of Data (see Physical Start Address of SyncM)
652    pub address: u16,
653    pub length: u16,
654    /// Defines Mode of Operation (see Control Register of SyncM)
655    pub control: u8,
656    /// don't care
657    pub status: u8,
658    pub enable: SyncManagerEnable,
659    pub usage: SyncManagerUsage,
660}
661data::bilge_pdudata!(SyncManager, u64);
662
663/// ETG.1000.6 table 24
664#[bitsize(8)]
665#[derive(FromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
666pub struct SyncManagerEnable {
667    pub enable: bool,
668    /// fixed content (info for config tool –SyncMan has fixed content)
669    pub fixed_content: bool,
670    /// virtual SyncManager (virtual SyncMan – no hardware resource used)
671    pub virtual_sync_manager: bool,
672    /// opOnly (SyncMan should be enabled only in OP state)
673    pub oponly: bool,
674    _reserved: u4,
675}
676
677/// ETG.1000.6 table 24
678#[bitsize(8)]
679#[derive(TryFromBits, Debug, Copy, Clone, Eq, PartialEq)]
680pub enum SyncManagerUsage {
681    Disabled = 0x0,
682    MailboxOut = 0x1,
683    MailboxIn = 0x2,
684    ProcessOut = 0x3,
685    ProcessIn = 0x4,
686}
687
688#[repr(packed)]
689#[derive(Debug, Clone, Eq, PartialEq)]
690pub struct DistributedClock {
691    pub period: u32,
692    pub shift0: u32,
693    pub shift1: u32,
694    pub sync1_period_factor: i16,
695    pub assign_activate: u16,
696    pub sync0_period_factor: i16,
697    pub name: u8,
698    pub description: u8,
699    pub _reserved0: [u8;4],
700}
701data::packed_pdudata!(DistributedClock);
702
703#[bitsize(8)]
704#[derive(TryFromBits, DebugBits, Copy, Clone, Eq, PartialEq)]
705pub struct SyncUnit {
706    pub separate_su: bool,
707    pub separate_frame: bool,
708    pub depend_on_input_state: bool,
709    pub frame_repeat_support: bool,
710    reserved: u4,
711}
712data::bilge_pdudata!(SyncUnit, u8);
713
714/**
715    header for category describing a reading or writing PDO
716
717    ETG.1000.6 table 25
718
719    following data is [`[PdoEntry; _]`](PdoEntry)
720*/
721#[repr(packed)]
722#[derive(Debug, Clone, Eq, PartialEq)]
723pub struct Pdo {
724    /// index of SDO configuring the PDO
725    pub index: u16,
726    /// number of entries in the PDO
727    pub entries: u8,
728    /// reference to DC-sync
729    pub synchronization: u8,
730    /// name of the PDO object (reference to category strings)
731    pub name: u8,
732    /// for future use
733    pub flags: u16,
734}
735data::packed_pdudata!(Pdo);
736
737/**
738    structure describing an entry in a PDO
739
740    ETG.1000.6 table 26
741*/
742#[repr(packed)]
743#[derive(Debug, Clone, Eq, PartialEq)]
744pub struct PdoEntry {
745    /// index of the SDO
746    pub index: u16,
747    /// index of field in the SDO (or 0 if complete SDO)
748    pub sub: u8,
749    /// name of this SDO
750    pub name: u8,
751    /// data type of the entry
752    pub dtype: u8,
753    /// data length of the entry
754    pub bitlen: u8,
755    /// for future use
756    pub flags: u16,
757}
758data::packed_pdudata!(PdoEntry);