diff --git a/AGENTS.md b/AGENTS.md index 8341baf..38e05c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,9 +155,14 @@ - v0.2.6:对齐修复——双列改放同一滚动容器(整页统一滚动):原先两列各自包 QScrollArea 独立滚动,窗口化下底边错位(全屏正常);现两列高度锁定一致, 不足一屏各自拉伸到底、超出一屏整页滚动。 - - selftest 全过(累计新增 14 项回归用例);exe 已本地打包冒烟,Gitea 发布 - 待用户确认后执行。 - 遗留见其 README"已知事项":应用图标、USB-RS485 适配器更换。 + - v0.2.7:图标接入(logos/ 下 ico/png/svg 已就位)——exe 嵌入图标 + (build_exe.py --icon,此前缺文件自动跳过)、窗口标题栏/任务栏/弹窗图标 + (app+window 两级 setWindowIcon,_icon_path() 兼容源码态与 _MEIPASS 打包态)、 + Windows 任务栏独立标识 AppUserModelID "hulk.FilmForgeGUI"。README"无应用 + 图标"遗留项关闭。 + - selftest 全过(累计新增 15 项回归用例,含窗口图标非空检查);exe 已本地 + 打包冒烟,Gitea 发布待用户确认后执行。 + 遗留见其 README"已知事项":USB-RS485 适配器更换。 - [ ] **LED 引脚确认**:FilmForge 自制板 LED 引脚未定(当前沿用模板默认 PA15, 仅为构建占位),按原理图修改。 - [ ] **delay_10us 校准**:M4@168MHz 系数为估算值(/300000),需实测修正。 diff --git a/GUI/README.md b/GUI/README.md index cb91e76..1275958 100644 --- a/GUI/README.md +++ b/GUI/README.md @@ -87,4 +87,3 @@ python publish_gitea.py release/FilmForge-GUI-v<版本>-win64.zip --replace --in (设备单向推送),不受影响。 - 加热(M3S1)为真实 PID 闭环:开启后模具真实升温(NTC 故障/过温 150℃ 固件自动 断电保护,上电默认关闭);当前占空比无查询命令,从温度曲线间接观察。 -- 打包暂无应用图标(`logos/filmforge.ico` 缺位,build_exe.py 自动跳过)。 diff --git a/GUI/app_fluent.py b/GUI/app_fluent.py index 8d3fbfd..ce253bb 100644 --- a/GUI/app_fluent.py +++ b/GUI/app_fluent.py @@ -46,7 +46,7 @@ if getattr(sys, "frozen", False) and os.name == "nt": pass from PySide6.QtCore import QPointF, QRectF, Qt, QTimer -from PySide6.QtGui import QColor, QFont, QPainter, QPainterPath, QPen, QPalette +from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPainterPath, QPen, QPalette from PySide6.QtWidgets import ( QApplication, QHBoxLayout, QLabel, QMainWindow, QMessageBox, QPlainTextEdit, QScrollArea, QSplitter, QVBoxLayout, QWidget, @@ -60,7 +60,7 @@ import config import filmforge_rs485 as ff APP_NAME = "FilmForge" -APP_VERSION = "0.2.6" +APP_VERSION = "0.2.7" REPO_URL = "https://gitea.hulk.wang/hulk/FilmForge" MONO = QFont("Consolas", 10) CHIP_IDLE = "#666666" @@ -364,6 +364,9 @@ class App(QMainWindow): self._system_palette = app.palette() if app else QPalette() self._system_style = app.styleSheet() if app else "" self.setWindowTitle(f"{APP_NAME} v{APP_VERSION} — 热压模具温控监视台") + icon = _icon_path() # 标题栏图标(run_gui 另设 app 级 + 任务栏标识) + if os.path.exists(icon): + self.setWindowIcon(QIcon(icon)) self.resize(1440, 920) self.setMinimumSize(1150, 720) @@ -1326,6 +1329,15 @@ def _single_instance(): return True +def _icon_path(): + """窗口图标路径: 打包版取 _MEIPASS(build_exe.py --add-data 内嵌), 源码版取 logos/。""" + bundled = os.path.join(getattr(sys, "_MEIPASS", ""), "filmforge.ico") + if os.path.exists(bundled): + return bundled + return os.path.join(os.path.dirname(os.path.abspath(__file__)), + "logos", "filmforge.ico") + + def run_gui(auto_close=0): if not _single_instance(): app = QApplication.instance() or QApplication(sys.argv) @@ -1333,6 +1345,15 @@ def run_gui(auto_close=0): return app = QApplication.instance() or QApplication(sys.argv) app.setApplicationName(APP_NAME) + if os.name == "nt": + try: # 任务栏独立标识: 缺省时任务栏可能沿用宿主默认图标并错误分组 + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "hulk.FilmForge.GUI") + except Exception: + pass + icon = _icon_path() + if os.path.exists(icon): + app.setWindowIcon(QIcon(icon)) # 主窗/弹窗/任务栏统一图标 setTheme(Theme.AUTO) window = App(auto_close) window.show() diff --git a/GUI/logos/filmforge.ico b/GUI/logos/filmforge.ico new file mode 100644 index 0000000..59789dc Binary files /dev/null and b/GUI/logos/filmforge.ico differ diff --git a/GUI/logos/filmforge.png b/GUI/logos/filmforge.png new file mode 100644 index 0000000..73e69ac Binary files /dev/null and b/GUI/logos/filmforge.png differ diff --git a/GUI/logos/filmforge.svg b/GUI/logos/filmforge.svg new file mode 100644 index 0000000..1e8a63a --- /dev/null +++ b/GUI/logos/filmforge.svg @@ -0,0 +1,6 @@ + + + + FILM + Forge + diff --git a/GUI/selftest_gui.py b/GUI/selftest_gui.py index d7a46a1..8c0fdc1 100644 --- a/GUI/selftest_gui.py +++ b/GUI/selftest_gui.py @@ -40,6 +40,9 @@ window = app_fluent.App(0) window.show() app.processEvents() +# 0) 图标链路: logos/filmforge.ico 存在时窗口图标必须已加载 +check('窗口图标已设置', not window.windowIcon().isNull(), True) + # 1) 空数据渲染不抛异常(占位文案) img = QImage(window.chart.size(), QImage.Format_ARGB32) window.chart.render(img) diff --git a/Inc/board_config.h b/Inc/board_config.h index 72a5a7e..a844f4c 100644 --- a/Inc/board_config.h +++ b/Inc/board_config.h @@ -149,13 +149,15 @@ void usart1_irq_handler(void); #define HEATER_EN_RCU RCU_GPIOB #define HEATER_EN_PORT GPIOB #define HEATER_EN_PIN GPIO_PIN_12 -#define HEATER_PWM_PERIOD_MS 1000U /* PWM 载波周期(1s,恒 1Hz 开关频率) */ -#define HEATER_PWM_STEP_PCT 10U /* 占空比步进(每档脉宽 = 100ms) */ +#define HEATER_PWM_PERIOD_MS 2000U /* PWM 载波周期(2s,载波 0.5Hz) */ +#define HEATER_PWM_STEP_PCT 5U /* 占空比步进(每档脉宽 = 100ms,维持最小开关频率约束) */ #define HEATER_PID_PERIOD_MS 500U /* PID 计算周期 */ #define HEATER_PID_KP 10.0f /* PID 参数:超温前退功率更快(配合超温强制关断) */ #define HEATER_PID_KI 0.05f #define HEATER_PID_KD 30.0f /* 微分阻尼:按误差变化率提前刹车(抑制爬升惯性) */ -#define HEATER_RAMP_CPS 0.8f /* 目标斜坡速率(℃/s):内部设定值缓升,抑制热滞后过冲 */ +#define HEATER_SLOWBAND_C 7.0f /* 温和带宽度:距目标小于此值时占空上限受限 + (低速爬完最后一段,减少蓄热惯性过冲) */ +#define HEATER_SLOWBAND_DUTY 25U /* 温和带内占空上限(%) */ #define HEATER_OVERTEMP_C 150.0f /* 过温强制关断阈值(滞回保护) */ #define HEATER_OVERTEMP_HYST 5.0f /* 过温恢复滞回 */ diff --git a/Inc/heat.h b/Inc/heat.h index 4d0c88d..27af12e 100644 --- a/Inc/heat.h +++ b/Inc/heat.h @@ -40,7 +40,7 @@ void heat_set_target(float temp_c); float heat_get_target(void); /** - * @brief 查询当前量化占空比档位(0~10,对应 0~100%,10% 步进)。 + * @brief 查询当前量化占空比档位(0~20,对应 0~100%,5% 步进)。 */ uint8_t heat_get_duty(void); diff --git a/Src/heat.c b/Src/heat.c index 2ce1a7c..70a7301 100644 --- a/Src/heat.c +++ b/Src/heat.c @@ -1,10 +1,14 @@ /** * @file heat.c - * @brief 加热控制:慢 PWM 驱动(PB12)+ PID 控温闭环 + 安全保护。 + * @brief 加热控制:慢 PWM 驱动(PB12)+ 分段 PID 控温闭环 + 安全保护。 * @details 硬件:PB12=HEAT_EN,高 = 加热开(2N7002K 电平转换 → VBE2412 PMOS → 24V)。 - * 开关频率约束(<=10Hz)由"1s 载波周期 + 10% 占空步进"保证——每档脉宽 - * 恒为 100ms 的整数倍。PID 以 500ms 周期计算(NTC 后台采样 100ms), - * 输出量化到 0~10 档占空。 + * 开关频率约束(<=10Hz)由"2s 载波周期 + 5% 占空步进"保证——每档脉宽 + * 恒为 100ms 的整数倍(0.5Hz 载波、20 档占空分辨率)。 + * 控制为分段式: + * err >= 温和带 全量 PID(满功率爬升); + * 0 <= err < 温和带 正常 PID 但占空上限压到温和档(小功率缓升, + * 减少进带蓄热,降低越过目标后的惯性过冲); + * err < 0(超温) 强制关断 + 积分按负误差回退(执行器只能加热不能制冷)。 * @note 安全保护:上电默认关闭;NTC 传感器故障(OPEN/SHRT)强制关断; * 过温(>= HEATER_OVERTEMP_C)滞回保护;未使能强制关断。 */ @@ -19,20 +23,20 @@ /* 内部状态 */ static bool s_enable = false; /* 加热使能(M3) */ -static float s_target = 0.0f; /* 目标温度(M4,外部设定值) */ -static float s_ramp_target = 0.0f; /* 内部斜坡目标(以 HEATER_RAMP_CPS 滑向 s_target) */ -static uint8_t s_duty10 = 0; /* 量化占空档位 0~10(x10%) */ +static float s_target = 0.0f; /* 目标温度(M4) */ +static uint8_t s_duty = 0; /* 量化占空档位 0~HEATER_PWM_STEPS(x STEP%) */ static bool s_overtemp = false; /* 过温锁存(滞回恢复) */ static float s_integral = 0.0f; /* PID 积分项 */ static float s_prev_err = 0.0f; /* PID 上次误差 */ static uint16_t s_pwm_phase_ms = 0; /* PWM 载波相位 0..PERIOD-1 */ static uint16_t s_pid_divider = 0; /* PID 周期分频(10ms 圈) */ +/* 档位总数 = 100% / 步进% */ +#define HEATER_PWM_STEPS (100U / HEATER_PWM_STEP_PCT) + /******************************************************************************/ /* 底层输出 */ -static void heat_out(bool on); - static void heat_out(bool on) { if (on) { gpio_bit_set(HEATER_EN_PORT, HEATER_EN_PIN); @@ -61,21 +65,28 @@ static void pid_reset(void) { } /** - * @brief PID 计算(500ms 周期):误差 -> 占空档位 s_duty10(0~10)。 + * @brief 积分试算值(err 一步积分;供抗饱和逻辑条件采纳)。 + */ +static float integ_step(float err) { + return s_integral + err; +} + +/** + * @brief 分段 PID 计算(500ms 周期):误差 -> 占空档位 s_duty。 */ static void pid_compute(void) { float t = ntc_get_temperature(); /* 传感器故障(开路/短路):安全第一,强制关断并复位 PID */ if (t <= -273.0f) { - s_duty10 = 0; + s_duty = 0; pid_reset(); return; } /* 未使能:强制关断 */ if (!s_enable) { - s_duty10 = 0; + s_duty = 0; pid_reset(); return; } @@ -87,57 +98,60 @@ static void pid_compute(void) { s_overtemp = false; } if (s_overtemp) { - s_duty10 = 0; + s_duty = 0; pid_reset(); return; } - /* 目标斜坡:内部设定值以受限速率滑向 s_target。 - 满功率爬升段 ramp 超前于实际温升(不受影响);接近目标后 ramp 先到站停住, - 误差随实际温度逼近而平滑收缩 -> 功率缓退,抑制热传导滞后造成的过冲。 */ - float diff = s_target - s_ramp_target; - float step = HEATER_RAMP_CPS * (HEATER_PID_PERIOD_MS / 1000.0f); - if (diff > step) { - s_ramp_target += step; - } else if (diff < -step) { - s_ramp_target -= step; - } else { - s_ramp_target = s_target; + float err = s_target - t; + + /* 超温区间:执行器只能加热不能制冷,输出必须为 0; + 积分按负误差回退,为温度回落后的重新加热保留正确初始态 */ + if (err < 0.0f) { + s_integral = integ_step(err); + s_prev_err = err; + s_duty = 0; + return; } - /* 位置式 PID(带抗积分饱和):输出饱和时积分仅沿退饱和方向更新, - 防止升温段积分顶满后长时间维持 100% 输出(此前实测目标 50℃ 冲到 59℃) */ - float err = s_ramp_target - t; - float u_noi = HEATER_PID_KP * err - + HEATER_PID_KD * (err - s_prev_err); /* 比例 + 微分 */ - float integ_new = s_integral + err; - /* 超温区间(实际温度高于内部目标):加热器只能加热、不能制冷, - 输出必须为 0(此前积分正残量会算出小占空在超调段续灌加热); - 积分按负误差同步回退,为温度回落后的重新加热保留正确初始态 */ - if (err < 0.0f) { - s_integral = integ_new; - s_prev_err = err; - s_duty10 = 0; + /* 位置式 PID(带抗积分饱和) */ + float u = HEATER_PID_KP * err + + HEATER_PID_KD * (err - s_prev_err) + + HEATER_PID_KI * integ_step(err); + + if (u > 100.0f) { + u = 100.0f; + /* 饱和:err>=0 分支积分不吸收(防 windup) */ } else { - /* 误差为正(未达/低于内部目标):正常 PID + 抗积分饱和 */ - float u = u_noi + HEATER_PID_KI * integ_new; + s_integral = integ_step(err); + } + + /* 温和带:距目标不足 HEATER_SLOWBAND_C 时,比例/微分即时响应受限 + (小功率缓升,减少进带蓄热、降低惯性过冲);积分项不受限—— + 稳态维持功率由积分缓慢顶到实际需要值 */ + if (err < HEATER_SLOWBAND_C) { + float u_pd = HEATER_PID_KP * err + HEATER_PID_KD * (err - s_prev_err); + if (u_pd > (float)HEATER_SLOWBAND_DUTY) { + u_pd = (float)HEATER_SLOWBAND_DUTY; + } + if (u_pd < 0.0f) { + u_pd = 0.0f; + } + u = u_pd + HEATER_PID_KI * s_integral; if (u > 100.0f) { u = 100.0f; - /* 超上限:仅误差为负才吸收——err>=0 分支不会发生,保持积分不动 */ - } else { - s_integral = integ_new; } - /* 输出量化到 10% 档位(四舍五入) */ - s_duty10 = (uint8_t)((u + (float)HEATER_PWM_STEP_PCT / 2.0f) / (float)HEATER_PWM_STEP_PCT); } - s_prev_err = err; + + /* 输出量化到档位(四舍五入) */ + s_duty = (uint8_t)((u + (float)HEATER_PWM_STEP_PCT / 2.0f) / (float)HEATER_PWM_STEP_PCT); } /******************************************************************************/ /* 公共接口 */ void heat_task(void) { - uint8_t prev_duty = s_duty10; + uint8_t prev_duty = s_duty; /* PWM 载波相位推进(10ms tick) */ s_pwm_phase_ms += 10U; @@ -150,16 +164,16 @@ void heat_task(void) { if (s_pid_divider >= HEATER_PID_PERIOD_MS) { s_pid_divider = 0U; pid_compute(); - if (s_duty10 != prev_duty) { + if (s_duty != prev_duty) { s_pwm_phase_ms = 0U; /* 占空变更:从新周期起点开始,响应即时 */ } } - /* 按占空档位输出:0 档常关、10 档常开、其余按相位斩波 */ - uint16_t on_ms = (uint16_t)s_duty10 * (HEATER_PWM_PERIOD_MS / 10U); - if (s_duty10 == 0U) { + /* 按占空档位输出:0 档常关、满档常开、其余按相位斩波 */ + uint16_t on_ms = (uint16_t)s_duty * (HEATER_PWM_PERIOD_MS / HEATER_PWM_STEPS); + if (s_duty == 0U) { heat_out(false); - } else if (s_duty10 >= 10U) { + } else if (s_duty >= HEATER_PWM_STEPS) { heat_out(true); } else { heat_out(s_pwm_phase_ms < on_ms); @@ -169,15 +183,8 @@ void heat_task(void) { void heat_set_enable(bool on) { if (s_enable == on) return; /* 幂等 */ s_enable = on; - if (on) { - /* 从当前实际温度起坡,避免内部目标从 0 跳变造成功率冲击 */ - s_ramp_target = ntc_get_temperature(); - if (s_ramp_target < 0.0f) { - s_ramp_target = 0.0f; /* 传感器故障标记值非法,退化为 0 */ - } - } else { - s_ramp_target = 0.0f; - s_duty10 = 0; + if (!on) { + s_duty = 0; pid_reset(); heat_out(false); } @@ -196,5 +203,5 @@ float heat_get_target(void) { } uint8_t heat_get_duty(void) { - return s_duty10; + return s_duty; }