SANPO SPI to RS485
SANPO SPI to RS485 is suitable for periodic control of RS485 motors and devices
from a Jetson, Raspberry Pi, or another host controller. The host puts the raw
device command into a fixed-length RT frame and reads feedback in the current
or a later SPI transfer.
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, before starting an SPI control program and sending
application data, explicitly reapply the RS485 settings through the management
serial port SANPO Studio Management Port; the SPI data channel itself does not
accept text AT+ commands.
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 and the SPI Mode (Recommended)
Install the dependency:
python3 -m pip install pyserial
The complete code below automatically finds both management ports of one connected SPINE V8 and sends the same settings to both MCUs. It therefore configures RS485-1 through RS485-4 without requiring any management-port, SPI chip select, or board-interface mapping. This example configures the common Unitree motor setting of 4 Mbps, 8-N-1 and switches both MCUs to SHORT for the 17-byte command.
import re
import time
import serial
from serial.tools import list_ports
RS485_COMMAND = "AT+SETRS485=4000000,1,0,8"
SPI_MODE = "SHORT"
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 request(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 not text:
raise RuntimeError(f"{stream.port}: {command}: no reply")
print(f"{stream.port}: {command} -> {text}")
return text
def has_line(reply, expected):
return expected in {line.strip() for line in reply.splitlines()}
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)
if not has_line(request(stream, RS485_COMMAND), "OK"):
raise RuntimeError(f"{device}: failed to configure RS485 bitrate")
if not has_line(request(stream, "AT+SPIMODE?"), f"SPIMODE:{SPI_MODE}"):
if not has_line(request(stream, f"AT+SPIMODE={SPI_MODE}"), "OK"):
raise RuntimeError(f"{device}: failed to configure SPI mode")
print("RS485-1..RS485-4 and SPI SHORT configured successfully.")
To use other serial settings, change the four numbers in RS485_COMMAND. If an
RS485 command is longer than 18 bytes, change SPI_MODE to LONG and make the
SPI application use 73-byte transfers. The 1_000_000 used to open the
management port is only an STM32 CDC serial-port parameter and does not
configure the physical CAN or RS485 bitrate. Physical bus parameters change
only through the explicit AT+SETRS485 command.
Parameter |
Supported values |
|---|---|
Baud rate |
|
Stop bits |
|
Parity |
|
Data bits |
|
SPI Settings and Length
Item |
SHORT |
LONG |
|---|---|---|
Total transfer |
23 bytes |
73 bytes |
Payload |
22 bytes |
72 bytes |
RS485 data area |
18 bytes |
68 bytes |
Final byte |
CRC-8 |
CRC-8 |
Both modes use SPI Mode 0 and MSB First. CRC-8 uses polynomial 0x07, initial
value 0x00, and no final XOR.
Select an RS485 Interface
SPI chip select |
Channel |
Target |
|---|---|---|
CS0 (first STM32) |
|
RS485-1 |
CS0 (first STM32) |
|
RS485-2 |
CS1 (second STM32) |
|
RS485-3 |
CS1 (second STM32) |
|
RS485-4 |
CS0 or CS1 (either chip select) |
|
Both RS485 interfaces on the current MCU |
When sending, 1/3 select the first interface on the selected STM32 and 2/4
select the second. Replies use local Channel 1/2 on the current STM32.
CS0 and CS1 on the host normally connect to the board’s two external chip
selects. The host and board must share ground; two GND connections are
recommended for stable data transfer.
SHORT Message
Send and receive use the same layout:
Byte positions |
Content |
|---|---|
0-1 |
Fixed |
2 |
Channel |
3 |
RS485 data length |
4-21 |
18-byte RS485 data area; unused bytes are |
22 |
CRC-8 of the first 22 bytes |
When sending a 17-byte Unitree control frame to Channel 2, the payload before CRC is:
52 54 02 11 FE EE 11 00 00 00 00 00 00 00 00 00 00 00 00 18 7C 00
Append the CRC-8 calculated over those 22 bytes.
LONG Message
Byte positions |
Content |
|---|---|
0-1 |
Fixed |
2 |
Channel |
3 |
RS485 data length |
4-71 |
68-byte RS485 data area; unused bytes are |
72 |
CRC-8 of the first 72 bytes |
The CRC algorithm is the same as SANPO SPI to CAN.
Read RS485 Feedback
After receiving RS485 data, the board places it in the same RT layout in the
SPI transmit queue:
Byte 2 identifies the first or second RS485 interface on the current MCU.
Byte 3 is the length of this feedback block.
Data starts at byte 4.
Unused data-area bytes are
00.
SPI is full duplex. Data read while sending a control frame may be old data or
all 00; if the target has not replied, continue sending an empty frame of the
same length to poll for feedback.
One RS485 receive event is not guaranteed to correspond to one complete motor protocol frame. Use Channel to identify the bus, then reassemble data according to the motor protocol’s header, length, and checksum.
Unitree GO-M8010 Example
Example program: SPI to RS485 Unitree motor example
sudo python3 spi2rs485_unitree_GM8010_demo_v8.py \
--spibus 0 \
--cs 0 \
--motors 1
Use the current help output of the example as the authority for parameter names. On first use, reduce the motion amplitude and keep an immediate power disconnect available.
Troubleshooting
Symptom |
Action |
|---|---|
All bytes are |
No feedback is queued; continue polling with an empty frame of the same length |
Reply Channel is 1/2 |
Replies use the local numbering of the current MCU; this is normal |
A 17-byte command works in SHORT but a longer command fails |
SHORT supports at most 18 bytes; switch to LONG |
CRC error |
Check Mode 0, MSB First, fixed length, SPI clock, and chip-select timing |
Device does not reply |
Check chip select, Channel, RS485 parameters, device ID, command checksum, A/B wiring, and power |
Feedback is split into multiple blocks |
Reassemble it on the host according to the device protocol; do not treat one SPI read as one motor reply |