# 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
```