# SANPO SPI 转 CAN SANPO SPI 转 CAN 适合 Jetson、树莓派或其他主控板周期控制经典 CAN 设备。主控板通过固定长度 SPI 报文发送 CAN数据,并在当前或后续传输中读取设备反馈。 SANPO SPI 转 CAN FD 请阅读 [SANPO SPI 转 FDCAN](spi_fdcan)。 ## SPI 参数 | 项目 | 设置 | | --- | --- | | SPI 参数 | Mode 0(CPOL=0、CPHA=0) | | 数据顺序 | MSB First | | SHORT模式 | 22 字节报文区 + 1 字节 CRC,共 23 字节 | | LONG模式 | 72 字节报文区 + 1 字节 CRC,共 73 字节 | | CRC-8 | 多项式 `0x07`,初始值 `0x00`,无结果异或 | 出厂默认 SHORT。经典 CAN 可使用 SHORT 或者 LONG,但是 CAN FD 只能在 LONG 下使用。如果不使用 FDCAN,建议使用开销更小的 SHORT。 切换模式: 接入开发板USB,Windows 会出现 6 个 USB 串口,其中2个为管理口,对应两颗STM32 MCU,通常在Windows中显示为 `SANPO Studio Management Port`。分别连接2个管理口,发送如下串口指令,进行模式设置: ```text AT+SPIMODE=SHORT AT+SPIMODE=LONG ``` 命令通过管理口按文本发送并以 `CRLF` 结尾。设置成功后会保存;主控应等待至少 10 ms,再使用新长度通信。 ## 示例程序:发送数据前设置 CAN 波特率 和 SPI 模式 安装依赖: ```bash python3 -m pip install pyserial ``` 下面的示例代码会自动找到2个管理口,并向两颗 MCU 发送 CAN-1~CAN-4 波特率配置。示例把经典 CAN 设置为 1 Mbps,并把两颗 MCU 都切换为 SHORT。 ```python import re import time import serial from serial.tools import list_ports FDCAN_VALUE = "FDCAN:1000000,5000000" FDCAN_COMMAND = "AT+SETFDCAN=1000000,5000000" 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, "AT+SETFDCAN?"), FDCAN_VALUE): if not has_line(request(stream, FDCAN_COMMAND), "OK"): raise RuntimeError(f"{device}: failed to configure CAN 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("CAN-1..CAN-4 and SPI SHORT configured successfully.") ``` 需要其他经典 CAN 波特率时,同时修改 `FDCAN_VALUE` 和 `FDCAN_COMMAND` 中 的第一个数字;如果 SPI 程序按 73 字节通信,把 `SPI_MODE` 改为 `LONG`。 5 Mbps 数据段参数不影响经典 CAN。代码中打开管理口使用的 `1_000_000` 只是 STM32 CDC 串口参数,不会设置 CAN 或 RS485 的物理波特率;物理总线参数只由明确的 `AT+SETFDCAN` 命令修改。 | 参数 | 支持值 | | --- | --- | | CAN波特率 | `1000000`、`500000`、`250000`、`125000`、`100000` | ## 选择 CAN 接口 | SPI 片选 | Channel | 发送目标 | | --- | --- | --- | | CS0(第一颗STM32) | `0x01` | CAN-1 | | CS0(第一颗STM32) | `0x02` | CAN-2 | | CS1(第二颗STM32) | `0x03` | CAN-3 | | CS1(第二颗STM32) | `0x04` | CAN-4 | | CS0或者CS1(任意片选) | `0x00` | 当前 STM32 的两路 CAN | 发送时 `1/3` 都选择当前 STM32 的第一路,`2/4` 都选择第二路。回包只使用本地 Channel `1/2`。主控中的 CS0、CS1 通常分别连接开发板的两个外部片选。注意:主控和开发板需要共地,接入两个GND可保证数据传输稳定性。 ## SHORT 模式 ### 扩展帧 | 字节位置 | 内容 | | --- | --- | | 0~1 | 固定 `45 54`,即 `ET` | | 2 | Channel | | 3~6 | 29 位 CAN ID,高字节在前 | | 7 | 数据长度 `0`~`8` | | 8~15 | 8 字节数据区,未使用部分填 `00` | | 16~21 | 填 `00` | | 22 | 前 22 字节的 CRC-8 | ### 标准帧 | 字节位置 | 内容 | | --- | --- | | 0~1 | 固定 `53 54`,即 `ST` | | 2 | Channel | | 3~4 | 固定 `00 00` | | 5~6 | 11 位 CAN ID,高字节在前 | | 7 | 数据长度 `0`~`8` | | 8~15 | 8 字节数据区,未使用部分填 `00` | | 16~21 | 填 `00` | | 22 | 前 22 字节的 CRC-8 | 例如,向 Channel 1、扩展帧 ID `0x0000FD01` 发送 8 字节数据时,CRC 前的 22 字节为: ```text 45 54 01 00 00 FD 01 08 01 02 03 04 05 06 07 08 00 00 00 00 00 00 ``` ## LONG 模式 LONG 的前 8 字节与 SHORT 相同,字节 8~71 为固定 64 字节数据区,最后的字节 72 为 CRC-8。经典 CAN 只使用数据区前 0~8 字节,其余填 `00`。 | 类型 | 字节 0~7 | 字节 8~71 | 字节 72 | | --- | --- | --- | --- | | 扩展帧 | `ET + Channel + ExtID(4B) + Len` | 64 字节数据区 | CRC-8 | | 标准帧 | `ST + Channel + 00 00 + StdID(2B) + Len` | 64 字节数据区 | CRC-8 | ## 计算 CRC ```python def crc8(data: bytes) -> int: crc = 0 for value in data: crc ^= value for _ in range(8): crc = ((crc << 1) ^ 0x07) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF return crc ``` SHORT 对前 22 字节计算,LONG 对前 72 字节计算,然后把结果放在最后一个字节。 ## 读取 CAN 反馈 SPI 是全双工接口。发送控制报文时同时读到的内容可能是之前缓存的反馈,也可能全为 `00`。发送后如果没有立即读到目标反馈,应继续发送同长度的全 `00` 空帧进行轮询。 有效反馈满足: - 总长度与当前 SHORT/LONG 模式一致。 - 最后一个字节 CRC 正确。 - 报文区以 `45 54` 或 `53 54` 开头。 - Channel 为本地 `01` 或 `02`。 - 字节 7 给出有效 CAN 数据长度。 一次读取不是一次请求的固定应答;主控应根据 CAN ID 和电机协议识别反馈。 ## 开发示例 - [SPI 转 CAN 小米 CyberGear 使用说明](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/guides/spi-can-cybergear.md) - [SPI 经典 CAN 标准帧使用说明](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/guides/spi-standard-can.md) ## 常见问题 | 现象 | 处理方法 | | --- | --- | | 收到全 `00` | 当前没有排队的反馈;稍后继续发送同长度空帧读取 | | CRC 错误 | 检查 Mode 0、MSB First、固定传输长度、SPI 时钟和片选时序 | | CAN 设备没有回复 | 检查片选、Channel、CAN ID、波特率、终端电阻、接线和供电 | | 报文发到两路 CAN | Channel 为 `00`;改为具体通道 | | 切换 LONG 后完全无响应 | 主控仍在发送 23 字节;改为每次完整发送 73 字节 |