Skip to Content
LessonsDay 04 — Bus & time sync

Day 04 — Bus topology & time sync: who owns the clock

Bus topology and time sync

Day 02 froze JointState / JointCommand. Day 03 added ImuSample, ContactState, and EstimatorState. Those structs are theater if joints and the IMU are stamped on different clocks, or if “latest packet” quietly replaces sample time.

Today freezes the wire and the timeline: which bus topology you run, who owns the master clock, and how the physical plant and the digital twin share one timebase without silent lag.

The bus is a control surface

Bandwidth is the brochure number. What balance and contact actually need:

PropertyWhy it matters
Cycle / jitterEstimator and WBC assume a period; jitter becomes phase noise in fusion
Max ageStale FzF_z or ω\boldsymbol{\omega} is worse than a dropped frame you detect
OrderingJoint vs IMU vs contact must be mergeable on one timeline
Fault visibilityBus-off, missed sync, CRC — publish them, don’t swallow

A 1 Mb/s bus with known 200 µs jitter and stamped samples beats a “fast” USB poll that delivers whatever arrived last with no age field.

Rule: higher layers consume sample time in a named clock domain, never “the freshest buffer.” If you cannot defend age and domain, you cannot defend multi-rate fusion.

Physical firmware and the twin publish the same message map. In sim you may inject ideal time — but then you must document the lie and add the measured delay budget before you claim transfer.

Topology: pick a spine, then hang limbs

Humanoids are graphs of nodes (joint MCUs, IMU bridges, FT front-ends, host / mid MCU). Three common spines:

SpineStrengthsCosts / traps
CAN / CAN-FDMulti-drop, cheap, many joint nodes; FD helps payloadArbitration jitter under load; need a clear ID / rate plan
EtherCATCyclic determinism, distributed clocks (DC), high PDO ratesTopology/cabling discipline; tooling; DC setup is not optional
HybridEtherCAT spine + CAN/SPI on a limb board; IMU local to a joint MCUTwo clock domains unless you bridge time carefully

Physical ↔ digital pairing for topology

PhysicalDigital twin / backing
Node IDs / station aliasesSame IDs in message map and URDF-adjacent config
Bitrate / cycle time (TcT_c)Twin step or bus model uses TcT_c (or documents ideal)
Who bridges IMU/FTSame attachment: “IMU via joint 2 node,” not a magic host topic
Redundancy / ring breakFault flags the twin can inject for policy tests

Exercise: draw your real wiring once. Count hops from IMU to estimator. If the path is “SPI → joint MCU → CAN → USB gadget → ROS → Python,” write the budgeted age for that path before you tune balance gains.

Time sync: sample ≠ transmit ≠ receive

Three times show up in every packet path:

  1. tsamplet_{\text{sample}} — when the ADC / encoder latch happened (what control wants)
  2. ttxt_{\text{tx}} — when the node put the frame on the wire (optional)
  3. trxt_{\text{rx}} — when the host saw it (easy to log, dangerous to treat as truth)

If nodes free-run, trxt_{\text{rx}} differences include transport and clock drift. Estimators that subtract “IMU time” from “joint time” using receive stamps invent phase.

Who owns the clock

Pick one master clock domain and name it in frames (e.g. host_mono, ec_dc, mcu0_tim2).

MechanismIdeaUse when
EtherCAT DCSlaves align to a distributed clock; latch near SYNCEtherCAT spine; you need sub-cycle alignment
Sync / heartbeat frameMaster broadcasts time or a pulse; slaves discipline local timersCAN-FD with a clear master
Host-stamped + max ageNo shared sync; host records trxt_{\text{rx}} and enforces age budgetsEarly bring-up only — graduate out before multi-contact
Explicit offset tableRare offline measure of fixed delaysStatic pipelines you cannot retime

Do not mix “IMU on USB wall time” with “joints on MCU tick” and call it fused. Convert into one domain or keep separate estimators until you can.

Shared wire header (extend Day 02 / 03 APIs)

Mirror the muscle and sensing APIs with an explicit header every cyclic sample carries:

BusHeader: node_id, seq t_sample # in clock_domain t_tx? # optional clock_domain # e.g. "ec_dc" | "host_mono" age_budget_ms # contract: drop/flag if exceeded faults # sync_lost, bus_off, crc, seq_gap, ... JointState ⊇ BusHeader ImuSample ⊇ BusHeader ContactState ⊇ BusHeader

Higher layers (WBC, RL, teleop) should not care whether the frame arrived on EtherCAT PDO or CAN-FD — only that t_sample and clock_domain are honest.

If the twin publishes perfect JointState at step kk and hardware delivers the same fields 3 ms late with jitter, policies trained in the twin will hate the real bus. Either model the age budget in sim or keep a bus replay layer that both sides can feed.

Rates that fit a humanoid spine

Illustrative starting points (measure yours; do not worship the table):

StreamTypical cyclic rateNotes
FOC current (local)10–20 kHzStays on the joint MCU — not on the spine
JointState on spine1–5 kHzPosition/velocity/iqi_q for mid-tier
ImuSample200–1000 HzOften local SPI → joint node → spine
ContactState0.5–2 kHzImpact path needs the high end
Estimator inmatches slowest honest inputAge flags beat interpolated fiction

Spine cycle TcT_c should be an integer multiple story: e.g. EtherCAT 1 kHz with local FOC at 20 kHz, or CAN-FD joint frames at 2 kHz with IMU every other cycle — written down, not accidental.

Bring-up sequence (before you trust fusion)

  1. Node map — every MCU and sensor bridge named; matches wiring and frames.
  2. Message map — IDs/PDOs for Day 02/03 structs; no anonymous “debug CAN.”
  3. Empty-bus jitter — master cycle alone; log period histogram.
  4. Loaded bus — all nodes at target rate; watch arbitration stalls / lost frames.
  5. Sync on — DC or heartbeat; measure skew between nodes (scope or hardware timestamp).
  6. Contracts — t_sample, clock_domain, age_budget_ms, fault bits live in CI logs.
  7. Then close estimator / balance loops that consume multiple sensors.

Skip to step 7 and you will debug “IMU lag” for a week when the real bug is an unsynced free-running node.

SymptomLikely bus / time cause
Balance OK in twin, tips on hardwareTwin used ideal time; hardware age never modeled
CoP / IMU events always “late” in plotsUsing trxt_{\text{rx}} as tsamplet_{\text{sample}}; USB or non-RT bridge
Joints smooth, attitude chatters after motors spinCAN load / priority; IMU frames starved under arbitration
Seq gaps only on one nodeBad termination, stub length, or that node’s TX queue
Works until EtherCAT cable nudgeRing/line topology break; missing link-down fault handling
Estimator diverges after hoursClock drift between domains; no sync discipline
“Latest” joint + old IMU looks fine until impactAge budget not enforced; impact needs aligned stamps

What to freeze this week

Extend frames (and your bus config repo) with:

  • Spine choice: CAN-FD / EtherCAT / hybrid — and why
  • Node ID map; which node bridges each IMU / FT
  • Named clock_domain and who is time master
  • Cycle time TcT_c, per-stream rates, and age_budget_ms per message class
  • BusHeader fields your firmware and sim will carry
  • Fault bits that force a safe torque policy (bus-off ≠ “hold last iqi_q forever”)

The lesson is not “buy EtherCAT because humanoids are fancy.” It is make the wire and the clock the same story in silicon and in the twin, so Day 03’s sensing APIs stay honest under load.

Next

Day 05 — Power domain and BMS: brownout under peak torque, DC bus sag on the same spine, and how voltage faults must cut torque before the estimator invents a new reality.

Last updated on