# SANPO USB to RS485 The SANPO USB to RS485 protocol controls RS485 devices from Windows or Linux and forwards RS485 data without requiring an additional driver. The raw RS485 data is wrapped with a SANPO header and trailer over the USB management serial port. These extra bytes are used only by the board; the RS485 device receives the raw data without a SANPO header or trailer. ## Connection and Communication Settings After connecting the board over USB, six serial ports appear: two management ports and four dedicated RS485 passthrough ports. In this protocol mode, send RS485 data through the two management ports, not the four dedicated passthrough ports; see [RS485 Passthrough Mode](rs485_passthrough) for the latter. One management port accesses RS485-1/2 and the other accesses RS485-3/4. On first use, connect only one device to confirm the USB-port-to-physical-interface mapping. On Linux, you can run the Python [serial-port identification example](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/rs485/identify_sanpo_serial_ports_v8.py) to identify the four RS485 serial ports. The factory RS485 settings are 4 Mbps, one stop bit, no parity, and eight data bits. However, some Linux serial enumeration may change the live communication settings. Therefore, in protocol mode, explicitly send `AT+SETRS485` before sending application data to reapply the RS485 settings. See [Communication Settings](baudrate_config) for details. Send `AT+` commands in text mode with the line ending set to `CRLF`. Do not send the characters `\r\n` as ordinary text. ```text AT+SETRS485=,,, ``` For example, configure the common Unitree motor setting of 4 Mbps, 8-N-1: ```text AT+SETRS485=4000000,1,0,8 ``` ## Automatically Configure All RS485 Bitrates (Recommended) Install the dependency: ```bash python -m pip install pyserial ``` The complete code below automatically finds both management ports of one connected SPINE V8 on Windows or Linux. It sends the same setting to both MCUs, thereby configuring RS485-1 through RS485-4 without requiring any management-port or board-interface mapping. This example configures the common Unitree motor setting of 4 Mbps, 8-N-1. ```python import re import time import serial from serial.tools import list_ports COMMANDS = ["AT+SETRS485=4000000,1,0,8"] def is_management_port(port): if (port.vid, port.pid) != (0x1209, 0x2323): return False info = " ".join(str(getattr(port, name, "") or "") for name in ("hwid", "interface", "location")).upper() return ("MI_01" in info or "MANAGEMENT PORT" in info or re.search(r":(?:X|\d+)\.1(?:\D|$)", info) is not None) def send_at(stream, command): stream.reset_input_buffer() stream.write(command.encode("ascii") + b"\r\n") stream.flush() deadline, reply = time.monotonic() + 1.0, bytearray() while time.monotonic() < deadline: reply.extend(stream.read(stream.in_waiting or 1)) if b"OK\r\n" in reply or b"ERR," in reply: break text = reply.decode("ascii", errors="replace").strip() if "OK" not in text: raise RuntimeError(f"{stream.port}: {command}: {text or 'no reply'}") print(f"{stream.port}: {command} -> {text}") ports = sorted(p.device for p in list_ports.comports() if is_management_port(p)) if len(ports) != 2: raise SystemExit(f"Expected 2 SANPO management ports, found {len(ports)}: {ports}") for device in ports: with serial.Serial(device, 1_000_000, timeout=0.05, write_timeout=1) as stream: time.sleep(0.05) for command in COMMANDS: send_at(stream, command) print("RS485-1..RS485-4 configured successfully.") ``` To use other serial settings, change the four numbers in `COMMANDS`. The `1_000_000` used to open the management port is only a CDC API parameter; it does not set the physical CAN or RS485 bitrate. Only the explicit `AT+SETRS485` command changes the physical RS485 settings. | Parameter | Supported values | | --- | --- | | Baud rate | `2400` to `4000000`, `5000000`, `6000000`, `7500000` | | Stop bits | `1` or `2` | | Parity | `0` none; `1` odd; `2` even | | Data bits | `8` or `9` | ## Frame Format Send and receive use the same outer format: | Field | Length | Description | | --- | ---: | --- | | Header | 2 bytes | Fixed `52 54`, or `RT` | | Channel | 1 byte | RS485 interface number | | Data length | 1 byte | Raw RS485 data length, up to 64 bytes | | Data | 0-64 bytes | Raw command or reply defined by the device vendor | | Trailer | 2 bytes | Fixed `0D 0A` | When sending to the RS485 bus, the board removes `RT`, Channel, length, and the trailer and sends only the raw data. On a device reply, it adds the `RT` wrapper and Channel identifies the first or second local interface. ## Unitree GO-M8010 Example Raw Unitree command: ```text FE EE 11 00 00 00 00 00 00 00 00 00 00 00 00 18 7C ``` Send these 17 bytes to Channel 2: ```text 52 54 02 11 FE EE 11 00 00 00 00 00 00 00 00 00 00 00 00 18 7C 0D 0A ``` `02` is the target interface and `11` is decimal 17 in hexadecimal. The RS485 bus receives only the 17-byte Unitree command beginning with `FE EE`. A motor reply may look like: ```text 52 54 02 10 FD EE ...16 bytes of motor feedback... 0D 0A ``` Complete example: [USB to RS485 Unitree motor example](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/rs485/usb2rs485_unitree_demo_v8.py) Every time it opens the management port, the example explicitly sends `AT+SETRS485=4000000,1,0,8` before sending motor frames. This restores the protocol-mode UART to the 4 Mbps, 8-N-1 setting required by the Unitree GM8010 even if a dedicated RS485 passthrough port was previously opened with another baud rate. The identical setting is reapplied to both RS485 interfaces on the current MCU without writing another Flash record. Windows example: ```powershell python usb2rs485_unitree_demo_v8.py --port COM9 --motors 1 --channel 2 ``` Linux example: ```bash sudo python3 usb2rs485_unitree_demo_v8.py --port /dev/ttyACM0 --motors 1 --channel 2 ``` ## Reply Handling One RS485 receive event represents one continuous block of received data and is not guaranteed to be one complete motor-protocol frame. Channel identifies the interface, but the host must reassemble complete replies using the motor protocol's header, length, and checksum. USB is also a byte stream. A single read may contain a partial frame or multiple frames. Find `52 54`, read the length field, and wait for the complete `6 + data_length` bytes. ## Troubleshooting | Symptom | Action | | --- | --- | | The command is sent to both RS485 interfaces | Channel is `0x00`; select a specific channel | | No reply after sending | Check USB port, Channel, RS485 settings, device ID, command checksum, A/B wiring, and power | | `AT+SETRS485?` reports 4 Mbps but protocol mode still receives no reply | The saved value may be correct while a passthrough port changed the live UART setting. Send `AT+SETRS485=4000000,1,0,8` again; the GM8010 example on this page does this automatically | | Reply Channel is 1/2 instead of 3/4 | Replies use local numbering on the current MCU; this is normal | | Reply is split into multiple blocks | Reassemble the outer frame using its `RT` length field, then parse the device-protocol Data |