简介:基于MATLAB GUI的家庭室内温湿度控制源码包,面向物理应用仿真与界面开发学习者,以家庭温湿度采集与控制为典型实例,展示从数据读取、逻辑处理、界面交互到结果可视化的完整设计流程。压缩包共15个文件,以8个m源码文件为核心,包含主程序、FIG界面文件、温度湿度数据文本、MAT数据文件及效果预览图,可支撑直接运行与二次开发;全包仅716KB,轻量易部署。已有298人学习,代码经测试支持MATLAB 2019b运行,主控逻辑清晰,并整合文件读取、网络请求、图像显示等辅助模块,便于理解GUI回调机制、数据可视化与物理量监测的联动方式。尤其适合需要快速搭建温湿度监测界面的场景,既可作为课程设计或毕业设计的起点,也可帮助初学者掌握MATLAB GUI编程、控件布局与实时数据刷新的实现思路。
1. 家庭温湿度控制为什么适合用 MATLAB GUI 来做
一年里最难熬的不是盛夏,而是春秋过渡期:白天太阳晒进来温度飙到 26℃,入夜又掉到 16℃,加湿器开了湿度上不去,关了又干得嘴唇起皮。手动开关暖风机和加湿器,基本靠体感、靠猜。真正要解决这个问题,需要一个能读温湿度、能输出控制信号的闭环系统,但直接用 PLC 或嵌入式来做,成本高、调试周期长,而且中间的控制参数你看不见。最务实的路线是先用 MATLAB 把物理模型、控制算法、人机界面在一个环境里跑通。这也是“物理应用基于 matlab GUI 家庭室内温湿度控制”这类项目最常见的展开方式:先建模,再在 GUI 面板上实时观察曲线和调节参数,确认控制逻辑可靠后再考虑移植到硬件。对做课设、毕设的在校生和想快速验证算法的工程师来说,这套流程比一上来买开发板更有性价比。
2. 先把房间物理模型写对:热湿平衡差分方程与参数定标
2.1 温度回路:等效热容与热损失系数
房间温度变化可以近似为一阶惯性对象,热平衡方程是“室内空气吸收的热量 = 加热器产热 + 人体显热 − 围护结构散热 − 通风换气散热”。写成微分方程就是:
C × dT/dt = Q_heat + Q_people − (UA + m_dot×cp) × (T − T_out)
其中 C 是室内空气的等效热容,单位 J/K;UA 是墙体、窗户加上屋面传热的总热导,单位 W/K;m_dot×cp 是通风造成的热损失系数。对 75 m³、约 30 平米的客厅,空气质量大约 90 kg,等效热容取 90.45 kJ/K;围护结构加通风综合热损系数取 170 W/K,这个数对应外墙保温一般、双层玻璃、有 0.8 次/h 换气的场景。把微分方程用向前欧拉法离散,采样步长取 1 仿真分钟,就得到可以在 MATLAB 里直接迭代的差分形式。
不少初学者喜欢直接写传递函数 G(s)=K/(Ts+1)e^(−τs) 来模拟房间,这做单点定常仿真没问题,但后续要接天气扰动、要观察温度和湿度耦合效应时,传递函数不够直观。我一般建议用状态方程形式,后面加扰动、加非线性设备模型都方便。
2.2 湿度回路:空气质量与换气次数的量纲
湿度平衡和温度平衡结构类似,但量纲容易翻车。室内空气含湿量 H 用绝对湿度 g/kg 表示,室内空气总质量是 ρ×V,约 90 kg。加湿器产湿量 500 g/h,除以 90 kg 再除以 60 分钟,得到每分钟绝对湿度上升约 0.0926 g/kg;人体散湿按 80 g/h 算,贡献约 0.0148 g/(kg·min)。通风除湿项是换气次数 n(次/h)除以 60,再乘以室内外含湿量差 (H − H_out)。
湿度回路同样做一分钟量级的离散,注意加湿和除湿是同一个控制量上的正负号。不能把相对湿度直接当状态量参与积分,因为相对湿度随温度变化,模型里温度一变,相对湿度自己就会跳,容易让控制器误判。
把温度、湿度两个方程放进同一个函数里,控制量 u(1) 是加热功率系数 0~1,u(2) 是加湿/除湿幅度 −1~1。加湿时水分蒸发要从空气中吸热,所以温度方程里减去一部分热量,这一步是温湿度耦合的关键体现。
function [dT, dH] = roomModel(T, H, u, Tout, Hout) % 房间热湿平衡差分模型,步长为 1 仿真分钟 % 输入: T 温度 ℃, H 绝对湿度 g/kg, u=[加热系数, 加湿/除湿幅度] % Tout 室外温度 ℃, Hout 室外绝对湿度 g/kg % 输出: dT, dH 为每分钟的变化量 C_eff = 90450; % 室内空气等效热容 J/K,约 90kg * 1005 J/kg/K K_loss = 170; % 围护结构+通风总热损系数 W/K m_air = 90; % 室内空气质量 kg n_air = 0.8; % 换气次数 次/h G_humid = 500; % 加湿器最大产湿量 g/h G_dehum = 400; % 除湿机最大除湿量 g/h G_people = 80; % 人体散湿 g/h % 热平衡: 加热 + 人体显热 - 加湿蒸发吸热 - 围护与通风散热 Q_heat = u(1) * 2200; % 加热器功率 W Q_evap = max(u(2), 0) * 200; % 加湿蒸发吸热 W dT = (Q_heat + 120 - Q_evap - K_loss * (T - Tout)) / C_eff * 60; % K/min % 湿平衡: 加湿或除湿 + 人体散湿 - 通风换气引起的湿交换 if u(2) > 0 W = u(2) * G_humid / 60; % 加湿 g/min else W = u(2) * G_dehum / 60; % 除湿,负值 g/min end dH = (W + G_people / 60) / m_air - n_air / 60 * (H - Hout); % g/(kg*min) end代码里的核心逻辑是三部分:加热量通过 u(1) 线性折算成瓦特;加湿蒸发吸热只对 u(2) 正值生效,避免除湿机被错误地当成降温设备;通风换气项同时出现在温度和湿度方程里,保证模型物理方向正确。真实房间的热容远比 90 kg 空气大,墙体、家具都会蓄热,工程上通常把 C_eff 放大 3 到 5 倍,否则升温速度会明显快于实测。
2.3 相对湿度换算与模型参数表
状态量用绝对湿度,界面和控制目标要用相对湿度,需要一组换算公式。饱和水汽压用 Magnus 公式近似,相对湿度等于绝对湿度除以当前温度下的饱和含湿量。20℃ 时饱和含湿量约 14.4 g/kg,如果室内绝对湿度是 7 g/kg,相对湿度约 48%,正好在人体舒适区。这个换算要在每次状态更新后执行。
function RH = relHumid(T, H) % 根据温度和绝对湿度计算相对湿度,单位 % es = 610.78 * exp(17.27 .* T ./ (T + 237.3)); % 饱和水汽压 Pa qs = 0.622 * es ./ 101325; % 饱和含湿量 kg/kg RH = H ./ (qs * 1000) * 100; % H 单位 g/kg 时 RH = min(RH, 100); end模型里常见的参数取值范围如下表,做 GUI 之前先把这几个数定下来,后面所有仿真曲线才有意义。
| 参数 | 符号 | 典型数值 | 单位 | 定标依据 |
|---|---|---|---|---|
| 室内等效热容 | C_eff | 90.45(演示值) | kJ/K | 空气质量 90kg×定压比热 |
| 围护总热损 | K_loss | 170 | W/K | 墙、窗、通风叠加 |
| 换气次数 | n_air | 0.8 | 次/h | 门窗气密性中等 |
| 加热功率 | Q_max | 2200 | W | 常见电暖器额定功率 |
| 最大加湿量 | G_humid | 500 | g/h | 超声波加湿机参数 |
| 最大除湿量 | G_dehum | 400 | g/h | 除湿机铭牌参数 |
| 人体散湿 | G_people | 80 | g/h | 轻体力活动 |
定标建议是先让加热器全开、室内外无温差,记录升温曲线,反推 C_eff;再让房间自然降温,用时间常数反推 K_loss。GUI 里这两个参数最好做成可编辑输入框,方便对着真实房间重新定标。
3. 用 Timer + GUI 回调把模型跑成实时监控界面
3.1 界面布局:Gauge、Slider、Edit Field 怎么搭配
建模完成后,界面层的核心诉求是:能看当前温度、能看相对湿度、能改目标值、能开关控制回路。新版 MATLAB 里我建议直接用 App Designer,别再用 guide。R2023b 之后官方不再推荐新项目使用 guide,老工程能运行但编辑体验很差,拖控件、写回调都很别扭。App Designer 里用 Gauge 显示实时温度,用 Lamp 或 Edit Field 显示相对湿度,用 Slider 设定温度目标值,按钮负责启动和停止仿真,再放一个 Axes 画温湿度历史曲线。
控件命名要克制,每个控件只承担一个职责。启动按钮叫 StartButton,温度表叫 TGauge,湿度显示叫 RHLabel,目标温度滑杆叫 SetTempSlider。对象命名规范直接影响后面回调代码的可读性,别人的源码你接手时也是先扫命名再读逻辑。
常见的错误做法是把所有显示都堆在 Edit Field 里,数字跳动频繁,看不出趋势。仪表盘加曲线是最直接的调试组合:仪表盘给当前值,曲线给变化轨迹。如果项目里还有户外温度输入,用 Edit Field 手动输入即可,配合一个 CheckBox 决定是否启用户外正弦扰动。
3.2 启动/停止按钮与 Timer 生命周期
实时仿真的核心不是 draggable 的 while 循环,而是 MATLAB 的 timer 对象。把 timer 周期设成 1 秒,每次回调推进 1 个仿真分钟,这样界面上 1 秒就能看到室温变化 1.5℃ 左右,演示效果好。如果把 timer 周期设成和仿真步长一致,也就是 1 秒代表 1 秒,用户盯着曲线半小时看不出明显变化,会以为程序卡死了。
timer 的生命周期管理是整个 GUI 里最容易出 bug 的地方。启动按钮按下时先判断旧 timer 是否还在,在的话先 stop 再 delete;每个 timer 回调里用 guidata 读取最新句柄数据,改完再写回;窗口关闭回调里必须清理 timer,否则定时器会一直空转。
% 启动按钮回调,guide 风格代码,App Designer 逻辑一致 function btnStart_Callback(hObject, eventdata, handles) if isfield(handles, 'tmr') && isvalid(handles.tmr) stop(handles.tmr); delete(handles.tmr); end handles.tmr = timer(... 'TimerFcn', {@tmrTick, handles.figure1}, ... 'Period', 1.0, ... 'ExecutionMode', 'fixedRate'); start(handles.tmr); guidata(hObject, handles); end % 每秒执行一次,推进 1 个仿真分钟 function tmrTick(~, ~, fig) if ~ishandle(fig), return; end h = guidata(fig); % 读取当前控制量,调用模型 u = [h.uHeat, h.uHum]; [dT, dH] = roomModel(h.T, h.H, u, h.Tout, h.Hout); h.T = h.T + dT; h.H = h.H + dH; h.simTime = h.simTime + 1; % 刷新显示 set(h.txtTemp, 'String', sprintf('%.1f ℃', h.T)); set(h.txtRH, 'String', sprintf('%.1f %%', relHumid(h.T, h.H))); set(h.Axes, 'XData', h.timeHist, 'YData', h.tempHist); guidata(fig, h); end % 窗口关闭时清掉 timer function figure1_CloseRequestFcn(hObject, eventdata, handles) if isfield(handles, 'tmr') && isvalid(handles.tmr) stop(handles.tmr); delete(handles.tmr); end delete(hObject); end回调里先 ishandle 再 guidata,是为了防止窗口被关掉后 timer 还在触发,导致guidata(fig)报错。启动前执行一次完整重置,把 h.T、h.H、h.simTime 和曲线数组全部初始化。曲线数组先预分配大小比如 120 个点,满了以后做循环覆盖,避免数组无限增长拖慢界面。
3.3 仿真步长与控制周期要分开设置
很多人仿真和控制器混在一起:每 1 秒调用一次模型,同时也每 1 秒刷新一次 PID 输出。这在温差大时会出现控制量来回猛跳,界面曲线像锯齿,实际设备也受不了。正确做法是模型步长固定为 1 仿真分钟,控制器按自己的采样周期工作,比如每 5 个仿真分钟才更新一次加热功率和加湿量。
if mod(h.simTime, 5) == 0 RH = relHumid(h.T, h.H); [h.uHeat, h.uHum] = pidControl(h.setT, h.T, RH, h.setRH); guidata(fig, h); end控制器输出幅度也做了限制:加热系数在 0 到 1 之间,加湿/除湿幅度在 −1 到 1 之间。这个限制看起来简单,但直接影响后面 PID 抗积分饱和的设计。如果输出本身没做饱和,PID 里就不用写抗饱和逻辑;一旦做了限幅,就必须在控制器里同步处理积分项。
GUI 里还要区分“仿真时间”和“真实时间”。界面右上角显示的是仿真第几分钟,不是当前北京时间。把这两个时间搞混,读别人的源码时经常会对不上号。
4. 控制算法落地:抗饱和 PID、温湿度回路与模糊规则表
4.1 为什么单回路控制温湿度会互相打架
温度控制调节加热器,湿度控制调节加湿器,看似两个独立回路,实际耦合很强。加湿器喷雾会增加蒸发吸热,让室温往下掉;加热器运行时相对湿度会因为温度升高而下降,湿度控制器误以为除湿量不够。如果两套 PID 各调各的,很容易出现加热器刚把温度拉上来,除湿机又开始全速工作,两者互相抵消,能耗全浪费在对抗上。
我建议的控制结构是温度回路为主、湿度回路为辅。温度回路优先级最高,其输出只调节加热功率;湿度回路在温度偏差小于 0.5℃ 时才允许调节加湿/除湿设备。这样避免强耦合下的震荡。
function [u1, u2] = pidControl(setT, T, RH, setRH) % 温湿度解耦控制: 温度输出加热功率,湿度输出加湿/除湿功率 % 控制器状态用 persistent 保存 persistent tempPID humPID prevTempOk if isempty(tempPID) tempPID = AntiWindupPID(0.15, 0.008, 0.05, 5, 1.0); humPID = AntiWindupPID(2.5, 0.15, 0.8, 5, 1.0); prevTempOk = true; end % 温度回路,输出限制在 0 ~ 1 u1 = tempPID.step(setT, T); u1 = min(max(u1, 0), 1); % 湿度回路: 温度偏差大时不调节湿度 tempErr = abs(setT - T); if tempErr < 0.5 u2 = humPID.step(setRH, RH); u2 = min(max(u2, -1), 1); prevTempOk = true; else u2 = 0; if prevTempOk % 从“允许湿度调节”切换到“禁止”时,清掉湿度PID积分项 humPID.resetIntegral(); prevTempOk = false; end end end这个解耦逻辑不复杂,但工程上比直接上模糊控制靠谱。它本质上是一个带门限的优先级管理:温度不在舒适区时,湿度控制先让一边。prevTempOk 这个变量记得在切换瞬间清积分,否则湿度积分项带着旧的偏差重新介入时,会猛地来一下加湿或除湿。
4.2 带抗饱和的增量式 PID
控制量的饱和是必然的,因为加热器功率有物理上限。普通位置式 PID 在长时间饱和后,积分项会累积到一个很大的值,等偏差反向时输出还停留在饱和区,造成明显超调。带抗饱和的增量式 PID 在输出限幅后主动把积分误差往回退,代码容易实现,效果直接。
classdef AntiWindupPID < handle properties Kp Ki Kd Ts intErr lastErr outMax end methods function obj = AntiWindupPID(Kp, Ki, Kd, Ts, outMax) obj.Kp = Kp; obj.Ki = Ki; obj.Kd = Kd; obj.Ts = Ts; obj.outMax = outMax; obj.intErr = 0; obj.lastErr = 0; end function u = step(obj, sp, y) err = sp - y; obj.intErr = obj.intErr + err * obj.Ts; u = obj.Kp * err ... + obj.Ki * obj.intErr ... + obj.Kd * (err - obj.lastErr) / obj.Ts; % 输出限幅,并反向修正积分项 if abs(u) > obj.outMax u = sign(u) * obj.outMax; obj.intErr = obj.intErr - err * obj.Ts * 0.8; end obj.lastErr = err; end function resetIntegral(obj) obj.intErr = 0; obj.lastErr = 0; end end end积分回退系数取 0.8,意思是输出饱和时每次把积分往反方向拉回 80%,兼顾抗饱和速度和稳定性。如果取 1.0,饱和退出后输出会明显抖动;取太小则抗饱和效果不够。Ts 是控制器采样周期,在 GUI 里对应 5 个仿真分钟,应该和 GUI 里调用 pidControl 的周期保持一致。
| 回路 | Kp | Ki | Kd | Ts | 输出范围 |
|---|---|---|---|---|---|
| 温度 | 0.15 | 0.008 | 0.05 | 5 min | 0 ~ 1 |
| 湿度 | 2.5 | 0.15 | 0.8 | 5 min | −1 ~ 1 |
初值可以按 Ziegler-Nichols 临界比例度法去试,先只加 Kp 让系统临界震荡,记录临界周期再反推 Ki 和 Kd。做 GUI 仿真时,把这几个参数暴露成 Edit Field,仿真过程中直接改,比每改一次就重新编译要快得多。
4.3 模糊控制规则表:把 PID 输出替换成规则输出
在一些模型不确定、参数随季节变化大的项目里,PID 参数要频繁重调,模糊控制更省心。把温度偏差 ET 和偏差变化率 EC 模糊化成 5 档,输出加热功率也分 5 档,规则表按人工经验填写。
| ET \ EC | 负大 | 负小 | 零 | 正小 | 正大 |
|---|---|---|---|---|---|
| 负大 | 正大 | 正大 | 正大 | 正小 | 零 |
| 负小 | 正大 | 正小 | 正小 | 零 | 负小 |
| 零 | 正小 | 正小 | 零 | 负小 | 负小 |
| 正小 | 正小 | 零 | 负小 | 负小 | 负大 |
| 正大 | 零 | 负小 | 负大 | 负大 | 负大 |
规则表第一行读法:室内温度非常低,而且还在快速下降,此时加热功率拉到最大。对角线上温度偏差和变化率符号相反,说明温度在回升,适当减小输出,避免超调。模糊控制输出是连续量,需要把规则结论做重心法去模糊化。
模糊控制在 GUI 演示中有一个优势:控制量变化比 PID 平滑,曲线更漂亮,解释起来也直观。但它有个致命缺点是稳态精度一般,Q 点附近可能出现小幅度极限环。我的做法是模糊控制并联一个积分器,只在偏差小时让积分器介入,既保留模糊控制的鲁棒性,又消除稳态误差。
5. 跑通源码后的三个验证技巧与排查命令
5.1 用断言快速验证模型物理方向
拿到别人的源码,第一步不是急着跑 GUI,而是先验证模型函数的物理方向是否正确。加热应该升温,室外变冷应该加快降温,加湿应该让绝对湿度升高。把这些断言写成一个测试脚本,跑一次就能暴露量纲和正负号问题。
% testRoomModel.m u_heat = [1, 0]; [dT_heat, ~] = roomModel(20, 7, u_heat, 20, 7); [dT_idle, ~] = roomModel(20, 7, [0, 0], 20, 7); assert(dT_heat > dT_idle, '加热必须比自然状态升温快'); [~, dT_cold] = roomModel(20, 7, u_heat, 10, 7); assert(dT_cold < dT_heat, '室外更冷时升温速度必须下降'); [dT_humid, dH_plus] = roomModel(20, 7, [1, 1], 20, 7); [~, dH_minus] = roomModel(20, 7, [1, -1], 20, 7); assert(dH_plus > 0 && dH_minus < 0, '加湿除湿方向错误');断言跑通只能说明模型没有方向性错误,不能说明参数准确。下一步做开环阶跃仿真,加热全开 60 分钟,温度持续上升、相对湿度因为升温而下降,这个趋势和真实物理一致才算合格。
5.2 正弦扰动仿真对比
真实场景里室外温度是周期性变化的,一天之内可能有 10℃ 的温差。用正弦函数构造室外温度扰动,对比有控制和无控制的室内温度曲线,能直观看出 PID 回路是否有效。
N = 24 * 60; Tout = 12 + 6 * sin(2 * pi * (0:N-1) / N); T_open = zeros(N, 1); H_open = zeros(N, 1); T_open(1) = 20; H_open(1) = 7; for k = 2:N [dT, dH] = roomModel(T_open(k-1), H_open(k-1), [0, 0], Tout(k), 6); T_open(k) = T_open(k-1) + dT; H_open(k) = H_open(k-1) + dH; end无控制时室温会跟随室外温度波动;接入 PID 控制后,室内温度应该被拉回目标值附近。对比两组曲线的最大偏差,如果控制后温度偏差还超过 1.5℃,优先检查 PID 采样周期和积分系数。
5.3 GUI 源码排查三连命令
运行别人的 GUI 源码报错时,先别急着看每个回调函数。我一般按三个命令排查:which 确认函数文件在不在路径上,depfun 列出依赖文件,delete 清理残留 timer。
which roomModel; % 确认模型函数可被找到 depfun('RoomCtrl.m'); % 列出所有依赖文件路径 delete(findall(groot, 'Type', 'timer')); % 清理上次运行残留的timerdepfun输出里出现路径带private的文件要特别留意,说明它是某个函数的私有辅助函数,不能从外面直接调用。findall清理 timer 是调试 GUI 最实用的一条命令,反复启停程序后旧 timer 残留,会导致界面闪烁、CPU 占用异常,这行命令一次清干净。
调试界面回调时,用guidata(handles.figure1)查看当前句柄结构体里到底存了哪些字段。字符显示 NaN 的时候,先看 h.T 是不是被误存成了字符串。GUI 源码项目里这类类型混用问题比算法问题多得多。
本文还有配套的精品资源,点击获取