尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

PLC Modbus 轮询卡顿?在 Qt 中实现异步读写队列与优先级调度,实测吞吐提升 3 倍

PLC Modbus 轮询卡顿?在 Qt 中实现异步读写队列与优先级调度,实测吞吐提升 3 倍 在 Qt 里用异步队列 优先级调度彻底解决轮询卡顿实测吞吐能从 80 帧/秒干到 240 帧/秒3 倍提升不吹牛。## 为什么卡同步轮询是原罪传统的 Modbus 轮询代码长这样看着简单实则埋雷。cpp// 错误示范同步阻塞轮询void Worker::poll() {while (m_running) {QThread::msleep(50); // 让出线程但GUI该卡还是卡QModbusDataUnit unit(ReadCoils, 0, 100);m_modbus-sendReadRequest(unit, 1); // 阻塞等待直到超时if (m_modbus-waitForResponse(1000)) {// 处理响应} else {// 重试逻辑这里一卡就是1秒}}}坑点在哪waitForResponse(1000) 是同步阻塞的PLC 响应慢时这个函数不会立刻返回。更致命的是如果你在 GUI 线程里干这事界面直接白屏。**别告诉我你没干过这事**——Qt 的 QModbusClient::waitForResponse 文档里白纸黑字写着“This function will block normally”谁用谁知道。## 解药异步写队列别让请求裸奔核心思路所有 Modbus 请求进队列由一个 QThread 专门消费绝不阻塞主线程。先定义请求结构体带上优先级和超时标志。cppstruct ModbusRequest {enum Priority { Low, High, Emergency };QModbusDataUnit::RegisterType type;int startAddr;int count;Priority priority;int timeoutMs; // 每个请求独立超时避免全局拖垮int retryCount; // 自动重试次数std::functionvoid(QModbusDataUnit) callback; // 完成回调在QT主线程执行};实现一个 ModbusQueue 类内部用 QList 做优先队列每来一个请求按优先级插队。这样紧急写操作比如急停位不会被 100 个读操作挤在最后面。cppclass ModbusQueue : public QObject {Q_OBJECTpublic:void enqueue(const ModbusRequest req) {QMutexLocker locker(m_mutex);if (req.priority ModbusRequest::Emergency) {// 紧急请求插到队首但不覆盖已有紧急请求m_queue.prepend(req);} else {// 低优先级排后面高优先级排前面auto it m_queue.end();for (auto r m_queue.begin(); r ! m_queue.end(); r) {if (r-priority req.priority) {it r;break;}}m_queue.insert(it, req);}}bool tryDequeue(ModbusRequest req) {QMutexLocker locker(m_mutex);if (m_queue.isEmpty()) return false;req m_queue.takeFirst();return true;}private:QListModbusRequest m_queue;QMutex m_mutex;};**坑点1**QMutexLocker 行尾加分号这是最常见的低级错误编译器不会报错但锁会失效。**坑点2**这里用了 prepend 和 insert 的组合别用 std::priority_queue因为你需要动态调整优先级比如重试时提升优先级。## 核心异步调度器超时重试不卡UI有了队列还得有个消费者线程。它从队列取请求发异步请求然后等待信号——而不是阻塞。cppclass ModbusScheduler : public QObject {Q_OBJECTpublic:ModbusScheduler(QModbusClient* client, ModbusQueue* queue): m_client(client), m_queue(queue) {// 连接响应信号connect(m_client, QModbusClient::responseReceived,this, ModbusScheduler::onResponse);connect(m_client, QModbusClient::errorOccurred,this, ModbusScheduler::onError);}void start() {QThread* thread new QThread(this);QObject::connect(thread, QThread::started, this,[this]() { this-pollNext(); });moveToThread(thread);thread-start();}private slots:void pollNext() {// 关键必须从队列取请求然后发异步请求ModbusRequest req;if (m_queue-tryDequeue(req)) {m_currentReq req;QModbusDataUnit unit(req.type, req.startAddr, req.count);m_client-sendReadRequest(unit, 1);// 启动超时定时器独立于请求m_timeoutTimer-start(req.timeoutMs);} else {// 队列空了睡10ms避免CPU空转QTimer::singleShot(10, this, ModbusScheduler::pollNext);}}void onResponse(QModbusDataUnit unit) {m_timeoutTimer-stop();// 处理数据通过信号发回主线程不直接改UIemit dataReady(m_currentReq.startAddr, unit.values());pollNext(); // 继续下一个请求}void onError(QModbusDevice::Error error) {if (m_currentReq.retryCount 0) {m_currentReq.retryCount--;// 重试请求但提高优先级m_currentReq.priority ModbusRequest::High;m_queue-enqueue(m_currentReq);} else {emit requestFailed(m_currentReq.startAddr);}pollNext();}private:QModbusClient* m_client;ModbusQueue* m_queue;QTimer* m_timeoutTimer;ModbusRequest m_currentReq;};**坑点3**sendReadRequest 是异步的返回后不能立刻发下一个请求——必须等 responseReceived 信号。如果不等Modbus 从站会认为你发非法报文直接断连。这就是为什么 pollNext() 只能在 onResponse 或 onError 里调用。**坑点4**超时定时器不能对静态值。PLC 在程序扫描周期长的站点读保持寄存器可能耗时 100ms你固定 50ms 超时会导致频繁重试反而增加负载。## 数据回GUI信号通道别手痒去this-ui调度器跑在独立线程数据要通过 emit 发到主线程然后在主线程里更新界面。cppclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow() {auto* scheduler new ModbusScheduler(m_client, m_queue);scheduler-start();connect(scheduler, ModbusScheduler::dataReady, this,[this](int addr, QVariantList values) {// 这个lambda在GUI线程执行安全更新UIui-tableWidget-setItem(0, 0,new QTableWidgetItem(values.at(0).toString()));});}};**坑点5**别把 QModbusClient 对象误放主线程否则信号跨线程连接会变得异常慢。在 start() 前把 m_client moveToThread 到调度线程——我这里为了简化没写但你记得做。实测这样改完后UI 全程流畅即使 PLC 有 2 秒无响应界面不卡无响应会在后台重试逻辑正确。## 性能对比80 到 240真实数据别听我吹上个简单基准。同一台工控机 i5-7500同一个西门子 S7-1200读 10 个保持寄存器。| 方案 | 平均吞吐(帧/秒) | CPU占用 | UI卡顿 ||------|---------------|--------|-------|| 同步轮询 50ms间隔 | 82 | 15% | 严重 || 异步队列 无优先级 | 210 | 9% | 无 || 异步队列 优先级 | 246 | 11% | 无 |数据流水线是pollNext() 立刻发下一个请求不需要等当前响应处理完再发——前提是 Modbus 从站支持 pipelineS7-1200 OK。如果你的从站不支持这个数字会打折扣但关键是不卡了。### 附不支持pipeline的PLC怎么办如果从站是老旧台达或者三菱 FX不支持连续发送报文你需要在 onResponse 里加 QThread::msleep(10) 做一些延时否则会丢帧。但即便这样吞吐也有 150且 UI 稳定。## 总结这套路的核心就三句话1. **永远别在 GUI 线程做 Modbus 同步请求**——这是你卡顿的根源。2. **用队列解耦请求生命周期**优先级插队保证了急停操作能及时响应。3. **异步信号驱动状态机**超时和重试放在独立线程UI 永远没有被阻塞的理由。
返回列表