Skip to content
Protocol

CAN protocol

4 min readFirmware v2 · protocol v2

Protocol v2 is how hosts and Tercio S1 drivers talk on the bus. It is small on purpose: an 11-bit ID says who and what, the first data byte says which command, and everything after is little-endian.

The contract lives in Firmware/src/protocol/Protocol.h; the Python and Arduino libraries and Tercio Control follow it. To try any frame, use the frame builder.

Physical CAN-FD, bit-rate switching on. 500 kbit/s arbitration, 2.5 Mbit/s data.
Identifiers 11-bit (standard).
Payload Up to 64 bytes: 1 opcode byte and up to 63 bytes of arguments.
Byte order Little-endian.
Units Positions f64 turns, velocities f32 turns/s, accelerations f32 turns/s².

CAN-FD frames come in fixed lengths (0–8, 12, 16, 20, 24, 32, 48, 64 bytes), so a payload is padded up to the next one. Ignore bytes after the fields you expect, and accept frames longer than the minimum.

The ID’s upper four bits are the function, the lower seven the node ID (1–127).

ID Function Direction
0x000 Broadcast Host → every node
0x080 + node Event (faults) Node → host
0x100 + node Command Host → node
0x180 + node Reply Node → host
0x200 + node Telemetry Node → host

Node 1 listens on 0x101 and 0x000, answers on 0x181, streams telemetry on 0x201 and reports faults on 0x081. Other Tercio nodes, such as the Tercio IMU, use the same scheme and share the 1–127 range; GetInfo’s last byte tells them apart.

Frame Data
Command [opcode] [arguments…]
Reply [opcode] [status] [reply data…]
Telemetry [0x01] [telemetry, 37 bytes] (Tercio IMU: [0x03] [62 bytes])
Event [0x02] [u16 faults] [u16 warnings] [u8 state]

Replies. Every command addressed to one node gets a reply on 0x180 + node, with the same opcode and a status. Set the top bit of the opcode (opcode | 0x80) and the node only replies if the command fails: use it to stream moves without doubling the traffic.

Broadcasts (ID 0x000) are not answered, except GetInfo and AssignNodeId. Only these commands act on a broadcast: GetInfo, AssignNodeId, Enable, Stop, EmergencyStop and Sync.

Move node 1 to 0.25 turn at the configured limits, and its reply:

0x101 13 00 00 00 00 00 00 D0 3F 00 00 00 00 00 00 00 00 00
│ └─ f64 position 0.25 │ └─ f32 vmax 0 └─ f32 amax 0
└─ MoveTo └─ flags
0x181 13 00
│ └─ status Ok
└─ MoveTo

Arguments in brackets are optional: leave them off the end of the frame, or send 0 for “use the default”.

0x00

GetInfo

Broadcast too

Identity: protocol and firmware version, hardware revision, encoder, node ID and the 12-byte unique ID. Broadcast, every node answers within 64 ms, staggered by unique ID.

Arguments
—
Reply data
Info (20 bytes)
Python
motor.info()
0x01

SaveConfig

Writes every parameter to flash. The step output pauses for the few milliseconds the write takes.

Arguments
—
Reply data
—
Refused with
Busy while moving or during a procedure. StorageError if the flash write failed.
Python
motor.save()
0x02

FactoryReset

Restores the factory defaults in RAM, keeping the node ID. Send SaveConfig to make it stick.

Arguments
—
Reply data
—
Refused with
Busy unless the motor is disabled.
Python
motor.factory_reset()
0x03

Reboot

Restarts the driver 20 ms after replying.

Arguments
—
Reply data
—
Python
motor.reboot()
0x04

ClearFaults

Clears latched faults. Hard faults stay if their cause is still there.

Arguments
—
Reply data
—
Refused with
Faulted if the driver is still hot, shorted or silent, the board is within 5 °C of the shutdown temperature, or the encoder still cannot be read.
Python
motor.clear_faults()
0x05

AssignNodeId

Broadcast only

Gives a new node ID to the driver with this unique ID, even when several drivers share an ID. It answers from its new ID. Applied immediately; send SaveConfig to keep it.

Arguments
u8[12] uid u8 id
Reply data
—
Refused with
Sent to a single node instead of broadcast: BadValue.
Python
bus.assign_node_id(uid, id)
0x08

GetParam

Reads one parameter.

Arguments
u8 param
Reply data
[u8 param][4-byte value]
Refused with
UnknownParam.
Python
motor.get_param(Param.param)
0x09

SetParam

Writes one parameter (RAM; SaveConfig persists). The reply carries the value as applied.

Arguments
u8 param f32 value
Reply data
[u8 param][4-byte value]
Refused with
UnknownParam, ReadOnly, BadValue (out of range) or Busy (the parameter can only change when stopped or disabled).
Python
motor.set_param(Param.param, value)
0x10

Enable

Broadcast too

1 energises the motor and holds position (only energises, without holding, until calibrated). 0 switches the bridge off and aborts any procedure.

Arguments
u8 on
Reply data
—
Refused with
Faulted while a hard fault is latched.
Python
motor.enable()
0x11

Stop

Broadcast too

Decelerates at the acceleration limit and holds. Also aborts a procedure (without a fault) and cancels a queued move.

Arguments
—
Reply data
—
Python
motor.stop()
0x12

EmergencyStop

Broadcast too

Switches the bridge off at once. The shaft coasts; a load can move it.

Arguments
—
Reply data
—
Python
motor.emergency_stop()
0x13

MoveTo

Moves to an absolute position along a trapezoidal profile. Speed and acceleration default to the limits and can only lower them.

Arguments
f64 position [u8 flags] [f32 vmax] [f32 amax]
Reply data
—
Refused with
Disabled, NotCalibrated, Faulted, or Busy during a procedure or in step/dir mode.
Python
motor.move_to(position)
0x14

MoveBy

Moves by a distance, counted from where the axis is heading, so back-to-back MoveBy commands add up.

Arguments
f64 position [u8 flags] [f32 vmax] [f32 amax]
Reply data
—
Refused with
As MoveTo.
Python
motor.move_by(position)
0x15

SetVelocity

Turns continuously, clamped to the velocity limit. 0 decelerates to a stop and holds.

Arguments
f32 velocity [f32 amax]
Reply data
—
Refused with
As MoveTo.
Python
motor.set_velocity(velocity)
0x16

SetZero

Declares the current position to be this value (0 if omitted). SaveConfig keeps the zero across power cycles.

Arguments
[f64 position]
Reply data
—
Refused with
Busy unless standing still.
Python
motor.set_zero(position)
0x17

Sync

Broadcast too

Starts every move queued with the Wait for Sync flag. Broadcast it to start several axes on the same instant.

Arguments
—
Reply data
—
Python
bus.sync()
MoveTo / MoveBy arguments17 bytes
OffsetFieldTypeMeaning
0positionf64Target (MoveTo) or distance (MoveBy), turns
8flags optionalu8bit 0: wait for Sync (deferred)
9vmax optionalf32Speed limit for this move, turns/s. 0 = Max velocity. Never above it.
13amax optionalf32Acceleration for this move, turns/s². 0 = Max acceleration.
0x20

Calibrate

Turns the motor a quarter turn each way to learn the encoder direction and check the scale, then sets zero here and saves. Enables the motor itself.

Arguments
—
Reply data
— (the procedure runs on; watch the state)
Refused with
Faulted without encoder readings, Busy during another procedure.
Python
motor.calibrate()
0x21

Home

Finds the reference with the homing parameters (a switch or a hard stop), backs off and sets zero there.

Arguments
—
Reply data
— (the procedure runs on; watch the state)
Refused with
As MoveTo.
Python
motor.home()
0x22

AutoTune

Runs back and forth between two positions, faster each pass, and keeps the fastest velocity and acceleration that pass three times. Saved automatically.

Arguments
f32 min f32 max
Reply data
— (the procedure runs on; watch the state)
Refused with
BadValue if the range is under 5/6 turn (0.8333 turns); otherwise as MoveTo.
Python
motor.auto_tune(min, max)

A procedure command replies as soon as the procedure starts. Follow it in telemetry: the state is Calibrating, Homing or Tuning while it runs, and the procedure step field says which phase. When it ends, the state returns to Holding; a failure also sets the matching fault.

Procedure Steps
Calibrate 0 settling, 1 turning forward, 2 measuring, 3 turning back, 4 checking
Home 0 searching, 1 settling, 2 backing off
AutoTune The current level, 0–18 (velocity 5 + 3 × level turns/s)
CodeStatusMeaning
0OkDone, or started (procedures).
1UnknownCommandThe firmware does not know this opcode. Update the firmware.
2BadLengthArguments missing, or a position/velocity is not a finite number.
3BadValueA value is out of range.
4UnknownParamNo parameter with this ID.
5ReadOnlyThe parameter cannot be written.
6BusyNot allowed right now: moving, a procedure is running, or the axis follows step/dir.
7NotCalibratedCalibrate first.
8FaultedA hard fault is latched, or its cause is still present.
9DisabledEnable the motor first.
10StorageErrorWriting flash failed.

Each node sends telemetry on 0x200 + node at Telemetry rate (100 Hz by default; 0 turns it off, 1000 Hz is the maximum).

Telemetry (after the 0x01 frame type)37 bytes
OffsetFieldTypeMeaning
0stateu8Axis state
1flagsu8Status flags
2faultsu16Latched fault bits
4warningsu16Live warning bits
6positionf64Measured position, turns
14targetf64Where the motion profile is now, turns
22velocityf32Measured velocity, turns/s
26following errorf32Target minus position, turns
30temperaturei16Board temperature in 0.1 °C
32supplyu16Supply voltage in mV
34procedure stepu8Phase of the running procedure
35sequenceu8Counts up by one per frame; gaps are lost frames
36loop loadu8Peak control-loop time since the last frame, % of the 50 µs budget

The state, fault, warning and flag values are listed in Faults and warnings.

When a new fault is raised, the node sends it at once on 0x080 + node, without waiting for telemetry:

Fault event (after the 0x02 frame type)5 bytes
OffsetFieldTypeMeaning
0faultsu16All latched fault bits
2warningsu16Live warning bits
4stateu8Axis state
GetInfo reply data21 bytes
OffsetFieldTypeMeaning
0protocolu8Protocol version: 2
1fw majoru8Firmware version
2fw minoru8
3fw patchu8
4hw revu8Hardware revision
5encoderu80 on-board AS5600, 1 external AS5600, 2 AS5048A
6node idu8Current node ID
7info flagsu8bit 0: running from the crystal
8uid12 bytesThe microcontroller’s unique ID: the driver’s serial number
20deviceu8Device class: 0 Tercio S1, 1 Tercio IMU. A 20-byte reply means an S1.

GetParam and SetParam carry one parameter as [u8 id] [4-byte value]: integers zero-extended to 32 bits, booleans 0 or 1, floats as IEEE 754 single precision, all little-endian. SetParam’s reply returns the value as the driver applied it.

IDParameterTypeWritable
0x01Node IDu8When stopped
0x02Telemetry rateu16Any time
0x03Stop if host is silent foru16Any time
0x10Full steps per revolutionu16When stopped
0x11Microstepsu16When stopped
0x12Run currentu16Any time
0x13Hold currentu8Any time
0x14Quiet mode (StealthChop)boolWhen stopped
0x15Quiet mode up tof32Any time
0x16Reverse directionboolWhen stopped
0x17Gear ratiof32When stopped
0x20Max velocityf32Any time
0x21Max accelerationf32Any time
0x22Proportional gainf32Any time
0x23Integral gainf32Any time
0x24Damping gainf32Any time
0x25Deadbandf32Any time
0x26Stall thresholdf32Any time
0x27Stall confirm timeu16Any time
0x28Soft limit, minf32Any time
0x29Soft limit, maxf32Any time
0x2AEnable at power-upboolAny time
0x30Encoderu8When disabled
0x31Encoder reversedboolWhen disabled
0x32CalibratedboolRead-only
0x40Stop at limit switchesboolAny time
0x41Switches pull to groundboolWhen stopped
0x42Follow step/dir inputboolWhen stopped
0x43EN input active lowboolAny time
0x50Methodu8Any time
0x51Search speedf32Any time
0x52Search currentu16Any time
0x53Back-off distancef32Any time
0x54Hard-stop sensitivityf32Any time
0x55Give up afteru16Any time
0x60Shut down atf32Any time

Defaults, ranges and meanings are on Parameters.

  • Discover: broadcast GetInfo. Each node replies from its own ID after a delay of 0–63 ms derived from its unique ID, so several nodes sharing an ID still answer one by one, each with its unique ID.
  • Assign: broadcast AssignNodeId with [uid 12 bytes] [u8 new id]. Only the node with that unique ID takes the new ID and replies from it. Send SaveConfig to the new ID to keep it.
  • SetParam NodeId to a single node works too: its reply still comes from the old ID, then it switches.

MoveTo and MoveBy with flag bit 0 set are queued: the node checks and stores the move, replies Ok and sets the move pending flag. A broadcast Sync starts every queued move on every node at the same moment. Stop, Enable 0 and EmergencyStop discard a queued move.