30 min

Servo IDs and the bus

Six motors share one wire, the ID is the primary key on that wire, and every motor ships with the same one.

Where you are. You have printed parts that fit and an arm you know not to finish yet. This lesson is the gate you stopped at: giving each motor a name, on a wire that has no idea how many devices are attached to it.

Two voices, one wire

Wire up two brand new motors, power the board, and ask a question: motor 1, where are you?

Both answer. They answer at the same instant, on the same wire, at a million bits per second, driving it in opposite directions wherever their bits differ. What comes back is not two replies. It is one damaged reply, or nothing at all, and the software’s best description of the event is that it waited and gave up.

Every STS3215 leaves the factory with the same identity. Twelve motors, twelve copies of the number 1.

Nothing here is broken. The bus is behaving exactly as designed. You just have not yet done the one thing the design assumes you did.

The idea in one paragraph

The six motors in an arm are not six devices with six cables. They are six devices on one shared wire, chained motor to motor from the control board out to the gripper. That wire is half duplex: the same conductor carries commands out and answers back, so exactly one device may talk at a time and the rule is enforced by convention rather than by hardware. Every transaction is therefore a question addressed to a number, and an answer from whoever owns that number. Nothing assigns the numbers for you. You assign them by hand, one motor at a time, physically alone on the bus, and the number is written into the motor’s own non-volatile memory so it survives power-off, disassembly and every reinstall of the software. Once that is done, the arm is addressable, and everything afterwards - calibration, teleoperation, recording, deployment - is reads and writes to numbered registers on numbered devices.

half duplex: one conductor, one talker at a time Board USB to the host 1 shoulder_pan 2 shoulder_lift 3 elbow_flex 4 wrist_flex 5 wrist_roll 6 gripper 1,000,000 baud - protocol 0 - 4096 encoder counts per turn reports 0-100
Six motors chained on one wire from the control board, IDs one through six mapped to joint names from shoulder to gripper

Wider than the screen; scroll it sideways.

What the wire actually carries

The defaults matter because you will see them in error messages long before you see them in documentation.

PropertyValue
Linkhalf-duplex TTL serial, three-pin connectors, daisy chained
Baud rate1,000,000 by default
Protocolversion 0, which supports a broadcast ping
Read timeout1000 ms
Encoder resolution4096 counts per turn
Scanned baud rates4800 through 1,000,000, in the usual eleven steps

Two of these are worth a second look. The broadcast ping is what lets a tool ask “everyone, identify yourselves” in one transaction, which is how discovery works and also exactly what a duplicate ID ruins. And 4096 counts per turn is the 12-bit absolute magnetic encoder Module 0 described, reporting roughly 0.088 degrees per count, absolute from the moment it powers up, which is why this arm never needs to home itself against a switch.

The names are not yours to choose

LeRobot hardcodes the mapping from joint name to ID. This is the table the rest of the module runs on.

JointIDReported as
shoulder_pan1degrees
shoulder_lift2degrees
elbow_flex3degrees
wrist_flex4degrees
wrist_roll5degrees
gripper60 to 100

The gripper is the odd one. Every other joint reports an angle; the gripper reports a percentage of open, always, whatever units the rest of the arm is configured to use. That is a deliberate interface choice and it will show up again in every dataset you record.

One at a time, gripper first

what happens when two motors share an ID Board asks: who is 1? ID 1 factory default ID 1 factory default Timeout one damaged reply, or none at all the procedure that prevents it One motor, alone nothing else on the bus Write ID and baud into EEPROM, once, forever Repeat six times gripper first, base last
Two motors sharing an ID answer the same question at once and the transaction fails; the procedure connects one motor alone, writes its ID to non-volatile memory, and repeats

Wider than the screen; scroll it sideways.

The procedure is dictated by the collision. With one motor on the bus there is no ambiguity, so the tool can address the factory default, write the real ID and baud rate into the motor, and move on. Connect two and there is no safe question to ask.

lerobot-setup-motors --robot.type=so101_follower --robot.port=<FOLLOWER_PORT>
lerobot-setup-motors --teleop.type=so101_leader  --teleop.port=<LEADER_PORT>

The tool prompts you through the six motors in reverse order: gripper first as ID 6, then wrist roll, wrist flex, elbow, shoulder lift, and shoulder pan last as ID 1. That order is fixed in the code - it literally iterates reversed(motors) - rather than being a suggestion. The reason is the one the previous lesson guessed at, and the assembly guide states it while walking you through the steps: when you move on from a motor you unplug the cable at the board end and leave it attached to the motor, “as it will already be in the right place”. The chain assembles itself behind you as you go.

What lives inside the motor

Module 0 opened a servo and found four things in one housing: a motor, a gearbox, an encoder, and a small controller you did not write, closing a position loop far faster than your code ever will. Here is that controller’s register map, or the part of it worth knowing.

inside one motor EEPROM survives power-off ID, Baud_Rate Min / Max_Position_Limit Homing_Offset, Operating_Mode Max_Torque_Limit, Protection_Current Max_Temperature_Limit, Overload_Torque P, I and D_Coefficient written at setup and calibration SRAM gone at power-off Torque_Enable Goal_Position Present_Position Present_Load, Present_Current Present_Temperature written and read every control cycle
Inside one servo: EEPROM holds identity, limits and gains across power cycles, while SRAM holds the live command and measurements that vanish at power-off

Wider than the screen; scroll it sideways.

RegisterAddressWhereWhat it is
ID5EEPROMthe address on the bus
Baud_Rate6EEPROM1,000,000 by default
Min/Max_Position_Limit9 / 11EEPROMtravel limits
Max_Temperature_Limit13EEPROMthermal cutout
Max_Torque_Limit16EEPROMhow hard it may ever push
P / D / I_Coefficient21 / 22 / 23EEPROMgains of the loop inside the box
Protection_Current28EEPROMcurrent cap
Homing_Offset31EEPROMwhere zero sits
Operating_Mode33EEPROMposition mode, for this arm
Overload_Torque36EEPROMwhat it drops to once overloaded
Torque_Enable40SRAMpowered or limp
Goal_Position42SRAMthe command
Present_Position56SRAMthe measurement
Present_Load60SRAMhow hard it is working
Present_Temperature63SRAMdegrees, readable live
Present_Current69SRAMcurrent draw

The split is the interesting part. EEPROM is persisted configuration: identity, limits, gains, the offset that defines zero. Written rarely, survives power loss, and is exactly the kind of state you forget exists until it disagrees with a file on your laptop. SRAM is the live process: the current command, the current measurement, gone the moment power drops.

Present_Temperature being readable is a small gift. A motor you can ask about its own temperature is a motor you can watch during a long teleoperation session, and watching it is much more informative than touching it afterwards.

Read the LEDs before you read the logs

Each motor has an LED. Powered up and healthy, they are steady red, all the way down the chain. That gives you a three-way triage that costs nothing and resolves most first-day failures.

What you seeWhat it means
Steady red all the way from gripper to basewiring is fine, look at software
One or more dark, or the chain stops part waywiring: reseat the three-pin cables, check the board’s supply, make sure each connector is fully clicked in
Blinkingthe motor is in an error state, usually overload or the wrong supply voltage

The LeRobot guide puts it in one sentence that deserves to be on a sticky note: most “timeout” errors are physical, not code.

There is one more source of confusing errors that is neither of those. LeRobot configures two read retries on these buses, and the source comment explains why: Feetech buses occasionally return a corrupted status packet, reported as Incorrect status packet!, especially when several joints move at once. A single such message is normal noise on a shared wire. A stream of them is a real problem.

The two ways to actually destroy one

Almost everything on this arm is recoverable. Two things are not.

Without hardware

  • Instead of the bus, MuJoCo addresses joints by name out of the MJCF. Look one up with mj_name2id and give it a name that does not exist.
  • Measure this: the difference between the two namespaces. A model file raises on a name collision or a missing name. A serial bus with two motors at id 1 raises nothing at all and answers with whichever replied first.
  • That is the whole lesson in one sentence, and it generalises: the failure modes that cost you an afternoon are the ones with no error path. Sim gives you an error path for free, which is exactly why it cannot teach you this one.

Check yourself

1. Every motor ships with ID set to 1. Why is that a sensible default rather than a manufacturing oversight?

Because the manufacturer cannot know your topology, and the ID is configuration rather than identity. One motor on a bus works out of the box with the default, which is the common case for anyone buying a single servo. Any multi-device bus requires an integration step where somebody decides who is who, and the only place that decision can be made is where the devices are assembled. The default is chosen so a single device is usable, not so twelve are.

2. Two motors share an ID. Why does the software report a timeout instead of “duplicate ID detected”?

Because nothing on the wire can detect it. The bus is half duplex with one conductor and no arbitration: when both motors answer the same question, both drive the line at once and the resulting waveform is neither reply. What arrives is a malformed packet or nothing, and the only observation the driver can make is that a well-formed answer did not appear before the timeout. A duplicate key is invisible to a store with no uniqueness constraint; you find it downstream, as corruption.

3. You set all six IDs, then unplug everything and rebuild the arm a week later. What survives, and what does not?

The IDs survive, along with baud rate, position limits, gains and the homing offset, because those are EEPROM: non-volatile configuration inside each motor. What does not survive is everything in SRAM, which is the live process state: Torque_Enable, the current Goal_Position, and the present measurements. Practically, the arm comes back addressable and configured, and comes back limp and with no commanded target, which is exactly the behaviour you want after a power cut.

4. LeRobot writes P = 16, I = 0, D = 32 into every motor on connect. Which loop are these the gains of, and why can your Python code not see its output?

The position loop inside the servo, the one Module 0 described as the controller you did not write. It compares the encoder reading against Goal_Position and drives current into the motor until the difference disappears, running far faster than your control loop. Your code sits outside the box: it writes an angle and reads an angle, and the current that got the arm there never crosses the boundary. Setting these gains is the only influence you have over how that inner loop behaves.

5. Failed to sync read 'Present_Position' on ids=[1,2,3,4,5,6]. What do you check, in order?

LEDs first, because most timeouts are physical. Dark motors or a chain that stops part way means cabling: reseat the three-pin connectors and check the board’s power lead, which is exactly the one that works loose. Blinking means an error state, usually overload or wrong supply voltage. Only with all six steady red do you move to software: the right serial port, the jumpers on the B channel if the board is a Waveshare, and duplicate or missing IDs from an interrupted setup. Note that an occasional Incorrect status packet! is normal noise on this bus and is what the two configured read retries exist to absorb.

6. Why does the gripper ship with its torque and current permanently capped at half?

Because it is the motor that burns out. A gripper closes on objects, and closing on something it cannot compress is a stall: full current, zero speed, all of the electrical power turning into heat in the windings, which is the failure mode Module 0 described. The other five joints stall occasionally; the gripper stalls as part of doing its job. The library caps it rather than trusting the operator, which tells you how routinely operators cook them.

Do this

Hardware procedure. Have both arms’ motors to hand, unconfigured, and nothing installed in the printed parts yet.

1. Install LeRobot and check what you got.

conda create -y -n lerobot python=3.12 && conda activate lerobot
conda install ffmpeg -c conda-forge
pip install 'lerobot[feetech,core_scripts,training]'

2. Find the ports, one arm at a time.

lerobot-find-port

It asks you to unplug a device so it can spot which port disappeared. Run it once per control board. Expect /dev/tty.usbmodem... on macOS and /dev/ttyACM0 on Linux, and on Linux expect to need sudo chmod 666 /dev/ttyACM0 before anything can open it.

3. Capture the state before you change anything.

cd module-04-hardware/code
python bus_preflight.py --follower-port /dev/ttyACM0 --leader-port /dev/ttyACM1

It lists the serial ports it can see, prints the ID plan, and prints the exact setup commands for your ports so you are pasting rather than typing. It does not touch the motors. It has not been run against a physical arm by the author, so read it before you trust it.

4. Set the follower’s IDs. Run the follower command from step 3, and follow the prompts with one motor connected at a time, gripper first. Check the cabling before every press of Enter. Tick each motor off on the build sheet as it is written.

5. Set the leader’s IDs the same way, with the leader command.

6. Acceptance test. Chain all six motors of one arm, power the board, and look at the LEDs: steady red the whole way from gripper to base. That is the state the next lesson assumes.

7. Now go back and build. With every motor configured, the assembly gate from the previous lesson is open.

What you can now do

You can explain why six motors on one wire need hand-assigned addresses, why the assignment happens one motor at a time in reverse order, and why a duplicate ID surfaces as a timeout rather than an error. You can read the register map of the controller Module 0 left as a black box, say which registers survive a power cut and which do not, and point at the exact numbers that admit the gripper is the motor most likely to die. And you can triage a silent bus from three LED states before opening a log file.

What you can now do

You can assign motor IDs in the right order, read the register map of the controller inside a servo, and triage a dead bus from the LEDs before you open a log file.