ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32+DS18B20温控器:从单总线时序到PID闭环控制实现

2026/9/14 14:29:30 拓冰建站 浏览量
STM32+DS18B20温控器:从单总线时序到PID闭环控制实现 简介一份基于STM32的DS18B20温度传感器与PID温控算法实验资源面向嵌入式学习者与需要实现精准温度控制的开发者。资源围绕单线通信、温度采集、PID参数整定与加热/冷却设备控制展开涵盖DS18B20工作原理、STM32编程及实时反馈调节等关键内容适合从入门到进阶的实践参考。压缩包共209个文件约6.8MB以C源码、头文件、Keil工程文件与编译输出为主含hex可烧录文件、电路相关文档及工程配置便于直接打开项目学习或二次开发。目前已有1085人学习下载。通过该实验可掌握单线总线协议、STM32外设使用与PID控制在温度场景中的完整落地方式获得从传感器数据读取到控制输出的闭环思路是一份兼顾原理与工程实践的参考资料。1. 从一根线读温度到闭环控温这个实验的真正难度在哪看到“DS18B20温度传感器实验_PID_STM32温控器”这类标题大部分人会先入为主地认为核心是“把温度读出来”。但DS18B20本身是个单总线从机读温度只是基础外设驱动真正把这个标题撑起来的是后面的STM32对PID算法的落地以及把PID输出变成可控加热功率的完整闭环。换句话说这个实验要交付的不是一个温度表而是一个能自动维持目标温度的温控器。它适合已经会点灯、会配置定时器、想开始做闭环控制项目的嵌入式开发者也适合正在做毕业设计或产品原型、需要一套能直接复用的PID温控方案的工程师。读完这篇文章你会得到一个从传感器时序、PID离散化实现到调参验证的完整技术路径。2. DS18B20时序与STM32读取单总线驱动的三个关键实现2.1 单总线协议没有时钟线时序窗口就是全部DS18B20使用Dallas单总线协议总线空闲时由上拉电阻维持高电平所有通信都通过主机把总线拉低再释放来完成。与I2C和SPI不同单总线没有专门的时钟线接收方完全依靠主机对电平翻转时序窗口的精确控制来判断每一位是0还是1。这对STM32来说反而是优势GPIO翻转延迟可控用微秒级延时函数就能精确复现时序。DS18B20的常规操作流程分为三步复位脉冲、ROM命令、功能命令每一步都有严格的时间参数要求。复位时主机把总线拉低至少480微秒再释放从机检测到下降沿后会主动拉低总线60~240微秒作为存在脉冲。主机收到存在脉冲后发送ROM命令单设备场景通常用0xCC跳过ROM匹配随后发0x44启动温度转换等待转换完成后发0xBE读取9字节暂存器数据。数据字节的每一位都是一个时隙写1时隙是拉低1~15微秒后释放写0时隙是拉低60~120微秒再释放读时隙是主机拉低1~15微秒后释放并在15微秒内采样总线电平。实际工程中通信失败多由三个原因造成上拉电阻缺失或阻值过大导致总线浮空GPIO输入输出模式切换不及时导致采样窗口错过延时函数不精确比如直接用HAL_Delay它的最小粒度是1毫秒根本无法满足微秒级时序要求。所以驱动实现里必须有一个可靠的微秒延时函数通常用DWT或定时器来实现。提示上拉电阻阻值建议取4.7kΩ到10kΩ之间。实验板上如果已经集成就不需要再外接但要确认其阻值和供电电压匹配。2.2 STM32 GPIO模拟时序模式切换与读写位的实现无论使用标准库还是HAL库DS18B20驱动核心都是GPIO的快速翻转和微秒级延时。这里给出基于HAL库的实现骨架重点在于模式切换函数。单总线通信要求主机在同一根线上完成输出和输入两种操作所以SetOutput和SetInput两个内联函数是必需的static inline void DS18B20_SetOutput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS18B20_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DS18B20_PORT, GPIO_InitStruct); } static inline void DS18B20_SetInput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS18B20_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(DS18B20_PORT, GPIO_InitStruct); }这段代码的核心是GPIO模式切换。HAL_GPIO_Init每次调用都要填充结构体并完成底层寄存器配置在时隙只有几十微秒的场景下开销可以接受。但如果后续要做高速读取或者系统主频较低建议直接操作寄存器来切换模式把配置过程压缩到寄存器级别的几条语句。模式切换的关键在于写时序必须用推挽输出读时序必须切回浮空输入否则总线电平会被输出寄存器锁住读不到从机拉低的信号。复位和读写一个位的函数是驱动里最底层的操作。复位函数先拉低总线500微秒然后释放并切换到输入模式在40微秒处读取存在脉冲uint8_t DS18B20_Reset(void) { uint8_t presence 1; DS18B20_SetOutput(); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(500); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); delay_us(30); DS18B20_SetInput(); delay_us(40); presence HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN); delay_us(200); return presence; /* 返回0表示检测到存在脉冲 */ }复位函数中500微秒的低电平时间要大于数据手册规定的480微秒最小值。释放总线后等待30微秒是为了让上拉电阻把总线恢复到高电平再切输入模式读取从机的存在脉冲。如果在释放瞬间就切输入总线高电平还没建立读取结果会误判为从机响应。40微秒处的采样点位于从机拉低总线的时间窗口内读到低电平说明设备在线读到高电平则说明总线上没有设备或接线错误。读写位的实现直接决定通信成败。读位时主机拉低总线2微秒后释放等待10微秒后采样写位时通过控制拉低后释放的时机来区分1和0uint8_t DS18B20_ReadBit(void) { uint8_t bit 0; DS18B20_SetOutput(); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(2); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); DS18B20_SetInput(); delay_us(10); bit HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN); delay_us(50); return bit; } void DS18B20_WriteBit(uint8_t bit) { DS18B20_SetOutput(); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); if (bit) { delay_us(5); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); delay_us(60); } else { delay_us(60); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); delay_us(5); } }写1时拉低5微秒后释放从机在15微秒内采样到高电平写0时拉低维持60微秒以上再释放从机采样到低电平。读时序则不同主机只需要拉低2微秒就释放然后在10微秒处读取总线状态这时从机如果回0会在15微秒内把总线拉低如果回1总线保持高电平。所有时间参数必须满足DS18B20手册中的时隙定义delay_us函数必须精确不能依赖HAL_Delay。2.3 温度转换与CRC校验保证读回来的数据可信完成复位和ROM命令后真正读温度只需要两个功能命令先发0x44启动温度转换再发0xBE读取暂存器。0x44命令发出后转换需要时间12位分辨率下最长750毫秒期间不能去读暂存器否则读到的是上一次转换的旧数据。常见做法是固定延时800毫秒精度要求高的系统可以读暂存器第7字节的bit7为1表示转换还在进行。float DS18B20_ReadTemperature(void) { uint8_t data[9] {0}; int16_t raw 0; float temp 0.0f; if (DS18B20_Reset() ! 0) return -999.0f; DS18B20_WriteByte(0xCC); /* 跳过ROM适用于单设备 */ DS18B20_WriteByte(0x44); /* 启动温度转换 */ delay_ms(800); /* 等待转换完成 */ if (DS18B20_Reset() ! 0) return -999.0f; DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); /* 读取暂存器 */ for (int i 0; i 9; i) { data[i] DS18B20_ReadByte(); } if (CRC8(data, 8) ! data[8]) return -999.0f; /* CRC校验失败 */ raw (data[1] 8) | data[0]; temp raw * 0.0625f; /* 12位分辨率每LSB对应0.0625℃ */ return temp; }这里用-999作为通信失败的哨兵值调用方必须检查返回值再决定是否参与PID计算。原始温度是一个16位有符号整数高位字节存在data[1]低位字节存在data[0]两者拼接后乘以0.0625就是摄氏温度。如果器件配置为9位分辨率换算系数要改成0.5但温控场景一律建议使用12位分辨率。CRC校验不能省略它是保证数据可信的最后防线。单总线在长线传输时容易受干扰校验失败时应丢弃本次数据而不是继续送入PID控制器。3. PID控制在STM32上的实现从差分方程到抗饱和代码3.1 位置式还是增量式温控场景为什么选前者当STM32通过DS18B20拿到当前温度后控制问题就变成了“如何让温度收敛到设定值”。PID控制器的输入是偏差e(k)目标值-当前值输出是给执行器的控制量。由于STM32只能按固定周期采样和计算PID必须以离散化的差分方程形式实现。常见的位置式PID离散形式为u(k)Kp·e(k)Ki·∑e(j)·TKd·(e(k)-e(k-1))/T。其中∑e(j)·T是对偏差的离散积分T为控制周期。与位置式对应的增量式PID计算的是控制量增量Δu(k)u(k)-u(k-1)公式为Δu(k)Kp·(e(k)-e(k-1))Ki·e(k)·TKd·(e(k)-2e(k-1)e(k-2))。增量式的优势在于输出只依赖最近几次偏差计算量小手动/自动切换时无扰动因此常用于步进电机这类执行器。但温控场景我一般选位置式原因有两个第一加热负载的输出本身就是0~100%功率位置式PID的输出直接对应“当前要输出多少功率”语义直观便于限幅和诊断第二位置式在表达积分饱和时更清晰而积分饱和恰恰是温控系统最需要处理的问题。增量式PID在温控里并非不能用但需要对累加输出做额外的限幅处理积分项的物理含义会被削弱。另一个选择依据是执行器类型如果你是用伺服电机驱动阀门调节流量增量式更合适如果是SSR或继电器控制加热丝位置式更合适。STM32的浮点运算能力跑位置式PID绰绰有余控制周期在200毫秒以上时每次计算占用CPU时间可以忽略。3.2 离散化代码实现限幅、积分饱和与计算顺序在STM32上实现PID先用结构体保存控制器状态包括目标值、三个系数、上一次偏差、积分累加值以及输出限幅。一个规范的位置式PID结构体和计算函数如下typedef struct { float target; /* 目标温度 */ float kp; /* 比例系数 */ float ki; /* 积分系数已乘以控制周期T */ float kd; /* 微分系数已除以控制周期T */ float prev_err; /* 上一次偏差 */ float integral; /* 积分累积值 */ float out_min; /* 输出下限 */ float out_max; /* 输出上限 */ } PID_t; float PID_Calc(PID_t *pid, float current) { float err pid-target - current; float output; /* 积分项限幅防止积分饱和导致大过冲 */ pid-integral err; if (pid-integral pid-out_max / pid-ki) pid-integral pid-out_max / pid-ki; if (pid-integral pid-out_min / pid-ki) pid-integral pid-out_min / pid-ki; /* 位置式PID三项独立计算后求和 */ output pid-kp * err pid-ki * pid-integral pid-kd * (err - pid-prev_err); pid-prev_err err; /* 总输出限幅 */ if (output pid-out_max) output pid-out_max; if (output pid-out_min) output pid-out_min; return output; }这个实现里最值得注意的地方是积分限幅。如果不限制积分项系统从低温启动时积分项会持续累积到一个很大的值等温度到达设定点附近时积分项的“热量惯性”还会继续推动输出造成明显过冲甚至持续振荡。把积分项限制在out_max/ki到out_min/ki之间等效于让积分项单独贡献时也不会超出执行器输出范围。这里的ki之所以能直接做除法是因为结构体中已经预先乘过了控制周期T这点在初始化时要注意。微分项使用的是前后两次偏差的差值没有除以T因为kd在初始化时已经预先除以了控制周期。这样设计的好处是调参时Kp、Ki、Kd三个参数可以直接对应“经典PID参数”不需要在公式里再换算。计算顺序也有讲究先更新积分项再计算总输出最后限幅避免限幅值影响prev_err的更新。prev_err始终记录的是本次计算的原始偏差与输出是否被限幅无关。3.3 控制周期和Kp/Ki/Kd的经验范围PID参数不是拍脑袋定的。在温控场景里有一个“比例带PB”的概念经常出现在PID仪表里PB100/Kp单位是百分比。调参时如果把PB设为10%对应的Kp就是10。TI是积分时间TD是微分时间经典PID形式是uKp·(e1/TI·∫e·dtTD·de/dt)。代码里的kiKp·T/TIkdKp·TD/T所以从仪表参数换算到代码参数需要知道控制周期T。控制周期T的选择对参数影响很大。温控系统的时间常数大通常T取200毫秒到2秒。如果T太短DS18B20的转换延迟会引入测量滞后微分项容易放大噪声如果T太长系统对扰动的响应会变慢。下面是一组针对加热腔体、DS18B20探头测温场景的经验参数范围控制周期TKpKi预乘TKd预除T适用场景200ms8~150.3~0.810~20小体积加热块热容小500ms5~100.2~0.55~10常规加热腔空气传热1000ms3~60.1~0.32~5大热容保温为主这组经验参数的逻辑是控制周期越长同样的偏差在单位时间内被积分项累积的量越大所以Ki要收小微分项作用的时间尺度拉长Kd也要相应减小。执行器如果是电磁继电器控制周期绝对不能小于继电器的机械动作时间建议大于2秒否则继电器频繁吸合释放寿命会急剧缩短。同一个PID算法配合SSR实现PWM控制和配合继电器实现开关控制参数几乎完全不同前者可以到几百毫秒的周期后者更适合以秒为单位的占空比控制。4. 温控执行链路与PWM映射让PID输出变成真实热量4.1 执行器选型SSR加PWM是稳定温控的主流方案PID计算出来的结果是一个0~100之间的浮点数要把它变成实际加热功率必须有执行链路。常见做法有两种一种是用固态继电器SSR接收PWM信号由SSR控制加热丝或加热棒的电源通断另一种是用电磁继电器把PID输出映射为“加热多少秒、停止多少秒”的脉冲序列。PWM加SSR的组合要稳定得多因为SSR没有机械触点开关频率可以到几十赫兹占空比直接对应加热功率控制精度高而且不存在继电器吸合释放时的电弧干扰。如果采用STM32的定时器产生PWM配置时要综合考虑控制周期和PWM频率。温控场景里PWM频率不需要太高1Hz到10Hz是常见区间。频率过高时交流SSR的过零触发无法精确跟随反而产生谐波频率过低时温度会出现以秒为单位的可见波动。假设系统主频84MHz配置TIM2输出100Hz PWM的代码如下TIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC; htim2.Instance TIM2; htim2.Init.Prescaler 840 - 1; /* 84MHz分频到100kHz */ htim2.Init.Period 999; /* 100kHz/1000 100Hz */ htim2.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim2); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);Prescaler和Period的关系是PWM频率定时器时钟/(Prescaler1)/(Period1)。84MHz先除以840得到100kHz再除以1000得到100Hz。CH1的比较寄存器范围是0到999对应0到100%占空比。设置比较值时用__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 500)即可输出50%占空比。配置完成后定时器会自动产生PWM波形占空比更新只需修改比较寄存器CPU不需要参与每个周期的电平翻转。4.2 PID输出映射与死区处理两个不能忽略的边界PID输出直接作为占空比百分比时要注意执行器的死区问题。SSR在低占空比时比如2%以下导通时间太短可能无法有效触发电磁继电器在输出为3%时对应的导通时间甚至达不到继电器的机械动作最小时间。反过来输出在98%以上时系统已经接近满功率控制器大概率工作在饱和区。所以需要一个映射函数把PID输出归一化到执行器能够稳定工作的区间uint16_t PID_OutputToPWM(float pid_out) { float duty pid_out; if (duty 2.0f) duty 0.0f; /* 下限死区直接关断 */ if (duty 98.0f) duty 100.0f; /* 上限饱和直接全开 */ return (uint16_t)(duty * 10.0f); /* 占空比换算到比较寄存器 */ }死区设置多大取决于负载功率和传感器位置。如果DS18B20探头紧贴加热体死区可以设5%因为传感器对微小功率变化非常敏感如果探头测的是空气温度死区设2%就够空气热容小微小占空比变化也会引起温度读数波动。映射函数尾部的duty*10是为了把0~100的百分比换算到0~999的比较寄存器范围。如果PWM周期改成499或其他值这里的系数也要同步调整。负载类型下限死区上限饱和说明加热丝SSR2%98%SSR在低占空比下触发不稳PTC加热片5%95%PTC功率随温度变化需预留余量继电器加热棒10%90%避免继电器频繁动作4.3 硬件连接与信号隔离别让加热回路干扰温度读数DS18B20的供电方式分寄生供电和外部供电两种。寄生供电只需要两根线数据线高电平时给传感器充电但温度转换期间总线必须保持强上拉否则传感器可能供电不足。温控系统强烈建议使用外部供电VDD接3.3V或5VGND与STM32共地DQ接GPIO并接一个4.7kΩ上拉电阻到VDD。外部供电下传感器的驱动能力更强时序窗口更稳定不会因为总线状态变化影响转换精度。加热负载的驱动回路必须与DS18B20的信号线保持距离。继电器或SSR切换的瞬间感性负载会产生很强的电磁干扰如果信号线与之平行走线超过20厘米DS18B20的读数会出现几十度的跳变。对于PID闭环来说这种跳变非常致命因为微分项会放大温度跳变产生一次虚假的功率冲击。信号线使用双绞线或屏蔽线屏蔽层单端接地是成本最低也最有效的抗干扰手段。电源部分建议把加热负载供电和逻辑供电分开共地但不同回路避免大电流在公共地线上产生压降干扰STM32的ADC和GPIO电平。5. 调参顺序与串口波形验证让PID参数真正收敛5.1 先测纯滞后再定控制周期调参之前先把控制周期确定下来。控制周期必须大于系统的纯滞后时间即“从PID输出变化到DS18B20检测到温度变化”的完整延迟包括加热体的热传导延迟和DS18B20最长750毫秒的转换延迟。最简单的方法是做一个阶跃测试满占空比加热记录从开启到DS18B20读数开始上升的时间用这个时间乘以1.5到2作为控制周期的初始值。纯滞后过长却用短周期控制会导致输出动作过快系统在设定点附近来回冲撞。5.2 临界比例度法从纯P到PB、TI、TD调参从纯P开始把积分项和微分项关闭逐步增大Kp。DS18B20温度在设定点附近出现等幅振荡时记下此时的Kp为临界增益Ku振荡周期为Tu然后按经典公式计算Kp0.6·KuTi0.5·TuTd0.125·Tu。温度系统的时间常数大Tu通常在2到10分钟整个实验需要耐心。如果时间不够可以做阶跃响应取温度上升曲线最大斜率处切线得到等效延迟L和时间常数T再套齐格勒-尼科尔斯时域整定公式。5.3 串口数据判读过冲、收敛与振荡调参质量需要用数据说话。把设定值、当前温度和PID输出通过串口以CSV格式输出每行三项波特率115200用串口助手或Python脚本记录曲线。观察三个指标升温阶段过冲是否超过5%到达设定点后的稳态误差以及扰动后能否快速恢复。过冲大就减Kp稳态误差持续存在就加Ki温度在设定点附近高频抖动就减Kd并检查DS18B20信号线是否受到加热回路干扰。一轮调参后记录下最终的Kp、Ki、Kd和控制周期后续更换加热体或探头位置时用同一套数据作为调参起点。本文还有配套的精品资源点击获取