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