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 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 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 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.
AT+SETRS485=<baudrate>,<stop_bits>,<parity>,<data_bits>
For example, configure the common Unitree motor setting of 4 Mbps, 8-N-1:
AT+SETRS485=4000000,1,0,8
Automatically Configure All RS485 Bitrates (Recommended)
Install the dependency:
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.
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 |
|
Stop bits |
|
Parity |
|
Data bits |
|
Frame Format
Send and receive use the same outer format:
Field |
Length |
Description |
|---|---|---|
Header |
2 bytes |
Fixed |
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 |
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:
FE EE 11 00 00 00 00 00 00 00 00 00 00 00 00 18 7C
Send these 17 bytes to Channel 2:
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:
52 54 02 10 FD EE ...16 bytes of motor feedback... 0D 0A
Complete example: USB to RS485 Unitree motor example
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:
python usb2rs485_unitree_demo_v8.py --port COM9 --motors 1 --channel 2
Linux example:
sudo python3 usb2rs485_unitree_demo_v8.py --port /dev/ttyACM0 --motors 1 --channel 2
Troubleshooting
Symptom |
Action |
|---|---|
The command is sent to both RS485 interfaces |
Channel is |
No reply after sending |
Check USB port, Channel, RS485 settings, device ID, command checksum, A/B wiring, and power |
|
The saved value may be correct while a passthrough port changed the live UART setting. Send |
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 |