场景:
1、上位机实现曲线和实时数据显示
2、通讯模块负责上位机与设备之间的数据 控制 状态等数据处理转发
需求:
1、上位机实现硬件测试
2、上位机创建试验,实时发送设备控制、温度设置、设备加热、三通阀控制、压缩机控制、循环泵控制等控制
3、上位机实现温度表PID整定
4、上位机实现下位机的电机控制
5、通讯模块可分布式部署
网络通讯:
1、上位机和通讯模块 采用socket 网络通讯;
2、通讯模块与设备PLC采用sna7协议通讯;
技术瓶颈
电机控制需要密集发送命令,因此通讯模块单独的线程无法满足需求;
解决方案:
通讯模块采用多线程方式解决
1、心跳线程
PLC需要通讯模块定时发送心跳,防止异常断连的情况,设备继续加热或制冷等
2、数据读取线程
为了防止数据获取的实时性的问题,需要单独的线程运行;
3、弹模测试电机控制线程
实验过程中,会频繁出现密集的发送电机移动命令,为了不影响其实时性,需要单独一个线程运行;
4、设备启停状态设置线程
主要设备的根据试验阶段的控制命令,需要实时的通知设备,进行及时状态变化,非常关键的一个控制,也不能因其他原因,卡顿等;
各自独立运行,互不影响,保证运行的时效性
线程创建过程:
1、class CSnap7Base : public QThread
该线程,主要负责与PLC的连接,管理读写PLC的主要线程,网络连接正常的情况,会启动线程函数run()
run函数中,会启动其他线程,常驻后台,根据状态机决定是否控制和空闲状态;
当出现网络异常需要自动重连,该线程会主动销毁的时候,其他线程也会同步销毁;自动成功之后,所有的线程在重新创建;
线程通过共享一个读写锁控制对PLC设备的底层操作;
2、class MotorCmdThread : public QThread
弹模控制线程
3、class HeartbeatThread : public QThread
心跳线程
4、class CtrlCmdThread : public QThread
控制命令线程
void CSnap7Base::run()
{
//启动独立电机线程,常驻后台
startMotorCmdThread(); //弹模线程
startHeartbeatThread();//心跳线程
startCtrlCmdThread(); //控制线程
while (!m_endThreadFlag)
{
。。。。。。。。
}
// ★ 退出前,停止线程
stopMotorCmdThread();
stopHeartbeatThread();
stopMotorCmdThread();
}
CtrlCmdThread.h
#ifndef CTRLCMDTHREAD_H
#define CTRLCMDTHREAD_H
#include <QThread>
class CSnap7Base;
class CtrlCmdThread : public QThread
{
public:
explicit CtrlCmdThread(CSnap7Base* owner, QObject* parent = nullptr);
protected:
void run() override;
private:
CSnap7Base* m_owner = nullptr;
};
#endif // CTRLCMDTHREAD_H
#include "CtrlCmdThread.h"
#include "Snap7Base.h"
CtrlCmdThread::CtrlCmdThread(CSnap7Base* owner, QObject* parent)
: QThread(parent)
, m_owner(owner)
{
}
void CtrlCmdThread::run()
{
if (!m_owner)
return;
try
{
m_owner->ctrlCmdThreadFunc();
}
catch (...)
{
}
}
业务处理函数:ctrlCmdThreadFunc(); 在主线程中处理,通过指针调用实现;heartbeatThreadFunc();
motorCmdThreadFunc();
业务说明:
例如上位机下发电机控制命令: 正向移动 反向移动 停止 弹模结束
1、上位机下发的命令,保存内存中,不直接操作设备,否则会出现堵塞(上位机下发的命令周期短,PLC命令执行完成的周期长)
例如:其中 ST_MOTOR_CTRL_CMD 有上位机下发的动作参数:方向 移动量和速度
void CSnap7Base::OnMessageReceivedMotorCmd(const ST_MOTOR_CTRL_CMD & data)
{
//m_iMotorMoveType: 0:停止 1:正向运动 2:反向运动
QMutexLocker locker(&m_motorCmdMutex);
//将 int 强转为枚举类型,方便阅读
MotorMoveType moveType = static_cast<MotorMoveType>(data.m_iMotorMoveType);
switch (moveType)
{
case MOTOR_ESTOP: // 3: 急停
m_bEstopPending = true; // 急停抢占
m_bHasLatestMove = false; // 丢弃未执行的普通移动
break;
case MOTOR_MOLD_DONE: // 4: 完成弹模
// 清空待处理命令,清空急停,回到空闲状态
m_bHasLatestMove = false;
m_bEstopPending = false;
// 如果此时电机序列走到一半,强制打断回到 IDLE(或者你想等它走完就不断言)
m_motorState = M_IDLE;
break;
case MOTOR_FORWARD: // 1: 正向运动
case MOTOR_REVERSE: // 2: 反向运动
case MOTOR_STOP: // 0: 停止 (如果停止也要发给PLC的话)
// 存入最新指令,等待线程执行
m_latestMove = data;
m_bHasLatestMove = true;
break;
default:
break;
}
}
线程函数:
//电机线程主循环 (上面示例不是这个线程函数,没关系都类似)
void CSnap7Base::motorCmdThreadFunc()
{
while (true)
{
// 1) 线程退出检测
{
QMutexLocker locker(&m_motorCmdMutex);
if (m_motorThreadStop) return;
}
qint64 now = QDateTime::currentMSecsSinceEpoch();
// 2) 急停抢占
{
QMutexLocker locker(&m_motorCmdMutex);
if (m_bEstopPending && !isEstopState(m_motorState))
{
m_bEstopPending = false;
m_bHasLatestMove = false;
m_motorState = M_ESTOP_DISABLE;
m_motorNextMs = now;
}
}
// 3) 空闲状态(空转)
if (m_motorState == M_IDLE)
{
QMutexLocker locker(&m_motorCmdMutex);
if (m_bHasLatestMove)
{
// 收到移动指令,启动完整序列
m_runningCmd = m_latestMove;
m_bHasLatestMove = false;
m_motorState = M_MOVE_ALARM1;
m_motorNextMs = now;
}
else
{
// 没有指令,释放锁,睡 10ms 然后继续空转等待
QThread::msleep(10);
continue;
}
}
// 4) 未到下一步时间,按时间片睡眠
if (now < m_motorNextMs)
{
qint64 waitMs = m_motorNextMs - now;
if (waitMs > 10) waitMs = 10;
QThread::msleep((unsigned long)waitMs);
continue;
}
// 5) 执行一步(持短锁,不持锁 sleep)
bool bConnected = false;
{
QWriteLocker locker(&m_rwMutex);
bConnected = m_bConnected;
if (bConnected)
{
execOneMotorStep();
}
else
{
m_motorState = M_IDLE; // 掉线直接放弃
}
}
if (!bConnected) continue;
// 6) 安排下一次时间
if (m_motorState == M_DONE)
{
//完整的移动序列走完,变成空闲,等待下一次指令下发
m_motorState = M_IDLE;
}
else
{
m_motorNextMs = QDateTime::currentMSecsSinceEpoch() + 100;
}
}
}
//单步执行
void CSnap7Base::execOneMotorStep()
{
// 调用前提:已持有 m_rwMutex,且 m_bConnected == true
switch (m_motorState)
{
// ============ 急停序列 ============
case M_ESTOP_DISABLE:
m_SnapWriter->writeStepAxisDisable(1);
m_motorState = M_ESTOP_POS0;
break;
case M_ESTOP_POS0:
m_SnapWriter->writeStepRelativePos(0);
m_motorState = M_ESTOP_SPEED0;
break;
case M_ESTOP_SPEED0:
m_SnapWriter->writeStepRelativeSpeed(0);
m_motorState = M_ESTOP_ALARM1;
break;
case M_ESTOP_ALARM1:
m_SnapWriter->writeStepAlarmReset(1);
m_motorState = M_ESTOP_ALARM0;
break;
case M_ESTOP_ALARM0:
m_SnapWriter->writeStepAlarmReset(0);
m_motorState = M_DONE;
break;
// ============ 普通移动序列 ============
case M_MOVE_ALARM1:
m_SnapWriter->writeStepAlarmReset(1);
m_motorState = M_MOVE_ALARM0;
break;
case M_MOVE_ALARM0:
m_SnapWriter->writeStepAlarmReset(0);
m_motorState = M_MOVE_DISABLE0;
break;
case M_MOVE_DISABLE0:
m_SnapWriter->writeStepAxisDisable(0);
m_motorState = M_MOVE_ENABLE1;
break;
case M_MOVE_ENABLE1:
m_SnapWriter->writeStepAxisEnable(1);
m_motorState = M_MOVE_POS;
break;
case M_MOVE_POS:
m_SnapWriter->writeStepRelativePos(m_runningCmd.m_dMotorMoveValue);
m_motorState = M_MOVE_SPEED;
break;
case M_MOVE_SPEED:
m_SnapWriter->writeStepRelativeSpeed(30);
m_motorState = M_MOVE_CMD0;
break;
case M_MOVE_CMD0:
m_SnapWriter->writeStepRelativeCmd(0);
m_motorState = M_MOVE_CMD1;
break;
case M_MOVE_CMD1:
m_SnapWriter->writeStepRelativeCmd(1);
m_motorState = M_DONE;
break;
default:
m_motorState = M_IDLE;
break;
}
}
//启动
void CSnap7Base::startMotorCmdThread()
{
{
QMutexLocker locker(&m_motorCmdMutex);
m_motorThreadStop = false;
}
if (!m_motorThread)
{
m_motorThread = new MotorCmdThread(this);
m_motorThread->start();
}
}
//停止
void CSnap7Base::stopMotorCmdThread()
{
{
QMutexLocker locker(&m_motorCmdMutex);
m_motorThreadStop = false;
}
if (!m_motorThread)
{
m_motorThread = new MotorCmdThread(this);
m_motorThread->start();
}
}
总结扩展的步骤:
1、创建一个单独的线程;
2、该线程的所有函数在主线程类内部实现;
主要:通知处理函数、线程主函数、线程启动 线程停止和其他的中间过程函数。
3、扩展的线程类,在构造的时候,已经把主线程类的指针传过去,这样,所有的线程都可以使用一个指针,操作PLC设备。