From f9a4b25d7d549f6a707f62c1e7611bf0be68bc94 Mon Sep 17 00:00:00 2001 From: Byons Date: Thu, 30 Jul 2026 11:24:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=8B=E5=8A=9B=E8=B0=83=E8=8A=82=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- InkJetPrinter.cpp | 334 +++++++++++++++++++++++++++------------ docs/项目架构说明文档.md | 22 +-- mainwidget.cpp | 30 +--- testprintwidget.cpp | 26 ++- 4 files changed, 260 insertions(+), 152 deletions(-) diff --git a/InkJetPrinter.cpp b/InkJetPrinter.cpp index 64606ff..3731840 100644 --- a/InkJetPrinter.cpp +++ b/InkJetPrinter.cpp @@ -347,6 +347,92 @@ struct _pid{ float integral_max; //积分限幅(根据实际调整) }pid; +// 双模式PID参数配置 +struct DualModePIDParams { + // 快速收敛模式(大偏差) + float fast_Kp; + float fast_Ki; // 通常为0 + float fast_Kd; + float fast_maxDelta; // 单次调整限幅 + int fast_cycle; // 调整周期(单位:100ms) + + // 精准稳压模式(小偏差) + float precision_Kp; + float precision_Ki; + float precision_Kd; + float precision_maxDelta; + int precision_cycle; + float integral_limit; // 积分限幅 + int stable_threshold; // 稳定判定阈值 + int stable_count_req; // 需要稳定的次数 + + // 模式切换阈值 + float switch_threshold; // 切换到精准模式的误差阈值 +}; + +// 无分流管设备参数 +static DualModePIDParams params_no_reclaim = { + // 快速模式 + 0.10f, // fast_Kp + 0.0f, // fast_Ki + 0.05f, // fast_Kd + 10.0f, // fast_maxDelta + 8, // fast_cycle (800ms) + + // 精准模式 + 0.025f, // precision_Kp + 0.010f, // precision_Ki + 0.03f, // precision_Kd + 3.0f, // precision_maxDelta + 10, // precision_cycle (1000ms) + 250.0f, // integral_limit + 10, // stable_threshold + 10, // stable_count_req + + 50.0f // switch_threshold +}; + +// 有分流管设备参数 +static DualModePIDParams params_with_reclaim = { + // 快速模式 + 0.12f, // fast_Kp + 0.0f, // fast_Ki + 0.08f, // fast_Kd + 12.0f, // fast_maxDelta + 8, // fast_cycle (800ms) + + // 精准模式 + 0.035f, // precision_Kp + 0.015f, // precision_Ki + 0.05f, // precision_Kd + 4.0f, // precision_maxDelta + 10, // precision_cycle (1000ms) + 300.0f, // integral_limit + 15, // stable_threshold + 8, // stable_count_req + + 50.0f // switch_threshold +}; + +// PID控制器状态 +struct PIDController { + float integral; // 积分累积 + float last_actual; // 上次实际值(用于微分先行) + int adjust_skip; // 周期计数器 + int stable_count; // 稳定计数器 + bool in_precision_mode; // 当前是否在精准模式 + + void reset() { + integral = 0.0f; + last_actual = 0.0f; + adjust_skip = 0; + stable_count = 0; + in_precision_mode = false; + } +}; + +static PIDController pid_ctrl; + void PID_init(){ pid.SetSpeed=0.0; @@ -358,6 +444,88 @@ void PID_init(){ pid.Kp=0.21; //0.25 pid.Ki=0.006; //0.015 pid.Kd=0.0; + + // 重置双模式PID控制器 + pid_ctrl.reset(); +} + +// 微分先行PID计算函数 +// derivative_on_measurement: 只对实际值求微分,避免设定值突变冲击 +float DualModePID_Calculate(float setpoint, float actual, const DualModePIDParams& params, + PIDController& ctrl, float u_current, float u_min, float u_max) { + float error = setpoint - actual; + float abs_error = (error < 0) ? -error : error; + + // 判断当前应该使用哪种模式 + bool use_precision = (abs_error <= params.switch_threshold); + + float Kp, Ki, Kd, max_delta; + + if (use_precision) { + // 精准稳压模式 + Kp = params.precision_Kp; + Ki = params.precision_Ki; + Kd = params.precision_Kd; + max_delta = params.precision_maxDelta; + ctrl.in_precision_mode = true; + } else { + // 快速收敛模式 + Kp = params.fast_Kp; + Ki = params.fast_Ki; // 通常为0 + Kd = params.fast_Kd; + max_delta = params.fast_maxDelta; + ctrl.in_precision_mode = false; + + // 快速模式下清零积分,避免积分累积 + ctrl.integral = 0.0f; + } + + // 比例项 + float p_term = Kp * error; + + // 积分项(带抗饱和) + float i_term = 0.0f; + if (use_precision && Ki > 0.0f) { + // 积分分离:只在精准模式下累积 + // 积分抗饱和:转速到达限位时停止累积 + bool at_upper_limit = (u_current >= u_max - 1.0f) && (error > 0); + bool at_lower_limit = (u_current <= u_min + 1.0f) && (error < 0); + + if (!at_upper_limit && !at_lower_limit) { + ctrl.integral += error; + + // 积分限幅 + if (ctrl.integral > params.integral_limit) { + ctrl.integral = params.integral_limit; + } else if (ctrl.integral < -params.integral_limit) { + ctrl.integral = -params.integral_limit; + } + } + + i_term = Ki * ctrl.integral; + } + + // 微分先行:只对实际值求微分,避免设定值突变时微分项冲击 + // 传统微分:Kd * (error - error_last) = Kd * [(set-act) - (set_last-act_last)] + // 微分先行:-Kd * (actual - actual_last) 当设定值不变时两者等价,但设定值变化时微分先行无冲击 + float d_term = 0.0f; + if (Kd > 0.0f) { + float derivative = -(actual - ctrl.last_actual); // 负号因为是对实际值求导 + d_term = Kd * derivative; + ctrl.last_actual = actual; + } + + // PID输出 + float delta_u = p_term + i_term + d_term; + + // 输出限幅 + if (delta_u > max_delta) { + delta_u = max_delta; + } else if (delta_u < -max_delta) { + delta_u = -max_delta; + } + + return delta_u; } static float u = 1.0; @@ -387,7 +555,7 @@ float PID_realize2(float speed, float act){ pid.err = pid.SetSpeed - pid.ActualSpeed; float delta_u = pid.Kp * (pid.err - pid.err_last) - + pid.Ki * pid.err; + + pid.Ki * pid.err; // 自适应非对称限幅: // 压力严重偏高(err < -30):允许快速减速,限制慢速加速 @@ -420,12 +588,14 @@ void InkJetPrinter::pressureAdj() m_stableCount = 0; for(int i = 0; i < 10; i++) m_pressureBuf[i] = 0; m_bufIdx = 0; + pid_ctrl.reset(); // 重置双模式PID控制器 sysPara->devOp->WriteMoterPump(); return; } if(!sysPara->para.gearPumpEnable) { + // 非使能状态保留旧PID逻辑 if(u > 0 || ((sysPara->para.pressure - sysPara->status.PRESSURE) > 0)) u = PID_realize((float)sysPara->para.pressure, (float)sysPara->status.PRESSURE); sysPara->para.gearPumpValue = (u > 0) ? (unsigned short)u : 0; @@ -435,6 +605,11 @@ void InkJetPrinter::pressureAdj() return; } + // 根据是否有分流管选择参数集 + const DualModePIDParams& params = sysPara->para.flagOfReclaimPumEnable + ? params_with_reclaim + : params_no_reclaim; + int maxValue = sysPara->para.flagOfReclaimPumEnable ? 420 : 800; if(maxValue < 60) maxValue = 60; @@ -447,9 +622,8 @@ void InkJetPrinter::pressureAdj() int actual = sysPara->status.PRESSURE; int target = (int)sysPara->para.pressure; - int cur; - // 每次进入都更新滑动平均缓冲(软启动用瞬时值,闭环用平均值) + // 每次进入都更新滑动平均缓冲 m_pressureBuf[m_bufIdx] = actual; m_bufIdx = (m_bufIdx + 1) % 10; int avgPressure = 0; @@ -458,7 +632,6 @@ void InkJetPrinter::pressureAdj() // --------------------------------------------------------------- // 硬保护:压力超过目标+50,立即强制减速,优先级最高 - // 覆盖自检、软启动、闭环所有阶段 // --------------------------------------------------------------- if(actual > target + 50) { @@ -467,142 +640,104 @@ void InkJetPrinter::pressureAdj() if(delta < -15.0f) delta = -15.0f; u += delta; clampU(); - m_lastPressure = actual; + + // 超调时清零积分,避免积分饱和 + pid_ctrl.integral = 0.0f; + sysPara->para.gearPumpValue = (unsigned short)(int)u; sysPara->devOp->WriteMoterPump(); return; } // --------------------------------------------------------------- - // 软启动:自适应步长,每 800ms 一次 - // 自检期间也走此逻辑(硬保护兜底),自检完成后重置重新收敛 - // 软启动阶段不做压力稳定检测,优先快速收敛 + // 双模式PID控制:根据误差大小自动切换快速/精准模式 + // 快速模式:大偏差快速收敛,周期800ms + // 精准模式:小偏差精准稳压,周期1000ms,等待压力稳定后调整 // --------------------------------------------------------------- - if(!m_rampDone) + + // 判断当前应该使用哪种模式 + float error = (float)(target - actual); + float abs_error = (error < 0) ? -error : error; + bool use_precision = (abs_error <= params.switch_threshold); + + int cycle = use_precision ? params.precision_cycle : params.fast_cycle; + + // 周期控制 + if(++pid_ctrl.adjust_skip < cycle) { - if(++m_adjSkip < 8) - { - sysPara->para.gearPumpValue = (unsigned short)(int)u; - sysPara->devOp->WriteMoterPump(); - return; - } - m_adjSkip = 0; - - int err = target - actual; - - if(err < 0) - { - // 超压:小步减速 - u -= 1.0f; - } - else - { - // 自适应步长:差得远步子大,差得近步子小 - float step; - if (err > 200) step = 5.0f; - else if(err > 100) step = 3.0f; - else if(err > 50) step = 2.0f; - else step = 1.0f; - u += step; - } - - clampU(); - - // 转速上限保护 - if(sysPara->para.flagOfReclaimPumEnable && - sysPara->status.gearPumpRunSpeed >= maxSpeed) - { - u -= 3.0f; - clampU(); - } - - cur = (int)u; - - // 进入闭环条件:压力已稳定在目标±50以内 - if(sysPara->status.selfCheckDone && err >= -50 && err <= 50) - { - m_rampDone = true; - m_adjSkip = 0; - } - - sysPara->para.gearPumpValue = (unsigned short)cur; + sysPara->para.gearPumpValue = (unsigned short)(int)u; sysPara->devOp->WriteMoterPump(); return; } - // --------------------------------------------------------------- - // 闭环:等压力稳定后再调速,自适应管路响应时间 - // - // 每100ms采样一次,连续3次压力变化<15认为稳定,才执行调速 - // 最长等待5s(50次)兜底,防止管路持续抖动导致永远不调 - // - // 调速后立即重置稳定计数,重新等待下一次稳定 - // - // 分区Kp:误差大步子大,误差小步子小 - // |err| > 100 → Kp=0.05,限幅±5 - // |err| 50~100→ Kp=0.04,限幅±4 - // |err| < 50 → Kp=0.03,限幅±3 - // --------------------------------------------------------------- + // 精准模式需要等待压力稳定 + if(use_precision) { // 稳定检测用平均值,滤掉传感器噪声 int pressureChange = avgPressure - m_lastPressure; if(pressureChange < 0) pressureChange = -pressureChange; m_lastPressure = avgPressure; - m_adjSkip++; // 复用为超时计数 - bool stable = (pressureChange < 10); - bool timeout = (m_adjSkip >= 50); // 5s兜底 + bool stable = (pressureChange < params.stable_threshold); if(stable) - m_stableCount++; + pid_ctrl.stable_count++; else - m_stableCount = 0; + pid_ctrl.stable_count = 0; - bool readyToAdj = (m_stableCount >= 15) || timeout; - - if(!readyToAdj) + // 如果还未稳定,继续等待 + if(pid_ctrl.stable_count < params.stable_count_req) { sysPara->para.gearPumpValue = (unsigned short)(int)u; sysPara->devOp->WriteMoterPump(); return; } - // 平均压力已稳定(或超时兜底),执行调速,重置计数 - m_adjSkip = 0; - m_stableCount = 0; + // 压力已稳定,执行调速,重置计数 + pid_ctrl.adjust_skip = 0; + pid_ctrl.stable_count = 0; - // 误差用平均值计算,避免瞬时噪声影响调速幅度 - float absErr = (float)(target - avgPressure); - if(absErr < 0.0f) absErr = -absErr; - - float kp, capDelta; - if(absErr > 100.0f) { kp = 0.05f; capDelta = 5.0f; } - else if(absErr > 50.0f) { kp = 0.04f; capDelta = 4.0f; } - else { kp = 0.03f; capDelta = 3.0f; } - - float err = (float)(target - avgPressure); - float delta_u = kp * err; - - if(delta_u > capDelta) delta_u = capDelta; - if(delta_u < -capDelta) delta_u = -capDelta; + // 精准模式使用平均压力计算 + float delta_u = DualModePID_Calculate((float)target, (float)avgPressure, params, + pid_ctrl, u, 60.0f, (float)maxValue); // 转速上限软限制:转速已达上限时禁止继续加速 if(sysPara->para.flagOfReclaimPumEnable && - sysPara->status.gearPumpRunSpeed >= maxSpeed) + sysPara->status.gearPumpRunSpeed >= maxSpeed) { if(delta_u > 0.0f) delta_u = 0.0f; } u += delta_u; clampU(); - cur = (int)u; - - sysPara->para.gearPumpValue = (unsigned short)cur; - sysPara->devOp->WriteMoterPump(); } + else + { + // 快速模式:立即调整,重置计数 + pid_ctrl.adjust_skip = 0; + pid_ctrl.stable_count = 0; + + // 快速模式使用瞬时压力计算 + float delta_u = DualModePID_Calculate((float)target, (float)actual, params, + pid_ctrl, u, 60.0f, (float)maxValue); + + // 转速上限软限制 + if(sysPara->para.flagOfReclaimPumEnable && + sysPara->status.gearPumpRunSpeed >= maxSpeed) + { + if(delta_u > 0.0f) delta_u = 0.0f; + } + + u += delta_u; + clampU(); + } + + sysPara->para.gearPumpValue = (unsigned short)(int)u; + sysPara->devOp->WriteMoterPump(); } + static bool firstTime = true; extern void MySleep(unsigned int msec); extern unsigned char test_flag; @@ -3256,5 +3391,6 @@ void InkJetPrinter::onDlgFileOpen(const QString& filename) return; } - loadWorkFileAndPreparePrint(filename); + auto ret = loadWorkFileAndPreparePrint(filename); + LogInfo::setRunStatusText(QString("Work file opened: %1, result: %2").arg(filename).arg(ret)); } diff --git a/docs/项目架构说明文档.md b/docs/项目架构说明文档.md index 12a67c4..bc2badb 100644 --- a/docs/项目架构说明文档.md +++ b/docs/项目架构说明文档.md @@ -469,17 +469,17 @@ PicScene (QGraphicsScene) # 场景,管理所有图元 └─── 软解动态图元 (PicDateItem PicScheduleShiftItem PicCodeItem) └── onPreviewScene() 与静态图元一样,进入onPreviewScene() └── variableItemsUpdate() 动态检查图元是否发生变化,更新数据 - └── SendPrintDataToFpga() 局部发送更新后的点阵数据至FPGA + └── SendPrintDataToFpga() 发送更新后的点阵数据至FPGA ``` **打印内容相关的几个重要的函数:** -[SendPrintDataToFpga()](../mainwidget.cpp#L935) 负责将打印数据、打印设置和变量控制通过串口发送给FPGA,FPGA接收后驱动喷头进行喷印。该函数被设计成支持全量更新与局部更新,静态图元在打印开始前一次性发送,而动态图元则在每次变化时局部更新。局部更新时,SendPrintDataToFpga不会发送打印设置和变量控制数据,只会按列为单位更新打印缓冲区的点阵数据。 +[SendPrintDataToFpga()](../mainwidget.cpp#L935) 负责将打印数据、打印设置和变量控制通过串口发送给FPGA,FPGA接收后驱动喷头进行喷印。静态图元在打印开始前一次性发送,而动态图元则在每次变化时更新打印数据。更新打印数据时,SendPrintDataToFpga不会发送打印设置和变量控制数据,只会按列为单位更新打印缓冲区的点阵数据。 [imageToPrintData()](../mainwidget.cpp#L675) 将 QImage 转换为点阵数据,该函数可将图像填充至打印缓冲区的指定位置,随后由 SendPrintDataToFpga() 发送至FPGA。动态软解图元(如日期、计数器、二维码)在每次变化时调用该函数刷新打印缓冲区。 [onPreviewScene()](../mainwidget.cpp#L1888) 是场景预览的核心函数,负责将场景中的所有图元渲染为 QImage,并调用 imageToPrintData() 全量写入打印缓冲区。 -[variableItemsUpdate()](../mainwidget.cpp#L1024) 动态检查图元是否发生变化,配合 imageToPrintData()、SendPrintDataToFpga() 实现局部更新打印数据。 +[variableItemsUpdate()](../mainwidget.cpp#L1024) 动态检查图元是否发生变化,配合 imageToPrintData()、SendPrintDataToFpga() 实现更新打印数据。 ### 7.2 从文件到打印的数据流向 @@ -546,7 +546,6 @@ RFIDManager::sig_showMsg ──→ InkJetPrinter::slt_showMsg() ``` - --- ## 8. RFID刷卡模块 @@ -560,7 +559,6 @@ RFIDManager::sig_showMsg ──→ InkJetPrinter::slt_showMsg() **功能**: - 墨水/溶剂添加验证(刷卡 + 物理添加) - 保养卡识别与提醒 -- 自动扣减卡内余额 - 卡片使用标记(防重复使用) - 超级卡的识别与处理 @@ -588,7 +586,7 @@ RFIDManager::sig_showMsg ──→ InkJetPrinter::slt_showMsg() **目标平台**: - ARM Linux: `TARGET = QtUI` -- 交叉编译: 使用 `linux-arm-g++` mkspec +- 交叉编译: 使用 `linux-aarch64-gnu-g++` mkspec - x86桌面: 使用 `linux-g++` mkspec,用于开发调试 ### 9.2 模块依赖 @@ -598,17 +596,21 @@ QT += core gui network serialport virtualkeyboard sql xml ``` ### 9.3 编译命令 +下面给出 InkjetUbuntu-T1 环境下调试、发布的编译方式 ```bash # ARM交叉编译 -qmake -spec /usr/local/Trolltech/QtEmbedded-4.6.3-arm/mkspecs/qws/linux-arm-g++ \ - "DEFINES+=ARM" "DEFINES+=QTV3" "DEFINES+=VNLG" +cd /path_to_your_arm_build_dir/ && /home/inkjet/QtSDK/bin/qmake /path_to_your_sources_dir/QtUI.pro -spec linux-aarch64-gnu-g++ CONFIG+=release +make -j # x86桌面编译(调试) -qmake "DEFINES+=X86TEST" -make +cd /path_to_your_x86_build_dir/ && /home/inkjet/QtSDKPC/bin/qmake /path_to_your_sources_dir/QtUI.pro -spec linux-g++ CONFIG+=debug CONFIG+=qml_debug +make -j ``` +### 9.4 发行编译 +VX5 Release编译发行.md 给出了如何发布带崩溃转储的程序的流程 + --- ## 10. 关键配置与宏定义 diff --git a/mainwidget.cpp b/mainwidget.cpp index 2537d00..4821190 100644 --- a/mainwidget.cpp +++ b/mainwidget.cpp @@ -416,8 +416,6 @@ void MainWidget::generateCharRamData() p_var->original = false; } - int j; - int width; if(p_var->bassAddr == -1){ if(sysPara->para.flagOfModuleDecimal || sysPara->para.flagOfModuleHide) @@ -531,32 +529,6 @@ QList MainWidget::variableItemsUpdate() return QList(); } - /* - 旧实现: - int totalOfProcessedDots = 0; - int totalOfDatetimeNumber = 0; - int nbInvaliedDatetimePreview = 0; - int nbClipDatetimePreview = 0; - - QList updateAreas; - - auto items = pPicScene->items(); - for(auto i : items) { - PicTextItem* item = dynamic_cast(i); - if(!item) - continue; - - ... - - auto boundingRect = pPicScene->itemsBoundingRect().toRect(); - QRect itemRect(item->pos().x() - boundingRect.x(), item->pos().y() - boundingRect.y(), img.width(), img.height()); - ... - - QRect intersectRect = itemRect.intersected(sceneRect); - ... - } - */ - int totalOfProcessedDots = 0; int totalOfDatetimeNumber = 0; int nbInvaliedDatetimePreview = 0; @@ -1204,7 +1176,7 @@ void MainWidget::initSheet() ui.m_StopBtn->setStyleSheet(stopBtnSheet); - constexpr char* reclaimBtnNormalStyle = R"( + const char* reclaimBtnNormalStyle = R"( QToolButton { padding: 16px 16px; border-radius: 6px; diff --git a/testprintwidget.cpp b/testprintwidget.cpp index 9747877..52d9e70 100644 --- a/testprintwidget.cpp +++ b/testprintwidget.cpp @@ -427,22 +427,20 @@ void TestPrintWidget::openWorkFile(QString filePath){ // 将场景中所有项移动,使 pPicScene->itemsBoundingRect() 的左上角对齐到 scene 的 (0,0) // 计算当前边界并做平移(仅在有偏移时执行) - { - QRectF bounds = pPicScene->itemsBoundingRect(); - if (!bounds.isNull()) { - qreal dx = -bounds.x(); - qreal dy = -bounds.y(); - if (!qFuzzyIsNull(dx) || !qFuzzyIsNull(dy)) { - const auto items = pPicScene->items(); - for (QGraphicsItem* it : items) { - // 仅移动属于场景根的项,避免重复移动子项 - if (!it) continue; - QPointF oldPos = it->pos(); - it->setPos(oldPos + QPointF(dx, dy)); - } + QRectF bounds = pPicScene->itemsBoundingRect(); + if (!bounds.isNull()) { + qreal dx = -bounds.x(); + qreal dy = -bounds.y(); + if (!qFuzzyIsNull(dx) || !qFuzzyIsNull(dy)) { + const auto items = pPicScene->items(); + for (QGraphicsItem* it : items) { + // 仅移动属于场景根的项,避免重复移动子项 + if (!it) continue; + QPointF oldPos = it->pos(); + it->setPos(oldPos + QPointF(dx, dy)); } } - } + } pViewer->centerOn(0, 0);