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);