# SANPO SPI to CAN SANPO SPI to CAN is suitable for periodic control of classic CAN devices from a Jetson, Raspberry Pi, or another host controller. The host sends CAN data in a fixed-length SPI frame and reads device feedback in the current or a later transfer. For SANPO SPI to CAN FD, see [SANPO SPI to FDCAN](spi_fdcan). ## SPI Settings | Item | Setting | | --- | --- | | SPI mode | Mode 0 (CPOL=0, CPHA=0) | | Bit order | MSB First | | SHORT | 22-byte payload + 1-byte CRC, 23 bytes total | | LONG | 72-byte payload + 1-byte CRC, 73 bytes total | | CRC-8 | Polynomial `0x07`, initial value `0x00`, no final XOR | The factory default is SHORT. Classic CAN can use SHORT or LONG, but CAN FD can only use LONG. If CAN FD is not needed, SHORT has lower overhead. To switch mode, connect USB to the board. Windows shows six USB serial ports, two of which are management ports for the two STM32 MCUs and are normally displayed as `SANPO Studio Management Port`. Connect to each management port and send: ```text AT+SPIMODE=SHORT AT+SPIMODE=LONG ``` Send the command as text with a `CRLF` line ending. The setting is saved after a successful response; the host must wait at least 10 ms before using the new frame length. ## Automatically Configure All CAN 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 CAN-1 through CAN-4 without requiring any management-port, SPI chip select, or board-interface mapping. This example configures classic CAN to 1 Mbps and switches both MCUs to 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.") ``` To use another classic CAN bitrate, change the first number in both `FDCAN_VALUE` and `FDCAN_COMMAND`. If the SPI application uses 73-byte transfers, change `SPI_MODE` to `LONG`. The 5 Mbps data-phase argument does not affect classic CAN. 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+SETFDCAN` command. | Parameter | Supported values | | --- | --- | | CAN bitrate | `1000000`, `500000`, `250000`, `125000`, `100000` | ## Select a CAN Interface | SPI chip select | Channel | Target | | --- | --- | --- | | CS0 (first STM32) | `0x01` | CAN-1 | | CS0 (first STM32) | `0x02` | CAN-2 | | CS1 (second STM32) | `0x03` | CAN-3 | | CS1 (second STM32) | `0x04` | CAN-4 | | CS0 or CS1 (either chip select) | `0x00` | Both CAN interfaces on the current STM32 | 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 ### Extended Frame | Byte positions | Content | | --- | --- | | 0-1 | Fixed `45 54`, or `ET` | | 2 | Channel | | 3-6 | 29-bit CAN ID, big-endian | | 7 | Data length `0` to `8` | | 8-15 | 8-byte data area; unused bytes are `00` | | 16-21 | `00` padding | | 22 | CRC-8 of the first 22 bytes | For example, the first 22 bytes when sending 8 bytes to Channel 1 with extended ID `0x0000FD01` are: ```text 45 54 01 00 00 FD 01 08 01 02 03 04 05 06 07 08 00 00 00 00 00 00 ``` ### Standard Frame | Byte positions | Content | | --- | --- | | 0-1 | Fixed `53 54`, or `ST` | | 2 | Channel | | 3-4 | Fixed `00 00` | | 5-6 | 11-bit CAN ID, big-endian | | 7 | Data length `0` to `8` | | 8-15 | 8-byte data area; unused bytes are `00` | | 16-21 | `00` padding | | 22 | CRC-8 of the first 22 bytes | ## LONG Message The first 8 bytes of LONG are the same as SHORT. Bytes 8-71 are a fixed 64-byte data area, and byte 72 is CRC-8. Classic CAN uses only bytes 0-8 of the data area; fill the rest with `00`. | Type | Bytes 0-7 | Bytes 8-71 | Byte 72 | | --- | --- | --- | --- | | Extended frame | `ET + Channel + ExtID(4B) + Len` | 64-byte data area | CRC-8 | | Standard frame | `ST + Channel + 00 00 + StdID(2B) + Len` | 64-byte data area | CRC-8 | ## Calculate 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 ``` Calculate CRC over the first 22 bytes in SHORT or the first 72 bytes in LONG, then put the result in the final byte. ## Read CAN Feedback SPI is full duplex. Data read while sending a control frame may be previously buffered feedback or all `00`. If the target feedback is not returned immediately, continue polling by sending an empty frame of the same length. Valid feedback must satisfy all of the following: - Total length matches the current SHORT/LONG mode. - The final byte contains a valid CRC. - The payload begins with `45 54` or `53 54`. - Channel is local `01` or `02`. - Byte 7 contains a valid CAN data length. A single read is not a fixed response to a single request. The host should use the CAN ID and motor protocol to identify feedback. ## Examples - [SPI to CAN CyberGear example](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/can/spi2can_cybergear_demo_v8.py) ## Troubleshooting | Symptom | Action | | --- | --- | | All bytes are `00` | No feedback is queued; continue polling with an empty frame of the same length | | CRC error | Check Mode 0, MSB First, fixed transfer length, SPI clock, and chip-select timing | | CAN device does not reply | Check chip select, Channel, CAN ID, bit rate, termination resistors, wiring, and power | | Frames are sent to both CAN interfaces | Channel is `00`; select a specific channel | | No response after switching to LONG | The host is still sending 23-byte frames; send 73 bytes for every transfer |