# SANPO SPI 转 RS485 SANPO SPI 转 RS485 适合 Jetson、树莓派或其他主控板周期控制 RS485 电机和设备。主控板把设备原始指令放入固定长度的 `RT` 报文,并在当前或后续 SPI 传输中读取反馈。 出厂默认 RS485 参数为 4 Mbps、1 个停止位、无校验、8 个数据位。但是某些 Linux 串口枚举会改变当前运行中的参数,因此,建议每次启动 SPI 控制程序、发送业务数据前,通过管理串口 `SANPO Studio Management Port` 显式重设 RS485 参数;SPI 数据通道本身不接收文本 `AT+` 命令。 发送 `AT+` 命令时使用文本模式,并把发送结束符设置为 `CRLF`。不要把字符 `\r\n` 当作普通文本发送。 ```text AT+SETRS485=<波特率>,<停止位>,<校验>,<数据位> ``` 例如,设置为宇树电机常用的 4 Mbps、8-N-1: ```text AT+SETRS485=4000000,1,0,8 ``` ## 示例程序:发送数据前设置 RS485 波特率 和 SPI 模式 安装依赖: ```bash python3 -m pip install pyserial ``` 下面的示例代码会自动找到并通过两个管理口设置 RS485-1~RS485-4 的波特率。示例程序设置为宇树电机常用的 4 Mbps、8-N-1,并把两颗 STM32 MCU 都切换为适合 17 字节指令的 SHORT 模式。 ```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.") ``` 需要其他串口参数时,修改 `RS485_COMMAND` 中的四个数字;RS485 指令超过 18 字节时,把 `SPI_MODE` 改为 `LONG`,并让 SPI 程序改用 73 字节传输。代码中打开管理口使用的 `1_000_000` 只是 STM32 CDC 串口参数,不会设置 CAN 或 RS485 的物理波特率;物理总线参数只由明确的 `AT+SETRS485` 命令修改。 | 参数 | 支持值 | | --- | --- | | 波特率 | `2400`~`4000000`, `5000000`,`6000000`,`7500000` | | 停止位 | `1` 或 `2` | | 校验 | `0` 无校验;`1` 奇校验;`2` 偶校验 | | 数据位 | `8` 或 `9` | ## SPI 参数和长度 | 项目 | SHORT | LONG | | --- | ---: | ---: | | 总传输长度 | 23 字节 | 73 字节 | | 报文区 | 22 字节 | 72 字节 | | RS485 数据区 | 18 字节 | 68 字节 | | 最后一个字节 | CRC-8 | CRC-8 | 两种模式都使用 SPI Mode 0、MSB First。CRC-8 多项式为 `0x07`,初始值 `0x00`,无结果异或。 ## 选择 RS485 接口 | SPI 片选 | Channel | 发送目标 | | --- | --- | --- | | CS0(第一颗 STM32) | `0x01` | RS485-1 | | CS0(第一颗 STM32) | `0x02` | RS485-2 | | CS1(第二颗 STM32) | `0x03` | RS485-3 | | CS1(第二颗 STM32) | `0x04` | RS485-4 | | CS0或者CS1(任意片选) | `0x00` | 当前 MCU 的两路 RS485 | 发送时 `1/3` 都选择当前 STM32 的第一路,`2/4` 都选择第二路。回包只使用当前 STM32 Channel `1/2`。主控中的 CS0、CS1 通常分别连接开发板的两个外部片选。注意:主控和开发板需要共地,接入两个GND可保证数据传输稳定性。 ## SHORT 报文 发送和接收使用相同布局: | 字节位置 | 内容 | | --- | --- | | 0~1 | 固定 `52 54`,即 `RT` | | 2 | Channel | | 3 | RS485 数据长度 `0`~`18` | | 4~21 | 18 字节 RS485 数据区,未使用部分填 `00` | | 22 | 前 22 字节的 CRC-8 | 将 17 字节宇树控制帧发送到 Channel 2 时,CRC 前的报文区为: ```text 52 54 02 11 FE EE 11 00 00 00 00 00 00 00 00 00 00 00 00 18 7C 00 ``` 最后增加对这 22 字节计算得到的 CRC-8。 ## LONG 报文 | 字节位置 | 内容 | | --- | --- | | 0~1 | 固定 `52 54`,即 `RT` | | 2 | Channel | | 3 | RS485 数据长度 `0`~`68` | | 4~71 | 68 字节 RS485 数据区,未使用部分填 `00` | | 72 | 前 72 字节的 CRC-8 | CRC 算法与 [SANPO SPI 转 CAN](spi_can)相同。 ## 读取 RS485 反馈 开发板收到 RS485 数据后,会使用同样的 `RT` 布局放入 SPI 发送队列: - 字节 2 表示反馈来自当前 MCU 的第一路或第二路 RS485。 - 字节 3 表示本次反馈的数据长度。 - 数据从字节 4 开始。 - 未使用的数据区填 `00`。 SPI 是全双工接口。发送控制帧时读到的可能是旧数据或全 `00`;如果还没有目标反馈,应继续发送同长度的全零空帧读取。 一次 RS485 接收事件不保证正好对应一条完整的电机协议帧。主控应使用 Channel 区分总线,再按照电机协议的帧头、长度和校验重组数据。 ## 宇树 GO-M8010 示例 完整示例:[SPI 转 RS485 宇树 GM8010 使用说明](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/guides/spi-rs485-unitree.md) ```bash sudo python3 spi2rs485_unitree_GM8010_demo_v8.py \ --spibus 0 \ --cs 0 \ --motors 1 ``` 参数名称以示例程序当前帮助信息为准。首次运行时应降低运动幅度并预留立即断电的安全措施。 ## 常见问题 | 现象 | 处理方法 | | --- | --- | | 收到全 `00` | 当前没有排队反馈;稍后继续发送同长度空帧 | | 回包 Channel 为 1/2 | 回包使用当前 MCU 的本地编号,属于正常行为 | | 17 字节指令在 SHORT 可用,较长指令失败 | SHORT 最多 18 字节;切换 LONG | | CRC 错误 | 检查 Mode 0、MSB First、固定长度、SPI 时钟和片选时序 | | 设备没有回复 | 检查片选、Channel、RS485 参数、设备 ID、指令校验、A/B 接线和供电 | | 反馈被拆成多段 | 按设备协议在主控端进行重组,不要把一次 SPI 读取视为固定的一条电机回包 |