Download Game! Currently 118 players. Last logged in:MarvinVerenSpannAtletico

Blitzer's Blog >> 72843

Back to blogs index
Posted: 29 Sep 2026 20:51 [ permalink ]
Because the physical length of a humanoid robot's CAN bus is roughly 1 to 2
meters, the electromagnetic propagation delay across the wire is about 5
nanoseconds. At a microsecond scale, propagation delay is effectively zero.
This means you can completely discard the complex two-way measurement of IEEE
1588 PTP (Delay_Req and Delay_Resp). A one-way broadcasta Simplified PTPis
mathematically sufficient.
However, the challenge lies in the silicon. Neither the ESP32 nor the RP2040
has native CAN FD hardware built-in; they require external SPI controllers
like the MCP2518FD. SPI transactions introduce variable software latency that
will instantly ruin microsecond synchronization.
1. The Hardware Interrupt (INT) Requirement
You cannot timestamp a message when the SPI read() finishes. The RTOS
scheduler or bus contention might delay that read by hundreds of microseconds.
Instead, you must wire the external CAN controller's hardware interrupt (INT)
pin directly to a high-priority GPIO interrupt on the microcontrollers. The
timestamp must be captured the exact nanosecond the CAN controller detects a
valid frame, completely bypassing the SPI bottleneck.
2. The Simplified PTP Sequence
Every 100ms, the central ESP32 orchestrates a synchronization wave. Because it
is broadcasting, every RP2040 on the bus receives the message simultaneously.
 1. Queue the Sync
   ESP32 (Master)
   The ESP32 pushes a SYNC frame to its CAN controller's transmit buffer.
 2. Hardware TX Timestamp
   ESP32 (Master)
   The moment the frame hits the wire, the CAN controller fires its TX
interrupt. The ESP32's ISR immediately records its local hardware microsecond
timer (let's call this T1).
 3. Hardware RX Timestamp
   RP2040 (Slaves)
   Simultaneously, every RP2040's CAN controller detects the Start of Frame
(SOF) and fires its RX interrupt. The RP2040 ISRs instantly record their local
hardware microsecond timers (T2).
 4. Send the Follow-Up
   ESP32 (Master)
   The ESP32 now reads T1 from its memory and transmits it in a standard CAN
FD payload as a FOLLOW_UP message.
 5. Calculate the Offset
   RP2040 (Slaves)
   The RP2040 receives the FOLLOW_UP payload via a standard, latency-tolerant
SPI read. It extracts T1 and calculates the raw difference: Offset = T1 - T2.
3. The Software Phase-Locked Loop (PLL)
You cannot simply add the calculated Offset to the RP2040's local timer. The
crystal oscillators on the different boards run at infinitesimally different
speeds due to manufacturing tolerances and temperature variations. If you just
apply the raw offset every 100ms, the RP2040's clock will "jump" forward or
backward. Sudden time jumps will cause catastrophic derivative spikes in your
PID loops (calculating velocity or acceleration requires a perfectly smooth
time delta).
Instead, you use a software Phase-Locked Loop to gently steer the RP2040's
logical clock rate.
/ RP2040 Core 0: PTP Synchronization Task

float clock_skew_rate = 1.0f; // 1.0 means perfectly matched to ESP32
int64_t clock_offset = 0;

void on_ptp_update(int64_t master_t1, int64_t local_t2) {
    int64_t error = master_t1 - (local_t2 + clock_offset);
    
    // PI Controller to steer the clock
    // Proportional: Correct the immediate time gap
    clock_offset += error * 0.1f; 
    
    // Integral: Correct the underlying crystal drift rate
    clock_skew_rate += (error * 0.0001f); 
}

/ Core 1: The 1000Hz PID Loop calls this to get the exact network time
int64_t get_network_time_us() {
    int64_t raw_local = get_hardware_timer_us();
    return (int64_t)(raw_local * clock_skew_rate) + clock_offset;
}

By applying a fractional multiplier (clock_skew_rate), the RP2040's logical
clock gracefully accelerates or decelerates to match the ESP32's master clock
exactly, ensuring your sensor fusion engine always divides by a smooth,
mathematically continuous dt.