Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Where to go next:
- Simulator and emulator -- [docs/SIMULATION.md](docs/SIMULATION.md)
- Tools and GUIs -- [docs/TOOLS.md](docs/TOOLS.md)
- Real-time performance tuning -- [docs/PERFORMANCE.md](docs/PERFORMANCE.md)
- ENI network configuration (experimental) -- [docs/ENI.md](docs/ENI.md)

## Platform support

Expand Down
68 changes: 68 additions & 0 deletions docs/ENI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# ENI support

KickCAT can load an EtherCAT Network Information (ENI) XML file and execute its
startup commands to configure a master bus. This support is **experimental** and
has been tested only with the in-process simulator.

Enable `eni_parser` to load XML files. The `eni_sequencer` example also needs
`esi_parser` and `master_examples` (all enabled by default). See
[BUILDING.md](BUILDING.md) for prerequisites:

```bash
source .venv/bin/activate
./scripts/configure.sh build --with=eni_parser --with=esi_parser --with=master_examples
./scripts/setup_build.sh build
cd build && make -j
cd ..
./build/examples/master/eni_sequencer/eni_sequencer \
-n unit/kickcat_eni_test_drives.xml \
-e simulation/slave_configs/ecat402-drive.xml
```

`eni_sequencer` creates a simulated bus from the ESI file. To inspect an ENI
without starting a bus, run `./build/examples/master/load_eni/load_eni -f FILE`.

## Using the sequencer

`ENI::loadFile()` returns a configuration for `ENI::Sequencer`. Keep that
configuration and the link alive for as long as the sequencer uses them. The
sequencer runs the ENI startup commands; do not also call `Bus::init()` or
`Bus::createMapping()`.

```cpp
ENI::Sequencer sequencer(bus, link, config);
sequencer.requestState(State::SAFE_OP);

std::vector<uint8_t> iomap(sequencer.processImageSize());
sequencer.mapProcessImage(iomap.data(), iomap.size());
bus.processDataReadWrite(error);

sequencer.requestState(State::OPERATIONAL, [&]()
{
bus.processDataReadWrite(error);
});
```

The process image becomes available at SAFE_OP. Outputs must be exchanged
before requesting OP; the callback keeps them cycling while the transition is
checked. See [`eni_sequencer.cc`](../examples/master/eni_sequencer/eni_sequencer.cc)
for a complete example. `cycleTime()` and `shiftTime()` expose the common DC
timing, when the ENI specifies it, for `Bus::enableDC()`.

## Current limits

- The detected slave count and identities must match the ENI and the devices'
SII data. Mailbox and process data layouts are checked against the registers
programmed by the ENI.
- SoE, EoE, FoE, AoE and VoE startup commands are not executed. Bootstrap,
HotConnect and `PreviousPort` checks are not supported.
- The regular `Bus` exchange builds its own frames from the process data
mapping read back from the slaves: ENI `<Cyclic>` frames only serve to check
that mapping, and their `CycleTime` is the application's to use.
`<ProcessImage>` variables are parsed but not used.
- Mailboxes are polled slave by slave: `MailboxStates` and the status FMMUs it
describes are not used.
- Each startup command uses its own round trip; `Requires=frame/cycle` is not
implemented. A failed command throws, without automatic recovery.
- `Validate` only supports the default `EQ` comparison: `NONE` skips the check
and the other comparison types are refused when loading the file.
1 change: 1 addition & 0 deletions examples/master/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ add_subdirectory(common)
add_subdirectory(ds402_drive)
add_subdirectory(easycat)
add_subdirectory(elmo)
add_subdirectory(eni_sequencer)
add_subdirectory(freedom-k64f)
add_subdirectory(ingenia)
add_subdirectory(load_eni)
Expand Down
5 changes: 5 additions & 0 deletions examples/master/eni_sequencer/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if (ENABLE_ENI_PARSER AND ENABLE_ESI_PARSER)
add_executable(eni_sequencer eni_sequencer.cc)
target_link_libraries(eni_sequencer kickcat argparse::argparse)
set_kickcat_properties(eni_sequencer)
endif ()
175 changes: 175 additions & 0 deletions examples/master/eni_sequencer/eni_sequencer.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#include <iostream>
#include <memory>
#include <vector>

#include <argparse/argparse.hpp>

#include "kickcat/Bus.h"
#include "kickcat/CoE/mailbox/response.h"
#include "kickcat/ENI/Parser.h"
#include "kickcat/ENI/Sequencer.h"
#include "kickcat/ESC/EmulatedESC.h"
#include "kickcat/ESI/Parser.h"
#include "kickcat/ESI/SIIBuilder.h"
#include "kickcat/Link.h"
#include "kickcat/LoopbackSocket.h"
#include "kickcat/PDO.h"
#include "kickcat/SocketNull.h"
#include "kickcat/slave/Slave.h"

using namespace kickcat;

namespace
{
constexpr uint32_t SLAVE_PD_SIZE = 4096;

struct SimulatedSlave
{
SimulatedSlave(ESI::Device device)
: dev(std::move(device))
, pdo(&esc)
, sl(&esc, &pdo)
, inputs(SLAVE_PD_SIZE, 0)
, outputs(SLAVE_PD_SIZE, 0)
{
CoE::materializeStorage(dev.dictionary);
esc.loadEeprom(ESI::buildEepromImage(dev));
if (dev.mailbox and dev.mailbox->coe)
{
mbx = std::make_unique<mailbox::response::Mailbox>(&esc, 1024);
mbx->enableCoE(dev.dictionary);
sl.setMailbox(mbx.get());
sl.setDictionary(&dev.dictionary);
}
pdo.setInput(inputs.data(), SLAVE_PD_SIZE);
pdo.setOutput(outputs.data(), SLAVE_PD_SIZE);
sl.start();
}

ESI::Device dev;
EmulatedESC esc;
PDO pdo;
slave::Slave sl;
std::unique_ptr<mailbox::response::Mailbox> mbx;
std::vector<uint8_t> inputs;
std::vector<uint8_t> outputs;
};
}

int main(int argc, char** argv)
{
argparse::ArgumentParser program("eni_sequencer");

std::string eni_file;
program.add_argument("-n", "--eni")
.help("ENI XML file describing the network")
.required()
.store_into(eni_file);

std::string esi_file;
program.add_argument("-e", "--esi")
.help("ESI XML file used to emulate every ENI slave (matched by product code)")
.required()
.store_into(esi_file);

try
{
program.parse_args(argc, argv);
}
catch (std::exception const& e)
{
std::cerr << e.what() << std::endl << program;
return 1;
}

ENI::Config config;
std::vector<std::unique_ptr<SimulatedSlave>> sim;
try
{
config = ENI::loadFile(eni_file);
ESI::Parser parser;
for (auto const& eni_slave : config.slaves)
{
ESI::DeviceFilter filter;
filter.product_code = eni_slave.info.product_code;
sim.push_back(std::make_unique<SimulatedSlave>(parser.loadDevice(esi_file, filter)));
}
}
catch (std::exception const& e)
{
std::cerr << "Cannot build the simulated network: " << e.what() << std::endl;
return 1;
}

bool outputs_valid = false;
auto tick = [&]()
{
for (auto& s : sim)
{
s->sl.routine();
if (outputs_valid and s->sl.state() == State::SAFE_OP)
{
s->sl.validateOutputData();
}
}
};

std::vector<EmulatedESC*> escs;
for (auto& s : sim)
{
escs.push_back(&s->esc);
}
auto link = std::make_shared<Link>(std::make_shared<LoopbackSocket>(escs, tick), std::make_shared<SocketNull>(), [](){});
Bus bus(link);

int cyclic_errors = 0;
auto cyclic = [&]()
{
bus.processDataReadWrite([&](DatagramState const&) { ++cyclic_errors; });
};

std::vector<uint8_t> iomap;
try
{
ENI::Sequencer sequencer(bus, link, config);
sequencer.requestState(State::SAFE_OP);

iomap.resize(sequencer.processImageSize());
sequencer.mapProcessImage(iomap.data(), iomap.size());
cyclic();

outputs_valid = true;
sequencer.requestState(State::OPERATIONAL, cyclic);
}
catch (std::exception const& e)
{
std::cerr << "ENI sequence failed: " << e.what() << std::endl;
return 1;
}

// The period the ENI frames were planned for, when the configurator gave one.
nanoseconds period = 1ms;
if ((not config.cyclic.empty()) and config.cyclic.front().cycle_time)
{
period = *config.cyclic.front().cycle_time;
}
printf("cycle period: %lld us\n", static_cast<long long>(duration_cast<microseconds>(period).count()));
for (int i = 0; i < 100; ++i)
{
cyclic();
sleep(period);
}
for (std::size_t i = 0; i < bus.slaves().size(); ++i)
{
Slave const& slave = bus.slaves()[i];
printf("slave %zu: station %u, state 0x%02x, inputs 0x%05x/%d, outputs 0x%05x/%d\n", i, slave.address,
static_cast<unsigned>(sim[i]->sl.state()), slave.input.address, slave.input.bsize, slave.output.address, slave.output.bsize);
}
printf("cyclic errors: %d\n", cyclic_errors);

if (cyclic_errors != 0)
{
return 1;
}
return 0;
}
18 changes: 17 additions & 1 deletion lib/master/include/kickcat/ENI/Sequencer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <functional>
#include <memory>
#include <optional>
#include <vector>

#include "kickcat/AbstractLink.h"
Expand All @@ -13,7 +14,7 @@ namespace kickcat::ENI
{
/// \brief Configure a bus through the state transitions described by an ENI.
/// \details Startup commands program the slaves; their SII data is checked against the ENI.
/// The mailbox layout is read back from the programmed registers.
/// Mailbox and process data layouts are read back from the programmed registers.
class Sequencer
{
public:
Expand All @@ -28,6 +29,17 @@ namespace kickcat::ENI
void requestState(State target, std::function<void()> const& background = [](){});
State state() const { return state_; }

/// \return the process image size, known once the ENI programmed the FMMUs (SAFE_OP and above)
std::size_t processImageSize() const;

/// \brief Bind slave input/output to iomap, see Bus::mapProcessImage. Needs SAFE_OP or above,
/// and no Bus::configureMailboxStatusCheck(): an ENI does not program its FMMUs.
void mapProcessImage(uint8_t* iomap, std::size_t iomap_size);

/// \return the DC cycle and shift shared by every DC slave of the ENI, nullopt without DC
std::optional<nanoseconds> cycleTime() const { return cycle_time_; }
std::optional<nanoseconds> shiftTime() const { return shift_time_; }

/// \return the ENI slave driven at a bus position
ENI::Slave const& slaveAt(std::size_t position) const;

Expand All @@ -41,17 +53,21 @@ namespace kickcat::ENI
void checkIdentity(std::size_t position);
void checkMailbox(std::size_t position);
void checkIdentification();
void readProcessDataLayout();

Bus& bus_;
std::shared_ptr<AbstractLink> link_;
Config const& config_;

std::vector<std::size_t> eni_of_position_;
std::vector<std::size_t> position_of_eni_;
std::optional<nanoseconds> cycle_time_;
std::optional<nanoseconds> shift_time_;

State state_ = State::INIT;
bool detected_ = false;
bool handlers_attached_ = false;
bool layout_known_ = false;
};
}

Expand Down
Loading
Loading