# 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. ```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 and the SPI Mode (Recommended) Install the dependency: ```bash 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. ```python 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 | `2400` to `4000000`, `5000000`, `6000000`, `7500000` | | Stop bits | `1` or `2` | | Parity | `0` none; `1` odd; `2` even | | Data bits | `8` or `9` | ## 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) | `0x01` | RS485-1 | | CS0 (first STM32) | `0x02` | RS485-2 | | CS1 (second STM32) | `0x03` | RS485-3 | | CS1 (second STM32) | `0x04` | RS485-4 | | CS0 or CS1 (either chip select) | `0x00` | 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 `52 54`, or `RT` | | 2 | Channel | | 3 | RS485 data length `0`-`18` | | 4-21 | 18-byte RS485 data area; unused bytes are `00` | | 22 | CRC-8 of the first 22 bytes | When sending a 17-byte Unitree control frame to Channel 2, the payload before CRC is: ```text 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 `52 54`, or `RT` | | 2 | Channel | | 3 | RS485 data length `0`-`68` | | 4-71 | 68-byte RS485 data area; unused bytes are `00` | | 72 | CRC-8 of the first 72 bytes | The CRC algorithm is the same as [SANPO SPI to CAN](spi_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](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/rs485/spi2rs485_unitree_GM8010_demo_v8.py) ```bash 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 `00` | 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 |