# USB to CAN - The following protocol applies to firmware V4.1 or later - Send the AT+VER serial command to check the firmware version; if no version is returned or it shows V33, please update to the latest firmware - The SANPO Motor Debug Tool can generate test code quickly and supports CAN/RS485 motors ## Protocol Overview | Use Case | USB TX (USB->CAN) | USB RX (CAN->USB) | | --- | --- | --- | | CAN extended frame motors | Header 2 bytes (0x45 0x54) + Channel 1 byte + CANID ext 4 bytes + data length (1 byte) + data (max 8 bytes) + tail 2 bytes (0x0D 0x0A) | Header 2 bytes (0x45 0x54) + Channel 1 byte + CANID ext 4 bytes + data length (1 byte) + data (max 8 bytes) + tail 2 bytes (0x0D 0x0A) | | CAN standard frame motors | Header 2 bytes (0x53 0x54) + Channel 1 byte + reserved 2 bytes (0x00 0x00) + CANID std 2 bytes + data length (1 byte) + data (max 8 bytes) + tail 2 bytes (0x0D 0x0A) | Header 2 bytes (0x53 0x54) + Channel 1 byte + reserved 2 bytes (0x00 0x00) + CANID std 2 bytes + data length (1 byte) + data (max 8 bytes) + tail 2 bytes (0x0D 0x0A) | - Use the SANPO Motor Debug Tool to generate test code quickly ### CAN Extended Frame Example TX frame | Header (2 bytes) | Channel (1 byte) | CANID (4 bytes) | Data length (1 byte) | Data (max 8 bytes) | Tail (2 bytes) | | --- | --- | --- | --- | --- | --- | | 0x45 0x54 | 0x00 | 0x01 0x7F 0xFF 0x07 | 0x08 | 0x7F 0x2A 0x7F 0xFF 0x05 0x1E 0x19 0x99 | 0x0D 0x0A | - Two serial ports appear after USB connects; they correspond to STM32(1) and STM32(2). Check the label on the back of the board; each port controls two CAN channels - Channel can be 0x01/0x02/0x03/0x04; 0x00 broadcasts to all CAN channels on the current port - Extended CANID is 29-bit, stored in the low 29 bits of the 4-byte CANID field RX frame | Header (2 bytes) | Channel (1 byte) | CANID (4 bytes) | Data length (1 byte) | Data (max 8 bytes) | Tail (2 bytes) | | --- | --- | --- | --- | --- | --- | | 0x45 0x54 | 0x00 | 0x02 0x00 0x09 0xFD | 0x08 | 0x7F 0xF9 0x7F 0xDF 0x7F 0xFF 0x01 0x11 | 0x0D 0x0A | ### Code Snippet (CAN Extended Frame, Python) Full example: usb2can_cybergear_sine_demo_v4.py ```python HEADER_NORMAL_EXT = bytes([0x45, 0x54]) TAIL = bytes([0x0D, 0x0A]) # TX (header 2 bytes + channel 1 byte + CANID 4 bytes + DLC 1 byte + data + tail 2 bytes) def build_extended_frame(channel: int, arbitration_id: int, data: list[int]) -> bytes: can_id_bytes = arbitration_id.to_bytes(4, byteorder="big", signed=False) payload = bytearray(HEADER_NORMAL_EXT) payload.append(channel & 0xFF) payload.extend(can_id_bytes) data_bytes = bytearray(data[:8]) payload.append(len(data_bytes)) payload.extend(data_bytes) payload.extend(TAIL) return bytes(payload) payload = build_extended_frame(channel=0x00, arbitration_id=0x0000FD01, data=[0x01] + [0x00] * 7) ser.write(payload) ser.flush() # RX (header 2 bytes + channel 1 byte + CANID 4 bytes + DLC 1 byte + data + tail 2 bytes) rx = ser.read(ser.in_waiting or 1) if rx[:2] == HEADER_NORMAL_EXT and rx[-2:] == TAIL: channel = rx[2] can_id = int.from_bytes(rx[3:7], byteorder="big", signed=False) dlc = rx[7] data = rx[8 : 8 + dlc] print(channel, f"0x{can_id:08X}", dlc, list(data)) ``` ### CAN Standard Frame Example TX frame | Header (2 bytes) | Channel (1 byte) | Reserved 2 bytes | CANID (2 bytes) | Data length (1 byte) | Data (max 8 bytes) | Tail (2 bytes) | | --- | --- | --- | --- | --- | --- | --- | | 0x53 0x54 | 0x00 | 0x00 0x00 | 0x01 0x42 | 0x08 | 0x48 0x45 0x4C 0x4F 0x01 0x02 0x03 0x04 | 0x0D 0x0A | - Two serial ports appear after USB connects; they correspond to STM32(1) and STM32(2). Check the label on the back of the board; each port controls two CAN channels - Channel can be 0x01/0x02/0x03/0x04; 0x00 broadcasts to all CAN channels on the current port - Standard CANID is 11-bit, stored in the low 11 bits of the 2-byte CANID field RX frame | Header (2 bytes) | Channel (1 byte) | Reserved 2 bytes | CANID (2 bytes) | Data length (1 byte) | Data (max 8 bytes) | Tail (2 bytes) | | --- | --- | --- | --- | --- | --- | --- | | 0x53 0x54 | 0x00 | 0x00 0x00 | 0x01 0x42 | 0x08 | 0x48 0x45 0x4C 0x4F 0x01 0x02 0x03 0x04 | 0x0D 0x0A | ### Code Snippet (CAN Standard Frame, Python) Full example: [usb2can_demo.py](https://gitcode.com/sanpo/robot/blob/v4/demo/usb_demo/usb2can_demo.py) ```python HEADER_NORMAL_STD = bytes([0x53, 0x54]) TAIL = bytes([0x0D, 0x0A]) # TX (header 2 bytes + channel 1 byte + reserved 2 bytes + CANID 2 bytes + DLC 1 byte + data + tail 2 bytes) def build_standard_frame(channel: int, can_id_11bit: int, data: bytes) -> bytes: if not 0 <= can_id_11bit <= 0x7FF: raise ValueError("standard CAN ID must be 0..0x7FF (11 bits)") can_id_bytes = can_id_11bit.to_bytes(2, byteorder="big", signed=False) dlc = len(data) return ( HEADER_NORMAL_STD + bytes([channel & 0xFF]) + bytes([0x00, 0x00]) # reserved + can_id_bytes + bytes([dlc]) + data + TAIL ) payload = build_standard_frame(channel=0x00, can_id_11bit=0x142, data=bytes([0x00] * 8)) ser.flush() ser.write(payload) ser.flush() # RX (header 2 bytes + channel 1 byte + reserved 2 bytes + CANID 2 bytes + DLC 1 byte + data + tail 2 bytes) rx = ser.read(ser.in_waiting or 1) if rx[:2] == HEADER_NORMAL_STD and rx[-2:] == TAIL: channel = rx[2] can_id = int.from_bytes(rx[5:7], byteorder="big", signed=False) dlc = rx[7] data = rx[8 : 8 + dlc] print(channel, f"0x{can_id:04X}", dlc, list(data)) ``` ## Motor Debug Tools - SANPO Motor Debug Tool supports CAN/RS485 motors (Xiaomi, Unitree, etc.) - [Xiaomi CyberGear motor tool](https://gitcode.com/sanpo/robot/tree/v4/tools/CyberGear.zip) Notes: Avoid Chinese characters in the install path or the tool may not launch. The Xiaomi tool switches the board into Xiaomi mode; send AT+ET to return to normal mode after use. ## Examples - USB to CAN extended-frame example (Python): Download Example: send a sine motion command to a Xiaomi CyberGear motor (ID 12) on Windows COM8(Linux /dev/ttyACM0), CAN-4 ```bash Windows: python3 usb2can_cybergear_sine_demo_v4.py --port COM8 --motors 12 --channel 4 Ubuntu(Jetson): sudo python3 usb2can_cybergear_sine_demo_v4.py --port /dev/ttyACM0 --motors 12 --channel 4 ```