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 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 |
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:
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:
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.
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 |
|
Select a CAN Interface
SPI chip select |
Channel |
Target |
|---|---|---|
CS0 (first STM32) |
|
CAN-1 |
CS0 (first STM32) |
|
CAN-2 |
CS1 (second STM32) |
|
CAN-3 |
CS1 (second STM32) |
|
CAN-4 |
CS0 or CS1 (either chip select) |
|
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 |
2 |
Channel |
3-6 |
29-bit CAN ID, big-endian |
7 |
Data length |
8-15 |
8-byte data area; unused bytes are |
16-21 |
|
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:
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 |
2 |
Channel |
3-4 |
Fixed |
5-6 |
11-bit CAN ID, big-endian |
7 |
Data length |
8-15 |
8-byte data area; unused bytes are |
16-21 |
|
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 |
|
64-byte data area |
CRC-8 |
Standard frame |
|
64-byte data area |
CRC-8 |
Calculate CRC
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 54or53 54.Channel is local
01or02.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
Troubleshooting
Symptom |
Action |
|---|---|
All bytes are |
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 |
No response after switching to LONG |
The host is still sending 23-byte frames; send 73 bytes for every transfer |