简介:面向自动化专业毕业设计与课程实践的恒温控制课题,这份文档围绕模糊PID算法在恒温水箱温度控制中的应用展开,目标是在40℃至90℃范围内实现某一设定温度,并将误差控制在0.5℃以内。压缩包内共1个doc文件,约698KB,内容为完整毕业设计文档,涵盖摘要、目录、系统硬件设计及温度控制模块等章节,涉及AT89S52单片机、DS18B20温度传感器、温度显示与可控硅加热控制等实现思路。已有298人学习,适合需要参考课题结构、算法选型与硬件方案的学生和技术人员。文档将模糊控制无需精确数学模型、适应性强的特点与PID控制精度高、静态性能好的优势结合,并给出模糊控制规则设计、PID参数调整及系统测试的论述,可作为恒温控制系统方案论证、毕业设计撰写和模糊PID入门学习的参考材料。
1. 从一台上冲 8℃ 的恒温水箱说起
实验室角落那台老恒温水箱,设定 60℃,用继电器位式控制,第一次升温直接冲到 68℃,然后开始来回跳,稳定下来要小二十分钟。换成固定参数的 PID,升温段快了,保温段又开始小幅振荡,因为升温时和保温时的对象特性根本不是一回事。这就是模糊 PID 恒温控制要解决的典型场景:对象大惯性、有滞后、只能加热不能制冷,参数还随环境漂。这套设计以 AT89S52 单片机为核心,DS18B20 采温,双向可控硅调功,把误差和误差变化率送进模糊规则表在线修整 PID 三个参数,在 40℃~90℃ 范围内把稳态误差压到 0.5℃ 以内。适合正在做自动化、机电方向课题、需要一套能焊能调的闭环温控实物的人,也适合想搞明白模糊 PID 到底怎么落到 8 位机上的人。
2. 系统总体方案:从设定值到加热丝的闭环链路
2.1 三段式闭环结构怎么接
整套系统是一个单闭环负反馈:键盘给设定值 T_set(40~90℃ 任意整数),DS18B20 每分钟若干次采回箱内实际温度 T_now,单片机算误差 e = T_set − T_now 和误差变化率 ec,模糊推理输出 PID 参数修正量,PID 再算出控制量 u,u 折算成可控硅在固定时间窗内的导通比例,电阻丝通电加热,热量通过水体传给传感器,闭环成立。
这里有个绕不开的物理约束:加热是单向的。温度冲过头,系统没有任何制冷手段,只能干等它自然散热。所以控制策略必须"提前收手"——误差还差两三度时就得开始砍功率,否则一定超调。固定 PID 做不到这一点,因为它不知道现在是"刚从 30℃ 往上冲"还是"已经在 58℃ 附近微调",而 e 和 ec 的组合恰好能把这个状态区分出来,这正是模糊层存在的意义。
数据流可以拆成四段看:给定段(键盘扫描 + 限幅)、反馈段(单总线采集 + 滤波)、决策段(模糊整定 + 增量式 PID)、执行段(周波过零触发)。每一段都有明确的故障表现,后面排查章节会按段来定位。
引脚分配先定死,硬件和软件都按这张表走,改动时不容易互相踩:
| 引脚 | 功能 | 说明 |
|---|---|---|
| P1.0 | 启动键 | 低电平有效,进入控温 |
| P1.1 / P1.2 / P1.3 | 百位+ / 十位+ / 个位+ | 设定值加 1,限幅 40~90 |
| P1.6 | 蜂鸣器 | 经反相器驱动,低电平鸣叫 |
| P1.7 | 可控硅触发 | 经光耦隔离后接双向可控硅门极 |
| P2.0 | DS18B20 DQ | 单总线,外接 4.7k 上拉 |
| P3.0 / P3.1 | 74LS164 数据 / 时钟 | 串行驱动三位数码管 |
2.2 主控选型:AT89S52 到底够不够用
AT89S52 是 8051 内核,8K 字节 Flash、256 字节片内 RAM、32 根 I/O、三个 16 位定时器、一个看门狗,DIP40 封装能手焊。放到这套系统里,程序量大概三四千字节,模糊规则表 49 条乘三个参数、每条一个字节,塞进 code 区毫无压力;真正要小心的是 256 字节 RAM,其中高 128 字节还和 SFR 重叠,实际能自由用的 data 区只有 128 字节左右。所以规则表、段码表这类常量一律加code关键字放 Flash,绝不能写成普通数组。
晶振用 6MHz,标准 12T 模式下机器周期是 2μs,定时器单次最长定时 65536×2μs ≈ 131ms。这个数字要记住:想做一个 15 秒的采样周期,必须在中断里再挂一个软件计数器,硬件定时器撑不到那么长。
上电初始化把 I/O、定时器、串口模式一次配好:
#include <reg52.h> sbit DQ = P2^0; // DS18B20 数据线 sbit HEAT = P1^7; // 可控硅触发 sbit BEEP = P1^6; // 蜂鸣器,低电平响 unsigned char tick_50ms = 0; // 50ms 软件计数 bit flag_sample = 0; // 采样标志 void system_init(void) { P1 = 0xFF; // 按键口先置高,蜂鸣器、加热默认关闭 HEAT = 0; BEEP = 1; TMOD = 0x01; // 定时器0,方式1,16位 TH0 = 0x9E; // 6MHz 下 50ms 初值:65536-25000=0x9E58 TL0 = 0x58; ET0 = 1; // 开定时器0中断 EA = 1; TR0 = 1; }TH0/TL0这对初值是按 6MHz、12T 算出来的:50ms 需要 25000 个机器周期,65536 − 25000 = 40536,即 0x9E58。换晶振必须重算,否则采样周期整体偏移,PID 的积分项会跟着失真。P1 = 0xFF这一句也别省,8051 的准双向口做输入前必须先写 1,很多"按键没反应"的玄学问题就是漏了这一句。
2.3 执行通道:从 P1.7 到双向可控硅
加热回路是 220V 市电串电阻丝,单片机绝对不能直接碰。中间这条链路是:P1.7 → 限流电阻 → 光耦(常用 MOC3021 随机型或 MOC3063 过零型)→ 双向可控硅门极 → 电阻丝。
限流电阻按光耦 LED 算:正向压降约 1.2V,触发电流取 15mA,R = (5 − 1.2) / 0.015 ≈ 253Ω,实际取 220Ω 或 330Ω 都在可用范围。可控硅按功率选,1kW 加热丝在 220V 下电流约 4.5A,留两倍余量选 BTA16 这类 16A 器件,必须加散热片;门极并联的 RC 吸收(常见 100Ω 串 0.1μF)用来压换向时的 dv/dt,省掉它,感性负载或者电网波动时很容易出现"关不断"。
触发方式建议用过零触发做周波控制:把 1 秒(50Hz 下是 50 个半波)当成一个时间窗,控制量 0~100 折算成窗口内导通的半波个数。相比移相调压,周波控制不产生高频干扰,对电阻丝这种大热惯性负载完全够用,也不会把单片机自己干扰到复位。
电源部分照老套路:220V 经变压器降到 9V 左右,桥堆整流,2200μF 电解滤波,再进 7805 出 5V。7805 要求输入输出压差至少 2~3V,也就是说滤波后的直流不能低于 7V,否则输出电压跟着塌,单片机会莫名其妙复位。7805 自身也要贴散热片,输入输出各并一个滤波电容,别省。
2.4 按键与报警:四键交互和越限提示
人机交互做得极简:四个键,启动、百位+、十位+、个位+,接 P1 低四位。按一次对应位加 1,百位在 4~9 之间循环其实等价于限制了 40~90 的范围,但软件里仍要做一次限幅校验,防止按键抖动导致数值跳到 0 或者 100 以上。
消抖不用搞复杂,10~20ms 的软件延时二次确认就够。真正容易翻车的是"连按加速"——用户按住不放时,如果每次都要等松手才响应,设个 85℃ 得按几十下。常见做法是检测到持续按下超过 500ms 后进入 100ms 一次的连发模式。
报警通道用 P1.6 经 74LS04 反相驱动有源蜂鸣器,低电平鸣叫。触发条件写两条:实测温度超过设定值一个阈值(比如 3℃)持续若干秒,或者传感器读回明显异常值。别只用"高于设定值"一个条件,稳态时温度在设定值上下 0.3℃ 波动,用单条件会叫个不停。
3. 温度采集与显示:DS18B20 单总线加 74LS164 静态驱动
3.1 单总线时序:复位、写、读三段
DS18B20 只用一根线完成供电(寄生模式)或通信,12 位分辨率对应 0.0625℃,出厂精度够用,-55~125℃ 量程覆盖本系统绰绰有余。代价是时序卡得很死:复位要拉低至少 480μs,然后释放等 15~60μs 的存在脉冲;写 1 是拉低不到 15μs 就释放,写 0 是拉低满 60μs;读的时候主机先拉低 1μs 再释放,15μs 内必须把线状态读走。
6MHz 晶振下一条_nop_()大约 2μs(12T 模式一个机器周期),延时函数只能靠空循环凑,不能指望delay()精确到微秒。稳妥的做法是把关键延时写成宏,再用示波器或者逻辑分析仪核对一次波形,之后就别再动它。
#include <intrins.h> #define DLY_2US() _nop_();_nop_() bit ds_reset(void) { bit presence; DQ = 0; delay_us(480); // 拉低 480us 以上 DQ = 1; delay_us(60); // 等 15~60us 存在脉冲 presence = DQ; // 0 表示器件在线 delay_us(420); // 补足复位时隙 return presence; } void ds_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; if (dat & 0x01) { // 写 1:短低脉冲 DLY_2US(); DQ = 1; delay_us(60); } else { // 写 0:长低脉冲 delay_us(60); DQ = 1; DLY_2US(); } dat >>= 1; } } unsigned char ds_read_byte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; DQ = 0; DLY_2US(); // 拉低 2us 后释放 DQ = 1; DLY_2US(); if (DQ) dat |= 0x80; // 15us 内采样 delay_us(60); } return dat; }三段函数里,ds_reset的返回值必须判:返回 1 说明没有器件应答,后面读回来的必然是 0xFF 或者 0x00,换算出来就是 85℃ 这种经典假值。ds_write_byte是低位先出,所以循环里用右移;ds_read_byte同理,先右移再填最高位,顺序写反了读出来就是位倒序。
3.2 温度换算:从 16 位补码到 0.1℃ 整数
一次完整采集是:复位 → 跳过 ROM(0xCC)→ 启动转换(0x44)→ 等 750ms(12 位分辨率的最长转换时间)→ 复位 → 跳过 ROM → 读暂存器(0xBE)→ 连读两个字节。
原始数据是 16 位带符号补码,低 4 位是小数部分,乘 0.0625 得摄氏度。本系统只需要 40~90℃ 正温区,但负温处理还是留着,方便以后拿去测别的对象。为了避免浮点,统一换算成"0.1℃ 为单位的整数":
int ds_read_temp10(void) { unsigned char tl, th; int raw, t10; if (!ds_reset()) return -9999; // 传感器掉线,返回错误码 ds_write_byte(0xCC); ds_write_byte(0x44); delay_ms(750); if (!ds_reset()) return -9999; ds_write_byte(0xCC); ds_write_byte(0xBE); tl = ds_read_byte(); th = ds_read_byte(); raw = (th << 8) | tl; t10 = (int)(((long)raw * 625) / 1000); // raw*0.0625*10 return t10; }raw * 625 / 1000这一步等价于乘 0.625,把 0.0625℃ 的刻度放大到 0.1℃,全程整数运算。中间必须转long,否则 raw 到 1000 以上时raw * 625会溢出 16 位。返回 −9999 作为错误码而不是 0,是为了让上层能把"传感器坏了"和"真的是 0℃"区分开。
3.3 数字滤波:中位值平均滤掉单次跳变
可控硅每次开关都是一次干扰源,单总线又只有一根线,偶发读到跳变值很正常。直接把这个值喂给 PID,输出会突然抽一下。工程上最省事的是中位值平均滤波:连续采 5 次,排序后去掉最大最小,剩下 3 个取平均。
int temp_filter(void) { int buf[5], i, j, tmp, sum = 0; for (i = 0; i < 5; i++) { buf[i] = ds_read_temp10(); if (buf[i] == -9999) return -9999; } for (i = 0; i < 4; i++) // 冒泡排序,5 个数够用 for (j = 0; j < 4 - i; j++) if (buf[j] > buf[j+1]) { tmp = buf[j]; buf[j] = buf[j+1]; buf[j+1] = tmp; } for (i = 1; i < 4; i++) sum += buf[i]; return sum / 3; }五次采集加排序在 6MHz 上大概几十毫秒,对温控这个时间尺度完全无感。注意 12 位转换本身要 750ms,连续采 5 次就是近 4 秒,所以这套滤波只适合放在慢速采样周期里,不能塞进 50ms 的中断。折中方案是每次中断只启动一次转换,用状态机轮转,四个周期凑够 4 个样本。
3.4 显示:74LS164 串行驱动三位共阳数码管
三位数码管显示设定值或实测值,用三片 74LS164 级联做串入并出,每片驱动一位。74LS164 没有锁存,数据在移位过程中会逐位透到输出端,视觉上表现为轻微拖影,对温度这种刷新频率不高的场合可以接受;介意的话换成带锁存的 74LS595。
原设计用 P3.0/P3.1 走串口方式 0 输出。这种方式省代码,但把串口占死了,后期想加上位机通信就冲突了。我的习惯是用两根普通 I/O 模拟时钟:
code unsigned char seg_code[] = { 0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90 }; // 共阳 0~9 sbit SDA_164 = P3^0; sbit CLK_164 = P3^1; void display_3digit(unsigned int val) { unsigned char i, d[3]; d[0] = val / 100 % 10; d[1] = val / 10 % 10; d[2] = val % 10; for (i = 0; i < 3; i++) { unsigned char dat = seg_code[d[i]]; unsigned char b; for (b = 0; b < 8; b++) { // 先送高位 SDA_164 = (dat & 0x80) ? 1 : 0; dat <<= 1; CLK_164 = 0; CLK_164 = 1; // 上升沿移入 } } }seg_code放在code区,段码是共阳接法(0 亮 1 灭),共阴要按位取反。发送顺序是先送高位、先送百位,这样三片级联移完之后,百位落在最后一片的输出上,接线顺序要对得上,接反了显示就是倒的。val传进来的是 0.1℃ 单位的整数时,记得先除以 10 再显示。
4. 模糊 PID 算法落地:从量化因子到查表输出
4.1 固定 PID 在这台水箱上为什么会翻车
水箱是个典型的大惯性滞后对象:加热丝通电,水温要几十秒才有明显反应;停止加热,余温还会继续往上传。固定参数的 PID 在升温段需要大 Kp 才能快速拉起来,可一到保温段,同样的 Kp 会让输出在设定值附近大幅摆动;把 Kp 调小,升温又慢得让人怀疑人生。更麻烦的是环境温度、水量变化都会改变对象特性,一组参数调好了换季就不好使。
模糊层干的事情很朴素:看当前误差 e 和误差变化率 ec 处在什么状态,动态地给 Kp、Ki、Kd 各加一个修正量。偏差大就加大 Kp 加快响应,偏差小就减 Kp 加 Ki 保精度,ec 大说明冲得快就加 Kd 压一压。这些规则用自然语言描述,不需要被控对象的传递函数,正好绕开了建模这一步。
4.2 量化因子与模糊化:把 ±10℃ 映射到 7 个语言值
误差 e 的实际范围取 ±10℃(也就是 ±100 个 0.1℃ 单位),误差变化率 ec 取 ±2℃/采样周期(±20 个单位)。两者都映射到 7 个语言值:NB、NM、NS、ZO、PS、PM、PB,对应整数 −3 到 +3。
量化因子就是缩放系数:Ke = 3 / 100 = 0.03,Kec = 3 / 20 = 0.15。8051 上跑浮点太奢侈,直接改成定点:e_q = e * 3 / 100,结果限幅在 ±3。这种"整数档位"实现相当于把隶属度函数简化成单点赋值,牺牲一点平滑性,换来的是几百个机器周期的开销,实际控温效果差别很小。
signed char quantize(int err, int range) { long q; q = (long)err * 3 / range; // 归一化到 -3~+3 if (q > 3) q = 3; if (q < -3) q = -3; return (signed char)q; }range传 100 表示误差通道,传 20 表示误差变化率通道。限幅这一步不能少,开机瞬间误差可能是 300 个单位,不限幅就查表越界,读到的是数组外面的垃圾数据,输出会直接打满。
4.3 规则表:49 条 if-then 写成二维数组
七个语言值两两组合,共 49 条规则。以 Kp 的修正为例,行是 ec(NB→PB),列是 e(NB→PB),表里的数字是修正档位 −3~+3:
| ec \ e | NB | NM | NS | ZO | PS | PM | PB |
|---|---|---|---|---|---|---|---|
| NB | PB | PB | PM | PM | PS | ZO | ZO |
| NM | PB | PB | PM | PS | PS | ZO | NS |
| NS | PM | PM | PM | PS | ZO | NS | NS |
| ZO | PM | PM | PS | ZO | NS | NM | NM |
| PS | PS | PS | ZO | NS | NS | NM | NM |
| PM | PS | ZO | NS | NM | NM | NM | NB |
| PB | ZO | ZO | NM | NM | NM | NB | NB |
把它翻译成整数索引就是table[ec+3][e+3],值 0~6 对应 PB~NB 七档,实际修正量再乘一个输出量化因子 Ku。Ki、Kd 各有一张同样结构的表,Ki 的规律通常是误差小时给大、误差大时给小;Kd 则是在 ec 大时适当加大抑制超调。
code signed char kp_rule[7][7] = { { 3, 3, 2, 2, 1, 0, 0}, { 3, 3, 2, 1, 1, 0,-1}, { 2, 2, 2, 1, 0,-1,-1}, { 2, 2, 1, 0,-1,-2,-2}, { 1, 1, 0,-1,-1,-2,-2}, { 1, 0,-1,-2,-2,-2,-3}, { 0, 0,-2,-2,-2,-3,-3} }; void fuzzy_tune(signed char eq, signed char ecq, int *kp, int *ki, int *kd) { signed char dkp = kp_rule[ecq + 3][eq + 3]; signed char dki = ki_rule[ecq + 3][eq + 3]; signed char dkd = kd_rule[ecq + 3][eq + 3]; *kp = KP0 + dkp * KU; // KP0 是基准值,KU 是输出量化因子 *ki = KI0 + dki * KU; *kd = KD0 + dkd * KU; if (*kp < 0) *kp = 0; // 参数不能为负 if (*ki < 0) *ki = 0; if (*kd < 0) *kd = 0; }三张表用code放 Flash,各占 49 字节。KP0/KI0/KD0是离线整定出来的基准参数,模糊层只在它附近做修正,这样即使规则表某一条写得不理想,输出也不会失控——这是我自己调试时最看重的一点,纯模糊控制器没法这么兜底。
4.4 增量式 PID 与可控硅占空比输出
PID 用增量式,输出的是控制量的变化量而不是绝对量,天然带抗积分饱和,切手动切自动也不会有冲击:
int pid_calc(int set, int now10, int kp, int ki, int kd) { static int e1 = 0, e2 = 0; static int u = 0; int e, du; e = set * 10 - now10; // 统一到 0.1℃ 单位 du = (kp * (e - e1) + ki * e + kd * (e - 2*e1 + e2)) / 100; u += du; if (u > 100) u = 100; // 限幅 0~100 占空比 if (u < 0) u = 0; e2 = e1; e1 = e; return u; }kp/ki/kd是放大 100 倍的整数,所以最后要除 100。set * 10是把设定温度换算到和实测一样的 0.1℃ 精度,两边单位不统一是增量式 PID 最常见的翻车点,现象是输出方向反了或者根本不动作。u的 0~100 直接对应周波控制的导通比例:1 秒 50 个半波,u=60 就是导通 30 个半波。
执行时用定时器配合过零检测,或者简单地按时间片统计:
void heat_output(unsigned char duty) { static unsigned char half_wave = 0; half_wave++; if (half_wave >= 50) half_wave = 0; HEAT = (half_wave < duty / 2) ? 1 : 0; }这段要在过零中断里调用,保证每次开关都发生在电压过零点。duty / 2是把百分比换算成半波个数,50 个半波正好 1 秒,分辨率 2%。想要更细就把时间窗拉长到 2 秒,代价是输出滞后变大。
4.5 定时器 0 调度:50ms 中断加软件计数器
整个软件的时间基准是定时器 0 的 50ms 中断,中断里只做三件事:重装初值、累加软件计数器、到点置采样标志。真正耗时的采集和运算放到主循环,中断里绝不调用 DS18B20 那套带几百微秒延时的函数,否则中断会被拉长到毫秒级,其他时序全乱。
void timer0_isr(void) interrupt 1 { TH0 = 0x9E; TL0 = 0x58; if (++tick_50ms >= 10) { // 10 × 50ms = 500ms tick_50ms = 0; flag_sample = 1; } } void main(void) { system_init(); while (1) { key_scan(); if (flag_sample) { int t10, e, ec; flag_sample = 0; t10 = temp_filter(); e = set_temp * 10 - t10; ec = e - last_e; last_e = e; fuzzy_tune(quantize(e,100), quantize(ec,20), &kp,&ki,&kd); duty = pid_calc(set_temp, t10, kp, ki, kd); } } }原设计把采样周期定在 15 秒,计数器写成>= 300。这个值对水箱来说偏保守,稳态调节会很迟钝,实际调试时我一般会缩到 500ms~1s,让模糊层能及时感知变化。周期改小以后,ec 的量纲跟着变,量化因子 Kec 要同步重算,不然规则表的触发点全偏了。
5. 联调避坑:五类现场问题与排查路径
5.1 现象:温度恒定显示 85℃,怎么加热都不变
原因基本锁定在单总线没通。DS18B20 上电默认输出 85℃,主机读不到有效数据时读回来的正是这个值。常见诱因有三个:DQ 线漏了 4.7k 上拉电阻(寄生供电模式下尤其致命)、复位后的存在脉冲没有判、微秒延时在 6MHz 下不准导致采样点错位。
解决顺序是先用万用表量 DQ 在空闲时是不是高电平,不是就先补上拉;再在ds_reset()里打断点或者点灯,看返回值是不是 0;最后拿逻辑分析仪抓一次复位波形,确认低电平宽度和存在脉冲窗口。延时函数不要凭感觉写,for循环的空转次数随编译优化等级变化,改用_nop_()拼出来的固定长度更可靠。如果用的是寄生供电,还要注意在转换期间把总线强上拉,否则转换电流不够,读回来也是 85℃。
5.2 现象:一加热数码管就乱码,偶尔整机复位
典型的干扰问题。可控硅在 220V 回路上开关,dv/dt 会通过共地路径串到单片机。三个地方要一起查:光耦有没有真正做电气隔离(用 MOC3021 这类光耦可控硅,而不是普通三极管光耦)、强电地和弱电地是不是只在一个点汇合、可控硅两端有没有 RC 吸收。
我一般按这个顺序处理:先给可控硅并 100Ω+0.1μF 吸收,再看复位脚有没有加 100nF 到地、上拉电阻是不是 10k;然后检查 7805 输入端电压,满载加热时如果掉到 7V 以下,输出就会塌,单片机必然复位,这种情况只能加大滤波电容或者换开关电源。最后是走线:220V 回路和信号线分开走,别捆在一起,光耦下方不要铺地。
5.3 现象:稳态时温度在设定值附近来回摆,幅度 1℃ 左右
这是极限环,多半是控制周期太长加上积分项在边界上反复累加。15 秒采样意味着每次调整都滞后于实际温度变化,控制器永远在追一个过时的误差。另外,如果输出已经打满 100 而误差还没归零,积分项会继续累积,等到温度回落时输出降不下来,必然冲过头。
处理办法有三条,通常一起上:把采样周期缩到 500ms 左右,让模糊层跟得上;给积分项加分离条件,误差绝对值大于 3℃ 时停止积分;在设定值附近设一个 ±0.2℃ 的死区,误差落在死区内就保持上一拍输出不动。死区是软件层面的妥协,但它对显示稳定性的改善立竿见影,0.5℃ 的精度要求完全覆盖得住。
5.4 现象:超调大,设定 60℃ 能冲到 66℃,然后等很久才回落
大惯性对象的通病,热量已经加进去了,温度还会继续往上走。问题出在升温段 Kp 给得太猛,或者规则表里"误差大就猛加输出"这条写得太激进。模糊规则应该体现"提前收手":当 e 为负(已经超过设定值)且 ec 也为负(还在往上涨)时,输出必须直接砍到 0,而不是慢慢减。
另一个隐蔽原因是加热丝功率和水体容量不匹配。功率过大时,最小可控占空比(周波控制下是 2%)带来的温升速率都可能超过自然散热速率,这时候无论算法怎么调,稳态都会缓慢爬升。遇到这种情况只能从硬件下手:换功率小一点的加热丝,或者把时间窗从 1 秒拉长到 4 秒,把 2% 的分辨率细化到 0.5%。
5.5 现象:停止加热后温度仍在缓慢上升,可控硅像是关不断
先分清是软件没关还是器件没关。把 P1.7 强行拉低,如果温度还在涨,那就是可控硅的问题。常见原因有三个:光耦选型用了随机触发型却没有配合过零检测,导致触发脉冲落在非过零点;RC 吸收里的电容漏电流偏大,在高温高湿环境里足以维持可控硅导通;双向可控硅的 dv/dt 耐量不够,换向时被自己重新触发。
排查时先断开光耦输出,看可控硅是否彻底关断,能关断就说明驱动侧有问题;再换一个品牌的吸收电容试;确认负载是不是纯电阻(电炉丝在冷态时电阻很低,浪涌电流大)。治本的办法是换过零触发型光耦,让每次导通都从过零点开始,同时给可控硅留足散热余量。
6. 二次开发:从固定查表到可在线整定的温控平台
这套代码跑通之后,最有价值的改造方向是让规则表离开代码区。把三张 7×7 的表放进 EEPROM(比如 24C02),开机读进 RAM,通过按键长按组合或者串口下发新表,就能在不重新烧片的情况下试不同规则。验证方法也简单:接一个串口打印,把每次的 e、ec、kp、ki、kd、duty、实测温度按行打出来,用上位机画成曲线,阶跃响应的超调量、上升时间、稳态误差一眼就能看出来。
串口这里有个坑必须提前规划:原设计把 P3.0/P3.1 借给 74LS164 做显示,串口就没了。要加上位机,显示部分得先改成普通 I/O 模拟移位时钟,或者换成 I2C 的数码管驱动,把串口腾出来。这一步改起来不大,但要是等算法调完才发现没口可用,返工就很烦。
| 改造项 | 现状 | 改法 | 验证指标 |
|---|---|---|---|
| 采样周期 | 15s | 缩到 500ms,重算 Kec | 稳态波动 ≤ 0.3℃ |
| 规则表 | 固化在 code 区 | 搬 EEPROM,串口可下发 | 换表后阶跃响应可比 |
| 积分处理 | 全程累加 | 误差 >3℃ 时分离 | 超调量下降 |
| 显示通道 | 占用串口 | 改普通 I/O 模拟时钟 | 串口可用于调试 |
| 传感器 | 单点 DS18B20 | 多点取平均或换 PT100 | 抗局部扰动 |
再往远走可以换 STM32 上跑浮点模糊推理,或者直接上自整定 PID,但那已经不是这套 8 位机方案要解决的问题了。这套设计的价值在于链路完整、每一段都能单独验证,改任何一处都有明确的观察点。
最后说个血泪教训:我早期调试时图省事,把 DS18B20 的采集直接写在定时器中断里,结果中断被拉长到几毫秒,数码管扫描全乱,还以为是显示驱动坏了,查了两天才发现是时序互相踩。从那以后我每次做带单总线的项目,中断里只置标志,所有带微秒延时的操作一律放主循环,改完再也没出过这类问题。希望帮到你。
本文还有配套的精品资源,点击获取