Skip to content

CAN buses and bandwidth

Stage 3 · Electrical, sensing and safety · ~30 min · Before this: 03 System architecture, 04 Actuators · numbers from the repository as of 2026-11-02

Every leg motor on Marvin Bot must receive a command and send back its position 500 times a second, over a pair of twisted wires shared with its neighbours. This page shows how long each message takes on the wire, why a bus should stay at most about half full, and why the robot ends up with seven buses rather than one.

Marvin Bot’s computer has to talk to 23 motors, many times a second: “go to this angle with this stiffness”, and back from each motor, “here is where I am and how hard I am pushing”. Running a separate cable from the computer to every motor would be a tangle, so motors share cables called buses, like several houses sharing one road. The rules for sharing that road are the CAN protocol (Controller Area Network), invented for cars and now common in robots.

The catch is that a shared road can fill up. If the messages for one control cycle do not fit in the time that cycle lasts, commands arrive late and the robot’s balance suffers. This page is about counting that traffic.

A CAN bus is a single pair of wires that every node on it (here: the computer’s CAN adapter and a handful of motors) listens to all the time. Only one node can talk at a time. Each message, called a frame, takes a fixed number of bit-times; at 1 Mbit/s (one million bits per second) one bit lasts 1 µs (one microsecond, a millionth of a second). If a control loop needs N frames every cycle, those frames have to fit in the cycle, with room to spare. That is the whole problem: frames × bits per frame × cycles per second must stay well below the bit rate.

Marvin Bot’s RobStride actuators use classic CAN at 1 Mbit/s with 29-bit identifiers, and each motor exchanges one command and one reply per control cycle (description/robot.yaml, robot/drivers/robstride.py).[9] “29-bit identifiers” means each message starts with a 29-bit label saying who it is for and what kind it is. A leg motor bus runs at 500 Hz, so each cycle lasts 2,000 µs. Three motors × 2 frames × 160 µs is 960 µs: the bus is busy 48% of the time.

Start here: click Marvin Bot today: hip bus, then the other presets, and watch the Bus load readout: the share of time the wires are busy. Green is at or below your load limit (60% by default), amber is above it. The frame settings (identifier, data bytes, stuffing) are explained under Counting the bits; you can leave them alone at first.

Presets

Physical layer

RobStride's motors use classic CAN; FD is shown as an alternative for other motors.
Classic CAN tops out at 1 Mbit/s; FD uses this rate for arbitration.
Only the data phase speeds up. The Orin's controllers support up to 15 Mbit/s with a suitable transceiver[8].

Frame

RobStride commands and replies carry 8 bytes. FD allows 12-64 in steps.
100% = the exact worst case a scheduler must plan for; real traffic is usually lower but data-dependent.

Traffic

How many motors share this one pair of wires.
RobStride: one command + one reply = 2.
Marvin Bot: legs 500 Hz, arms 250 Hz.
The highest share of busy time you are willing to plan for; the rest is spare room for errors and retries. The repo uses 60%.
Bits per frame
–
Time per frame
–
Bus time per cycle
–
Bus load
–
Max motors at your limit
–
Last reply after
–

How to read this picture: the top strip is one control cycle (2,000 µs at 500 Hz) drawn left to right; each block is one frame on the wire, commands from the computer (the "host") in blue and replies in orange. Empty strip is idle bus time; the green dashed box marks your load limit. The lower strip zooms into a single frame and shows its fields to scale, with the inserted stuff bits in violet. "Last reply after" is how long into the cycle the final motor's answer arrives.

command (host → motor)reply (motor → host)past the load limitstuff bits

The wires: a differential pair with a resistor at each end

Section titled “The wires: a differential pair with a resistor at each end”

CAN sends each bit as the difference between two wires, CAN_H and CAN_L. In the “dominant” state (a logical 0) a transmitter pulls CAN_H up to about 3.5 V and CAN_L down to about 1.5 V; in the “recessive” state (logical 1) nobody drives, and both wires sit near 2.5 V. Noise picked up by a twisted pair hits both wires almost equally, so the receiver, which only looks at the difference, ignores it.[5]

A 120 Ω resistor at each physical end of the bus matches the cable’s characteristic impedance, so a fast edge is absorbed at the end instead of reflecting back and corrupting later bits. Two 120 Ω resistors in parallel read 60 Ω across CAN_H and CAN_L with the power off: a quick check that a bus is terminated correctly.[5] Marvin Bot’s harness puts one terminator at the hub end and one on the last motor of each bus (docs/design/assembly_wiring.md).

Arbitration: the lowest identifier wins, and nobody loses time

Section titled “Arbitration: the lowest identifier wins, and nobody loses time”

Every frame begins with an identifier. If two nodes start at once, they both send their identifiers bit by bit while listening. A dominant 0 overwrites a recessive 1, so a node that sent a 1 but hears a 0 knows it has lost, stops, and tries again when the bus is free. The frame with the numerically lowest identifier gets through untouched. This is called bitwise, non-destructive arbitration.[1] It makes CAN a priority bus, and it has a consequence for timing: a frame that is ready can still be delayed by up to one whole frame that already started (no node can interrupt a frame), plus every higher-priority frame that is waiting.

Classic CAN 2.0A uses 11-bit identifiers; CAN 2.0B adds an “extended” format with 29 bits.[1] RobStride packs a lot into those 29 bits: a 5-bit communication type, a 16-bit field (for the command type 1 it holds the feed-forward torque; for the reply it holds fault and mode bits) and the 8-bit motor ID (robot/drivers/robstride.py, after RobStride’s manuals[9]).

A classic extended data frame with s data bytes is made of:

Field Bits What it is for
Start of frame (SOF) 1 one dominant bit that wakes every receiver
Identifier A, SRR, IDE, identifier B, RTR 11 + 1 + 1 + 18 + 1 the 29-bit identifier and flags; arbitration happens here
Reserved bits r1, r0; DLC 2 + 4 DLC = data length code (0-8 bytes)
Data 8s the payload: 64 bits for RobStride’s 8 bytes
CRC 15 checksum (cyclic redundancy check): lets the receiver detect a corrupted frame
CRC delimiter, ACK slot, ACK delimiter 1 + 1 + 1 any receiver that got the frame right overwrites the ACK slot with a dominant bit
End of frame (EOF) 7 seven recessive bits
Intermission (inter-frame space) 3 minimum gap before the next frame may start

With 8 data bytes that is 131 bits. Then comes bit stuffing: CAN receivers resynchronise their clocks on edges, so after five identical bits in a row the transmitter inserts one opposite “stuff” bit, which the receiver removes. Stuffing applies from the SOF to the end of the CRC (54 + 8s bits for an extended frame, 34 + 8s for a standard one).[1] In the worst case a stuff bit can appear after the first five bits and then after every four more (because the stuff bit itself starts the next run), so Davis, Burns, Bril and Lukkien give the worst-case frame length as[2]

C = g + 8s + 13 + ⌊(g + 8s − 1) / 4⌋ bits
g = 54 (29-bit ID) or 34 (11-bit ID); s = data bytes; 13 = CRC delimiter, ACK, EOF and intermission (not stuffed)
29-bit, 8 bytes: 54 + 64 + 13 + ⌊117/4⌋ = 131 + 29 = 160 bits → 160 µs at 1 Mbit/s
11-bit, 8 bytes: 34 + 64 + 13 + ⌊97/4⌋ = 111 + 24 = 135 bits

The same formula is used by the Linux can-utils bus-load tool and the kernel’s CAN length helpers.[3][4] It is the length a schedule must assume, because the stuffing depends on the data (an angle that happens to be 0x0000 or 0xFFFF stuffs a lot).

load = (motors × frames per motor × loop rate × bits per frame) / bit rate
hip bus: 3 × 2 × 500 /s × 160 bit / 1,000,000 bit/s = 0.48 → 48%

Why keep load at or below about 50-60%? Three reasons:

  • Error recovery. A corrupted frame is followed by an error frame and a retransmission, which eats bus time the schedule did not plan.
  • Latency (delay): a frame that becomes ready while others queue must wait for all of them; the fuller the bus, the longer and more variable that wait (the jitter), and response-time analysis of the kind Davis et al. formalised shows the low-priority worst case growing sharply as load approaches 100%.[2]
  • Reply gaps. The motors do not answer instantly: each reply comes some tens of microseconds after its command, and those gaps are idle bus time that the load figure does not count.

A 50-60% ceiling is engineering practice rather than a number from the CAN standard; Marvin Bot’s robot.yaml sets max_load: 0.6.

CAN FD (“flexible data-rate”) is a newer version of CAN that sends the payload faster. In short: for 8-byte messages it halves the frame time, but Marvin Bot’s RobStride motors do not speak it.

Go deeper: how CAN FD counts its bits

CAN FD keeps the arbitration phase at the nominal rate (so every node still hears every identifier bit) and then, if the BRS (bit-rate switch) bit is set, switches to a faster bit rate for the control bits, data and CRC, before switching back for the acknowledge. It also allows up to 64 data bytes.[6][7] The playground counts an FD extended frame as the Linux kernel does: 41 header bits plus the data are dynamically stuffed, then a CRC field with a stuff counter, CRC-17 (up to 16 bytes) or CRC-21, and fixed stuff bits, then ACK, EOF and intermission.[4] For 8 bytes that is 171 bits in the worst case, of which 57 run at the nominal rate and 114 at the data rate: 57 µs + 22.8 µs ≈ 80 µs at 1/5 Mbit/s, half the 160 µs of a classic frame. (The exact point where the rate switches is inside the BRS and CRC-delimiter bits; the split here is accurate to about one bit.)

On Linux, CAN controllers appear as network interfaces through SocketCAN: you bring one up with ip link set can0 up type can bitrate 1000000 and programs read and write frames through ordinary sockets.[10] The Jetson AGX Orin has two on-board CAN controllers (MTTCAN, classic up to 1 Mbit/s and FD up to 15 Mbit/s data rate) that need external 3.3 V transceivers (the small chip that turns the controller’s logic signals into the voltages on the wires).[8]

More buses come from USB adapters; many use the open gs_usb protocol (the candleLight firmware), for which Linux has a driver built in.[11] USB adds its own latency and jitter (USB frames are scheduled on a 125 µs microframe grid at high speed, 1 ms at full speed), which is why Marvin Bot plans a bench timing test before relying on adapters.

The bus map below is computed live: the first load column uses the repo’s 160 bits, the second the playground’s stuffing setting (the same 160 bits by default). The repo’s figures are 48% for the hip, shank and right-arm buses and 40% for the left arm.

Interfaces (BOM, docs/decisions.md 2026-10-29): the Orin’s two on-board controllers carry the arm buses; one dual-channel isolated USB-CAN adapter per leg carries its hip and shank buses; a single-channel adapter carries the battery bus. That choice is “to be confirmed by the bench timing test” (roadmap item 13). A microcontroller bridge that runs the 500 Hz exchange itself is the fallback if USB jitter is too high.

Why 500 Hz when the policy (the trained neural-network controller) only runs at 50 Hz? The policy runs at 50 Hz, so each policy step spans 10 motor cycles on the legs and 5 on the arms; the faster motor loop exists so that the motors’ own PD control (RobStride’s MIT-style “operation control” mode: τ = kp(qtarget − q) + kd(q̇target − q̇) + τff) gets fresh targets and the controller gets fresh feedback often. (PD stands for proportional-derivative: a spring-and-damper rule, explained in 15 PD control; kp and kd are the spring and damper strengths, q the joint angle, q̇ its speed and τff an extra feed-forward torque.) The battery bus, the seventh, carries the BMS reports from 09 Power and battery.

Option For Against
One bus per leg (6 motors at 500 Hz) fewer adapters and wires 96% worst-case load (repo: “roughly 80-90%”): no room for errors, retries or parameter reads
One bus per leg at 250 Hz 48% load below the 500 Hz leg command rate in requirement R43
Two buses per leg (chosen) 48% worst case; a fault on one bus leaves the other half of the leg talking more adapters, a 6-pin leg signal connector
CAN FD at 5 Mbit/s about half the frame time needs motors whose firmware speaks FD; RobStride’s documented protocol is classic CAN
Microcontroller bridge deterministic timing next to the bus, USB only at 50 Hz custom firmware to write and maintain
EtherCAT or another fieldbus (industrial Ethernet-based motor networks) much higher bandwidth not offered by the chosen motors
  • How many motors per bus, and at what loop rate (the two multiply). Trade: fewer buses mean fewer adapters and cables, but six leg motors on one bus at 500 Hz is 96% load; keeping one bus means dropping to about 300 Hz.
  • Which frame length to budget. Trade: the exact worst case (160 bits) is safe but plans for traffic you rarely see; a typical value (about 150) fits about 7% more traffic but can be overrun by unlucky data.
  • Which load ceiling to accept (50%, 60%, more). Trade: a higher ceiling fits more motors or a faster loop, but leaves less room for parameter reads, error frames and logging, and makes delays more variable.
  • USB-CAN adapters, on-board controllers or a microcontroller bridge, once the bench timing test reports latency and jitter. Trade: adapters are cheap and quick but add USB delay; a bridge gives steady timing but means writing and maintaining firmware.
  • Whether to require CAN FD of future actuators. Trade: about half the frame time, so room for more motors per bus, but it rules out today’s RobStride motors.
Your decision note

Built from your settings above. Paste it into an issue, a design discussion or an AI assistant session.

Next: 11 Sensors and compute looks at the other end of these wires: the computer that sends the commands, and the sensors that tell it where the robot is.

  1. Robert Bosch GmbH, CAN Specification version 2.0 (1991) — used for: frame fields, standard and extended identifiers, arbitration, bit stuffing rule.
  2. R. I. Davis, A. Burns, R. J. Bril, J. J. Lukkien, Controller Area Network (CAN) schedulability analysis: refuted, revisited and revised, Real-Time Systems 35(3), 2007 — used for: worst-case frame length formula (g + 8s + 13 + ⌊(g + 8s − 1)/4⌋), response time growth with load.
  3. can-utils, canframelen.h (Czech Technical University in Prague) — used for: confirmation that Linux bus-load tools use the same worst-case length (80 + 10s bits for 29-bit frames).
  4. Linux kernel, include/linux/can/length.h — used for: classic and CAN FD field counts (41-bit FD extended header, CRC-17/21 fields with fixed stuff bits, 9-bit footer, 3-bit intermission).
  5. Texas Instruments, Introduction to the Controller Area Network (CAN), SLOA101B — used for: differential signalling, dominant/recessive levels, 120 Ω termination.
  6. F. Hartwich, Bit time requirements for CAN FD, iCC 2013, CAN in Automation — used for: bit-rate switching after arbitration.
  7. CAN in Automation, Designing a CAN network — used for: CAN FD payload up to 64 bytes; data-phase rate limited by topology and transceivers.
  8. NVIDIA, Jetson Linux Developer Guide: Controller Area Network (CAN) — used for: two MTTCAN controllers on AGX Orin, classic up to 1 Mbit/s, FD data up to 15 Mbit/s, external 3.3 V transceiver, bring-up commands.
  9. RobStride, Product_Information (user manuals RS00, RS03, RS04, RS06, RS10P) — used for: 1 Mbit/s classic CAN, 29-bit identifier layout, operation-control command and feedback reply (as implemented in robot/drivers/robstride.py).
  10. Linux kernel documentation, SocketCAN — used for: CAN as network interfaces, ip link configuration.
  11. candleLight_fw (gs_usb firmware) — used for: the open gs_usb USB-CAN protocol supported by the Linux gs_usb driver.