# SANPO SPI to FDCAN SANPO SPI to FDCAN is suitable for periodic control of FDCAN devices from a Jetson, Raspberry Pi, or another host controller. CAN FD is available only in SPI LONG mode, with a fixed 73-byte transfer. For SANPO SPI to classic CAN, see [SANPO SPI to CAN](spi_can). ## Switch to LONG Before Use The factory default SPI mode is SHORT, which cannot transmit or receive CAN FD. First switch the mode through a management port. After connecting USB, Windows shows six USB serial ports; two 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=LONG ``` Use `CRLF` as the line ending. After receiving `OK`, wait at least 10 ms before sending 73-byte SPI frames. The setting is saved to Flash and remains LONG after power-up. Also query the CAN FD bit rates: ```text AT+SETFDCAN? ``` The factory defaults are a 1 Mbps arbitration phase and 5 Mbps data phase, with BRS enabled. The data phase can be set to 2, 3, 4, or 5 Mbps; see [Communication Settings](baudrate_config). ## Automatically Configure All CAN FD Bitrates and SPI LONG (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 a 1 Mbps arbitration phase, a 5 Mbps data phase, and ensures that both MCUs use the LONG mode required by CAN FD. ```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 = "LONG" 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 FD 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/FDCAN-1..CAN/FDCAN-4 and SPI LONG configured successfully.") ``` To use other CAN FD bitrates, change the arbitration and data-phase numbers in both `FDCAN_VALUE` and `FDCAN_COMMAND`. The data phase supports 2, 3, 4, and 5 Mbps. 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 | | --- | --- | | Arbitration phase | `1000000`, `500000`, `250000`, `125000`, `100000` | | Data phase | `2000000`, `3000000`, `4000000`, `5000000` | ## SPI Settings | Item | Setting | | --- | --- | | SPI mode | Mode 0 (CPOL=0, CPHA=0) | | Bit order | MSB First | | Transfer | 72-byte payload + 1-byte CRC, 73 bytes total | | CRC-8 | Polynomial `0x07`, initial value `0x00`, no final XOR | ## Select a CAN FD 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 FD 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. ## Data Length Code | Code | Bytes | Code | Bytes | | ---: | ---: | ---: | ---: | | `00` | 0 | `08` | 8 | | `01` | 1 | `09` | 12 | | `02` | 2 | `0A` | 16 | | `03` | 3 | `0B` | 20 | | `04` | 4 | `0C` | 24 | | `05` | 5 | `0D` | 32 | | `06` | 6 | `0E` | 48 | | `07` | 7 | `0F` | 64 | ## 73-Byte Message ### Extended Frame | Byte positions | Content | | --- | --- | | 0-1 | Fixed `45 46`, or `EF` | | 2 | Channel | | 3-6 | 29-bit CAN ID, big-endian | | 7 | Length code `00`-`0F` | | 8-71 | Fixed 64-byte data area; unused bytes are `00` | | 72 | CRC-8 of the first 72 bytes | ### Standard Frame | Byte positions | Content | | --- | --- | | 0-1 | Fixed `53 46`, or `SF` | | 2 | Channel | | 3-4 | Fixed `00 00` | | 5-6 | 11-bit CAN ID, big-endian | | 7 | Length code `00`-`0F` | | 8-71 | Fixed 64-byte data area; unused bytes are `00` | | 72 | CRC-8 of the first 72 bytes | For example, when sending 16 bytes to Channel 1 with extended ID `0x1F100101`, the first 24 bytes are: ```text 45 46 01 1F 10 01 01 0A 01 01 01 01 42 00 00 00 01 01 64 00 78 56 34 12 ``` Append 48 `00` bytes and then the CRC-8 calculated over the first 72 bytes. ## 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 ``` ## Read CAN FD Feedback SPI is full duplex. Data read while sending a command may be old feedback or all zeros. Continue sending 73-byte empty frames to read later feedback. For each non-zero frame, check in order: 1. The byte-73 CRC equals the calculated CRC of bytes 0-71. 2. The header is `SF` or `EF`. 3. Channel is `1/2` on the current STM32. 4. The length code is valid. 5. The CAN ID and data match the target device protocol. ## Troubleshooting | Symptom | Action | | --- | --- | | All bytes are `00` | No feedback is queued; continue polling with a 73-byte empty frame | | No response in 23-byte mode | CAN FD requires LONG; after switching, send 73 bytes per transfer | | CRC error | Check Mode 0, MSB First, fixed length, SPI clock, and chip-select timing | | CAN FD device does not reply | Check arbitration/data rates, BRS, Channel, ID, termination resistors, wiring, and power | | Length code is correct but data is incomplete | Fill the complete 64-byte data area and pad unused bytes with `00` |