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)

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

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