From 3b0a4daa150016e44541589c0d5a6c3712a0c001 Mon Sep 17 00:00:00 2001 From: Philippe Leduc Date: Thu, 1 Oct 2026 12:21:26 +0200 Subject: [PATCH] ENI: Describe an EtherCAT Network Information configuration - Model an ENI (ETG.2100) without any XML dependency, so generated code can build it on targets without an XML library. - Reuse the stack types where ENI and ESI describe the same thing: commands and ESM states, transitions, mailbox protocols, CoE InitCmds, PDO entries and data types, SyncManager settings. - Address a datagram as it goes on the wire, and check InitCmd replies against their masked expected data (the ETG.2100 default Validate comparison). --- lib/CMakeLists.txt | 2 + lib/include/kickcat/ENI/Config.h | 219 +++++++++++++++++++++++++++++++ lib/src/ENI/Config.cc | 26 ++++ unit/CMakeLists.txt | 1 + unit/src/ENI/Config-t.cc | 65 +++++++++ 5 files changed, 313 insertions(+) create mode 100644 lib/include/kickcat/ENI/Config.h create mode 100644 lib/src/ENI/Config.cc create mode 100644 unit/src/ENI/Config-t.cc diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 67a5a031..264a53b1 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -20,6 +20,8 @@ set(KICKCAT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/CoE/mailbox/response.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/CoE/CiA/DS402/StateMachine.cc + ${CMAKE_CURRENT_SOURCE_DIR}/src/ENI/Config.cc + ${CMAKE_CURRENT_SOURCE_DIR}/src/EoE/protocol.cc ${CMAKE_CURRENT_SOURCE_DIR}/src/FoE/protocol.cc diff --git a/lib/include/kickcat/ENI/Config.h b/lib/include/kickcat/ENI/Config.h new file mode 100644 index 00000000..6445afb9 --- /dev/null +++ b/lib/include/kickcat/ENI/Config.h @@ -0,0 +1,219 @@ +#ifndef KICKCAT_ENI_CONFIG_H +#define KICKCAT_ENI_CONFIG_H + +#include +#include +#include +#include +#include + +#include "kickcat/CoE/protocol.h" +#include "kickcat/ESI/Device.h" +#include "kickcat/OS/Time.h" +#include "kickcat/protocol.h" + +namespace kickcat::ENI +{ + namespace requirement + { + enum Type : uint8_t + { + NONE, + FRAME, // command needs its own frame + CYCLE, // command needs its own cycle + }; + } + + // Only the ETG.2100 default comparison (EQ): the reply must equal data once masked. + struct Validate + { + std::vector data; + std::vector mask; // ANDed with the reply; bytes past its end are compared whole + milliseconds timeout{0}; + + // Compare the first data.size() bytes of a reply. False when the reply is shorter than data. + bool matches(uint8_t const* reply, std::size_t reply_size) const; + }; + + // An EtherCAT datagram as ENI InitCmds and cyclic commands write it. + struct Datagram + { + std::string comment; + Command cmd = Command::NOP; + uint32_t address = 0; // as on the wire: logical for LRD/LWR/LRW, createAddress(adp, ado) otherwise + std::vector data; // expands to zeroes + std::optional expected_wkc; // ; unchecked when absent + }; + + // ECatCmdType: raw EtherCAT datagram sent during a transition. + struct InitCmd : Datagram + { + Transitions transitions = 0; + bool before_slave = false; + requirement::Type frame_requirement = requirement::NONE; + uint16_t retries = 0; + std::optional validate; + milliseconds timeout{0}; // only when there is no + }; + + struct CoEInitCmd : CoE::InitCmd + { + milliseconds timeout{0}; + uint8_t ccs = 0; // samples use 1 for download (write), spec table says otherwise + bool fixed = false; + bool disabled = false; + }; + + struct Pdo + { + uint16_t index = 0; + std::string name; + bool fixed = false; + bool mandatory = false; + std::optional sm; + std::vector exclude; + std::vector entries; + }; + + struct SyncManagerSettings : ESI::SmInfo + { + uint8_t index = 0; // N of + std::vector pdos; + }; + + struct ProcessDataRange + { + uint32_t bit_start = 0; + uint32_t bit_length = 0; + uint16_t sm_mask = 0; // @Sm0..@Sm15 + }; + + struct ProcessData + { + std::vector send; // outputs + std::vector recv; // inputs + std::vector sms; + std::vector rx_pdos; + std::vector tx_pdos; + }; + + struct MailboxArea + { + uint16_t start = 0; + uint16_t length = 0; + }; + + struct Mailbox + { + MailboxArea send; // master -> slave + MailboxArea recv; // slave -> master + std::optional poll_time; + std::optional status_bit_addr; + std::optional bootstrap_send; + std::optional bootstrap_recv; + uint16_t protocols = 0; // OR of eeprom::MailboxProtocol + std::vector coe_init_cmds; + uint32_t unsupported_init_cmds = 0; // AoE/EoE/FoE/SoE/VoE InitCmds, not modelled + }; + + struct PreviousPort + { + bool selected = false; + uint8_t port = 0; // A=0 .. D=3 + std::optional phys_addr; + }; + + struct Dc + { + bool potential_reference_clock = false; + bool reference_clock = false; + std::optional cycle_time0; + std::optional cycle_time1; + std::optional shift_time; + }; + + // Explicit device identification: the slave register at ado holds value. + struct Identification + { + uint16_t ado = 0; + uint16_t value = 0; + }; + + struct SlaveInfo + { + std::string name; + std::optional phys_addr; + std::optional auto_inc_addr; + std::optional identification; + uint32_t vendor_id = 0; + uint32_t product_code = 0; + uint32_t revision_no = 0; + std::optional serial_no; + }; + + struct Slave + { + SlaveInfo info; + ProcessData process_data; + std::optional mailbox; + std::vector init_cmds; + std::vector previous_ports; + std::optional dc; + }; + + struct Master + { + std::string name; + std::vector init_cmds; + }; + + struct CyclicCmd : Datagram + { + uint8_t states = 0; // OR of State::INIT, PRE_OP, SAFE_OP and OPERATIONAL + std::optional input_offs; + std::optional output_offs; + }; + + struct Frame + { + std::string comment; + std::vector cmds; + }; + + struct Cyclic + { + std::string comment; + std::optional cycle_time; // period the frames were planned for; nullopt leaves it to the application + std::vector frames; + }; + + struct Variable + { + std::string name; + CoE::DataType data_type = CoE::DataType::UNKNOWN; + uint32_t bit_size = 0; + uint32_t bit_offs = 0; + }; + + struct ProcessImageArea + { + uint32_t byte_size = 0; + std::vector variables; + }; + + struct ProcessImage + { + ProcessImageArea inputs; + ProcessImageArea outputs; + }; + + struct Config + { + Master master; + std::vector slaves; + std::vector cyclic; + std::optional process_image; + }; +} + +#endif diff --git a/lib/src/ENI/Config.cc b/lib/src/ENI/Config.cc new file mode 100644 index 00000000..84fce165 --- /dev/null +++ b/lib/src/ENI/Config.cc @@ -0,0 +1,26 @@ +#include "kickcat/ENI/Config.h" + +namespace kickcat::ENI +{ + bool Validate::matches(uint8_t const* reply, std::size_t reply_size) const + { + if (reply_size < data.size()) + { + return false; + } + + for (std::size_t i = 0; i < data.size(); ++i) + { + uint8_t byte_mask = 0xFF; + if (i < mask.size()) + { + byte_mask = mask[i]; + } + if ((reply[i] & byte_mask) != data[i]) // ETG.2100: the mask applies to the reply only + { + return false; + } + } + return true; + } +} diff --git a/unit/CMakeLists.txt b/unit/CMakeLists.txt index 1e753cda..ff2d7473 100644 --- a/unit/CMakeLists.txt +++ b/unit/CMakeLists.txt @@ -36,6 +36,7 @@ add_executable(kickcat_unit src/adler32_sum-t.cc src/CoE/OD-t.cc src/ESI/Parser-t.cc src/ESI/SIIBuilder-t.cc + src/ENI/Config-t.cc src/CoE/DS402StateMachine-t.cc src/CoE/DS402Drive-t.cc src/mailbox/request-t.cc diff --git a/unit/src/ENI/Config-t.cc b/unit/src/ENI/Config-t.cc new file mode 100644 index 00000000..b34b61f4 --- /dev/null +++ b/unit/src/ENI/Config-t.cc @@ -0,0 +1,65 @@ +#include + +#include "kickcat/ENI/Config.h" + +using namespace kickcat; + +namespace +{ + ENI::Validate makeValidate(std::vector data, std::vector mask) + { + ENI::Validate v; + v.data = std::move(data); + v.mask = std::move(mask); + return v; + } + + bool matches(ENI::Validate const& v, std::vector const& reply) + { + return v.matches(reply.data(), reply.size()); + } +} + +TEST(ENIConfig, validate_compares_every_byte) +{ + std::vector expected(10, 0); + std::vector reply(10, 0); + ENI::Validate eq = makeValidate(expected, {}); + ASSERT_TRUE(matches(eq, reply)); + + reply[9] = 1; + ASSERT_FALSE(matches(eq, reply)); + reply[9] = 0; + reply[0] = 1; + ASSERT_FALSE(matches(eq, reply)); +} + +TEST(ENIConfig, validate_mask) +{ + // AL status 0x0014: SAFEOP with the error bit set + std::vector al_status = {0x14, 0x00}; + ASSERT_FALSE(matches(makeValidate({0x04, 0x00}, {0x1f, 0x00}), al_status)); + ASSERT_TRUE (matches(makeValidate({0x04, 0x00}, {0x0f, 0x00}), al_status)); +} + +TEST(ENIConfig, validate_uses_the_validate_width) +{ + std::vector reply = {0x02, 0x00, 0xAA, 0xBB}; + ASSERT_TRUE(matches(makeValidate({0x02, 0x00}, {0x1f, 0x00}), reply)); + + std::vector short_reply = {0x02}; + ASSERT_FALSE(matches(makeValidate({0x02, 0x00}, {0x1f, 0x00}), short_reply)); +} + +TEST(ENIConfig, validate_mask_applies_to_the_reply_only) +{ + // ETG.2100: reply AND mask, then compare with Data as written + std::vector reply = {0x15}; + ASSERT_FALSE(matches(makeValidate({0x15}, {0x0f}), reply)); + ASSERT_TRUE (matches(makeValidate({0x05}, {0x0f}), reply)); + + // A mask shorter than the data leaves the remaining bytes compared whole + std::vector al_status = {0x14, 0x02}; + ASSERT_TRUE (matches(makeValidate({0x04, 0x02}, {0x0f}), al_status)); + ASSERT_FALSE(matches(makeValidate({0x04, 0x00}, {0x0f}), al_status)); +}