程序员第一次控制工业步进电机:Python + Modbus RTU,从串口报文到自动定位(附可运行代码)

19 阅读5分钟

本文用 Python(pyserial)从零实现一个步进电机驱动器的控制客户端:Modbus RTU 帧构造、CRC16 校验、写目标位置、轮询运行状态,最终封装成一个 30 行的 StepperDriver 类,实现“一行代码让电机走到指定位置”。全程无需 PLC。代码中的寄存器布局为示意,实际以驱动器协议手册为准。


一、背景:软件工程师为什么要碰这个

最近接了个厂内小项目:给产线的单轴送料机构做参数管理工具。硬件侧选了驱控一体步进电机驱动器(本文用广州致良运控的 SD6 做测试)——这类驱动器内置了运动控制,对外暴露 Modbus RTU 通讯口。

翻译成人话:它就是一个挂在 RS485 总线上的、带寄存器的“智能外设” 。对我们来说,控制它和控制任何一个串口设备没有本质区别——不需要懂 PLC,只需要会发 Modbus 帧。

二、准备工作

硬件:SD6 驱控一体步进电机驱动器、USB 转 RS485 模块、步进电机、开关电源。接线按手册:电源、电机、A/B 通讯线。

软件:

pip install pyserial

先确认基本串口参数(以手册为准,本文示意用 8N1、19200bps)。

三、Modbus RTU 帧:手写一遍就懂了

Modbus RTU 的请求帧结构很简单:

[站号 1B][功能码 1B][数据 N B][CRC16 2B 低字节在前]

写单个寄存器用功能码 0x06。假设(示意)寄存器布局如下:

寄存器地址说明
目标位置0x0000int,脉冲数或用户单位
运行速度0x0001Hz
加减速时间0x0002ms
启动命令0x0003写 1 启动
状态字0x0010bit0 运行中 / bit1 到位 / bit2 报警

CRC16 的坑:Modbus 用的是 CRC16/MODBUS 变体,多项式 0xA001(反射),低字节在前。当年我在这儿浪费了一小时,直接给结论:

def crc16(data: bytes) -> bytes:
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    return bytes([crc & 0xFF, crc >> 8])  # 低字节在前!

四、先裸发一帧验证链路

目标:向 1 号站的 0x0000 寄存器写入 3000(目标位置)。

import serial


ser = serial.Serial('COM3', 19200, bytesize=8, parity='N',
                    stopbits=1, timeout=0.5)


frame = bytes.fromhex('01 06 00 00 0B B8')          # 3000 = 0x0BB8
frame += crc16(frame)
ser.write(frame)
print('resp:', ser.read(8).hex())                    # 正常响应应与请求帧一致

发出去之后,电机不会动——因为我们只写了位置,还没发启动命令。这正是调试期最安全的地方:参数写入与动作触发是分离的。

链路验证通过的标准:响应帧 == 请求帧。收不到响应?按这个顺序查:站号 → 波特率/校验位 → A/B 线是否接反 → CRC。

五、封装成 StepperDriver 类

裸发帧太原始,封装一层,让调用方用“人话”操作:

class StepperDriver:
    def __init__(self, port: str, addr: int = 1, baud: int = 19200):
        self.ser = serial.Serial(port, baud, bytesize=8,
                                 parity='N', stopbits=1, timeout=0.5)
        self.addr = addr


    def _request(self, pdu: bytes, resp_len: int) -> bytes:
        frame = bytes([self.addr]) + pdu
        frame += crc16(frame)
        self.ser.write(frame)
        resp = self.ser.read(resp_len)
        if len(resp) < resp_len or resp[:2] != frame[:2]:
            raise TimeoutError(f'无响应或响应异常: {resp.hex()}')
        if not self._check_crc(resp):
            raise ValueError('CRC 校验失败')
        return resp


    @staticmethod
    def _check_crc(resp: bytes) -> bool:
        return crc16(resp[:-2]) == resp[-2:]


    def write_register(self, addr: int, value: int):
        pdu = bytes([0x06]) + addr.to_bytes(2, 'big') + \
              value.to_bytes(2, 'big')
        self._request(pdu, 8)


    def read_status(self) -> int:
        pdu = bytes([0x03]) + (0x0010).to_bytes(2, 'big') + (1).to_bytes(2, 'big')
        resp = self._request(pdu, 7)
        return resp[3]                      # 状态字(示意:1字节)


    def move_to(self, position: int, speed: int, accel_ms: int = 200):
        self.write_register(0x0000, position)   # 目标位置
        self.write_register(0x0001, speed)      # 速度
        self.write_register(0x0002, accel_ms)   # 加减速时间
        self.write_register(0x0003, 1)          # 启动!


    def wait_done(self, timeout: float = 30.0):
        import time
        t0 = time.time()
        while time.time() - t0 < timeout:
            status = self.read_status()
            if status & 0x02:                   # 到位位(示意)
                return True
            if status & 0x04:                   # 报警位(示意)
                raise RuntimeError('驱动器报警')
            time.sleep(0.1)
        raise TimeoutError('等待到位超时')

六、三行代码,让电机自己走到位

drv = StepperDriver('COM3', addr=1)
drv.move_to(position=3000, speed=1500, accel_ms=300)
drv.wait_done()
print('定位完成 ✅')

再往上加一层,就是多轴场景:每台驱动器一个站号,实例化多个对象,轮询各自状态——一个简易的“轴群监控面板”就是几个 for 循环的事,这里不展开。

七、踩坑记录

  1. CRC 字节序:低字节在前。高字节在前就是不通,报文一个字都不会错,纯粹是校验被拒——最阴的一个坑;
  2. 串口被占用:调试软件和你的脚本别同时开,Windows 下串口是独占的;
  3. timeout 太短:多段加减速末端轮询别太密,建议 ≥100ms 间隔,总线没必要打满;
  4. 单位换算:位置寄存器的“脉冲数”和机械位移之间隔着丝杆导程和细分,写个换算函数,别在业务代码里硬算;
  5. 务必核对协议手册:本文寄存器地址全部为示意布局,不同型号的地址表、状态位定义都不一样——接入前拿官方协议文档对一遍。

八、总结

对软件工程师来说,驱控一体驱动器只是串口总线上一个“会转的寄存器集合”:写位置、写速度、写启动、读状态——四个寄存器操作封装完,工业控制和调一个普通外设没有区别。硬件团队负责机构与参数,软件团队用几十行代码接管参数管理与状态监控,分工干净,联机方案的天花板也高。


觉得有帮助的话点个赞 ⭐ 收藏一下;评论区可以聊:你们的产线数据采集是怎么做的?下篇可能写“给这个类加个 Web 控制面板(Node.js + SerialPort)”,感兴趣的扣个 1。

标签:#Python #硬件 #IoT #Modbus #嵌入式 #串口通信