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
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
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.)
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
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