简介:这份 PDF 是一份面向嵌入式初学者与课程设计/毕业设计学生的温度控制系统完整设计报告,围绕 STM32F103 主控展开,解决温室等场景下温度感知与恒温调节的工程实现问题。报告从方案论证、系统框图入手,依次给出电源模块、STM32F103 最小系统、DS18B20 温度采集、OLED 显示、电阻加热丝控温与按键电路的设计思路,并配套主程序、温度采集、控温、显示、按键调控等流程图说明;控制策略采用 PID 位置式算法输出 PWM 调节加热功率,使温度稳定在预设值,读者可据此理清从硬件选型、电路搭建到软件模块化编程的完整链路,也能借鉴其中的调试与验证思路。资源包共 1 个 PDF 文件,约 658KB,篇幅紧凑但含摘要、目录、电路与软件设计及附录,适合直接用于报告撰写参考或实验复现。目前已有 6719 人学习下载,可作为嵌入式温度闭环控制入门与课程实践的对照材料。
1. 温度控制的“玄学”:为什么到了设定值就关加热,反而永远稳不住
“温度一到就断电,低了再通电”——这是做温控时冒出的第一个想法,也最容易把系统做废。设定 30℃,加热丝全功率每分钟升 2℃、环境每分钟散 1℃:断电后温度因热惯性继续冲高,跌回 30℃ 重新通电时又来不及刹车,于是绕设定值画出振幅 1~2℃ 的锯齿。振幅跟你把阈值调得多细几乎无关,它由加热功率、热惯性和采样周期共同决定。
换成连续可调功率,问题就从“开还是关”变成“这一刻给百分之多少”。这套基于 STM32F103 的温度控制系统把开关换成了阀门:DS18B20 采温,OLED 显示,按键改目标,位置式 PID 算功率百分比,PWM 驱动电阻加热丝。单总线时序、占空比到平均功率的换算、单向执行器饱和、积分抗饱和都压在同一个最小系统里,任何一层塌掉都还能跑,只是精度悄悄劣化。
2. 感知链路:DS18B20 凭什么一根线就把温度读成 12 位数字
DS18B20 把温度敏感元件、ADC、暂存器和 64 位 ROM 编码全部集成在一颗三极管样的封装里,对外只留 VDD、GND、DQ 三只脚。DQ 接在 STM32F103 的 PA1 上,一根线既送命令又收数据,某些接法下还兼做供电。省线的代价是把时序责任全部推给主机:单总线没有“波特率”这个概念,每一位都由主机主动发起,位的值是靠低电平持续时间区分的。
2.1 单总线不是“一根线通信”这么简单,它是一套微秒级的握手协议
一次完整的温度读取要过四道关:复位与存在脉冲、ROM 命令、功能命令、逐个读写时隙。复位是主机把总线拉低至少 480 微秒,然后释放;在线的从机必须在 15~60 微秒内把总线再拉低 60~240 微秒作为应答,主机会在释放后约 70 微秒采样电平,读到低才算“有人”。这一步没过,后面所有字节都是空转。
写时隙的规则最容易写错。主机拉低总线产生下降沿,从机在下降沿之后 15~60 微秒的窗口内采样:如果这时总线还是低,它就认为收到 0;如果已经被释放拉高,就是 1。所以写 1 是“拉低 1~15 微秒就赶紧放手”,写 0 是“一直拉到 60 微秒时隙结束”。读时隙反过来:主机先拉低至少 1 微秒再释放,把总线交给从机,然后在 15 微秒内采样,读到的就是这一位。
/* 单总线时序:PA1 双向 GPIO,所有延时单位为微秒 */ #define OW_LOW() (GPIOA->BRR = GPIO_Pin_1) #define OW_HIGH() (GPIOA->BSRR = GPIO_Pin_1) #define OW_IN() (GPIOA->CRL = (GPIOA->CRL & ~(0xF << 4)) | (0x4 << 4)) /* 上拉输入 */ #define OW_OUT() (GPIOA->CRL = (GPIOA->CRL & ~(0xF << 4)) | (0x3 << 4)) /* 推挽输出 */ static void ow_delay_us(uint16_t us) { /* DWT 计数器或 SysTick 做的忙等 */ } /* 复位:480us 低电平 -> 释放 -> 70us 后采样存在脉冲 */ uint8_t ow_reset(void) { uint8_t presence; OW_OUT(); OW_LOW(); ow_delay_us(480); OW_IN(); /* 释放总线,靠上拉电阻回高 */ ow_delay_us(70); presence = (GPIOA->IDR & GPIO_Pin_1) ? 0 : 1; /* 低电平 = 从机应答 */ ow_delay_us(410); /* 补足一个完整时隙 */ return presence; } /* 写一位:短低 = 1,长低 = 0,时隙总长都是 60us 量级 */ void ow_write_bit(uint8_t bit) { OW_OUT(); OW_LOW(); if (bit) { ow_delay_us(6); OW_IN(); ow_delay_us(64); } else { ow_delay_us(60); OW_IN(); ow_delay_us(10); } }延时函数是整个驱动的地基。如果拿for空循环凑微秒,编译器优化等级一改、主频一调,时序就整体漂移,表现是“有时候能读到、有时候读到 85℃”。常见的实现思路是用 DWT 周期计数器或者 1MHz 的自由运行定时器,把延时和主频解耦,改主频时只改一个换算常数。
另一个隐性约束是中断。单总线的时隙只有几十微秒,任何一次中断响应加上服务程序超过这个窗口,这一位就废了。所以读写的临界段要么关总中断,要么把单总线任务的优先级提到所有可能长耗时中断之上。这也是为什么很多实现干脆放弃硬件 UART 去模拟单总线——波特率可变、时序由主机掌控,硬件外设反而不好对齐。
2.2 分辨率、转换时间与 0x0550:读数里藏着的三个陷阱
第一个陷阱是上电默认值。DS18B20 的暂存器在上电后温度字段是 0x0550,按 12 位、0.0625℃/LSB 换算正好是 85℃。如果复位没成功、转换命令没发出去、或者转换还没完成就去读,读回来的就是这个值。它看起来完全像一个合法温度,所以最难发现。稳妥的做法是在读数之后显式判一次raw == 0x0550,命中就丢弃本次采样并重试一次,而不是直接送进 PID。
第二个陷阱是分辨率与转换时间的绑定关系。9 位对应 93.75 毫秒转换、0.5℃ 分辨率;10 位 187.5 毫秒、0.25℃;11 位 375 毫秒、0.125℃;12 位 750 毫秒、0.0625℃。分辨率越高,控制周期被拉得越长,这是后面第 6 章要专门算的账。
第三个陷阱是负温度。原始数据是两个字节拼出来的有符号数,负温用的是补码。如果程序里按uint16_t拼接再乘 0.0625,零下环境会读出一个巨大的正数。必须显式转成int16_t再算。
/* 返回 12 位原始温度值(已做符号扩展),实际温度 = raw * 0.0625℃ */ int16_t ds18b20_read_raw(void) { uint8_t sp[9]; int16_t raw; if (!ow_reset()) return INT16_MIN; /* 无应答,本次作废 */ ow_write_byte(0xCC); /* Skip ROM:总线上只有一颗 */ ow_write_byte(0x44); /* Convert T:启动一次转换 */ delay_ms(750); /* 12 位分辨率必须等满 */ if (!ow_reset()) return INT16_MIN; ow_write_byte(0xCC); ow_write_byte(0xBE); /* Read Scratchpad:连读 9 字节 */ for (uint8_t i = 0; i < 9; i++) sp[i] = ow_read_byte(); if (ow_crc8(sp, 8) != sp[8]) return INT16_MIN; /* CRC 不符直接丢弃 */ raw = (int16_t)((sp[1] << 8) | sp[0]); /* 低字节在前,按有符号拼 */ if (raw == 0x0550) return INT16_MIN; /* 上电默认值,判为无效 */ return raw; }第 9 个字节是前面 8 字节的 CRC8,多项式 0x31、初值 0。加上它之后,偶发的时序抖动会被拦在控制环外面,而不是变成一个能把 PID 带偏的假温度。这一点在长导线、加热丝干扰强的场合尤其值钱。
3. 执行链路:一个 0~100 的数字,怎么变成加热丝的等效功率
温度本身是连续量,但单片机只能输出高低两种电平。PWM 就是在这两者之间造出一个“中间态”:在一个固定周期里,高电平占多大比例,负载就拿到多大比例的能量。加热丝是纯阻性负载,功率跟电压平方成正比,所以占空比 D 与平均功率 P = D·U²/R 之间是严格的线性关系——注意是与功率线性,与温度完全非线性,升温速率还取决于散热。
3.1 占空比到平均功率:为什么 1 kHz 对加热丝等于一个可调直流源
电阻加热丝有热惯性,时间常数通常在秒到分钟量级,而 PWM 周期是毫秒级。几千赫兹的斩波在加热丝上被彻底滤平,它的温度只响应平均功率,看不到开关纹波。选频率时有两头约束:低于一百赫兹,走线上的电动力和可听噪声会显现,长导线甚至能听到嗡嗡声;高到几十千赫兹,MOS 的开关损耗和驱动能力开始成为负担。1 kHz 到 10 kHz 是比较舒服的区间。
分辨率同样要做取舍。定时器计数时钟 1 MHz、重装值 999,得到 1 kHz 的 PWM 和 1/1000 的占空比粒度,对温控绰绰有余。如果为了凑更高频率把重装值压到 99,占空比就只有 1% 的台阶,稳态微调时温度会围着设定值一格一格地跳。
/* PA2 = TIM3_CH3(AFIO 重映射),72MHz 主频下: PSC=71 -> 计数时钟 1MHz,ARR=999 -> PWM 频率 1kHz,分辨率 1/1000 */ void pwm_init(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN; RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; GPIOA->CRL = (GPIOA->CRL & ~(0xF << 8)) | (0xB << 8); /* 复用推挽 50MHz */ TIM3->PSC = 71; TIM3->ARR = 999; TIM3->CCR3 = 0; TIM3->CCMR2 = (TIM3->CCMR2 & ~0x70) | 0x60; /* OC3M=110,PWM 模式 1 */ TIM3->CCMR2 |= 0x08; /* OC3PE,比较值预装载 */ TIM3->CCER |= 0x0100; /* 使能 CH3 输出 */ TIM3->CR1 |= 0x0080; /* ARPE,重装值预装载 */ TIM3->CR1 |= 0x0001; /* 启动计数 */ } void pwm_set_duty(float percent) { if (percent < 0.0f) percent = 0.0f; /* 只能加热,负输出裁掉 */ if (percent > 100.0f) percent = 100.0f; TIM3->CCR3 = (uint16_t)(percent * 10.0f); /* 0~100% -> 0~1000 */ }预装载位(OC3PE、ARPE)不是可有可无的装饰。PID 在任意时刻更新 CCR,如果没有预装载,新值可能在当前周期的中间被写入,导致这一拍出现半个周期的高电平毛刺。开启预装载后,新比较值只在计数器归零时生效,每个周期的波形都是完整的。
3.2 三极管加场效应管两级驱动:为什么不能拿 PA2 直接推 MOS
STM32 的 IO 推挽输出只能提供几十毫安的瞬时电流,而 MOS 的栅极是个电容,开通瞬间需要一股脉冲电流把电荷灌进去或抽出来。直接拿 IO 驱动,栅极充放电变慢,MOS 长时间停在放大区而不是开关区,损耗和发热都上来了。中间加一级三极管做电流放大,问题就解决了:PA2 的 PWM 只负责让三极管导通或截止,三极管再去快速拉低、拉高 MOS 的栅极。
另一个原因是电平。N 沟道 MOS 做低边开关时源极接地,栅极电压高于阈值就能开通,3.3V 逻辑可以直接驱动一部分型号;但要做高边开关,栅极电压必须高于电源电压,3.3V 根本不够。这套设计用的是低边接法:加热丝一端接 12V,另一端接 MOS 的漏极,源极接地,PWM 控制的是“通到地”的路径。
加热丝虽然是阻性负载,但走线有电感,关断瞬间会产生反向尖峰。实际板子上通常在加热丝两端并一只反向快恢复二极管做续流,再并一个小容量陶瓷电容压高频毛刺。这些细节在原理图上只是两个符号,缺了之后的表现是“偶尔复位”或“OLED 花屏”,而不是干脆不工作——属于典型的血泪经验型故障。
4. 控制律:位置式 PID 的输出,为什么要同时接受三重限幅
PID 的公式本身没什么神秘:比例项看当前误差,积分项看历史误差的累积,微分项看误差变化的趋势。真正决定它能不能在温控里稳定工作的,不是公式,而是三个限幅和对执行器物理约束的承认。
4.1 位置式与增量式的分岔:为什么温控场景选前者
位置式直接算出控制量的绝对值,也就是这一拍该给多少占空比;增量式算出的是相对上一拍的改变量,需要额外维护一个累加器。
温控里选位置式的理由很实际:执行器本身就是绝对量,PWM 占空比给 37% 就是 37%,物理意义一眼可见,调试时把输出打到串口上就能看懂。增量式的累加器在断电、手动干预、执行器饱和时都要额外处理“积分记忆怎么对齐”,反而增加了出错面。
typedef struct { float kp, ki, kd; float target; /* 目标温度,℃ */ float integral; /* 积分累加项(已限幅) */ float err_prev; /* 上一拍误差 */ float out_min, out_max; /* 输出限幅,对应占空比 0~100 */ float i_min, i_max; /* 积分限幅 */ } pid_t; float pid_calc(pid_t *p, float meas) { float err = p->target - meas; float out; p->integral += err; if (p->integral > p->i_max) p->integral = p->i_max; /* 抗积分饱和 */ if (p->integral < p->i_min) p->integral = p->i_min; out = p->kp * err + p->ki * p->integral + p->kd * (err - p->err_prev); p->err_prev = err; if (out > p->out_max) out = p->out_max; if (out < p->out_min) out = p->out_min; /* 下限 0,负输出裁掉 */ return out; }这段代码里,kp、ki、kd的量纲跟采样周期绑在一起。如果控制周期是 1 秒,ki就是“每秒每摄氏度误差贡献多少占空比”;周期改成 0.5 秒而不改ki,积分作用会翻倍,整定好的参数立刻失稳。所以周期必须固定,且写死在参数整定的假设里。
4.2 积分饱和、输出限幅与单向执行器:三个必须一起做的约束
电阻加热丝只能加热不能制冷,这是整定思路的分水岭。当温度远低于设定值,误差长时间为正,积分项一路累加;等温度终于爬上来,积分项已经攒成一个巨大的值,输出被顶在 100% 下不来,温度必然冲过设定值一大截。这就是积分饱和,在开门、换气、初始冷启动这类场景里几乎必然出现。
第一重限幅是积分限幅:给integral设上下界,让积分的作用有天花板。上界的经验取法是“能让系统在稳态时维持目标温度所需的最小占空比”对应的积分量,再留一倍余量。
第二重限幅是输出限幅:out严格夹在 0 到 100 之间。这一条不只是保护执行器,也是让 PID 输出和物理世界对齐——负数占空比不存在。
第三重是变化率限制,可选但很有用:限制每个周期占空比的最大变化量,比如一拍最多变 10%。它能压掉微分项引入的尖峰,代价是响应变慢一点。
微分项要单独说。误差的一阶差分对噪声极其敏感,DS18B20 的 0.0625℃ 量化噪声在 1 秒周期下做差分,会被放大成可观的抖动。常见做法是对微分项做一阶低通,或者干脆先只用 PI、等系统稳住了再考虑加 D。这套系统的对象是慢过程,D 的收益本来就不大。
5. 排查:温控系统最常翻车的五处,以及它们背后的时序真相
5.1 采集与显示侧
现象:刚上电,OLED 稳定显示 85.00℃,按键改目标值也不变。 原因:读回的是 DS18B20 上电默认值 0x0550。多数情况是复位脉冲之后没有正确等待存在脉冲,或者转换命令发出后没等满 750 毫秒就去读暂存器。 解决:在读数函数里显式判raw == 0x0550并丢弃重试;同时把ow_reset()的返回值真正用起来,无应答就别往下走。用示波器看 DQ 线上的复位波形,正常应该是“长低 480 微秒、释放、从机拉低 60~240 微秒”三段。
现象:温度能读,但每隔几十秒跳一次离谱值,PID 输出跟着抖。 原因:单总线时隙只有几十微秒,被其他中断打断就会读错位。OLED 刷屏、串口打印这类耗时操作如果在读时序中间插进来,必然翻车。 解决:把 DS18B20 的读写放进不可打断的临界段,或者至少把它的优先级提到刷屏之上;读回数据必须过 CRC8,不匹配就丢弃而不是将就使用。
现象:OLED 偶尔整屏不刷新,程序卡在 I2C 等待应答的循环里。 原因:软件模拟 I2C 时,如果 SDA/SCL 上电时序不对,或者从机在某一拍没有拉低 SDA 应答,阻塞式的等待循环就再也出不来。 解决:所有 I2C 等待都加超时计数,超时就复位总线(发 9 个时钟脉冲再发停止条件);上电时先把 SCL、SDA 都拉高保持几毫秒再开始通信。
5.2 控制与执行侧
现象:加热丝一通电,OLED 花屏、单片机复位或者程序跑飞。 原因:加热回路的大电流和 MCU 共用一段地线或同一个 5V 电源,开关瞬间的地弹把 3.3V 拉塌。原理图上写着“5V 经 7812 输出 12V”这类接法也需要警惕——78 系列是线性降压器,输入必须高于输出,5V 进是出不来 12V 的,这类笔误在整合电路图时很常见。 解决:加热回路单独供电,MOS 做低边开关,大电流回路与小信号回路单点接地;MCU 电源入口加 100nF 加 470uF 的去耦组合;加热丝两端并续流二极管吸收关断尖峰。
现象:按一下 K1,目标温度直接跳好几度。 原因:按键接 PB0~PB3 用外部中断检测,机械触点抖动在几毫秒内会产生一串下降沿,中断被触发很多次。 解决:中断服务函数里只置标志、不做业务;主循环按 10 毫秒周期扫描,连续两次读到同一电平才认账,做 20 毫秒消抖。步进值的改变(K3)和复位(K4)也走同一套扫描逻辑,避免中断里改全局变量带来的竞态。
现象:温度稳住了,但始终围着设定值小幅游走,怎么调 Kp 都压不下去。 原因:微分项在放大传感器的量化噪声,或者采样周期不固定导致积分项的实际增益在漂。 解决:先把 D 关掉,用纯 PI 把稳态调稳;采样周期用定时器中断严格固定,不用delay()凑;确认积分项确实被限幅,而不是一直在缓慢爬升。
6. 设计取舍:为什么把采样钉在 1 Hz、把分辨率压在 11 位
这套系统里最容易被忽略、又最影响最终精度的决定,是采样周期。DS18B20 的转换时间和分辨率是死绑的:9 位 93.75 毫秒、10 位 187.5 毫秒、11 位 375 毫秒、12 位 750 毫秒。想跑 12 位就至少得等 750 毫秒,控制周期被硬生生压到 1 秒以上;想跑 10 Hz 就只能用 9 位,分辨率 0.5℃。
判据在对象身上。加热丝加一个小空间,热时间常数通常在一百秒量级,采样周期取对象时间常数的十分之一就够,也就是 10 秒都行,1 Hz 已经相当富裕。再快,采到的只是传感器噪声和量化抖动,微分项会把它放大成执行器的抖动。所以这里的取舍是:宁可要 11 位的 0.125℃ 分辨率和 375 毫秒转换时间,把控制周期放在 0.5~1 秒,也不要 12 位换来的更长回路延迟。0.125℃ 已经比加热丝在小功率下的实际调节能力还细,再高的分辨率是数字上的自我安慰。
第二个取舍是位置式 PID 配单向执行器。输出被夹在 0~100%,负输出直接裁掉,意味着降温只能靠自然散热。这决定了整定必须偏向“慢升、不超调”:Kp 给大了会冲过设定值然后长时间等它掉下来,观感上比慢一点升上去差得多。我一般会把积分上限压到“满功率的三分之一”附近,让系统在冷启动时不会攒下太多积分。
第三个取舍是要不要加死区。温度在设定值 ±0.2℃ 来回跳是正常的量化加噪声,加个小死区能让执行器少动几次,代价是稳态误差变大。温室这类场景对 ±0.5℃ 不敏感,加死区划算;对精度敏感的场景就别加,老老实实把 PI 调稳。
验证方法其实很朴素:先做开环标定,固定 30% 占空比,环境 20℃,记录从 20℃ 升到 30℃ 用了多久,估出对象增益和热时间常数;再做阶跃响应,设定值一次加 5℃,用串口把每拍的温度和占空比打出来画曲线,重点看上升时间、超调量和稳态时占空比的波动幅度。超调大就减 Kp 或压积分上限,稳态爬得慢就加一点 Ki,全程别碰 Kd。
从那以后我每次调温控,都先把“最小能稳住温度的那个占空比”单独测一遍,把它写进积分限幅的上界,再开始整定 Kp——跳过这一步的直接后果,就是冷启动时那段漫长的积分饱和和随之而来的大超调。这套资料里的设计报告把电路图、模块连接和主程序、采集、控温、显示、按键五张流程图都摊开了,PA1 接传感器、PA6/PA7 走 OLED、PA2 出 PWM、PB0~PB3 接四个按键,正好可以对着上面这几层机制逐条核对,改起来心里有底。希望帮到你。
本文还有配套的精品资源,点击获取