# SANPO USB to FDCAN The SANPO USB to FDCAN protocol controls FDCAN devices from Windows or Linux and forwards FDCAN data without requiring an additional driver. The raw FDCAN frame is wrapped with a SANPO header and trailer for transmission over the USB management serial port. These extra bytes are used only by the board; the FDCAN device receives the raw FDCAN frame without a SANPO header or trailer. For SANPO USB to classic CAN, see [SANPO USB to CAN](usb_can). ## Connection After connecting the board over USB, six USB serial ports appear. Two management ports carry this protocol; Windows normally displays them as `SANPO Studio Management Port`. One management port accesses CAN-1/2 and the other accesses CAN-3/4. The other four ports are independent RS485 serial ports. `COM` and `/dev/ttyACM*` numbering is not guaranteed to follow the board silkscreen; initially connect only one CAN device to confirm the mapping. On Linux, you can run the Python [serial-port identification example](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/rs485/identify_sanpo_serial_ports_v8.py) to identify the two management ports. ## Automatically Configure All CAN FD Bitrates (Recommended) Install the dependency: ```bash python -m pip install pyserial ``` The complete code below automatically finds both management ports of one connected SPINE V8 on Windows or Linux. It sends the same setting to both MCUs, thereby configuring CAN-1 through CAN-4 without requiring any management-port or board-interface mapping. This example configures a 1 Mbps arbitration phase and a 5 Mbps data phase. ```python import re import time import serial from serial.tools import list_ports COMMANDS = ["AT+SETFDCAN=1000000,5000000"] 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 send_at(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 "OK" not in text: raise RuntimeError(f"{stream.port}: {command}: {text or 'no reply'}") print(f"{stream.port}: {command} -> {text}") 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) for command in COMMANDS: send_at(stream, command) print("CAN/FDCAN-1..CAN/FDCAN-4 configured successfully.") ``` To use other bitrates, change the arbitration and data-phase numbers in `COMMANDS`. The `1_000_000` used to open the management port is only a CDC API parameter; it does not set the physical CAN or RS485 bitrate. Only the explicit `AT+SETFDCAN` command changes the physical bus settings. | Parameter | Supported values | | --- | --- | | Arbitration phase | `1000000`, `500000`, `250000`, `125000`, `100000` | | Data phase | `2000000`, `3000000`, `4000000`, `5000000` | ## Data Length Code The CAN FD data-frame length field uses this code and supports up to 64 data bytes: | Code | Data bytes | | --- | ---: | | `00` | 0 | | `01` | 1 | | `02` | 2 | | `03` | 3 | | `04` | 4 | | `05` | 5 | | `06` | 6 | | `07` | 7 | | `08` | 8 | | `09` | 12 | | `0A` | 16 | | `0B` | 20 | | `0C` | 24 | | `0D` | 32 | | `0E` | 48 | | `0F` | 64 | ## CAN FD Extended Frame Send and receive use the same format: | Field | Length | Description | | --- | ---: | --- | | Header | 2 bytes | Fixed `45 46`, or `EF` | | Channel | 1 byte | CAN FD interface number | | CAN ID | 4 bytes | 29-bit extended ID, big-endian | | Length code | 1 byte | `00` to `0F` | | Data | 0-64 bytes | Byte count is determined by the length code | | Trailer | 2 bytes | Fixed `0D 0A` | Send 16 data bytes to Channel 1 with extended ID `0x1F100101`: ```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 0D 0A ``` Here `0A` indicates a 16-byte data area. ## CAN FD Standard Frame Send and receive use the same format: | Field | Length | Description | | --- | ---: | --- | | Header | 2 bytes | Fixed `53 46`, or `SF` | | Channel | 1 byte | CAN FD interface number | | Reserved | 2 bytes | Fixed `00 00` | | CAN ID | 2 bytes | 11-bit standard ID, big-endian | | Length code | 1 byte | `00` to `0F` | | Data | 0-64 bytes | Byte count is determined by the length code | | Trailer | 2 bytes | Fixed `0D 0A` | Send 12 data bytes to Channel 2 with standard ID `0x142`: ```text 53 46 02 00 00 01 42 09 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0A ``` ## Parse Replies Motor feedback uses the same `SF` or `EF` format. Use the header to distinguish standard and extended frames, the length code to determine the data length, and Channel to identify the first or second interface on the current MCU. USB is a byte stream. One serial read may contain a partial frame, a complete frame, or multiple frames. Parse continuously by header, length code, and trailer; do not assume that one `read()` returns exactly one frame. ## SANPO J8108-R8 FDCAN Example Example program: [read-only management-protocol J8108-R8 FDCAN probe](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/fdcan/usb2fdcan_j8108_demo_v8.py) After installing `pyserial`, run the program. It automatically finds both SANPO management ports and probes local Channels 1 and 2 on each MCU, covering all four CAN/FDCAN paths: ```bash python3 usb2fdcan_j8108_demo_v8.py ``` The program sends `AT+SETFDCAN=1000000,5000000` to both management ports before probing all four local Channel paths. It sends only SANPO FDCAN v1 `HELLO` and `STATE_REQUEST` messages; it never enables the motor or sends a motion target. With V82, applying unchanged FDCAN parameters restores the live controllers without writing Flash again. Use `--list-only` to enumerate management ports without configuring bitrates or sending a test frame. The J8108-R8 must already be configured and rebooted into the `SANPO FDCAN Profile`; the example does not switch or write the motor Profile. If USB metadata is incomplete, or to narrow troubleshooting to one path, specify a Linux management port and local Channel explicitly: ```bash python3 usb2fdcan_j8108_demo_v8.py \ --ports /dev/ttyACM0 \ --channels 1 \ --node-id 1 ``` For an existing J8108-R8 configured for 1M/2M, add `--data-bitrate 2000000`. `MISS` is expected on port/channel paths without a motor. See the [J8108-R8 FDCAN demo guide](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/guides/j8108-fdcan.md) for complete instructions. ## Troubleshooting | Symptom | Action | | --- | --- | | CAN FD does not reply | Check arbitration/data rates, BRS, termination resistors, and whether the other device supports CAN FD | | Reply Channel is only 1/2 | Replies use local numbering on the current MCU; this is normal | | Multiple frames are concatenated | Parse continuously by header, length code, and trailer instead of serial read boundaries |