Skip to content
Draft
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 examples/master/marvin/marvin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ int main(int argc, char *argv[])
bus.isDCSynchronized(1000ns);
}

timer.apply_offset(bus.dcMasterOffset());
timer.wait_next_tick();
}

Expand Down
11 changes: 11 additions & 0 deletions lib/include/kickcat/OS/Timer.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,23 @@ namespace kickcat
/// Wait until the next timer tick (blocking call)
std::error_code wait_next_tick();

/// \brief Adjust the timer phase to track an external time reference (e.g. EtherCAT DC).
/// \details Call this every cycle before wait_next_tick() with bus.dcMasterOffset().
/// A positive offset means the master is ahead of the reference: the timer slows down.
/// Uses a first-order IIR filter to smooth the correction.
/// Pass 0ns when no reference is available (no-op when filtered offset is zero).
void apply_offset(nanoseconds raw_offset);

protected:
private:
static constexpr int64_t OFFSET_FILTER_DEPTH = 256;
static constexpr int64_t OFFSET_CORRECTION_DIVISOR = 16;

std::string name_;
nanoseconds period_;
nanoseconds next_deadline_{};
nanoseconds last_wakeup_{};
nanoseconds filtered_offset_{0ns};

Mutex mutex_{};
ConditionVariable stop_{};
Expand Down
13 changes: 10 additions & 3 deletions lib/master/include/kickcat/Bus.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,8 @@ namespace kickcat
void processMessages(std::function<void(DatagramState const&)> const& error);

/// \brief Send drift compensation datagrams to maintain DC synchronization
/// \details Writes the current master time to the DC reference clock slave's system time register (0x0910)
/// using FPWR, then reads it back with FRMW so that each slave on the segment updates its
/// local clock offset accordingly.
/// \details Reads the DC reference clock's system time register (0x0910) with FRMW so that each
/// slave on the segment updates its local clock offset accordingly.
/// Called cyclically during process data exchange, and repeatedly (15 000 times) during
/// static drift compensation at DC initialization.
/// \param error Callback invoked when a datagram error occurs
Expand All @@ -129,6 +128,12 @@ namespace kickcat
/// \return true if all DC slaves are synchronized within the given threshold
bool isDCSynchronized(nanoseconds threshold = 1000ns, bool log_all = false);

/// \brief Return the signed offset between the master OS clock and the DC network time.
/// \details Positive means the master clock is ahead of the network clock.
/// Updated every cycle by sendDriftCompensation().
/// \return 0ns if DC is not active or not yet measured.
nanoseconds dcMasterOffset() const;


enum Access
{
Expand Down Expand Up @@ -238,6 +243,8 @@ namespace kickcat
uint16_t irq_mask_{0};

Slave* dc_slave_{nullptr};
nanoseconds dc_network_time_{0ns}; // last reference clock system time (EtherCAT epoch)
nanoseconds dc_master_time_{0ns}; // master OS time when FRMW response was captured

MailboxStatusFMMU mailbox_status_fmmu_{MailboxStatusFMMU::NONE};
};
Expand Down
27 changes: 22 additions & 5 deletions lib/master/src/dc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -472,20 +472,37 @@ namespace kickcat

void Bus::sendDriftCompensation(std::function<void(DatagramState const&)> const& error)
{
auto process = [](DatagramHeader const*, uint8_t const*, uint16_t wkc)
// FRMW reads the reference clock's system time and writes it to all subordinate clocks.
// The reference clock free-runs on its own quartz (offset was set during enableDC).
// Using only FRMW (no FPWR) avoids injecting master clock jitter and NTP corrections
// into the network - the slave PLLs track the stable ESC oscillator instead.
auto process = [this](DatagramHeader const*, uint8_t const* data, uint16_t wkc)
{
if (wkc == 0)
{
dc_error("Invalid working counter: %" PRIu16 "\n", wkc);
return DatagramState::INVALID_WKC;
}

uint64_t raw_network_time = 0;
std::memcpy(&raw_network_time, data, sizeof(raw_network_time));
dc_network_time_ = nanoseconds(raw_network_time);
dc_master_time_ = since_epoch();

return DatagramState::OK;
};

nanoseconds now = since_ecat_epoch();
uint64_t raw_now = now.count();
link_->addDatagram(Command::FPWR, createAddress(dc_slave_->address, reg::DC_SYSTEM_TIME), &raw_now, sizeof(uint64_t), process, error);
link_->addDatagram(Command::FRMW, createAddress(dc_slave_->address, reg::DC_SYSTEM_TIME), nullptr, sizeof(uint64_t), process, error);
link_->addDatagram(Command::FRMW, createAddress(dc_slave_->address, reg::DC_SYSTEM_TIME), nullptr, sizeof(uint64_t), process, error);
}


nanoseconds Bus::dcMasterOffset() const
{
if (dc_slave_ == nullptr or dc_master_time_ == 0ns)
{
return 0ns;
}
return dc_master_time_ - to_unix_epoch(dc_network_time_);
}


Expand Down
6 changes: 6 additions & 0 deletions lib/src/OS/Unix/Timer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ namespace kickcat
period_ = period;
}

void Timer::apply_offset(nanoseconds raw_offset)
{
filtered_offset_ = (filtered_offset_ * (OFFSET_FILTER_DEPTH - 1) + raw_offset) / OFFSET_FILTER_DEPTH;
next_deadline_ -= filtered_offset_ / OFFSET_CORRECTION_DIVISOR;
}

std::error_code Timer::wait_next_tick()
{
{
Expand Down
6 changes: 6 additions & 0 deletions lib/src/OS/Windows/Timer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ namespace kickcat
period_ = period;
}

void Timer::apply_offset(nanoseconds raw_offset)
{
filtered_offset_ = (filtered_offset_ * (OFFSET_FILTER_DEPTH - 1) + raw_offset) / OFFSET_FILTER_DEPTH;
next_deadline_ -= filtered_offset_ / OFFSET_CORRECTION_DIVISOR;
}

std::error_code Timer::wait_next_tick()
{
{
Expand Down
Loading