Skip to content
Tercio S1

Motion

6 min readFirmware v2 · protocol v2

Everything the S1 can do with a shaft. Examples are in Python with unit=Unit.DEGREES; the same commands are in Tercio Control, the Arduino library and on the wire.

On the wire and in the firmware, positions are turns of the encoder shaft as 64-bit floats, speeds are turns/s and accelerations turns/s². The Python and Arduino libraries convert from the unit you choose per motor; Tercio Control has a units switch in the top bar.

The S1 counts whole turns while it runs, so positions keep growing through continuous rotation. At power-up it starts from the encoder’s angle relative to the saved zero, between −½ and +½ turn.

A motor must be enabled before it moves. Enabled, it holds its position in closed loop; disabled, the bridge is off and the shaft turns freely.

motor.enable() # energise and hold here
motor.disable() # bridge off

With Enable at power-up on (the default), the S1 enables itself at start-up. An uncalibrated motor is energised but not in closed loop until you run Calibrate.

MoveTo goes to an absolute position, MoveBy goes a distance. Both follow a trapezoidal profile: accelerate, cruise, decelerate, landing on the target without overshoot. The profile is recomputed every 50 µs, so a new move can replace one in progress at any time and the axis blends into it.

motor.move_to(90) # at the configured limits
motor.move_to(90, velocity=45) # slower: at most 45 °/s
motor.move_by(-360, acceleration=720) # gentler ramp
motor.move_to(0, wait=True) # block until it has settled
  • velocity and acceleration can only lower the configured Max velocity and Max acceleration, never exceed them. Omit them (or send 0) to use the limits.
  • MoveBy counts from where the axis is heading, not where it is now, so several MoveBy sent in a row add up exactly.
  • A move is refused with Disabled, NotCalibrated, Faulted, or Busy while a procedure runs or step/dir mode is on.

When is a move done? Watch the telemetry: state Holding and the settled flag mean the axis reached the target and is within the deadband. wait=True in Python does exactly that.

SetVelocity turns continuously at a speed, ramping with the acceleration limit. 0 decelerates to a stop and holds.

motor.set_velocity(180) # 180 °/s, positive direction
motor.set_velocity(-90) # reverse, blending through zero
motor.set_velocity(0) # stop and hold

The speed is clamped to Max velocity. With soft limits set, velocity mode stops exactly at the boundary.

What happens Motor after
Stop Decelerates at Max acceleration. Also cancels a queued move and aborts a procedure. Holding
Emergency stop Switches the bridge off at once. Disabled, shaft free
Disable Same as emergency stop. Disabled
Esc in Tercio Control Broadcast Stop to every node. Holding

Stop and Emergency stop can be broadcast to every node in one frame: bus.stop_all(), bus.emergency_stop_all().

To start several axes on the same instant, queue a move on each with deferred, then broadcast Sync. Each axis starts its own profile when the Sync frame arrives; all receive it at the same time.

x = Stepper(bus, 1, unit=Unit.DEGREES)
y = Stepper(bus, 2, unit=Unit.DEGREES)
x.move_to(360, deferred=True)
y.move_to(-180, deferred=True) # the telemetry flag "move pending" is now set on both
bus.sync() # both start now

Tercio Control’s Move together panel, on the bus overview, does the same: it queues a target on each ticked motor, then broadcasts Sync.

To make the axes also arrive together, scale each axis’s speed and acceleration by its share of the longest distance. Scaling both by the same factor keeps a trapezoidal move’s duration unchanged:

def move_together(bus, moves, velocity, acceleration):
"""moves: [(stepper, target)]. velocity/acceleration: for the longest move, within every axis's limits."""
distances = [abs(target - motor.position) for motor, target in moves]
longest = max(distances) or 1.0
for (motor, target), distance in zip(moves, distances):
k = max(distance / longest, 1e-3)
motor.move_to(target, velocity=velocity * k, acceleration=acceleration * k, deferred=True)
bus.sync()
move_together(bus, [(x, 360), (y, -90)], velocity=720, acceleration=2000)

SetZero declares the current position to be a value (0 by default) without moving. The S1 keeps the zero in its settings, so Save afterwards to keep it across power cycles. Calibration and homing set zero themselves.

motor.set_zero() # here is 0
motor.set_zero(90) # here is 90°
motor.save()

Soft limits keep every move inside a range: set Soft limit, min and Soft limit, max (Settings → Motion, in turns). They are active when min is less than max. Targets outside the range are clamped to the nearest limit.

Homing finds a physical reference, backs off from it and makes that point zero. Configure it under Settings → Homing, then run Home.

Method Seeks Stops at
Switch IN1 (negative) Negative direction The IN1 switch
Switch IN2 (positive) Positive direction The IN2 switch
Hard stop, negative Negative direction A mechanical stop, detected by the following error
Hard stop, positive Positive direction A mechanical stop

The sequence:

  1. The S1 lowers the motor current to Search current (800 mA by default) for the whole procedure.
  2. It moves at Search speed until the switch triggers, or, for hard-stop homing, until the following error exceeds Hard-stop sensitivity.
  3. It waits 100 ms, backs off by Back-off distance, waits again, and sets zero there. The homed flag is set.

Homing fails (fault Homing failed) if nothing is found within Give up after seconds, or if the axis stalls before a switch.

The S1 can take its target from a motion controller’s STEP and DIR pulses while it keeps the loop closed. Wire J5 as described in Wiring, then:

  1. Calibrate the motor over CAN first. Step/dir mode only runs once calibrated.
  2. Set Full steps per revolution, Microsteps and Gear ratio to match the controller’s steps-per-unit setting.
  3. Turn on Follow step/dir input (Settings → Inputs), then Save to motor.

Each rising edge on STEP moves the target by one step:

Steps per turn
One step moves
Speed at that rate

  • The motor follows the pulses while EN is active (low by default). When EN goes inactive, the bridge switches off.
  • Commands that move the axis over CAN are refused with Busy in step/dir mode. Telemetry, parameters and stops still work.
  • A stall stops the axis and holds it. Clear the fault to follow the pulses again.

Auto-tune finds how fast your motor can go with its real load. It runs back and forth between two positions, starting at 5 turns/s and 30 turns/s², and raises both each pass (by 3 turns/s and 8 turns/s²) up to 59 turns/s. When the motor slips, it drops back one level and confirms that level with three clean passes. The result becomes Max velocity and Max acceleration and is saved.

  • The range must be at least ⅚ turn, and long enough for the motor to reach each speed. A longer range tests higher levels.
  • Expect the motor to stall on purpose near the end. The load must tolerate that, and the range must be free of obstacles.
v, a = motor.auto_tune(0, 3600) # degrees: ten turns of travel
print(f"max {v:.0f} °/s, {a:.0f} °/s²")

In Tercio Control: Control → Setup → Find speed limits.

For many small moves in a row (a joystick, a trajectory from your code), skip the replies:

for angle in path:
motor.move_to(angle, ack=False) # opcode | 0x80: the S1 answers only if it refuses
time.sleep(0.005)

Set Stop if host is silent for (Settings → Protection) so a stalled program stops the axis: the S1 decelerates and raises Command timeout when no command arrives for that long while it is moving.

Each S1 streams telemetry at Telemetry rate (100 Hz by default, up to 1000 Hz): state, flags, faults, warnings, position, target, velocity, following error, temperature, supply, procedure step and control-loop load. New faults are sent at once as an event, without waiting for the next telemetry frame.

t = motor.telemetry
print(t.state.name, t.position, t.following_error, t.temperature_c)
bus.on_fault = lambda node, faults, warnings, state: print(node, faults)

The full layout is in CAN protocol → Telemetry.