# USB 转 CAN
- 如下协议定义只适用于固件 V4.1 版本及以后
- 发送 AT+VER 串口指令,可以查询固件版本,如未返回版本号或者返回 V33,请下载最新固件
- 支持 SANPO 电机调试工具 快速生成调试代码,支持全系列 CAN/RS485 协议电机
## 协议说明
| 适用场景 | USB 发送协议(USB->CAN) | USB 接收协议(CAN->USB) |
| --- | --- | --- |
| CAN 扩展帧电机 | 固定帧头 2 个字节(0x45 0x54)+ Channel 1 个字节 + 扩展帧 CANID 4 个字节 + 数据长度(1 字节)+ 数据(最大 8 字节)+ 固定帧尾 2 个字节(0x0D 0x0A) | 固定帧头 2 个字节(0x45 0x54)+ Channel 1 个字节 + 扩展帧 CANID 4 个字节 + 数据长度(1 字节)+ 数据(最大 8 字节)+ 固定帧尾 2 个字节(0x0D 0x0A) |
| CAN 标准帧电机 | 固定帧头 2 个字节(0x53 0x54)+ Channel 1 个字节 + 预留 2 个字节(0x00 0x00)+ CANID 标准帧 2 个字节 + 数据长度(1 字节)+ 数据(最大 8 字节)+ 固定帧尾 2 个字节(0x0D 0x0A) | 固定帧头 2 个字节(0x53 0x54)+ Channel 1 个字节 + 预留 2 个字节(0x00 0x00)+ CANID 标准帧 2 个字节 + 数据长度(1 字节)+ 数据(最大 8 字节)+ 固定帧尾 2 个字节(0x0D 0x0A) |
- 使用 SANPO 电机调试工具 快速生成测试代码
### CAN 扩展帧报文示例
发送报文
| 固定帧头(2 字节) | Channel(1 字节) | CANID(4 字节) | 数据长度(1 字节) | 数据(最大 8 字节) | 固定帧尾(2 字节) |
| --- | --- | --- | --- | --- | --- |
| 0x45 0x54 | 0x00 | 0x01 0x7F 0xFF 0x07 | 0x08 | 0x7F 0x2A 0x7F 0xFF 0x05 0x1E 0x19 0x99 | 0x0D 0x0A |
- 接入 USB 后,会显示 2 个串口,两个串口分别对应 STM32(1) 和 STM32(2),请查看开发板背面的说明,每个串口控制 2 路 CAN 接口
- Channel 为 0x01 或者 0x02 或者 0x03 或者 0x04,指定向哪个 CAN 接口发送报文,如果为 0x00,将向当前串口上的所有 CAN 接口发送报文
- 扩展帧 CANID 为 29bit,占据 CANID 4 个字节的低 29bit
接收报文
| 固定帧头(2 字节) | Channel(1 字节) | CANID(4 字节) | 数据长度(1 字节) | 数据(最大 8 字节) | 固定帧尾(2 字节) |
| --- | --- | --- | --- | --- | --- |
| 0x45 0x54 | 0x00 | 0x02 0x00 0x09 0xFD | 0x08 | 0x7F 0xF9 0x7F 0xDF 0x7F 0xFF 0x01 0x11 | 0x0D 0x0A |
### 代码片段(CAN 扩展帧,Python)
(完整样例请参考 usb2can_cybergear_sine_demo_v4.py
```python
HEADER_NORMAL_EXT = bytes([0x45, 0x54])
TAIL = bytes([0x0D, 0x0A])
# 发送(帧头2字节 + channel1字节 + CANID4字节 + DLC1字节 + data + 帧尾2字节)
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()
# 接收(帧头2字节 + channel1字节 + CANID4字节 + DLC1字节 + data + 帧尾2字节)
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 标准帧报文示例
发送报文
| 固定帧头(2 字节) | Channel(1 字节) | 预留 2 个字节 | CANID(2 字节) | 数据长度(1 字节) | 数据(最大 8 字节) | 固定帧尾(2 字节) |
| --- | --- | --- | --- | --- | --- | --- |
| 0x53 0x54 | 0x00 | 0x00 0x00 | 0x01 0x42 | 0x08 | 0x48 0x45 0x4C 0x4F 0x01 0x02 0x03 0x04 | 0x0D 0x0A |
- 接入 USB 后,会显示 2 个串口,两个串口分别对应 STM32(1) 和 STM32(2),请查看开发板背面的说明,每个串口控制 2 路 CAN 接口
- Channel 为 0x01 或者 0x02 或者 0x03 或者 0x04,指定向哪个 CAN 接口发送报文,如果为 0x00,将向当前串口上的所有 CAN 接口发送报文
- 标准帧 CANID 为 11bit,占据 CANID 2 个字节的低 11bit
接收报文
| 固定帧头(2 字节) | Channel(1 字节) | 预留 2 个字节 | CANID(2 字节) | 数据长度(1 字节) | 数据(最大 8 字节) | 固定帧尾(2 字节) |
| --- | --- | --- | --- | --- | --- | --- |
| 0x53 0x54 | 0x00 | 0x00 0x00 | 0x01 0x42 | 0x08 | 0x48 0x45 0x4C 0x4F 0x01 0x02 0x03 0x04 | 0x0D 0x0A |
### 代码片段(CAN 标准帧,Python)
(完整代码请参考 usb2can_demo.py
```python
HEADER_NORMAL_STD = bytes([0x53, 0x54])
TAIL = bytes([0x0D, 0x0A])
# 发送(帧头2字节 + channel1字节 + 预留2字节 + CANID2字节 + DLC1字节 + data + 帧尾2字节)
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()
# 接收(帧头2字节 + channel1字节 + 预留2字节 + CANID2字节 + DLC1字节 + data + 帧尾2字节)
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))
```
## 电机调试工具
- SANPO 电机调试工具 支持通用 CAN/RS485 电机,包括小米、宇树、灵足、达秒、脉塔、翎控等
- 小米 CyberGear 电机官方调试软件:[下载地址](https://gitcode.com/sanpo/robot/tree/v4/tools/CyberGear.zip)
注意:保存路径中不能有中文,否则软件无法启动
注意:小米官方调试软件协议特殊,连接小米官方调试软件以后,开发板会切换为小米模式,使用完成后,请发送 AT+ET 串口指令切换为正常模式
## 开发样例
- USB 转 CAN 扩展帧样例程序(Python 版本):下载地址
例如:向小米 CyberGear CAN 电机(电机 ID 为 12)下发正弦运控指令,电机接入开发板串口 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
```