STM32F103+DS18B20温控系统:PID与PWM闭环设计 简介面向嵌入式入门与课程设计场景的一份温度控制系统实践文档围绕 STM32F103 主控、DS18B20 测温、OLED 显示、电阻加热丝与按键设定展开重点讲清 PID 位置式算法调节 PWM 占空比以稳定目标温度的完整思路。压缩包仅含 1 个 PDF 文件大小约 658KB内容以设计报告为主体串联系统方案、电源与最小系统、温度采集、显示、控温及按键电路并给出主程序、温度采集、控温、显示和按键调控的程序流程便于对照理解软硬件分工。该资料已有 6719 人学习适合电子、自动化等专业学生做课程设计或毕业设计参考。读者可从中获取较完整的方案论证、电路模块划分、控制算法落地方式与程序流程图用于搭建温控实验、梳理答辩思路或排查测温与加热控制中的常见问题。1. 从一台总在目标值上下晃的育苗箱说起前阵子有位做小型育苗箱的开发者拿着这份《基于 STM32F103 的温度控制系统设计》来找我问的不是 PID 公式怎么写而是“板子通电后温度一直围着设定值来回荡是不是算法写错了”。这类问题我碰得太多了——STM32F103 加 DS18B20、电阻加热丝、OLED 这套组合原理图上没几个元件真正让人熬夜的地方全在单总线时序、采样周期和 PWM 跟热惯性怎么匹配上。这份资源是一套完整的设计报告包含电路图、系统框图和各模块软件流程图主线很清晰DS18B20 测温、OLED 显示、按键改目标值、PID 位置式算法算占空比、PWM 驱动电阻丝加热。它适合正在做温控小项目、想一次把闭环跑通的开发者也适合拿它当骨架改成恒温箱、孵化器、小型恒温槽的版本。2. 硬件链路拆解从 5V 插座到电阻丝六块电路怎么咬合很多人拿到这类报告先翻软件章节我觉得顺序反了。温控系统的脾气一半写在电路上电源怎么分级、传感器上拉多大、驱动级是几级放大这些定下来之后软件才有得调。这一章按信号流向把六块电路捋一遍顺手把原报告里几处容易照抄出错的地方标出来。2.1 电源分级3.3V 管控制12V 管发热整个系统其实有两套电压域混在一起想就容易乱。控制侧是 5V 输入经一片 AMS1117-3.3 这类三端 LDO 降到 3.3V供 STM32F103 最小系统、DS18B20 和 OLED 使用功率侧是 12V专门喂电阻加热丝。这两路必须分开走线加热丝的浪涌电流不要从 MCU 的地线上回去。AMS1117 的压差在 1.1V 左右5V 降到 3.3V 是够的但它的封装散热能力有限输入输出各挂一个 10μF 钽电容加一个 0.1μF 瓷片是常规做法靠近芯片引脚放。原报告里写“5V 电源经过 7812 三端稳压器输出 12V”这一句照抄会翻车7812 是线性稳压器要求输入比输出高 2V 以上5V 进去只能出来 3V 出头加热丝基本没反应。正确做法是 12V 单独进来或者用升压模块从 5V 升到 12V再或者干脆换成 5V 加热丝降低整机复杂度。这条我在第 5 章还会展开因为它是我见过复现失败率最高的一个点。电源指示和保险也别省。电阻丝短路或者 MOS 击穿时一颗自恢复保险丝能救回整块板子。2.2 最小系统与引脚分配一张表看清谁接了谁STM32F103 最小系统本身没什么花样8MHz 外部晶振配两颗 20~22pF 负载电容复位脚一个 10k 上拉加 100nF 对地BOOT0 用 10k 下拉保证从 Flash 启动每个电源脚旁边一颗 0.1μF 退耦。真正值得记的是引脚分配因为后面写代码全靠它对上号功能引脚外设/模式备注DS18B20 数据PA1GPIO 开漏/输入切换需 4.7k 上拉到 3.3VPWM 输出PA2TIM2_CH3 复用推挽驱动加热丝OLED SCLPA6GPIO 推挽软件 I2C模块自带 4.7k 上拉OLED SDAPA7GPIO 开漏软件 I2C同上按键 K1~K4PB0~PB3上拉输入 EXTI0~3按下接地下降沿触发调试串口PA9/PA10USART1打印温度与占空比选 PA2 做 PWM 是有讲究的它是 TIM2 的通道 3而 TIM2 挂在 APB1 上定时器时钟是 72MHz分频灵活做 1% 分辨率的占空比很轻松。PA6/PA7 用软件模拟 I2C 而不是硬件 I2C1是这类小项目的普遍选择——SSD1306 的时序宽容度高软件模拟不用处理硬件 I2C 的总线死锁出问题时打断点就能看波形。2.3 温度采集DS18B20 靠一根线说话上拉电阻不能省DS18B20 是数字温度传感器内部集成了感温元件、ADC 和单总线接口直接输出数字量省掉了外部运放和 AD 转换。它的测温范围是 -55℃ 到 125℃在 -10℃ 到 85℃ 区间精度 ±0.5℃分辨率可以配成 9~12 位对应 0.5℃、0.25℃、0.125℃、0.0625℃ 四档。12 位模式下单次转换最长 750ms这个数字直接决定了后面 PID 的采样周期不是随便定的。接线只有三根VDD、GND、DQ。DQ 接 PA1并且必须在 DQ 和 3.3V 之间挂一颗 4.7k 上拉电阻。单总线靠“主机拉低、释放后由上拉拉高”来产生电平和时序没有这颗电阻总线永远是低电平读出来就是一串 0 或者固定的 85℃。很多人用模块而不是裸芯片模块板上已经带了上拉这时候别再并一颗并了反而让上升沿变缓长线时更容易出错。供电方式有两种。寄生供电是只有两根线DQ 在总线高电平时给芯片内部电容充电转换期间需要主机强上拉外部供电是三根线VDD 直接给 3.3V时序最稳。我一般直接上三线多一根线省掉一堆玄学问题。2.4 显示与按键I2C 占两个脚四个键走外部中断显示用的是 0.96 寸 OLED驱动芯片 SSD1306128×64 点阵四针 I2C 接口。四根线里 GND、VCC 是供电SCL 接 PA6SDA 接 PA7从机地址通常是 0x788 位写地址或者 0x3C7 位地址具体看模块背面的电阻焊盘。屏幕主要显示两行内容当前实测温度和设定目标温度刷新率不用高200ms 一次肉眼就完全够刷太快反而占 CPU。按键四个K1 加、K2 减、K3 切换步进值、K4 恢复到初始温度。硬件上是独立按键一端接 PB0~PB3一端接地内部上拉打开平时高电平按下变低。原报告用外部中断检测PB0~PB3 分别对应 EXTI0~EXTI3 四条独立中断线在 F103 上它们是各自独立的中断向量写起来很直接。用中断的好处是不用在主循环里轮询坏处是机械按键抖动会进好几次中断——这一点在第 5 章会重点说处理不好就是“按一下跳三度”。2.5 控温执行级PWM 不是直接驱动中间要过两级从 PA2 出来的 PWM 只有 3.3V、几毫安不可能直接推电阻丝。原报告的做法是两级第一级用 S8050 这类 NPN 三极管做电平转换和电流放大第二级用场效应管做开关源极接地、漏极接加热丝负极加热丝另一端接 12V。MOS 要选逻辑电平型的像 AO3400、IRLZ44N 这种栅极开启电压在 2V 以下的才能被 3.3V 可靠驱动用 IRF540 这种标准 MOS 配 3.3V 栅压很可能只开了一半管子发烫还带不动负载。栅极串一个 100Ω 左右的电阻抑制振铃栅源之间再挂一个 10k 下拉保证 MCU 复位期间 MOS 是关断的这一点不能省否则上电瞬间加热丝就是全功率。电阻丝是纯阻性负载理论上不需要续流二极管但如果走线长、有电感成分并在加热丝两端加一颗快恢复二极管更稳妥。PWM 频率的选择是个折中。频率高10kHz 以上人耳听不见但 MOS 开关损耗上升频率低几十 Hz开关损耗小但电阻丝会有轻微的脉冲感。我一般取 1~10kHz配合后面要讲的 PID 周期控制效果和发热都正常。3. 软件骨架主循环跑四件事PID 只在 1 秒节拍上动手硬件定下来之后软件的结构其实已经被约束死了DS18B20 转换要 750ms所以温度不可能读得比这更快PID 的输出必须跟采样周期对齐否则积分项算出来的量纲是错的。这一章按初始化、采集、运算、输出、显示五段把代码骨架拆开每段都能直接抄进工程里改。3.1 初始化顺序与 while(1) 的分工主程序的流程在原报告里画得很简单初始化 → 循环调用各模块函数。实际写的时候初始化顺序是有讲究的顺序错了会出现“屏幕亮了但温度不刷新”这类怪现象。int main(void) { /* 1. 系统时钟外部 8MHz 晶振倍频到 72MHz */ SystemInit(); Delay_Init(); /* 基于 SysTick 的毫秒延时1ms 中断 */ /* 2. 执行器先初始化并保持关断避免上电瞬间加热 */ Heater_PWM_Init(); /* PA2 - TIM2_CH3, 占空比先写 0 */ Heater_SetDuty(0); /* 3. 传感器、显示、按键 */ DS18B20_Init(); /* PA1 配置为开漏输出 */ OLED_Init(); /* PA6/PA7 软件 I2C */ Key_Init(); /* PB0~PB3 上拉输入 EXTI */ /* 4. 从 Flash 读上次保存的目标温度没有就用默认值 */ target_temp Param_Load(); if (target_temp 15.0f || target_temp 60.0f) target_temp 25.0f; PID_Init(pid, 8.0f, 0.6f, 20.0f); /* Kp/Ki/Kd 初值后面现场调 */ /* 5. 启动第一次转换之后靠节拍推进 */ DS18B20_StartConvert(); while (1) { if (tick_1s_flag) /* SysTick 累加出来的 1 秒节拍 */ { tick_1s_flag 0; float cur DS18B20_ReadTemp(); /* 读上一次转换结果 */ DS18B20_StartConvert(); /* 立刻启动下一次 */ float duty PID_Calc(pid, target_temp, cur); Heater_SetDuty(duty); /* 0~100 映射到 CCR */ OLED_ShowTemp(cur, target_temp); /* 刷新显示 */ } if (tick_200ms_flag) /* 按键扫描/界面刷新用慢节拍 */ { tick_200ms_flag 0; Key_Process(); /* 处理中断里置的标志 */ } } }这里的关键是先关执行器再初始化其他外设。很多板子上电第一次跑就烧了加热丝原因就是 MOS 的栅极在 MCU 复位期间处于浮空状态被干扰拉高。把 PWM 初始化和Heater_SetDuty(0)放到最前面等于给自己留了一颗后悔药。另外注意DS18B20_ReadTemp和DS18B20_StartConvert是一对12 位分辨率下转换要 750ms所以读的是上一次启动的转换结果读完立刻启动下一次形成流水线。这样温度数据永远是“最多 1 秒前”的不会因为等待转换把主循环卡死。3.2 单总线时序复位、写位、读位三段代码DS18B20 的所有通信都建立在三种基本时序上复位脉冲、写时隙、读时隙。时间单位是微秒靠Delay_us死等。这段代码是整份报告里最值得逐行抠的部分因为它的容错率极低。/* DS18B20 挂在 PA1方向需要在输出和输入之间切换 */ #define DQ_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_1) #define DQ_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_1) #define DQ_READ() GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) /* 复位主机拉低至少 480us释放后等 15~60us传感器回拉低 60~240us */ uint8_t DS18B20_Reset(void) { uint8_t ack; DQ_OUT(); /* PA1 切输出 */ DQ_LOW(); Delay_us(500); /* 480us 是下限给足 500us */ DQ_HIGH(); DQ_IN(); /* 释放总线切输入等待应答 */ Delay_us(70); /* 采样窗口落在 15~60us 之后 */ ack DQ_READ(); /* 0 表示传感器拉低了总线应答成功 */ Delay_us(430); /* 补足整个时隙总时长约 1ms */ return ack; } /* 写一位拉低 1~15us 后释放主机在时隙内保持不同时长区分 0 和 1 */ void DS18B20_WriteBit(uint8_t bit) { DQ_OUT(); DQ_LOW(); if (bit) { Delay_us(6); DQ_HIGH(); Delay_us(64); } /* 写 1短低电平 */ else { Delay_us(60); DQ_HIGH(); Delay_us(10); } /* 写 0长低电平 */ } /* 读一位主机拉低 1us 以上再释放15us 内采样总线电平 */ uint8_t DS18B20_ReadBit(void) { uint8_t bit; DQ_OUT(); DQ_LOW(); Delay_us(2); DQ_IN(); /* 释放由传感器决定电平 */ Delay_us(10); /* 采样点要落在 15us 之内 */ bit DQ_READ(); Delay_us(50); /* 补足 60us 以上的读时隙 */ return bit; }三个函数的参数只有延时值但每个延时都有边界含义。复位时拉低 500us 是为了超过 480us 的下限采样点放在释放后 70us 是为了落在传感器的应答窗口里写 1 的拉低时间 6us 必须大于 1us、小于 15us写 0 的 60us 必须落在 60~120us 之间读位时拉低 2us 后必须立刻切输入晚一点点总线就被主机自己按住了读回来永远是 1。还有一个隐形的坑Delay_us的精度依赖于 SysTick 和编译器优化等级。开 -O2 优化后循环被优化掉延时可能缩短一半读出来的数据就成了随机数。我一般把Delay_us写成带__NOP()的循环并加volatile计数或者直接用定时器计数器做微秒延时别指望空循环。拿到原始数据后是两字节低字节在前拼成 int16 再乘 0.0625 就是摄氏度float DS18B20_ReadTemp(void) { uint8_t buf[2]; int16_t raw; DS18B20_Reset(); DS18B20_WriteByte(0xCC); /* 跳过 ROM单传感器场景 */ DS18B20_WriteByte(0xBE); /* 读暂存器 */ buf[0] DS18B20_ReadByte(); /* 温度低字节 */ buf[1] DS18B20_ReadByte(); /* 温度高字节 */ raw (int16_t)((buf[1] 8) | buf[0]); return raw * 0.0625f; /* 12 位分辨率1 LSB 0.0625℃ */ }0xCC是跳过 ROM 匹配总线上只有一颗传感器时用这个命令能省掉 64 位地址的读取如果要做多点测温就得先0x33读 ROM 再逐个匹配。0.0625这个系数只对 12 位分辨率成立如果配置成 9 位系数是 0.5读取时低几位是无效的直接乘会得到一堆假精度。3.3 位置式 PID误差怎么变成占空比控制算法用的是位置式 PID输出直接是本次应该给出的控制量占空比而不是增量。位置式的公式好理解把历史误差全累加进积分项乘以系数后加上比例项和微分项。typedef struct { float Kp, Ki, Kd; float integral; /* 误差累加 */ float last_err; /* 上一次误差算微分用 */ float out_max; /* 输出上限对应 100% 占空比 */ } PID_t; float PID_Calc(PID_t *p, float target, float actual) { float err target - actual; float out; p-integral err; /* 积分限幅防止长时间到不了目标导致积分项累到天上 */ if (p-integral 500.0f) p-integral 500.0f; if (p-integral -500.0f) p-integral -500.0f; out p-Kp * err p-Ki * p-integral p-Kd * (err - p-last_err); p-last_err err; /* 输出限幅负值一律按 0 处理加热丝不能制冷 */ if (out p-out_max) out p-out_max; if (out 0.0f) out 0.0f; return out; }这段代码里三个参数要分开看。Kp决定响应速度给大了会震荡给小了升不上去Ki负责消掉稳态误差也就是“总是差 0.8℃ 到不了”这种现象但它也是震荡的元凶必须限幅Kd抑制超调对温度这种大惯性对象有用但会放大传感器噪声DS18B20 分辨率 0.0625℃ 本身就有量化抖动Kd太大输出会一跳一跳。限幅有两处都不能省。integral限幅是抗积分饱和从室温升到 60℃ 的过程中误差一直是正的大数积分项会累到很大等温度到了目标值积分项还得慢慢往回退结果就是冲过头再慢慢降下来。输出限幅保证 PID 的输出永远落在 0 到out_max之间不会算出负占空比写进 CCR 造成异常。3.4 PWM 配置72MHz 怎么切出 10kHz 和 1% 分辨率用标准外设库配 TIM2 通道 3目标是 10kHz 开关频率、1% 的占空比分辨率void Heater_PWM_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); /* PA2 复用推挽交给 TIM2_CH3 */ gpio.GPIO_Pin GPIO_Pin_2; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); /* 72MHz / (711) 1MHz 计数时钟(991) 100 个计数一周期 - 10kHz */ tb.TIM_Period 99; tb.TIM_Prescaler 71; tb.TIM_ClockDivision TIM_CKD_DIV1; tb.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tb); oc.TIM_OCMode TIM_OCMode_PWM1; /* 计数小于 CCR 时输出有效 */ oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; /* 上电占空比 0 */ oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC3Init(TIM2, oc); TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); } /* duty 传 0~100映射到 CCR 0~100 */ void Heater_SetDuty(float duty) { if (duty 0.0f) duty 0.0f; if (duty 100.0f) duty 100.0f; TIM_SetCompare3(TIM2, (uint16_t)duty); }两处参数是有因果关系的预分频 71 把 72MHz 分到 1MHz周期 99 意味着一个 PWM 周期是 100 个计数正好 10kHz同时 CCR 取 0~100 就自然对应 0~100%不需要再做换算。如果把TIM_Period改成 999频率降到 1kHz分辨率提高到 0.1%但那样 CCR 的取值范围变成 0~1000Heater_SetDuty里的映射也得跟着改两处不同步是最常见的低级 bug。TIM_OC3PreloadConfig和TIM_ARRPreloadConfig要打开这样占空比和周期的修改会在下一个更新事件才生效避免在计数值中间改 CCR 产生毛刺。3.5 显示与按键的刷新节拍显示和按键不能跟着 PID 那个 1 秒节拍走。屏幕 1 秒刷一次肉眼能看出跳变按键 1 秒响应一次用户会以为坏了。我一般拆成三档节拍1 秒做采集和 PID200ms 做按键扫描和界面刷新10ms 做一次轻量的按键状态机。OLED 走软件 I2C一次全屏刷新大概要几毫秒到十几毫秒取决于模拟 I2C 的速率。所以刷屏不要整屏清、整屏写只更新温度数字那几十个像素SSD1306支持按页写和按列写改局部区域能省不少时间。显示格式上两行就够第一行T:25.4C第二行SET:30.0C再留一行显示占空比百分比调试时非常有用——看着占空比从 60% 慢慢降到 12% 并稳定住就知道 PID 在工作。4. 从设计报告到能跑的工程编译、烧写、分段联调原报告给的是原理图、框图和流程图没有给完整的工程文件。要把它变成能烧进板子的固件中间这一段得自己补。我按顺序说一下我一般的做法从建工程到第一次闭环跑通快的话一个下午。4.1 工程目录与库的选择库有两种选法标准外设库SPL和 HAL 库。这份报告是外设寄存器风格代码里直接用GPIO_InitTypeDef、TIM_OCInitTypeDef跟标准外设库是对得上的照着写最省事。HAL 库的写法是HAL_GPIO_Init、HAL_TIM_PWM_Start逻辑一样但要重新组织对新手反而多一层抽象。我建议复现时就用标准外设库工程结构按下面这样分改起来不打架TempCtrl/ ├── CMSIS/ # 内核头文件和启动文件 ├── FWLIB/ # 标准外设库源码 ├── USER/ │ ├── main.c # 初始化和主循环 │ ├── stm32f10x_it.c # 中断服务函数 │ └── system_stm32f10x.c # 时钟配置 ├── HARDWARE/ │ ├── ds18b20.c/.h # 单总线驱动 │ ├── oled.c/.h # SSD1306 驱动 │ ├── key.c/.h # 按键与外部中断 │ └── heater.c/.h # PWM 与执行器 └── APP/ ├── pid.c/.h # PID 算法 └── param.c/.h # 参数保存驱动层和算法层分开好处是换传感器或者换执行器的时候只动 HARDWARE 目录PID 一行都不用改。.h里只放函数声明和宏具体实现全在.c里避免头文件互相包含导致的重复定义。4.2 时钟树和定时器分配F103 的时钟树要理顺外部 8MHz 晶振经 PLL 九倍频到 72MHz 作为 SYSCLKAHB 不分频APB2 也是 72MHzAPB1 最高 36MHz 但定时器时钟会倍频回 72MHz。这个细节决定了 TIM2 的输入时钟是 72MHz 而不是 36MHz上面 PWM 的分频计算就是基于 72MHz 来的。定时器资源分配上TIM2 给了 PWMSysTick 给了毫秒节拍这两个够用。如果还要做串口超时或者输入捕获测按键长按再开一个 TIM3。注意 TIM2 是 32 位定时器不是F103 的 TIM2 是 16 位在 72MHz 下最大计数值 65535做长延时不够但做 PWM 和短延时完全没问题。4.3 分段验证先点亮 OLED再读温度最后闭环不要一次把全部代码烧进去然后祈祷它工作。我一般分四步验证每步都有一个明确的“通过标准”第一步只跑 OLED。写一段循环显示Hello和自增数字能稳定显示、不花屏、不掉帧就算过。这一步能同时验证 I2C 时序、地址对不对、供电是否正常。第二步加 DS18B20。串口打印读到的原始两字节和换算后的温度用手捏住传感器看数值是否上升用冰袋靠近看是否下降。通过标准是室温下读数在合理区间、变化方向正确、连续读 100 次没有出现 85 或者 -127 这种异常值。第三步只跑 PWM不接 PID。用万用表量加热丝两端电压或者串一个电流表占空比从 0 逐步给到 100看电压是否线性跟随。这一步验证的是 MOS 有没有被可靠驱动、电源带不带得动。第四步闭环。给一个比室温高 10℃ 的目标值观察温度曲线和占空比变化。通过标准是能升到目标值附近、超调不超过 1~2℃、稳定后波动在 ±0.3℃ 以内。这个顺序的好处是任何一步出问题排查范围都被锁死在一个模块里不用面对“整机不工作”的黑匣子。4.4 现场整定 Kp/Ki/Kd 的试法温控对象的惯性很大参数不能套别人的。我一般按这个顺序试先把Ki和Kd清零只留Kp从 1 开始加每加一次给一个阶跃目标观察响应。Kp加到温度开始出现明显震荡的那个值然后退回它的一半作为比例项的基准。再加上Ki从小往大加比如 0.1、0.3、0.6观察稳态误差是否消除。如果加Ki之后开始周期性震荡说明积分太强退回去如果恒温时温度总是差一点点说明积分不够继续加。最后加Kd一般从 10 到 30 之间试。温度传感器分辨率有限Kd太大会把量化噪声放大成输出抖动表现为占空比忽高忽低、继电器或者 MOS 频繁动作。如果发现占空比抖得厉害先减Kd。一个实用的技巧把目标温度、实测温度、占空比三个量通过串口按 CSV 格式打出来导进表格画曲线比盯着 OLED 看直观得多。超调多少、稳定时间多久、稳态波动多大全在图上。4.5 用串口打印把黑匣子打开温控调试最怕的是不知道内部发生了什么。串口打印是成本最低的观测手段USART1 用 PA9/PA10115200 波特率每秒打一行printf(T%.2f,SET%.2f,DUTY%.1f,ERR%.2f\r\n, cur_temp, target_temp, duty, target_temp - cur_temp);格式固定、带单位、带误差项方便事后用脚本解析。别在中断里调printf它内部有阻塞会打乱时序放在主循环的 1 秒节拍里最安全。如果要看单总线时序就得动示波器了把探头挂在 PA1 上抓复位脉冲宽度对不对一眼就能看出来。5. 避坑与排查六个最容易翻车的地方这一章是我这些年帮人看板子攒下来的清单每一条都对应一个具体现象。碰到问题时按顺序对一遍比盲目改代码高效。5.1 DS18B20 永远读回 85现象串口或者屏幕上的温度一直是 85.00℃手捏传感器也不变。原因DS18B20 上电复位后暂存器里的默认值就是 85℃读到它说明转换根本没执行或者读取的时序完全没对上。常见诱因有三个DQ 上没有 4.7k 上拉总线释放后无法回到高电平Delay_us在优化等级提高之后被压缩复位脉冲宽度不够读时隙里主机拉低后没有及时切换成输入把总线按死了。解决先拿万用表量 DQ 对 3.3V 的电阻确认在 4.7k 左右把Delay_us改成基于定时器计数或者带volatile的循环并在示波器上确认复位低电平宽度大于 480us检查DQ_IN()宏里是否同时改了GPIO_Mode和GPIO_Pin只改一个不会生效。5.2 OLED 不亮或者满屏雪花现象上电后屏幕全黑或者显示一堆随机的点阵噪点偶尔闪一下正常内容。原因I2C 地址不对是最常见的0x78 和 0x3C 是同一个地址的两种写法写错一个就是无应答其次是初始化命令序列不完整SSD1306 需要按顺序发关闭显示、设置时钟分频、设置复用率、设置显示偏移、开电荷泵、设置扫描方向等十几条命令漏一条就可能不亮再就是供电有些模块只支持 3.3V接 5V 会烧掉稳压或者逻辑异常。解决先用最简单的“读应答”测试确认地址写一个只有I2C_Start、发地址、看ACK的函数能应答再往下走初始化序列照驱动芯片手册逐条对特别是0x8D电荷泵设置后面必须跟0x14开启供电按模块标注来不确定就用 3.3V。5.3 加热丝一直满功率温度冲过设定值现象刚上电温度就往上涨OLED 上占空比显示 100%PID 怎么调都拉不回来最后超过目标值好几度才慢慢降。原因三个可能。一是 MOS 栅极在 MCU 复位期间浮空被拉高或者栅极下拉电阻漏焊二是 PID 的积分项没有限幅误差累加了几百输出一直顶在最大值三是 PWM 极性配反了TIM_OCPolarity_Low配PWM1模式CCR 为 0 时输出反而是高电平。解决先量上电瞬间 MOS 栅极电压正常应该是 0V如果不是就查下拉电阻和初始化顺序再打开积分限幅和输出限幅把integral钳在合理范围最后核对TIM_OCMode和TIM_OCPolarity的组合用示波器看 CCR0 和 CCR50 时的实际波形。5.4 按一下键跳好几度现象按一下 K1目标温度从 25 直接跳到 28 甚至更多有时候不按也会自己变。原因机械按键的抖动在按下和松开的几毫秒内会产生多次电平跳变用外部中断检测时每一次跳变都会触发一次中断中断服务里直接改目标值就被加了四五次。如果中断里还调用了printf或者刷屏耗时变长抖动更容易被捕捉到。解决中断服务函数里只置标志、关中断或者记录时间戳真正的处理放到 200ms 节拍的主循环里做并且加 20ms 的软件消抖/* EXTI0 中断只记录不处理业务 */ void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { key_flag[0] 1; /* 交给主循环 */ EXTI_ClearITPendingBit(EXTI_Line0); } } /* 主循环 200ms 调用一次 */ void Key_Process(void) { static uint8_t last_state[4] {1,1,1,1}; uint8_t now; for (int i 0; i 4; i) { now GPIO_ReadInputDataBit(GPIOB, (uint16_t)(1 i)); if (last_state[i] 1 now 0) /* 下降沿且状态确实变了 */ { Delay_ms(20); /* 软件消抖 */ if (GPIO_ReadInputDataBit(GPIOB, (uint16_t)(1 i)) 0) Key_Action(i); } last_state[i] now; } }硬件上在按键两端并一颗 100nF 电容也能压住大部分抖动软硬一起上最省心。5.5 7812 出不来 12V加热丝半温不火现象按报告接好 7812量输出只有 3V 多加热丝摸着温温的温度根本上不去。原因7812 是线性稳压器要求输入输出压差在 2V 以上5V 输入根本不可能输出 12V。原报告这一处描述本身就有问题照抄必翻车。解决给加热丝单独供 12V和 5V 控制电源分开如果没有 12V 电源用升压模块从 5V 升到 12V注意升压模块的电流能力要留 1.5 倍余量更简单的办法是换 5V 加热丝直接从 5V 取电整机只有一个电源代价是同功率下电流更大、线损更明显。5.6 MCU 在加热丝启动瞬间复位现象温度接近目标值时加热丝开始频繁通断这时候屏幕闪一下或者直接重启串口输出乱码。原因加热丝的浪涌电流把 5V 电源拉低而 MCU 和加热丝共用一条地线地弹导致 MCU 的复位脚或者电源脚瞬间跌到阈值以下。解决控制地和功率地分开走最后在电源入口处单点汇合在 MOS 的源漏之间加吸收电容加热丝两端并一颗 TVS5V 输入端加大电容到 470μF 以上复位脚并一颗 100nF 到地抗干扰。这几条一起上基本能压住。6. 二次开发把定值温控改成能远程设定、掉电记住参数把闭环跑通只是起点真正拿去用的时候需求马上就来了目标温度不想每次上电都重新按、想从串口或者手机改参数、想把历史温度存下来看趋势。这一章说三个改动方向都是在这套骨架上加不用重构。第一个改动是串口协议改目标温度。最简单的做法是定长帧AA 55帧头 目标温度两字节放大 10 倍存整数 校验和。收数据用中断 环形缓冲主循环里取出来解析不要在中断里做浮点运算。协议定了之后手机端或者上位机只要按格式发就行STM32 这边加一个UART_RecvTask就够。第二个改动是参数掉电保存。F103 内部有 Flash拿最后一页一般是 2KB 一页地址 0x0801FC00存目标温度和 PID 三个参数。写之前先擦除整页写的时候按半字写写完回读校验。要注意 Flash 擦写次数有限标称 1 万次左右按键改一次存一次会加速损耗我一般的做法是“改完 10 秒没有新操作才落盘”或者只在长按确认键时存。第三个改动是加温度曲线。OLED 只有 128×64画不了太复杂的图但把最近 60 个采样点的温度画成一条折线完全够用纵轴范围跟着当前目标值动态调整。实现上维护一个 60 长度的环形缓冲每次刷新时把数组重画一遍占空比和当前温度分两行显示在顶部。改造点涉及文件改动量验证方式串口设定温度usart.c、main.c约 80 行发帧后看 OLED 上 SET 值是否变化参数掉电保存param.c、stm32f10x_flash.c约 60 行断电重启看目标值是否保持温度曲线oled.c、main.c约 100 行人为吹热气看曲线是否跟着动超温报警heater.c、key.c约 40 行把目标值设低于室温看是否停加热并闪屏有一点要提醒改 Flash 参数的时候别把程序和参数放同一页否则擦除参数会把代码一起擦了板子直接变砖。查一下链接脚本里代码段的结束地址从它后面取一整页来用最稳。调这套东西我最深的教训是每次动加热丝之前先把 MOS 栅极对地量一遍确认它是关断的再上电。有一次偷懒跳过了这一步栅极下拉电阻虚焊上电三秒钟加热丝就红了那一刻的反应速度比任何 PID 都重要。从那以后我每次改完硬件都强制走一遍“量栅极、量电源、空载跑一次 PID”的流程多花五分钟省掉一块板子。希望帮到你。本文还有配套的精品资源点击获取