# zdt_foc_canGUI_linux **Repository Path**: reflex-robotics/zdt_foc_can-gui_linux ## Basic Information - **Project Name**: zdt_foc_canGUI_linux - **Description**: 为ZDT电机编写的CAN通讯驱动以及上位机,运行在linux环境下 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-26 - **Last Updated**: 2026-09-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # zdt_foc 张大头(ZDT)闭环步进 / FOC 驱动器的 **Linux 上位机驱动库**。把散落在 13 个 STM32 单命令例程里的私有协议整理成一个通用类,跑在普通 PC 上(Ubuntu + SocketCAN),一台电机一个对象。 - 目标设备:ZDT_X42_V2 / ZDT_X57_V2 一类带 FOC 闭环驱动板的步进电机 - 通讯方式:**私有协议 over CAN**(驱动器 `Checksum` 菜单为 `0x6B` / `XOR` / `CRC-8` 时启用) - 依赖:**无**。只用 Python 3.10+ 标准库的 `socket.AF_CAN`,不需要 python-can,也不需要 PEAK 的闭源 SDK - 附带 **Tkinter 图形上位机**(`python3 -m zdt_foc.gui`)与命令行工具,见下文 ## 硬件与接口准备 驱动器侧(小屏幕菜单): | 菜单 | 需要设成 | 说明 | | --- | --- | --- | | `P_Serial` | `CAN1_MAP` | 把通讯端口复用为 CAN | | `CAN_Baud` | `500000` | 默认 500K,要和下面的 bitrate 一致 | | `Checksum` | `0x6B` | 本库默认值;选 `XOR`/`CRC-8` 时构造时传 `checksum=` | | `Response` | `Receive`(默认) | 若设为 `None` 驱动器不回命令,需要 `expect_ack=False` | | `ID_Addr` | `1`(默认) | 多机时每台设不同地址 | PC 侧(以 PEAK PCAN-USB 为例,内核 `peak_usb` 驱动会自动创建 `canX`): ```bash lsusb | grep -i peak # 确认设备在 sudo ip link set can0 up type can bitrate 500000 # 拉起接口 ip -details link show can0 # 看状态和位定时 ``` 别忘了总线两端各一个 120Ω 终端电阻。 ## 快速上手 ```python from zdt_foc import SocketCanTransport, ZdtMotor with SocketCanTransport("can0") as bus: m = ZdtMotor(bus, addr=1) print(m.read_version()) # 固件 201 / 硬件 120 print(m.read_status()) # 0x03 [已使能、到位] m.enable() m.move_by(360.0, speed_rpm=300) # 相对当前位置 CW 转一圈 m.wait_in_position() print(m.read_position()) # 360.0 m.move_to(0.0, speed_rpm=300, acc_rpm_s=1000, dec_rpm_s=1000) # 带加减速回零点 m.wait_in_position() m.disable() ``` 角度和转速都是**带符号**的,正数 CW、负数 CCW,内部自动拆成协议要求的符号字节,不用自己管方向。 ## 命令行工具 调试阶段最省事的用法,不用写脚本: ```bash python3 -m zdt_foc.cli scan # 扫描总线上有哪些电机地址 python3 -m zdt_foc.cli info # 版本/状态/位置/转速/电压/温度 python3 -m zdt_foc.cli monitor --interval 0.5 # 循环刷新实时数据 python3 -m zdt_foc.cli enable python3 -m zdt_foc.cli move 360 --speed 300 --wait python3 -m zdt_foc.cli move 0 --absolute --acc 1000 --dec 1000 --wait python3 -m zdt_foc.cli vel 500 # 速度模式 500 RPM python3 -m zdt_foc.cli torque 800 # 力矩模式 800 mA python3 -m zdt_foc.cli home --mode nearest python3 -m zdt_foc.cli stop python3 -m zdt_foc.cli clear-stall # 解除堵转保护 python3 -m zdt_foc.cli --addr 2 --channel can1 info ``` ## 图形界面上位机 Tkinter 实现,布局对照上位机设计图,零额外依赖(tkinter 属标准库;Debian/Ubuntu 缺失时 `sudo apt install python3-tk`): ```bash sudo python3 -m zdt_foc.gui # 真机,需管理员权限(连接时自动设置 CAN 接口速率) python3 -m zdt_foc.gui --mock # 无硬件预览(MemoryTransport,免提权) ``` - **CAN 通讯设置**:接口下拉(自动扫描 `/sys/class/net`,多适配器可切换)、CAN 速率 10 档、电机地址、校验方式;连接时核对接口实际速率,不一致自动设置 - **读取系统状态**:单次读取 + **实时读取**(独立轮询线程 + 专用 socket,与其它操作并行互不阻塞),周期 50-5000 ms 可调,连续失败自动停止 - **修改驱动参数**:0x42/0x48 字段级读写,24 项配置全部可编辑;改 CAN 速率/校验/端口复用会弹窗警告(写入后链路参数即时生效) - **原点回零**:参数读写(转速/电流等带范围校验,超范围弹窗)+ 触发回零 / 单圈零点 / 强制中断 - **运动控制**:力矩 / 速度 / 直通限速 / 梯形四面板;底排使能、停止、位置清零、同步运动、解除堵转、修改 ID 地址 - **通讯日志**:右侧独立窗口与主区同高,每行带时间戳——TX/RX 黑色、成功绿色、失败红色 ## API 一览 `ZdtMotor` 的方法按功能分组: **使能与停止** — `enable(on=True)`、`disable()`、`stop()` **三种运动模式** | 方法 | 协议命令 | 说明 | | --- | --- | --- | | `set_torque(current_ma, ramp_ma_s=)` | `0xF5` | 力矩模式,电流带符号,±4000 mA | | `set_velocity(rpm, ramp_rpm_s=)` | `0xF6` | 速度模式,转速带符号 | | `move_by(deg, speed_rpm)` | `0xFB` | 相对位置,直通限速 | | `move_to(deg, speed_rpm)` | `0xFB` | 绝对位置,直通限速 | | `move_by/to(..., acc_rpm_s=, dec_rpm_s=)` | `0xFD` | 给了加减速就自动切梯形曲线模式 | **等待运动完成** — `wait_in_position()` 轮询状态标志位(通用)、`wait_reached()` 等驱动器主动发的到位返回(需要 `Response` 菜单含 `Reached`) **读取** — `read_position()`、`read_target_position()`、`read_setpoint_position()`、`read_position_error()`、`read_velocity()`、`read_temperature()`、`read_bus_voltage()`、`read_bus_current()`、`read_phase_current()`、`read_encoder_raw()`、`read_encoder_linear()`、`read_pulse_count()`、`read_input_pulse()`、`read_version()`、`read_phase_rl()`、`read_pid()`、`read_status()`、`read_home_status()` **回零** — `set_home_zero()`、`home(mode)`、`wait_home()`、`home_and_wait()`、`abort_home()`、`read_home_params()`、`write_home_params()` **触发动作** — `clear_position()`、`clear_stall()`、`calibrate_encoder()`、`factory_reset()` **驱动参数(0x42 / 0x48)** — `read_drive_config()`(24 项结构化解析)、`read_drive_config_raw()`(原始字节)、`write_drive_config(params, save=)`;`read_sys_status_raw()`(0x43 原始字节) **修改配置** — `set_microstep()`、`set_address()`、`set_ctrl_mode()`、`write_pid()` **多机同步** — `MotorGroup`,见 `examples/05_multi_sync.py` 单位约定:角度 `°`、转速 `RPM`、电流 `mA`、温度 `°C`、参数里的时间 `ms`(超时参数用秒)、总线电压 `V`(协议是 mV,这里除了 1000)。 ## 错误处理 所有异常都派生自 `ZdtError`: | 异常 | 触发条件 | 常见原因 | | --- | --- | --- | | `ZdtTimeout` | 没收到应答 | 接线/速率/地址不对;`Response` 菜单设为 `None` | | `ZdtChecksumError` | 收到了但校验字节不对 | 校验方式与驱动器菜单不一致;总线受干扰 | | `ZdtCommandError` | 驱动器回 `01 00 EE 6B` | 命令被判为非法 | | `ZdtConditionError` | 驱动器回状态 `0xE2` | 触发了堵转保护、电机未使能、正在回零 | | `ZdtProtocolError` | 应答结构不符 | 功能码/长度不对,分包丢失 | | `ZdtHomingFailed` | 回零结束但置位失败标志 | 超时、限位没碰到、零点无效 | `motor.stats` 里有 `sent` / `timeout` / `checksum_error` 计数,长时间跑可以用来观察链路质量。 ## 代码结构 ``` zdt_foc/ protocol.py 协议编解码:功能码、校验、组包、解包。纯函数,不碰 IO transport.py 物理层:Transport 抽象 + SocketCanTransport + MemoryTransport motor.py ZdtMotor / MotorGroup,面向使用者的类 gui.py Tkinter 图形上位机(实时读取 / 驱动参数 / 运动控制) cli.py 命令行工具 errors.py 异常类型 examples/ 7 个可直接跑的示例(08 为扭簧手感 / 软件阻抗控制) tests/ 70 个离线用例,不需要硬件 ``` 分层的意义在于 `protocol.py` 产出的是**完整的私有协议帧**(`地址 + 功能码 + [辅助码] + 数据 + 校验字节`),这串字节与物理层无关: - 走 CAN 时由 `transport.py` 按手册 7.2.2 拆包:帧 ID = `(地址 << 8) | 包号`,每包首字节重复功能码,每包最多 7 字节数据; - 将来若要挂串口 TTL/RS232/RS485,同一帧直接整串发出即可,新增一个 `Transport` 子类就行,`protocol.py` 和 `motor.py` 都不用动。 跑测试: ```bash python3 -m unittest discover -s tests -v ``` ## 关于 Modbus-RTU 驱动器还支持 Modbus-RTU(`Checksum` 菜单选 `Modbus` 时切换过去),本库**暂未实现**。 需要说明的是它和私有协议**不是同一套命令表**,不是换个物理层就能复用:Modbus 只有 `0x04`/`0x06`/`0x10` 三个功能码,功能靠寄存器地址区分,校验是 CRC16,32 位数据规定先低字后高字。两者没有一个字节能共用,而且驱动器上是二选一的。 真要加的话,思路是在 `protocol.py` 旁边新增一个平行的 `modbus.py`(寄存器映射 + CRC16 + 另一套应答解析),`ZdtMotor` 通过后端选择分派,对上层暴露的 API 保持不变。 ## 与官方 STM32 例程的差异 本库的协议字段依据是《ZDT_X57_V2 步进闭环驱动说明书 Rev1.0》第七、十三章,并与官方例程 `ZDT_X42_V2.c` 的组包顺序逐条对照过。发送侧的字节完全一致,差异集中在接收侧——官方例程那部分基本是空缺的: 1. **不解析应答**。例程发完就不管了,`0x02` 收到、`0xE2` 条件不满足、`01 00 EE` 命令错误、位置模式的 `0x9F` 到位返回,一律不区分,调用者也拿不到成功与否。 2. **多包应答会丢**。读 PID(`0x21`,19 字节)、读回零参数(`0x22`,18 字节)、读系统状态(`0x43`)的应答超过 8 字节,驱动器会分多个 CAN 帧回来,而例程的 `can` 结构只存最后一帧,前面的直接被覆盖。本库按包号重组,并用应答长度表判断何时收齐。 3. **`ZDT_X42_V2_Receive_Data` 有两个 bug**:地址取自 `can.CAN_TxMsg.ExtId` 而不是 `CAN_RxMsg`;校验字节的下标错位一个字节(`rxCount` 是 DLC,校验字节在 `rxCmd[rxCount]` 而非 `rxCmd[rxCount-1]`),所以例程里 `if(rxCmd[rxCount-1] == 0x6B)` 实际比较的是命令状态字节,永远不成立。 4. **"触发回零"例程读错了功能码**:注释说读回零状态标志位,代码里传的是 `S_CPOS`(`0x36`,实时位置),应为 `S_OFLAG`(`0x3B`)。 5. 单位换算要调用者自己做,且各命令倍率不同(位置 ×10、位置误差 ×100、转速 ×10),容易抄错。 ## 尚未验证 / 已知限制 这几点我没有硬件在手无法确认,实际接上电机时留意一下: 1. **应答帧 ID 的高字节是否回填电机地址**。手册只规定了主机发送时 ID 的构成,没写从机应答时是否照此回填,官方例程也没用到应答的 ID。本库默认相信它(`trust_can_id_addr=True`),并据此做多机地址过滤。如果单机能通、多机读不到应答,构造时传 `SocketCanTransport(..., trust_can_id_addr=False)` 试试。 2. **`0x42` 读驱动参数 / `0x43` 读系统状态的字段布局**已按手册 13.2/13.3 节结合真机抓包数据定案(`0x42` 应答 = 地址 + 功能码 + [总长 0x25][参数个数 0x18] + 32 字节参数 + 校验字节;`0x43` 载荷 34 字节),GUI 里做了字段级解析。若你的固件版本布局不同,以抓包为准。 3. **`0x46` 修改控制模式**来自官方例程,手册命令列表未收录,取值含义以驱动器 `Ctrl_Mode` 菜单为准。 4. `wait_in_position()` 靠轮询"到位标志位",命令刚下发、电机还没起步时可能读到上一次的到位状态,所以默认先等 `settle_delay=0.05s` 再开始轮询。高速短距离运动可以适当调大。 ## 参考资料 - 《ZDT_X57_V2 步进闭环驱动说明书 Rev1.0》第 7 章(通讯控制)、第 8 章(原点回零)、第 13 章(附录:XOR / CRC-8) - 《ZDT_X57_V2 Modbus-RTU 指令说明》(Modbus 寄存器映射,本库未实现的部分) - 官方 STM32 例程 `zdt_motor_X_v2/STM32F1_CAN通讯__*/BSP/ZDT_X42_V2.c`