Heater: PID v0.1.14 - banded controller + sensor glitch guards

Rebuild PID as three-zone banded controller: far field P/D full power,
This commit is contained in:
2026-10-15 20:55:02 +08:00
parent 8035f9b7d3
commit 8900240c65
6 changed files with 473 additions and 85 deletions
+227 -5
View File
@@ -81,6 +81,182 @@
- 协议要点:全幂等(ack 丢可重发)、响应超时建议 300ms×3 次、温度一律 ASCII - 协议要点:全幂等(ack 丢可重发)、响应超时建议 300ms×3 次、温度一律 ASCII
- **PID 抗积分饱和**(5b24b95):条件积分(back-calculation lite)抑制输出限幅 - **PID 抗积分饱和**(5b24b95):条件积分(back-calculation lite)抑制输出限幅
期间的积分累积。 期间的积分累积。
- **PID 全面重写(2026-10-15,待真机验证)**——真机 30℃ 阶跃 +2.4℃ 过冲、
30~60s 周期衰减震荡(GUI 截图)驱动,三个根因全部修复:
① `s_prev_err` 在 err>=0 路径从不更新(c9aa416 early-return 重写时把末尾的
`s_prev_err = err` 弄丢)→ D 项退化为 +Kd*err 正向比例,"微分阻尼"从未生效,
且把抗饱和判据钉在饱和态冻死积分(仿真中大阶跃贴温和带边缘极限环、到不了
目标);改**微分基于温度变化率(D on measurement,-Kd*ΔT/拍)**,`s_prev_temp`
全路径刷新,u 钳 [0,100] 防负值量化回绕;
② 积分无界(err<0 每拍 -|err| 无下限)→ 过冲/下调目标后积分深负,温和带内
u=min(25,u_pd)+Ki*I 永远 <=0 → 加热长时间锁死、跌出温和带才全功率(极限环);
改**积分钳 [0, HEATER_PID_I_MAX=2000]**(Ki*I_MAX=100%);
③ 12ba9de 的目标斜坡在 c9aa416 被整段删除 → 恢复 `HEATER_RAMP_CPS 0.8℃/s`
+ 使能时从当前温度起坡(`s_ramp_target`,升/降同限速);
另修三项:④ 抗饱和判据改看**最终输出** u(原先看温和带重算前的 u);⑤ PWM
相位**仅升档复位**(原先降档也复位,降占空反而补发一整段 ON 脉冲,系统性
多给热);⑥ NTC 故障 **3 拍(1.5s)去抖确认**——真机曲线两个孤立 OPEN 毛刺
每次把 PID 状态整个清零也是震荡推手(毛刺拍保持输出不动;注意毛刺根因在
采样/硬件侧仍待查,GUI 按原始采样报故障不变)。
离线仿真(`sim_pid_check.py`:一阶热容+传感器滞后模型、2s/5% 慢 PWM 执行器、
旧/新同模型对比):30℃ 阶跃到达后波动 ±0.4℃→+0.04℃;50℃ 阶跃旧版极限环
到不了目标 vs 新版 94s 到达零过冲;50→40 下调 44s 恢复、波动 <±0.6℃。
固件版本升 **V0.1.0**(M999 报 `v41.0.1.0`),已烧录上板(OpenOCD GD-LINK,
dump_image 读回与 elf 逐字节比对一致;注意该 xpack OpenOCD 的
flash write/verify 回显日志会丢失,核验用 dump_image+文件比对)。
设计说明:保持**位置式** PID——执行器是绝对占空设定,分段强制关断/使能切换/
故障复位在位置式下最干净;增量式的抗饱和优势已由积分钳位+条件积分覆盖。
- **PID V2 抗震荡迭代(2026-10-15,V0.1.1 已烧录待真机确认)**——V0.1.0 真机
复测:过冲/OPEN 毛刺问题已消(峰 +0.4~0.5、无故障标记),但目标附近仍有
**±0.7~1.0℃/15~20s 三角波极限环**(50℃ 目标实测截图)。根因:err<0 硬切
0% + err>=0 仅 Kp*err,稳态维持功率(~10% 级)全靠 Ki=0.05 慢填(要几分钟)
→ 目标附近等效"P+上侧死区"继电器,叠加热滞后即极限环。V2 改法(`heat.c`
pid_compute 重写核心):
① **连续 PID 覆盖全误差区间**——去掉 err<0 硬切,过目标后 P 负向顶、积分
正向维持,输出钳 [0,100] 天然处理"只能加热";
② **Ki 0.05→0.25**(稳态功率 ~1 分钟内建立;扫参显示 0.5 在传感器滞后 8s
工况下积分环失稳 ±0.9、1.0 更差,0.25 全工况稳定);
③ **积分吸收限窄带**:仅 |err|<HEATER_PID_INTEG_ZONE_C(2℃) 且 P/D 未被
钳(u_pd_raw∈(0,cap))时吸收——爬升/温和带钳位段不积分,防瞬态 windup
(Ki 提快后若爬升段照吸,30℃ 阶跃仿真冲到 36℃);err<0 且 u>0 照常有界
回退;I_MAX 同步 2000→400(Ki*I_MAX=100% 不变)。
仿真(`sim_pid_check.py`,V1/V3 对比 + 参数扫描 `python sim_pid_check.py
sweep`,基准/恶劣两工况):恶劣工况 50℃ 阶跃到达后 [49.70,50.11](过冲
+0.11℃)、末端残余 ±0.02℃;30℃ 阶跃末端 ±0.01~0.02℃;50→40 下调 66s
恢复 ±0.02℃。已烧录 V0.1.1(M999 报 `v41.0.1.1`,dump 比对核验一致)。
- **PID V3 修"突然飙升 4℃"(2026-10-15,V0.1.2)——DAPLink 旁路抓证定位**:
V0.1.1 真机稳态后温度以 0.12℃/s 平滑飙升 +2~4℃ 再回落。定位手段(可复用):
① COM5 脚本探针(`probe_once.py`/`probe_state.py`)确认目标 50/加热 ON,
并现场抓到 **14 帧连续 OPEN(~1.4s)**——NTC 间歇开路真实存在(硬件侧
待修);② **OpenOCD `mrw` 旁路观测**(`observe2.tcl`+`obs_decode.py`,
每 500ms 不 halt 读 RAM:duty/integral/ramp/target/temp——注意该 xpack
OpenOCD 的 echo/mdw 控制台回显会丢失,须用 TCL 落文件)。全程轨迹证实
**无故障参与的 90~120s 自持极限环**:积分 0↔65 打摆(真机 50℃ 维持功率
~10-15%,即 I≈40~60 才是正确稳态),机理=**err<0 时 P 被钳 0、输出只剩
Ki·I 恒定值**——过线后输出不随超温幅度收敛→温度必然冲过头→过冲把积分
排空→排空后 sag→重建再过冲。NTC 毛刺清零积分只是点火器。V0.1.2 修复:
① **err<0 时 P 负项+D 直接抵扣积分后钳 0**(输出随超温幅度立即收敛,
积分按剩余需要维持→可收敛到稳态不再打摆);② **NTC 短故障(≤3s 且
|ΔT|<2℃)无扰还原积分**(确认沿暂存 `s_fault_stash_I/T`,防毛刺清零
引发 sag-重建-过冲链);③ I_MAX 400→200(Ki·I_MAX=50%,120℃ 模具维持
余量足够)。判定测试基建:`test_run.py`(固定序列 M999→M2S500→M4S50→
M3S1→N 秒→收尾;**预定义判据 J1 上报率/J2 稳态均值±0.5/J3 稳态极值±1.0/
J4 全程 max≤52/J5 无故障**,>54℃ 自动熔断 M3S0——开发/测试分工,脚本
自动判定避免自判自;注意 FF485 frame.payload 已是 str)。
- **PID V3 判定迭代与收敛(V0.1.2→V0.1.3,2026-10-15,900s 判定测试)**:
- V0.1.2(P-veto + 故障无扰还原 + I_MAX 200)判定:J1/J2/J5 PASS,但
**J3 FAIL(稳态窗 [46.6,52.1])、J4 FAIL(max 52.5)**——P-veto 消灭了
积分排空式打摆(观测器证实收敛末段积分稳在 ~48=真实维持功率、温度
±0.2),但双热容被控对象(鳍片+本体,NTC 只见鳍片)每次顶/谷都给
对方"预存"动量,15 分钟才收敛;且斜坡 0.8℃/s 远快于对象,爬升段误差
长期 ~2℃ 把积分提前充到 30~50,首冲 +2.5℃。
- V0.1.3:**HEATER_RAMP_CPS 0.8→0.25**(爬升误差贴地、积分只在近目标
低功率段建立)+ **HEATER_SLOWBAND_DUTY 25→15**(近目标动作放柔,
少给鳍片预存过热)。41℃ 冷启判定:**J1~J5 全 PASS**——到达 69s、
首冲 +1.20℃、全程 max 51.2、稳态窗 [49.0,50.9]、均值 49.95、
1795 帧 0 故障。观测器同步确认:远场(err>7)才见 75% 满功率,
温和带内 15% 缓爬,50.06℃ 到达时积分 35.9/占空 5%。
- 测试基建沉淀:`test_run.py`(判定+熔断)、`observe2.tcl`+`obs_decode.py`
(DAPLink 旁路,**注意符号地址随 bss 布局变,重编译后须 nm 重取**;
观测文件被占用时追加不清空)、`sim_pid_check.py`(含 V1/V3/V4 对照、
毛刺/噪声注入、扫参)。CMD 坑:`if exist X del X && ...` 文件不存在时
整链跳过;多行 `python -c` 偶发被吞——写文件执行。
- **遗留(硬件侧)**:NTC 间歇开路(现场抓到 14 帧连续 OPEN ~1.4s,
短故障无扰还原已缓解控制影响,但根因在采样/接触)——查 NTC 焊接/
压接/走线,必要时 ntc.c 采样侧加防护。独立代码审查 agent 未在时限内
返回,本轮定位以仪器化数据(COM5 帧流 + DAPLink 内存轨迹)为准。
- **PID 调试全程速览(2026-10-15,V0.1.0→V0.1.14,判定测试自动出结论)**:
| 版本 | 关键改动 | 判定/实测结果 | 结论 |
|---|---|---|---|
| V0.1.0 | 全面重写:D-on-measurement+积分钳位+斜坡恢复+故障去抖 | 过冲 +2.4→+0.4 | 旧版 D 项失效/积分无界/斜坡缺失三根因修复 |
| V0.1.1 | Ki 0.05→0.25+窄带吸收 | ±1℃ 三角波极限环 | err<0 硬切=继电器死区 |
| V0.1.2 | P 参与过线区+故障无扰还原+I_MAX200 | J3/J4 FAIL | P-veto 生效但收敛 15min |
| V0.1.3 | 斜坡 0.25C/s+温和带 15% | J1~J5 全 PASS(旧判据) | 稳态 [49.0,50.9] 不满足 ±0.5 |
| V0.1.4 | 占空抖动 1.25% 分辨率 | p2p=1.8 FAIL | 量化非主因 |
| V0.1.5 | Kp5/Kd60/Ki0.15 连续 PID | p2p=4.2 FAIL | ±2C/90s 慢摆,调参到头 |
| V0.1.6 | 分区 bang-bang+慢积分 | 卡 48.6C FAIL | 带外平衡点陷阱 |
| V0.1.7 | 带外也吸收 | p2p=4.1 FAIL | 吸收/回退不对称→I 上漂 90 |
| V0.1.8 | 对称慢速积分 | p2p=4.0 FAIL | 积分稳了;秒级读数坏值穿透 |
| V0.1.9 | 斜率限幅+带内退闸 | 无法判定 | NTC 持续开路(后重焊恢复) |
| V0.1.10 | 去上限+RAMP2.0+温和带25% | p2p=2.7 FAIL | 快斜坡生效;坏值仍是主力 |
| V0.1.11 | NTC 5 点滑动中值 | **p2p=1.30** | 中值滤波立竿见影 |
| V0.1.12 | 近场 ASSIST 减速曲线 | 实时 p2p=**0.31** | 最后 2C 推力真空修复;判定窗被传感器事件污染 |
| V0.1.13 | 大偏差冻结(>1.5C) | p2p=1.60 | 塌陷深谷消除(47.1→49.0) |
| V0.1.14 | 对称积分加倍 0.30 | p2p=1.80 | 与 13 同水平=硬件平台期 |
**最终状态(V0.1.14 在板)**:到达 26s(30→50C,提速 9 倍);控制环本体
(无事件段)p2p 0.3~0.9C 达标;判定窗残余 1.6~1.8C 全部来自 NTC 焊接点
秒级接触不良(-2.5C/4.5s 塌陷、顶格 52.2C 卡死 10s,1.5C 以内坏值仍会穿透
三重滤波)。**下一步:彻底重焊/更换 NTC(根治手段),固件三重防护
(中值+斜率限幅+大偏差冻结)已就位,重焊后 `python test_run.py 1200` 一键
重判(J3 即 ±0.5 规格)。测试环境 55C 上限已按用户要求移除(真机靠物理
温度开关)。**
- **±0.5℃ 波动压榨迭代(2026-10-15,V0.1.3→V0.1.6,用户规格 ±0.5C,
判据 J3 收紧为稳态窗 [目标±0.5])**——V0.1.3 稳态窗 [49.0,50.9](压着
旧判据 ±1.0 过)不可接受,连续三轮仪器化迭代:
- **V0.1.4 占空抖动(Bresenham)**:载波 2s=4 拍分摊档位余数,平均分辨率
5%→1.25%(真机维持功率 ~10-12% 恰卡在 10/15 档间,固定取整迫使
0↔15% 满幅切换)。观测器证实抖动生效(duty 5/10/15/20/25 细粒度),
**但极限环未消(J3 p2p=1.8 FAIL)→ 量化不是主因**。
- **V0.1.5 降增益强阻尼(Kp 10→5 / Kd 30→60 / Ki 0.25→0.15 / 窄带 2→1)**:
**J3 p2p=4.2 FAIL**——变成 ±2C/90s 慢摆。观测器抓到机理:带内半周期
净吸积分 +18(err 0.5~1.0 持续 30s),过线后 P-veto 每拍泄 2.8 快速
排空 → 维持功率记忆反复丢失 → 谷底深潜 48.6。**连续 PID 在本对象
(双热容+单向执行器+0.1C 读数量化)上调参到头**;Kd=60 还放大 NTC
0.1C 量化跳变成 ±6% duty 抖动。
- **V0.1.6 分区 bang-bang + 慢积分学习(结构性换型)**:作用分三区——
带内 |err|<HEATER_PID_BAND_C(0.4C):P/D 退出,u=Ki*I 恒功率,积分慢速
学习维持功率(I+=err*0.3/拍);带外下方:P 拉回(温和带钳位不变),
积分不吸收;带外上方:关断 + I 固定 0.3/拍限速回退(保住维持功率
记忆)。波动幅度被物理限制在带宽+滑行量,I 学准后驻留带内。
Kd 回 30(60 的噪声放大不可用)、I_MAX 回 400(Ki*I_MAX=60%)。
- **V0.1.6 判定 FAIL(带外平衡点陷阱)**:观测器抓到温度卡 48.6C——P 输出
恰好平衡散热、err 停带外,积分只在带内学习永远没机会介入。V0.1.7 修复
(带外下方 P 未被钳时同样慢速吸收):前半程良好(49.8/integ 37 稳),但
**吸收(0.3/拍)与回退(0.3/拍固定)不对称 → I 上漂到 90 持续超供**,后半程
大摆(J3 p2p=4.1 FAIL)。V0.1.8 改全区域对称慢速(GAIN=BLEED=0.15):
积分不漂了(35~42 窄幅)、带内占有率 44%,**但 J3 仍 FAIL(p2p=4.0)**——
csv 分析抓到 **-1.2C/0.5s 读数突跳 + 5s 后 +1.8C 回弹**(非故障级 NTC
毛刺,真实热速率 <=0.2C/拍),毛刺触发 25% 猛拉 ~10s 直接制造 +2C 过冲。
- **V0.1.9(待判定)**:① NTC 读数斜率限幅(HEATER_NTC_SLEW_C=0.5C/拍,
超限沿限幅值过渡、连续 2 拍放行——毛刺对环路的冲击源头掐断);② 带内
功率线性退闸(下缘 100% 维持量 -> 上缘 0%,过线前功率已趋零,蓄热滑行
收窄)。已烧录 MATCH。
- **NTC 硬件恶化为持续开路(阻塞判定)**:V0.1.9 判定测试全程 M1/M2 报
OPEN(帧流正常、命令全 ok),加热安全关断(故障路径实测有效)。用户
重焊后恢复(30.1C 稳定读数),进入 V0.1.10~V0.1.14 快速迭代:
- **V0.1.10 去上限 + 提速**:测试脚本去掉 54C 自动熔断/J4 上限判据(用户
明确:真机靠物理温度开关,55C 只是测试环境约束);RAMP 0.25→2.0(实测
0.25 时斜坡本身就是瓶颈,9C 走 36s);温和带 15%→25%。判定:reach 49s
但 J3 p2p=2.7 FAIL、首冲 +3.3。
- **V0.1.11 NTC 5 点滑动中值**(ntc.c 采样侧):V0.1.10 稳态摆的根源确认为
秒级读数坏值穿透斜率限幅(中值滤波只挡 <250ms 毛刺)。效果显著:
稳态 p2p 4.0→**1.30**([49.40,50.70])。温和带 7→3C、25→35%。
- **V0.1.12 近场 ASSIST 减速曲线**:分段速率分析钉死最后 2C 爬 141s 的瓶颈
=近场推力真空(P 随 err 缩小消失、I 未成熟)。近场扩展区 [-0.3,3.0)
合并退闸+缓爬:u = Ki*I + ASSIST 退闸曲线 + Kd,上限 NEAR_MAX_DUTY 50%。
实时观测 p2p **0.31C**(达标水平);但判定窗两极值(47.1 塌陷 + 52.2
顶格卡死)均为传感器事件:**-2.5C/4.5s 塌陷与顶格 10s,焊接点仍不稳**。
- **V0.1.13 大偏差冻结**:滤波值与原始读数偏差 >1.5C(HEATER_NTC_JUMP_C)
冻结不跟随,回弹自动恢复(真实冷却 ~0.05C/拍永不误触发)。判定 p2p
1.60([49.00,50.60])——塌陷深谷消除。reach 32s(提速 7 倍)。
- **V0.1.14 对称积分加倍**(GAIN=BLEED 0.15→0.30,I 充满 400s→200s,修复
判定窗内驻留点偏低):p2p 1.80([49.20,51.00]),与 V0.1.13 同水平
(差异在传感器事件噪声内)——**到达当前硬件条件下的平台期**。
- **当前结论**:控制环本体(无事件段)p2p≈0.3~0.9 达标;残余 1.6~1.8 全部
来自 NTC 焊接点秒级接触不良(塌陷/顶格事件,1.5C 以内的坏值仍会穿透
防护)。**下一步是彻底重焊/更换 NTC 及其引线**,固件侧三重防护
(中值+斜率限幅+大偏差冻结)已就位。到达速度 26s(30→50C,提速 9 倍)。
测试/观测基建:test_run.py(无熔断版,J1/J2/J3±0.5/J4 无故障自动判定)、
observe2.tcl+obs_last.py(**每次重编译必须 nm 重取符号地址**)、
seg_rate.py(分段爬升速率)、log_tail.py(实时帧解析)。
中间数据(obs_samples.txt/test_data.csv/test_events.log)已清理,
由测试/观测脚本重新生成。
- 方法论沉淀:单状态热模型仿真始终未复现真机极限环(双热容建不进单
状态),本轮所有定位直接以 DAPLink 观测器 + COM5 判定脚本为准;
观测符号地址 V0.1.4 起稳定(s_duty=0x20000459 / s_integral=0x20000454 /
s_ramp_target=0x2000045c / s_target=0x20000460 / s_enable=0x20000464)。
- **50℃→40℃ 阶跃验证脚本**(`temp_test_com.py`,工作区修改未提交):M2S500 - **50℃→40℃ 阶跃验证脚本**(`temp_test_com.py`,工作区修改未提交):M2S500
全曲线采样,输出超调/settle 时间/稳态误差/降温段统计(结果以实跑为准)。 全曲线采样,输出超调/settle 时间/稳态误差/降温段统计(结果以实跑为准)。
- **电源保持 PA11 实装**(`pwr_hold.h/pwr_hold.c`):上电最先拉高 PA11 维持 - **电源保持 PA11 实装**(`pwr_hold.h/pwr_hold.c`):上电最先拉高 PA11 维持
@@ -122,8 +298,12 @@
- [ ] **适配器更换**:换可靠的 USB-RS485 适配器(当前自动方向切换间歇丢响应)。 - [ ] **适配器更换**:换可靠的 USB-RS485 适配器(当前自动方向切换间歇丢响应)。
- [x] **加热驱动 + PID 闭环**(PB12 慢 PWM 1s/10% 步进 + PID 500ms 周期, - [x] **加热驱动 + PID 闭环**(PB12 慢 PWM 1s/10% 步进 + PID 500ms 周期,
安全保护齐全;实测 30℃ 闭环通过:30s 爬升、超调 1.4℃、60s 收敛中)。 安全保护齐全;实测 30℃ 闭环通过:30s 爬升、超调 1.4℃、60s 收敛中)。
- [ ] **PID 参数整定**:初值 Kp=8/Ki=0.05/Kd=0(board_config.h 宏),按真实 - [ ] **NTC 彻底重焊/更换(当前头号事项)**:焊接点秒级接触不良已用
工况温度(80~120℃)与负载重新整定,抑制 ~7% 超调。 DAPLink+COM5 数据链钉死(-2.5C/4.5s 塌陷、顶格卡死、曾持续开路),
是 ±0.5C 规格最后的拦路虎;固件三重防护已就位(V0.1.14)。
- [ ] **±0.5℃ 规格复判**:NTC 重焊后 `python test_run.py 1200`(J3=±0.5);
当前稳态控制环本体 p2p 0.3~0.9C 已达标。真实工况温度(80~120℃)下
需复测整定(Kp=8/Ki=0.15/Kd=30、ASSIST 35%、I_MAX400,board_config.h)。
- [x] **专用 GUI 上位机 v0.2.0**:v0.1.0 已完成 COM5 真机全链路验证;v0.2.0 与 - [x] **专用 GUI 上位机 v0.2.0**:v0.1.0 已完成 COM5 真机全链路验证;v0.2.0 与
实装固件对齐——去除"固件预留/仅软件状态"旧表述(协议模块预设说明、温控 实装固件对齐——去除"固件预留/仅软件状态"旧表述(协议模块预设说明、温控
卡片标题/备注、开启加热确认弹窗改为真实控温警示、ack 提示),GUI README 卡片标题/备注、开启加热确认弹窗改为真实控温警示、ack 提示),GUI README
@@ -140,7 +320,7 @@
"未连接串口"丢弃)。selftest 全过(新增 8 项回归用例);已打包冒烟并发布 "未连接串口"丢弃)。selftest 全过(新增 8 项回归用例);已打包冒烟并发布
Gitea Release `FilmForge-GUI-v0.2.1`(无应用图标——`logos/filmforge.ico` Gitea Release `FilmForge-GUI-v0.2.1`(无应用图标——`logos/filmforge.ico`
仍缺位)。 仍缺位)。
- **GUI v0.2.2~v0.2.4(已修复待真机确认,未发布)**: - **GUI v0.2.2~v0.2.13(递进修复/新功能,Gitea 发布待确认)**:
- v0.2.2:串口线程 `read(256)` 在持续帧流下攒满 256 字节才返回(win32 字节 - v0.2.2:串口线程 `read(256)` 在持续帧流下攒满 256 字节才返回(win32 字节
间隔超时因帧间隔更短永不触发,100ms 上报 ≈2.5s 一批),曲线/命令/响应全部 间隔超时因帧间隔更短永不触发,100ms 上报 ≈2.5s 一批),曲线/命令/响应全部
3 秒一卡、一股脑一大坨——真机日志确认每簇 ≈25 帧、间隔恒 3s。已改 3 秒一卡、一股脑一大坨——真机日志确认每簇 ≈25 帧、间隔恒 3s。已改
@@ -185,8 +365,50 @@
_last_set 单槽位罕见误补偿(8s 自愈)、build_exe remove_stale 无提示强杀 _last_set 单槽位罕见误补偿(8s 自愈)、build_exe remove_stale 无提示强杀
运行实例、M888 后固定 2s 重查可能早于固件就绪、_danger_reset 把去重忽略记成 运行实例、M888 后固定 2s 重查可能早于固件就绪、_danger_reset 把去重忽略记成
"已下发"。协议建议的 300ms×3 自动重发维持不做(设计决策,用户拍板)。 "已下发"。协议建议的 300ms×3 自动重发维持不做(设计决策,用户拍板)。
- selftest 全过(累计新增 15 项回归用例,含窗口图标非空检查);exe 已本地 - v0.2.10:**曲线 Y 轴手动量程 + "暂无数据"提示不消失修复**——①"实时温度曲线"
打包冒烟,Gitea 发布待用户确认后执行。 卡新增 Y 轴行:下限/上限输入框 + 自动勾选钮(默认勾选=随可见数据自适应;
取消勾选=按输入框固定量程,非法输入回退最近一次自适应范围并经状态栏提示;
首次切手动按当前自适应范围预填平滑接管;固定量程下曲线越界 setClipRect
裁剪回绘图区、当前值标记超量程不画,实时读数卡仍可见)。模式+量程随
`filmforge_gui.json` 持久化(y_axis 键,存值非法一律回自动)。
②提示文字不消失根因(用户标注截图):`set_source` 仅在构建期调用一次且在
那里缓存 `_has_data` 布尔——数据经同一列表引用追加后该布尔永不刷新 → 改为
paintEvent 实时判定(图表本就持有引用,无需其他调用点适应)。selftest 新增
15 项回归(切换往返/兜底预填/无效回退/量程落地/越界条带像素=0/新图数据到位
提示像素=0;断言须取当前主题曲线色、上条带避开 y=9 行图例色块),总数 74
全过;离屏整窗预览目检(手动/自动对照)布局正常。
- v0.2.11:**Y 轴量程控制改右缘滑条(用户要求放右侧、滑条形式)**——移除
v0.2.10 的下限/上限输入框行,"Y轴自动/手动"勾选钮移到暂停/清空行右端;
TemperatureChart 右缘新增滑条栏(GUTTER_W 18px:轨道+量程带+双把手):
上把手=上限、下把手=下限、把手中带拖拽=整段平移(跨度保持)、点击轨道
空白把对应边界直接设到该处;把手在**参考域**(全量数据范围 ±15%,`_y_ref`)
上映射像素,auto 态把手半透明、首次任意拖拽即切手动(`yManualEngaged`
同步按钮态+配置),松手量化 0.1℃ 经 `yRangeChanged` 记设置(最小间距
0.5℃),按钮一键回自动;越界裁剪沿 v0.2.10 不变。坑两条(勿重蹈):
①拖拽中的界值必须取**实时显示范围**而非 `_cached_layout` 绘制缓存——
连续两次拖拽之间没有重绘,缓存是陈旧范围(实测 pan 把量程整段拖跳);
②selftest 固件用真实 `filmforge_gui.json` 时"初始自动"断言会被用户存的
手动模式击穿——改指向临时目录设置文件彻底隔离。
selftest 重写第 8 段共 81 项全过(把手命中/拖拽切手动/量化/最小间距/
平移保跨度/裁剪条带/提示消失);离屏手动/自动滑条目检通过。
- v0.2.12:**滑条域定值 -20~120℃ + 标题行并排(用户截图标注 3 处)**——
①滑条参考域从"全量数据 ±15%"改为**固定 -20~120℃**(Y_RAIL_LO/HI 宏):
把手像素位置直接对应 ℃ 读数,空数据/冷机不再出现 -5.0 这类随数据漂移的
误导把手位;命中/拖拽/钳位全部按此域,140℃ 域上 1px≈0.53℃(精度靠点击
轨道空位直达边界;selftest 拖拽断言改为"目标值反算像素",不再依赖域宽);
②暂停/清空并入卡片标题行(实时温度曲线 [暂停][清空]…[Y轴手动][2分钟]
一行放齐,两组按钮不再各占一行悬在半空;顺带清掉 v0.2.11 重构残留的
孤儿按钮组代码;此卡不走 self._card 通用头,内联构建支持多 trailing);
③实测/目标图例移到绘图区左上(右侧被滑条把手/值标签占用)。selftest
81 项全过;离屏目检(空态/手动拖后)通过。
- v0.2.13:**把手值标签悬停门控(用户标注:常驻会遮挡曲线)**——上下限
两个数值标签不再常驻,仅鼠标悬停在滑条栏(或拖拽中)才显示
(`_y_hover` 翻转才重绘;leaveEvent 途中拖拽不退出悬停态,松手按位置
重算)。selftest 新增 2 项(未悬停标签区像素=0 / 悬停出现标签),共 83
项全过。exe 重新打包见下。
- selftest 全过(v0.2.13 后共 83 项回归);**v0.2.13 exe 已重新打包并冒烟
通过**(release/FilmForge.exe + FilmForge-GUI-v0.2.13-win64.zip,
依次取代 v0.2.10/v0.2.11/v0.2.12 包),Gitea Release 待用户确认后执行。
遗留见其 README"已知事项":USB-RS485 适配器更换。 遗留见其 README"已知事项":USB-RS485 适配器更换。
- [x] **GUI 慢性卡顿修复(根因已定位并修复 v0.2.8,待真机确认)**: - [x] **GUI 慢性卡顿修复(根因已定位并修复 v0.2.8,待真机确认)**:
根因/机理/实测数据见上方 v0.2.8 条目(`cursor.Start` 实例级枚举访问在 根因/机理/实测数据见上方 v0.2.8 条目(`cursor.Start` 实例级枚举访问在
+32 -6
View File
@@ -152,12 +152,38 @@ void usart1_irq_handler(void);
#define HEATER_PWM_PERIOD_MS 2000U /* PWM 载波周期(2s,载波 0.5Hz) */ #define HEATER_PWM_PERIOD_MS 2000U /* PWM 载波周期(2s,载波 0.5Hz) */
#define HEATER_PWM_STEP_PCT 5U /* 占空比步进(每档脉宽 = 100ms,维持最小开关频率约束) */ #define HEATER_PWM_STEP_PCT 5U /* 占空比步进(每档脉宽 = 100ms,维持最小开关频率约束) */
#define HEATER_PID_PERIOD_MS 500U /* PID 计算周期 */ #define HEATER_PID_PERIOD_MS 500U /* PID 计算周期 */
#define HEATER_PID_KP 10.0f /* PID 参数:超温前退功率更快(配合超温强制关断) */ #define HEATER_PID_KP 8.0f /* PID 参数:带外下方 P 拉回增益(带内 P 退出) */
#define HEATER_PID_KI 0.05f #define HEATER_PID_KI 0.15f /* 输出映射:u 贡献 = KI*I;I 全区域对称慢速学习 */
#define HEATER_PID_KD 30.0f /* 微分阻尼:按误差变化率提前刹车(抑制爬升惯性) */ #define HEATER_PID_KD 30.0f /* 微分(仅带外下方起作用):爬升段提前退功率 */
#define HEATER_SLOWBAND_C 7.0f /* 温和带宽度:距目标小于此值时占空上限受限 #define HEATER_PID_BAND_C 0.3f /* 作用分区带宽:带内恒功率(Ki*I),带外
(低速爬完最后一段,减少蓄热惯性过冲) */ 开关+P 修正——波动幅度被物理限制在
#define HEATER_SLOWBAND_DUTY 25U /* 温和带内占空上限(%) */ 带宽+滑行量 */
#define HEATER_PID_INTEG_GAIN_IN 0.30f /* 带内/带外下方积分学习增益(对称):
I += err*GAIN;0.15 时 I 充满需 ~400s,
判定窗内驻留点偏低 0.5C(ASSIST 补缺口
但驻留偏欠温),加倍后 ~200s 到位 */
#define HEATER_PID_INTEG_BLEED 0.30f /* 带外上方每拍固定回退(=GAIN,对称) */
#define HEATER_NTC_SLEW_C 0.5f /* NTC 读数斜率限幅(C/PID 拍):单拍变化超过
此值判为读数毛刺(真实热速率 <=0.2C/拍),
沿限幅值过渡、连续 2 拍超限放行真实值 */
#define HEATER_NTC_JUMP_C 1.5f /* 秒级坏值冻结阈值:斜率超限持续且累计偏差超过
此值时冻结滤波值(真机焊接点接触不良会产出
-2.5C/4.5s 塌陷与顶格卡死,中值滤波挡不住) */
#define HEATER_PID_I_MAX 400.0f /* 积分钳位上限:KI*I_MAX = 60% 占空(120C 模具
维持功率余量);下限恒为 0 */
#define HEATER_FAULT_RESTORE_MAX_S 3.0f /* NTC 短故障(去抖确认后恢复)无扰还原积分的
时长上限;超过则从零重建(安全优先) */
#define HEATER_RAMP_CPS 2.0f /* 内部设定值斜坡速率(℃/s):实测 0.25 时斜坡
本身就是瓶颈(9C 走 36s,加热器只是"追"设定点);
2.0 基本不设障,到达速度由加热功率与温和带
上限决定。分区控制器爬升段不积分,快斜坡
不会像旧连续 PID 那样预充积分导致过冲 */
#define HEATER_SLOWBAND_C 3.0f /* 温和带宽度:缓爬段(原 7C 太宽,25% 上限下
净升温仅 0.08C/s,爬升 234s 的主瓶颈) */
#define HEATER_SLOWBAND_DUTY 35U /* 近场缓爬辅助满档(%):err=+SLOWBAND_C 处
的推力,随 err 线性退闸(提供与积分成熟度
无关的确定爬升推力,修复最后 2C 爬 141s) */
#define HEATER_NEAR_MAX_DUTY 50U /* 近场扩展区推力上限(%):快而不过冲 */
#define HEATER_OVERTEMP_C 150.0f /* 过温强制关断阈值(滞回保护) */ #define HEATER_OVERTEMP_C 150.0f /* 过温强制关断阈值(滞回保护) */
#define HEATER_OVERTEMP_HYST 5.0f /* 过温恢复滞回 */ #define HEATER_OVERTEMP_HYST 5.0f /* 过温恢复滞回 */
+4 -3
View File
@@ -12,9 +12,10 @@ void heat_init(void);
/** /**
* @brief 加热控制任务(在主循环中周期调用,主循环周期 10ms)。 * @brief 加热控制任务(在主循环中周期调用,主循环周期 10ms)。
* @details 内部包含:慢 PWM 载波(1s 周期、10% 步进,脉宽恒 >=100ms)、 * @details 内部包含:慢 PWM 载波(2s 周期、5% 步进,脉宽恒 >=100ms)、
* PID 计算(500ms 周期,输出量化到占空档位)、安全保护 * 目标斜坡 + 连续 PID(500ms 周期,全误差区间连续输出、微分基于
* (传感器故障强制关断 / 过温滞回保护 / 未使能强制关断)。 * 温度变化率、积分窄带吸收+钳位)、安全保护(传感器故障去抖确认后
* 强制关断 / 过温滞回保护 / 未使能强制关断)。
*/ */
void heat_task(void); void heat_task(void);
+175 -68
View File
@@ -1,16 +1,26 @@
/** /**
* @file heat.c * @file heat.c
* @brief 加热控制:慢 PWM 驱动(PB12)+ 分段 PID 控温闭环 + 安全保护。 * @brief 加热控制:慢 PWM 驱动(PB12)+ 斜坡设定值 + 分段 PID 控温闭环 + 安全保护。
* @details 硬件:PB12=HEAT_EN,高 = 加热开(2N7002K 电平转换 → VBE2412 PMOS → 24V)。 * @details 硬件:PB12=HEAT_EN,高 = 加热开(2N7002K 电平转换 → VBE2412 PMOS → 24V)。
* 开关频率约束(<=10Hz)由"2s 载波周期 + 5% 占空步进"保证——每档脉宽 * 开关频率约束(<=10Hz)由"2s 载波周期 + 5% 占空步进"保证——每档脉宽
* 恒为 100ms 的整数倍(0.5Hz 载波、20 档占空分辨率)。 * 恒为 100ms 的整数倍(0.5Hz 载波、20 档占空分辨率)。
* 控制为分段式: * 控制结构(平滑逼近目标,抑制过冲/震荡):
* err >= 温和带 全量 PID(满功率爬升); * 1. 目标斜坡:内部设定值以 HEATER_RAMP_CPS 限速滑向 M4 目标,
* 0 <= err < 温和带 正常 PID 但占空上限压到温和档(小功率缓升, * 设定值阶跃/跳变不直接进入误差;
* 减少进带蓄热,降低越过目标后的惯性过冲); * 2. 全误差区间连续输出:err >= 温和带 P/D 全量;0 <= err < 温和带
* err < 0(超温) 强制关断 + 积分按负误差回退(执行器只能加热不能制冷)。 * P/D 钳到 HEATER_SLOWBAND_DUTY(积分不参与钳);err < 0(超过
* @note 安全保护:上电默认关闭;NTC 传感器故障(OPEN/SHRT)强制关断; * 内部目标)P 负项直接抵扣积分后钳 0——P 参与使输出随超温幅度
* 过温(>= HEATER_OVERTEMP_C)滞回保护;未使能强制关断。 * 立即收敛,积分只按剩余需要维持(若 P 铰 0 积分独撑,过线后
* 输出不随 err 变化,形成 sag->重建->过冲->排空自持极限环);
* 3. 微分基于温度变化率(D on measurement,-Kd*ΔT/拍):设定值
* 变化不产生微分踢;升温时提前退功率,回落越过目标时提前止跌;
* 4. 积分钳位 [0, HEATER_PID_I_MAX],吸收仅限目标近旁窄带
* (HEATER_PID_INTEG_ZONE_C)且 P/D 未被钳时(防爬升段
* windup);err<0 且 u>0 时小量回退,积分向稳态收敛;
* 稳态维持功率由积分承载。
* @note 安全保护:上电默认关闭;NTC 传感器故障(OPEN/SHRT,连续 3 拍去抖确认,
* 单拍毛刺不触发;短故障恢复无扰还原积分)强制关断;过温
* (>= HEATER_OVERTEMP_C)滞回保护;未使能强制关断。
*/ */
#include "heat.h" #include "heat.h"
@@ -23,17 +33,27 @@
/* 内部状态 */ /* 内部状态 */
static bool s_enable = false; /* 加热使能(M3) */ static bool s_enable = false; /* 加热使能(M3) */
static float s_target = 0.0f; /* 目标温度(M4) */ static float s_target = 0.0f; /* 目标温度(M4,外部设定值) */
static float s_ramp_target = 0.0f; /* 内部斜坡目标(以 HEATER_RAMP_CPS 滑向 s_target) */
static uint8_t s_duty = 0; /* 量化占空档位 0~HEATER_PWM_STEPS(x STEP%) */ static uint8_t s_duty = 0; /* 量化占空档位 0~HEATER_PWM_STEPS(x STEP%) */
static bool s_overtemp = false; /* 过温锁存(滞回恢复) */ static bool s_overtemp = false; /* 过温锁存(滞回恢复) */
static float s_integral = 0.0f; /* PID 积分项 */ static float s_integral = 0.0f; /* PID 积分累加(钳 [0, HEATER_PID_I_MAX]) */
static float s_prev_err = 0.0f; /* PID 上次误差 */ static float s_prev_temp = 0.0f; /* 上拍温度(微分 on measurement 用) */
static float s_duty_acc = 0.0f; /* 占空抖动累加器(档位单位,<1) */
static uint8_t s_fault_debounce = 0; /* NTC 故障去抖(连续故障拍数,255 封顶记时长) */
static float s_fault_stash_I = 0.0f; /* 故障确认时刻积分暂存(短故障无扰还原用) */
static float s_fault_stash_T = 0.0f; /* 故障前末次正常温度(还原条件校验用) */
static float s_t_filt = 25.0f; /* 斜率限幅后的温度(读数毛刺防护) */
static uint8_t s_slew_cnt = 0; /* 连续超斜率拍数(真实快速变化放行计数) */
static uint16_t s_pwm_phase_ms = 0; /* PWM 载波相位 0..PERIOD-1 */ static uint16_t s_pwm_phase_ms = 0; /* PWM 载波相位 0..PERIOD-1 */
static uint16_t s_pid_divider = 0; /* PID 周期分频(10ms 圈) */ static uint16_t s_pid_divider = 0; /* PID 周期分频(10ms 圈) */
/* 档位总数 = 100% / 步进% */ /* 档位总数 = 100% / 步进% */
#define HEATER_PWM_STEPS (100U / HEATER_PWM_STEP_PCT) #define HEATER_PWM_STEPS (100U / HEATER_PWM_STEP_PCT)
/* NTC 故障确认拍数(每拍 = 1 个 PID 周期 500ms;连续 3 拍 = 1.5s) */
#define NTC_FAULT_CONFIRM_PID_STEPS 3U
/******************************************************************************/ /******************************************************************************/
/* 底层输出 */ /* 底层输出 */
@@ -56,38 +76,74 @@ void heat_init(void) {
/******************************************************************************/ /******************************************************************************/
/* PID */ /* PID */
/**
* @brief 复位 PID 积分与历史误差(使能切换/传感器故障时调用,防积分残留冲击)。
*/
static void pid_reset(void) {
s_integral = 0.0f;
s_prev_err = 0.0f;
}
/**
* @brief 积分试算值(err 一步积分;供抗饱和逻辑条件采纳)。
*/
static float integ_step(float err) {
return s_integral + err;
}
/** /**
* @brief 分段 PID 计算(500ms 周期):误差 -> 占空档位 s_duty。 * @brief 分段 PID 计算(500ms 周期):误差 -> 占空档位 s_duty。
*/ */
static void pid_compute(void) { static void pid_compute(void) {
float t = ntc_get_temperature(); float t_raw = ntc_get_temperature();
/* 传感器故障(开路/短路):安全第一,强制关断并复位 PID */ /* NTC 读数斜率限幅(V0.1.9)+ 大偏差冻结(V0.1.13):真实热速率
if (t <= -273.0f) { <=0.2C/拍,单拍 |Δ|>0.5C 判为读数毛刺,沿限幅值过渡,连续 2 拍超限
s_duty = 0; 后——若累计偏差 > HEATER_NTC_JUMP_C(秒级塌陷/卡死事件,真机实测
pid_reset(); -2.5C/4.5s 塌陷与 +2.3C/10s 顶格,焊接点接触不良)则冻结滤波值
return; 不再跟随,直到读数回弹到偏差带内自动恢复。真实温度变化永远不会
触发冻结(最大冷却 ~0.05C/拍)。过温方向冻结偏差最大 1.5C,安全。 */
float t = t_raw;
if (t_raw > -273.0f) {
float d_raw = t_raw - s_t_filt;
if (d_raw > HEATER_NTC_SLEW_C || d_raw < -HEATER_NTC_SLEW_C) {
if (s_slew_cnt < 2U) {
s_slew_cnt++;
t = s_t_filt + ((d_raw > 0.0f) ? HEATER_NTC_SLEW_C : -HEATER_NTC_SLEW_C);
} else if (d_raw > HEATER_NTC_JUMP_C || d_raw < -HEATER_NTC_JUMP_C) {
t = s_t_filt; /* 秒级坏值:冻结 */
} else {
s_slew_cnt = 2U; /* 偏差带内:放行 */
}
} else {
s_slew_cnt = 0U;
}
s_t_filt = t;
} else {
s_slew_cnt = 0U;
} }
/* 未使能:强制关断 */ /* NTC 故障去抖:单拍毛刺(采样/EMI)保持输出不动;连续确认才安全关断。
毛刺拍不更新任何 PID 状态(含 s_prev_temp),下拍用真实温差继续。
确认沿暂存积分与故障前温度——短故障恢复时无扰还原(见恢复分支)。 */
if (t <= -273.0f) {
if (s_fault_debounce < 255U) {
s_fault_debounce++;
}
if (s_fault_debounce >= NTC_FAULT_CONFIRM_PID_STEPS) {
if (s_fault_debounce == NTC_FAULT_CONFIRM_PID_STEPS) {
s_fault_stash_I = s_integral;
s_fault_stash_T = s_prev_temp;
}
s_duty = 0;
s_integral = 0.0f;
s_duty_acc = 0.0f;
s_prev_temp = t;
}
return;
}
/* 故障恢复:短故障(<=HEATER_FAULT_RESTORE_MAX_S)且温度未显著漂移
(|ΔT|<2℃)时还原暂存积分——避免毛刺清零后稳态功率丢失引发
sag->重建->过冲循环;长故障/温度已漂移则从零重建(安全优先) */
if (s_fault_debounce >= NTC_FAULT_CONFIRM_PID_STEPS) {
float fault_dur_s = (float)s_fault_debounce * (HEATER_PID_PERIOD_MS / 1000.0f);
float drift = t - s_fault_stash_T;
if (fault_dur_s <= HEATER_FAULT_RESTORE_MAX_S && drift < 2.0f && drift > -2.0f) {
s_integral = s_fault_stash_I;
}
}
s_fault_debounce = 0U;
/* 未使能:强制关断(每拍刷新微分基线,恢复时无假温差) */
if (!s_enable) { if (!s_enable) {
s_duty = 0; s_duty = 0;
pid_reset(); s_integral = 0.0f;
s_prev_temp = t;
return; return;
} }
@@ -99,52 +155,88 @@ static void pid_compute(void) {
} }
if (s_overtemp) { if (s_overtemp) {
s_duty = 0; s_duty = 0;
pid_reset(); s_integral = 0.0f;
s_duty_acc = 0.0f;
s_prev_temp = t;
return; return;
} }
float err = s_target - t; /* 目标斜坡:内部设定值限速滑向 s_target(升/降同限速)。
小步进(斜坡快于温升)全程落在温和带内缓升;大步进爬升段 ramp 超前、
/* 超温区间:执行器只能加热不能制冷,输出必须为 0; 到站后误差随实际温度平滑收缩,抑制热滞后过冲。 */
积分按负误差回退,为温度回落后的重新加热保留正确初始态 */ float diff = s_target - s_ramp_target;
if (err < 0.0f) { float ramp_step = HEATER_RAMP_CPS * (HEATER_PID_PERIOD_MS / 1000.0f);
s_integral = integ_step(err); if (diff > ramp_step) {
s_prev_err = err; s_ramp_target += ramp_step;
s_duty = 0; } else if (diff < -ramp_step) {
return; s_ramp_target -= ramp_step;
}
/* 位置式 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 { } else {
s_integral = integ_step(err); s_ramp_target = s_target;
} }
/* 温和带:距目标不足 HEATER_SLOWBAND_C 时,比例/微分即时响应受限 float err = s_ramp_target - t;
(小功率缓升,减少进带蓄热、降低惯性过冲);积分项不受限—— float d_t = t - s_prev_temp; /* 本拍温度变化(D on measurement) */
稳态维持功率由积分缓慢顶到实际需要值 */
if (err < HEATER_SLOWBAND_C) { /* 分区作用(V0.1.12):近场扩展区 [-BAND, SLOWBAND_C) 合并"带内退闸"
float u_pd = HEATER_PID_KP * err + HEATER_PID_KD * (err - s_prev_err); 与"带外下方"为一条连续减速曲线——u = Ki*I(维持功率,不衰减)+
if (u_pd > (float)HEATER_SLOWBAND_DUTY) { ASSIST 退闸曲线(err=+SLOWBAND_C 满档快爬、err=+BAND 处归零)+ Kd 减速。
u_pd = (float)HEATER_SLOWBAND_DUTY; 原结构近场推力真空(P 随 err 缩小消失、I 未成熟,最后 2C 爬 141s),
ASSIST 提供与积分成熟度无关的确定推力。远场 err >= SLOWBAND_C:P/D
全量 + I。过温缘 err < -BAND:关断 + I 限速回退。 */
float u;
if (err >= HEATER_SLOWBAND_C) {
float u_pd = HEATER_PID_KP * err - HEATER_PID_KD * d_t;
if (u_pd > 100.0f) {
u_pd = 100.0f;
} }
if (u_pd < 0.0f) { if (u_pd < 0.0f) {
u_pd = 0.0f; u_pd = 0.0f;
} }
u = u_pd + HEATER_PID_KI * s_integral; u = u_pd + HEATER_PID_KI * s_integral;
if (u < 100.0f) {
s_integral += err * HEATER_PID_INTEG_GAIN_IN;
}
if (u > 100.0f) { if (u > 100.0f) {
u = 100.0f; u = 100.0f;
} }
} else if (err >= -HEATER_PID_BAND_C) {
float assist = (float)HEATER_SLOWBAND_DUTY * (err + HEATER_PID_BAND_C)
/ (HEATER_SLOWBAND_C + HEATER_PID_BAND_C);
u = HEATER_PID_KI * s_integral + assist - HEATER_PID_KD * d_t;
s_integral += err * HEATER_PID_INTEG_GAIN_IN;
if (u > (float)HEATER_NEAR_MAX_DUTY) {
u = (float)HEATER_NEAR_MAX_DUTY; /* 近场推力上限:快而不过冲 */
}
if (u < 0.0f) {
u = 0.0f;
}
} else {
s_integral -= HEATER_PID_INTEG_BLEED;
u = 0.0f;
}
if (s_integral > HEATER_PID_I_MAX) {
s_integral = HEATER_PID_I_MAX;
}
if (s_integral < 0.0f) {
s_integral = 0.0f;
} }
/* 输出量化到档位(四舍五入) */ s_prev_temp = t;
s_duty = (uint8_t)((u + (float)HEATER_PWM_STEP_PCT / 2.0f) / (float)HEATER_PWM_STEP_PCT);
/* 输出 Bresenham 抖动量化:载波 2s = 4 个 PID 拍,档位余数按拍分摊,
平均分辨率 5%/4 = 1.25%。稳态维持功率(实测 ~10-12%)恰落在整数档
之间时,固定取整会迫使输出在相邻档间满幅切换(±1℃ 极限环残因);
抖动后 PI 可停在真实需要的平均功率上。脉宽仍为 100ms 整数倍,
开关频率仍 0.5Hz,硬件约束不变 */
s_duty_acc += u / (float)HEATER_PWM_STEP_PCT;
if (s_duty_acc >= (float)(HEATER_PWM_STEPS + 1)) {
s_duty_acc = (float)(HEATER_PWM_STEPS + 1);
}
{
uint8_t level = (uint8_t)s_duty_acc; /* u >= 0,截断即 floor */
s_duty_acc -= (float)level;
s_duty = level;
}
} }
/******************************************************************************/ /******************************************************************************/
@@ -164,8 +256,10 @@ void heat_task(void) {
if (s_pid_divider >= HEATER_PID_PERIOD_MS) { if (s_pid_divider >= HEATER_PID_PERIOD_MS) {
s_pid_divider = 0U; s_pid_divider = 0U;
pid_compute(); pid_compute();
if (s_duty != prev_duty) { if (s_duty > prev_duty) {
s_pwm_phase_ms = 0U; /* 占空变更:从新周期起点开始,响应即时 */ s_pwm_phase_ms = 0U; /* 仅升档复位相位:立即起算新 ON 窗口;
降档不复位——相位已过新窗口即刻关断,
不再额外补发一段 ON 脉冲 */
} }
} }
@@ -183,9 +277,22 @@ void heat_task(void) {
void heat_set_enable(bool on) { void heat_set_enable(bool on) {
if (s_enable == on) return; /* 幂等 */ if (s_enable == on) return; /* 幂等 */
s_enable = on; s_enable = on;
if (!on) { if (on) {
/* 从当前实际温度起坡:避免内部目标从 0 跳变造成功率冲击;
微分基线/读数滤波基线同步取当前温度,首拍无假温差 */
s_ramp_target = ntc_get_temperature();
if (s_ramp_target < 0.0f) {
s_ramp_target = 0.0f; /* 传感器故障标记值非法,退化为 0 */
}
s_prev_temp = s_ramp_target;
s_t_filt = (s_ramp_target > 0.0f) ? s_ramp_target : 25.0f;
s_slew_cnt = 0U;
s_fault_debounce = 0U;
} else {
s_ramp_target = 0.0f;
s_duty = 0; s_duty = 0;
pid_reset(); s_integral = 0.0f;
s_duty_acc = 0.0f;
heat_out(false); heat_out(false);
} }
} }
+33 -1
View File
@@ -97,11 +97,43 @@ float ntc_read_temperature(void) {
static float s_ntc_last_temp = -273.15f; /* 最新温度(初值 = 开路标记) */ static float s_ntc_last_temp = -273.15f; /* 最新温度(初值 = 开路标记) */
static uint16_t s_ntc_sample_divider = 0; /* 主循环 10ms 圈分频计数 */ static uint16_t s_ntc_sample_divider = 0; /* 主循环 10ms 圈分频计数 */
/* 滑动中值滤波(V0.1.11):真机读数毛刺可持续 1.4s+(曾抓到 14 帧连续
OPEN),会穿透控制环的斜率限幅并触发猛拉。最近 NTC_MEDIAN_N 个样本取
中值——单点/短串毛刺被完全剔除,滞后仅 ~0.2s(远小于低通滤波)。
故障标记值(-273/-274)参与排序:偶发 OPEN 被中值滤掉,连续 >=3 帧才
成为中值(等效又一层去抖),故障判定语义不变。 */
#define NTC_MEDIAN_N 5U
static float s_ntc_win[NTC_MEDIAN_N];
static uint8_t s_ntc_win_idx = 0;
static uint8_t s_ntc_win_fill = 0;
void ntc_sample_task(void) { void ntc_sample_task(void) {
/* 主循环周期 10ms:按 NTC_SAMPLE_PERIOD_MS 分频触发一次采样 */ /* 主循环周期 10ms:按 NTC_SAMPLE_PERIOD_MS 分频触发一次采样 */
if (++s_ntc_sample_divider >= (NTC_SAMPLE_PERIOD_MS / 10U)) { if (++s_ntc_sample_divider >= (NTC_SAMPLE_PERIOD_MS / 10U)) {
s_ntc_sample_divider = 0; s_ntc_sample_divider = 0;
s_ntc_last_temp = ntc_read_temperature();
float v = ntc_read_temperature();
s_ntc_win[s_ntc_win_idx] = v;
s_ntc_win_idx = (uint8_t)((s_ntc_win_idx + 1U) % NTC_MEDIAN_N);
if (s_ntc_win_fill < NTC_MEDIAN_N) {
s_ntc_win_fill++;
}
/* 插入排序后取中值(5 点,开销可忽略) */
float tmp[NTC_MEDIAN_N];
for (uint8_t i = 0; i < s_ntc_win_fill; i++) {
tmp[i] = s_ntc_win[i];
}
for (uint8_t i = 1; i < s_ntc_win_fill; i++) {
float key = tmp[i];
int8_t j = (int8_t)(i - 1);
while (j >= 0 && tmp[j] > key) {
tmp[j + 1] = tmp[j];
j--;
}
tmp[j + 1] = key;
}
s_ntc_last_temp = tmp[s_ntc_win_fill / 2U];
} }
} }
+2 -2
View File
@@ -2,8 +2,8 @@
set(PROJECT_NAME "FilmForge_FW") set(PROJECT_NAME "FilmForge_FW")
set(BOARD_TYPE_CODE 41) set(BOARD_TYPE_CODE 41)
set(VERSION_MAJOR 0) set(VERSION_MAJOR 0)
set(VERSION_MINOR 0) set(VERSION_MINOR 1)
set(VERSION_PATCH 1) set(VERSION_PATCH 14)
set(VERSION "V${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") set(VERSION "V${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
string(TIMESTAMP BUILD_DATE "%Y-%m-%d") string(TIMESTAMP BUILD_DATE "%Y-%m-%d")