概述
本文介绍了一个基于PyQt5和Modbus协议的电机位置控制系统,该系统能够通过图形界面控制电机运动,包括绝对位置移动、回零操作和返回原点等功能。系统采用Modbus RTU协议与电机驱动器通信,实现了精确的位置控制。
系统架构
该系统主要由以下几个部分组成:
-
用户界面层:基于PyQt5的图形界面
-
通信层:使用pymodbus库实现Modbus RTU协议通信
-
设备管理层:通过pyudev库自动检测USB设备
-
业务逻辑层:实现电机控制的核心功能
主要功能
1. 设备自动检测
系统使用pyudev库自动检测连接的USB设备,通过供应商ID和产品ID识别特定的串口设备:
python
def find_usb_device(vendor_id, product_id): context = pyudev.Context() for device in context.list_devices(subsystem='tty'): if device.get('ID_VENDOR_ID') == vendor_id and device.get('ID_MODEL_ID') == product_id: return device.device_node return None
2. Modbus通信
系统建立了Modbus RTU客户端,配置了串口参数:
python
client = ModbusClient( port=usb_port, # 动态检测的设备端口 baudrate=38400, parity='N', stopbits=1, bytesize=8, timeout=2 )
3. 电机控制功能
系统实现了以下电机控制功能:
-
绝对位置移动:通过设置目标位置和转速控制电机运动
-
回零操作:将电机移动到原点位置
-
返回原点:将电机返回到初始位置
-
紧急停止:立即停止电机运动
4. 状态监控
系统定时读取并显示PA_006寄存器的值,实时监控电机状态:
python
def update_pa006(self): """更新 PA_006 寄存器内容""" value = self.read_register(PA_006) if value is not None: self.pa006_label.setText(f"PA_006 寄存器内容: {value}") else: self.pa006_label.setText("PA_006 寄存器内容: 读取失败")
技术特点
-
自动设备检测:无需手动配置串口号,自动识别设备
-
单位转换:支持毫米和脉冲数之间的转换
-
异常处理:完善的错误处理和日志记录
-
实时状态显示:定时更新电机状态信息
-
用户友好的界面:简洁直观的操作界面
import sys
import time
import threading
import logging
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox
)
from PyQt5.QtCore import Qt, QTimer
from pymodbus.client import ModbusSerialClient as ModbusClient
import pyudev
# 配置日志
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 查找 USB 设备
def find_usb_device(vendor_id, product_id):
context = pyudev.Context()
for device in context.list_devices(subsystem='tty'):
if device.get('ID_VENDOR_ID') == vendor_id and device.get('ID_MODEL_ID') == product_id:
return device.device_node
return None
# 初始化 Modbus 客户端
usb_port = find_usb_device("0403", "6001") # 替换为实际的 idVendor 和 idProduct
if not usb_port:
raise Exception("未找到设备")
# Modbus 客户端
client = ModbusClient(
port=usb_port, # 动态检测的设备端口
baudrate=38400,
parity='N',
stopbits=1,
bytesize=8,
timeout=2
)
# 电机参数
MOTOR_SLAVE_ID = 7 # 电机从站地址
PULSES_PER_REV = 1000 # 每转脉冲数
MM_PER_REV = 78 # 每转距离(mm)
# 寄存器地址
PA_006 = 0x006 # 需要显示的寄存器
PA_036 = 0x036 # 转速设置
PA_037 = 0x037 # 定位目标低16位
PA_038 = 0x038 # 定位目标高16位
PA_04E = 0x04E # 控制字
PA_011 = 0x011 # DI0 功能设置为原点开关
PA_040 = 0x040 # 回零模式
PA_041 = 0x041 # 回零速度
PA_042 = 0x042 # 回零爬行速度
PA_040 = 0x040 # 位置置0
class MotorControlApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("电机位置控制")
self.setGeometry(100, 100, 400, 400)
# 主布局
self.main_widget = QWidget()
self.setCentralWidget(self.main_widget)
self.layout = QVBoxLayout(self.main_widget)
# 目标位置输入
self.position_label = QLabel("目标位置 (脉冲数),负数表示反向:")
self.position_input = QLineEdit()
self.layout.addWidget(self.position_label)
self.layout.addWidget(self.position_input)
# 转速输入
self.speed_label = QLabel("转速 (r/min):")
self.speed_input = QLineEdit()
self.layout.addWidget(self.speed_label)
self.layout.addWidget(self.speed_input)
# 启动按钮
self.start_button = QPushButton("启动运动")
self.start_button.clicked.connect(self.start_motion)
self.layout.addWidget(self.start_button)
# 停止按钮
self.stop_button = QPushButton("停止运动")
self.stop_button.clicked.connect(self.stop_motion)
self.layout.addWidget(self.stop_button)
# 回零按钮
self.home_button = QPushButton("回零")
self.home_button.clicked.connect(self.home_motor)
self.layout.addWidget(self.home_button)
# 返回按钮
self.return_button = QPushButton("返回")
self.return_button.clicked.connect(self.return_to_origin)
self.layout.addWidget(self.return_button)
# 显示 PA_006 寄存器内容
self.pa006_label = QLabel("PA_006 寄存器内容: 未读取")
self.layout.addWidget(self.pa006_label)
# 状态显示
self.status_label = QLabel("状态: 空闲")
self.layout.addWidget(self.status_label)
# 初始化 Modbus 连接
self.connect_modbus()
# 记录初始位置
self.initial_position = 0
# 定时器:定期读取 PA_006 寄存器内容
self.timer = QTimer()
self.timer.timeout.connect(self.update_pa006)
self.timer.start(1000) # 每 1 秒更新一次
def connect_modbus(self):
"""连接 Modbus"""
try:
if client.connect():
logging.info("Modbus 连接成功")
else:
logging.error("Modbus 连接失败")
except Exception as e:
logging.error(f"Modbus 连接异常: {e}")
def read_register(self, address):
"""读取 Modbus 寄存器"""
try:
if client.connect():
response = client.read_holding_registers(address=address, count=1, slave=MOTOR_SLAVE_ID)
if not response.isError():
return response.registers[0]
else:
logging.error(f"读取寄存器 0x{address:04X} 失败: {response}")
else:
logging.error("Modbus 连接丢失")
except Exception as e:
logging.error(f"读取寄存器失败: {e}")
return None
def write_register(self, address, value):
"""写入 Modbus 寄存器"""
try:
if client.connect():
client.write_register(address=address, value=value, slave=MOTOR_SLAVE_ID)
logging.info(f"写入寄存器 0x{address:04X},值: {value}")
else:
logging.error("Modbus 连接丢失")
except Exception as e:
logging.error(f"写入寄存器失败: {e}")
def update_pa006(self):
"""更新 PA_006 寄存器内容"""
value = self.read_register(PA_006)
if value is not None:
self.pa006_label.setText(f"PA_006 寄存器内容: {value}")
else:
self.pa006_label.setText("PA_006 寄存器内容: 读取失败")
def mm_to_pulses(self, length_mm):
"""将长度(mm)转换为脉冲数"""
return int((length_mm / MM_PER_REV) * PULSES_PER_REV)
def start_motion(self):
"""启动绝对运动"""
try:
# 设置目标位置(脉冲数)
# 获取长度输入
length_mm = float(self.position_input.text())
# 将长度转换为脉冲数
target_position = self.mm_to_pulses(length_mm)
# 将目标位置拆分为高16位和低16位
high_word = (target_position >> 16) & 0xFFFF # 高16位
low_word = target_position & 0xFFFF # 低16位
# 写入高16位到 PA_037
self.write_register(PA_037, high_word)
# 写入低16位到 PA_038
self.write_register(PA_038, low_word)
# 设置转速
speed = int(self.speed_input.text())
self.write_register(PA_036, speed)
# 触发绝对运动
self.write_register(PA_04E, 0x03)
self.status_label.setText("状态: 运行中")
logging.info(f"电机启动,目标位置: {target_position} 脉冲,转速: {speed} r/min")
except ValueError:
QMessageBox.warning(self, "输入错误", "请输入有效的数字")
except Exception as e:
logging.error(f"启动运动失败: {e}")
def return_to_origin(self):
"""返回到初始位置"""
try:
# 写入高16位到 PA_037
self.write_register(PA_037, 0)
# 写入低16位到 PA_038
self.write_register(PA_038, 0)
# 设置转速
speed = int(self.speed_input.text())
self.write_register(PA_036, speed)
# 触发相对运动
self.write_register(PA_04E, 0x03)
self.status_label.setText("状态: 返回中")
logging.info(f"电机返回中,目标位置: {self.initial_position} 脉冲,转速: {speed} r/min")
except Exception as e:
logging.error(f"返回失败: {e}")
def stop_motion(self):
"""停止运动"""
try:
# 复位控制字
self.write_register(PA_04E, 0x20)
self.status_label.setText("状态: 已停止")
logging.info("电机已停止")
except Exception as e:
logging.error(f"停止电机失败: {e}")
def home_motor(self):
"""回零"""
try:
# 1. 设置 DI0 功能为原点开关
self.write_register(PA_011, 1) # PA-011=1
logging.info("设置 DI0 功能为原点开关")
# 2. 设置回零模式为正向原点模式
self.write_register(PA_040, 24) # PA-040=24
logging.info("设置回零模式为正向原点模式")
# 3. 设置回零速度和爬行速度
self.write_register(PA_041, 40) # 回零速度
self.write_register(PA_042, 10) # 回零爬行速度
logging.info("设置回零速度和爬行速度")
# 4. 触发回零操作(PA-04E 的 bit5 置 1)
self.write_register(PA_04E, 0x10) # PA-04E=0x10
logging.info("触发回零操作")
# 5. 位置置0
self.write_register(PA_040,35)
logging.info("原点位置置0")
self.status_label.setText("状态: 回零中")
logging.info("电机回零中...")
except Exception as e:
logging.error(f"回零失败: {e}")
# 运行程序
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MotorControlApp()
window.show()
sys.exit(app.exec_())