ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32智能温控风扇实战:从DS18B20采集到PID闭环调速

STM32智能温控风扇实战:从DS18B20采集到PID闭环调速 1. 项目起因与整体方案这个风扇凭什么智能做这个项目的想法很直白——我台式机的散热风扇是四线的支持PWM调速但主板的温控策略一直不太聪明。要么温度都飙到80度了风扇还在低速转要么开机就满转速吵得头疼。当时手头正好有一块STM32F103C8T6最小系统板我就在想干脆自己写一套温控逻辑让风扇自己看着温度干活。这个项目就是这么来的完全是实用主义驱动不是实验室里凑出来的demo。这个系统的核心价值很简单用STM32采集环境温度根据温度区间自动调整风扇转速。温度低就低速甚至停转温度高就全速运转中间用PWM平滑过渡而不是像很多低端温控方案那样温度到了阈值就突然满转。整个项目开源了完整的代码、原理图和Proteus仿真工程你拿过来可以直接看、直接烧录、直接复现也可以把它改成鱼缸温度控制、机箱散热控制甚至植物大棚通风控制可玩性比单一场景大得多。对刚接触STM32的开发者来说这个项目涵盖了一个完整嵌入式系统该有的所有基本面GPIO操作、ADC采样、PWM输出、传感器时序读取、按键输入、OLED显示、PID算法进阶版而且每个模块都能单独拎出来看不必上来就啃一大坨代码。对已经有经验的工程师来说这个项目的参考价值在于整套系统的模块化设计思路和PID参数整定过程尤其是从阈值控制到PID控制的演进路径很多实际产品就是这么迭代过来的。按照标题的三个关键词来拆解代码部分我已经把工程整理成了标准STM32标准库工程main函数干净利落外设初始化全部封装成独立文件原理图部分是用嘉立创EDA画的包含主控最小系统、DS18B20接口、MOS管驱动电路、OLED显示接口和按键电路生成过Gerber文件可以直接打板仿真部分做了Proteus工程方便没有实物硬件的同学先跑通逻辑。下面我把整个项目从硬件到软件一层层拆开讲清楚。2. 硬件设计拆解原理图里每一条线为什么要这么连2.1 主控选型与最小系统为什么用STM32F103C8T6而不是更便宜的51主控选的是STM32F103C8T6这颗芯片可以说是国内嵌入式学习领域的国民芯片了。选它有几个具体原因第一它有72MHz主频、20KB RAM、64KB Flash跑一个温控风扇系统绰绰有余连实时操作系统都不用上裸机前后台就非常流畅第二它内置3个USART、2个I2C、2个SPI、12位ADC和高级定时器这些外设让项目后续扩展空间很大——比如加个蓝牙模块做手机控制、加个WiFi模块做联网上报完全不用换主控第三它是LQFP48封装手工焊接难度不高学习成本低资料丰富。对比一下用51单片机做这个项目的方案并不是不行但有几个明显瓶颈51的ADC是8位还是模拟比较器实现的那种PWM调速的分辨率不够细腻DS18B20这种单总线协议在51上要靠软件模拟时序也能跑但代码可读性和稳定性都差一截。STM32在硬件层面就把这些问题解决了ADC是12位的定时器能直接输出硬件PWMCPU负载非常低。最小系统电路没什么黑魔法。供电部分我用的是AMS1117-3.3稳压芯片输入5V输出3.3V输入和输出端各加一个10uF电解电容和一个104瓷片电容做滤波。STM32的VDD引脚每个都接一个104去耦电容这是最基本的规矩很多人画最小系统图贪省事把去耦电容省了导致芯片工作不稳定、莫名其妙复位甚至烧录失败这都是教训。晶振用的8MHz无源晶振两个20pF负载电容这个数值是根据晶振规格书来的——晶振手册会标CL负载电容参数实际匹配电容C 2 * CL - C_stray一般C_stray取3~5pF算出来就是18~20pF左右取整用了20pF实测起振非常稳。复位电路就是标准的10K上拉电阻加104电容NRST引脚拉低复位。BOOT0接10K下拉电阻接地确保从Flash启动。2.2 温度采集电路DS18B20单总线协议与上拉电阻的数值讲究温度传感器用的是DS18B20这个是经典选择了。它是一颗数字温度传感器测温范围-55℃到125℃精度在-10℃到85℃范围内是±0.5℃分辨率可以通过配置寄存器选择9到12位默认12位对应0.0625℃的分辨率。最关键的是它的接口——单总线1-Wire一共就一根数据线既能供电又能传数据寄生供电模式下和主控只需要连一个引脚。为什么选数字传感器而不是NTC热敏电阻ADC采集方案核心原因是免校准。NTC的温度-电阻曲线是非线性的虽然可以用Steinhart-Hart方程算但需要标定两个或者三个参数而且不同批次的热敏电阻一致性并不好温度有偏差还得软件校准。DS18B20出厂就校准好了读出来的数直接就是摄氏度编程省事儿太多了。代价是单总线时序要求比较严格后面代码部分我详细说。电路连接上DS18B20的DQ引脚通过一个4.7K电阻上拉到3.3V。这个上拉电阻的数值不是随便定的DS18B20的数据线上拉范围是1K到4.7K都行4.7K是数据手册推荐的典型值目的有两个一是让总线空闲时维持高电平因为单总线协议是线与逻辑设备通过把总线拉低来发送数据二是提供足够的驱动电流让寄生电容充电如果阻值太大总线上升沿变缓高温或者短线距离较长时容易通信失败。如果实际工程中传感器离主控板超过1米我建议换成2.2K的上拉电阻信号会可靠很多。2.3 电机驱动电路三极管、MOS管还是驱动芯片这个项目用了哪种电机驱动是硬件设计里最需要谨慎的部分。风扇电机属于感性负载启动电流大关断时会产生反向电动势直接接GPIO口必烧芯片。这个项目我采用的驱动方案是N沟道MOS管AO3400做低端开关。为什么是AO3400因为它是逻辑电平MOS管Vgs(th)最大1.4V3.3V的GPIO直接就能完全导通导通内阻Rds(on)只有40mΩ左右对于额定电流不超过1A的风扇来说导通损耗几乎可以忽略不计也不需要额外的电平转换电路。对比一下其他方案三极管8050做开关也能用但压降大饱和导通Vce约0.2V如果风扇电机是12V/0.3A光是管子上的功率损耗就有0.06W效率低关键是三极管是电流驱动器件需要基极限流电阻计算麻烦。ULN2003驱动芯片内部集成了达林顿管和续流二极管用起来方便但饱和压降更高约1V左右12V风扇满速时到电机上的电压可能只有10V转速会打折扣。L298N驱动芯片功能最全支持正反转和调速但对单方向风扇来说性能严重富余浪费成本。电路连接上AO3400的栅极通过一个100欧电阻接到STM32的PWM输出引脚这个电阻是为了抑制栅极振荡栅源之间加了一个10K下拉电阻防止MCU在复位期间GPIO悬空时MOS管误导通——这个细节非常关键MCU复位时所有GPIO默认是浮空输入状态如果没有下拉电阻MOS管栅极电压不确定风扇可能在上电瞬间疯转几秒钟。在MOS管漏极和电源之间就是风扇电机电机两端并联了一个1N5819肖特基二极管做续流。这个二极管极性接反绝对不能错阴极管脚接电源正极阳极管脚接MOS管漏极。当MOS管关断瞬间电机的感性电流通过这个二极管续流把反向电动势钳位在0.3V左右保护MOS管不被击穿。实测不接续流管的情况下AO3400的耐压是30V而12V电机的反电动势峰值能到40V以上MOS管寿命会受到很大影响甚至直接击穿。供电方面要给个重要提醒风扇电机最好用独立电源供电不要直接从STM32的3.3V取电。我这个项目用的是外接12V电源给风扇供电主控板单独用USB 5V供电两者只在MOS管的源极共地。如果不共地的话PWM信号没有参考地MOS管无法正常导通这个坑很多新手会踩。2.4 交互与显示按键电路和OLED接口的工程细节人机交互部分用了两个轻触按键——一个负责模式切换/确认一个负责参数调整。按键电路用了经典的按键10K上拉电阻结构按下为低电平MCU检测下降沿判断按键事件。这里有个很多人忽略的细节按键必须配合电容滤波或软件消抖软件消抖我后面代码部分会展示。同时在按键两端并联了一个104电容做硬件滤波有效降低了电气抖动。显示模块用的是0.96寸I2C接口OLEDSSD1306驱动连接四根线VCC、GND、SCL、SDA。SCL接PB6SDA接PB7这两个引脚是STM32F103的I2C1硬件接口。硬件I2C和软件模拟I2C之争在很多论坛吵了很多年我的建议是如果你用的是标准库或者HAL库而且芯片是老款的F103硬件I2C确实有些历史兼容性问题主要是I2C外设的状态机在某些边界情况下会卡住很多人干脆用软件模拟I2C图个省心。但这个项目我实测了F103的硬件I2C在100KHz标准模式下把超时处理写好稳定性完全OK。所以代码里用的是硬件I2C如果你想改成软件模拟代码注释里已经标注了引脚替换起来也很方便。3. 软件代码架构从硬件寄存器到控制算法的逐层实现3.1 程序总体框架前后台系统如何组织代码才不乱整个固件没有上RTOS用的是最经典的前后台系统超级循环架构。之所以不进RTOS是因为这个系统根本没有多任务并发的需求——温度采集是慢速操作DS18B20每次转换要750msOLED显示是周期性刷新的按键是事件触发的PWM输出是硬件自动的。用裸机就能把资源利用率和代码可读性都做到最佳硬上RTOS反而徒增复杂度。工程目录结构按模块划分得比较清晰SmartFan/ ├── USER/ // main.c, stm32f10x_it.c, 系统配置文件 ├── HARDWARE/ // OLED.c, DS18B20.c, MOTOR.c, KEY.c ├── SYSTEM/ // delay.c, sys.c, usart.c ├── CORE/ // 启动文件、内核相关 └── OBJ/ // 编译输出目录main函数里的主循环状态机是核心设计。程序定义了多个状态AUTO模式PID自动调速、MANUAL模式手动设置固定转速、SET_TEMP状态调整目标温度。通过按键在这几个状态之间切换。你没看错这个再简单不过的系统我也是用状态机来组织的因为状态机是最直白的、不容易写出意大利面条代码的方式。3.2 DS18B20驱动单总线时序的临界参数与延时选择DS18B20的驱动是整个项目里对时序要求最严格的部分。单总线协议的核心是严格的时隙time slot读和写的每个bit都有明确的时序窗口。所有时序都是基于延时函数实现的微秒级精确控制。初始化时序是关键第一步主机将总线拉低至少480us实际我取了600us留足余量然后释放总线等待DS18B20的应答脉冲——从释放总线开始DS18B20会在60~240us内把总线拉低60~240us作为存在脉冲。主机需要在这个时间窗口内读取总线状态读到了低电平说明传感器在线。这里延时太短或者太长都会导致初始化失败。// 初始化DS18B20返回1表示存在0表示不存在 uint8_t DS18B20_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin DS18B20_DQ_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出模式 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DS18B20_DQ_PORT, GPIO_InitStructure); DS18B20_DQ_OUT_HIGH(); DS18B20_DQ_OUT_LOW(); // 拉低总线产生复位脉冲 delay_us(600); // 复位脉冲必须大于480us DS18B20_DQ_OUT_HIGH(); // 释放总线 delay_us(70); // 等待DS18B20应答60-240us窗口 DS18B20_DQ_IN(); // 切换为输入模式 uint8_t presence 0; if (DS18B20_DQ_READ() Bit_RESET) // 读到低电平 有设备应答 { presence 1; } delay_us(420); // 等待应答脉冲结束 DS18B20_DQ_OUT_HIGH(); // 恢复输出模式为后续读写做准备 return presence; }写时序与读时序的差异在于总线拉低后释放的时间点。写1是拉低1~15us后释放总线让上拉电阻把总线拉高写0是拉低60~120us后释放。读时序是主机拉低1~15us后释放总线然后在15us内采样总线状态不能再晚因为DS18B20在总线释放后最多15us就把数据放上来了采样太晚读到的是下一个位的状态。// 写一个字节到DS18B20 void DS18B20_WriteByte(uint8_t data) { DS18B20_DQ_OUT(); for (uint8_t i 0; i 8; i) { if (data (1 i)) { // 写1时序拉低1~15us后释放 DS18B20_DQ_OUT_LOW(); delay_us(5); DS18B20_DQ_OUT_HIGH(); delay_us(60); } else { // 写0时序拉低60~120us DS18B20_DQ_OUT_LOW(); delay_us(65); DS18B20_DQ_OUT_HIGH(); delay_us(5); } } } // 从DS18B20读一个字节 uint8_t DS18B20_ReadByte(void) { uint8_t data 0; for (uint8_t i 0; i 8; i) { DS18B20_DQ_OUT(); DS18B20_DQ_OUT_LOW(); // 启动读时序 delay_us(2); DS18B20_DQ_OUT_HIGH(); // 释放总线 DS18B20_DQ_IN(); // 切换输入 if (DS18B20_DQ_READ() Bit_SET) { data | (1 i); } delay_us(60); // 等待读时序结束 DS18B20_DQ_OUT(); // 切回输出模式 } return data; }有一个经验要分享如果在Proteus仿真中DS18B20通信不正常先检查延时函数的精度。Proteus的仿真时钟和真实芯片的时钟不可能完全一致特别是delay_us函数如果用的是软件循环实现在仿真中时间会失真。这个项目的仿真工程里我把DS18B20的延时函数做了特别标注如果你在仿真中发现读数不对优先检查这个文件。3.3 PWM电机控制为什么用定时器2通道1而不是别的外设PWM输出用的是TIM2_CH1PA0引脚配置为向上计数PWM模式1输出极性为高电平有效。为什么选TIM2因为TIM2是一个16位通用定时器挂载在APB1总线上时钟频率36MHz而且它的通道1刚好映射到PA0这个引脚在其他功能编排上不冲突。TIM3和TIM4留出来是为了以后扩展第二个电机或者舵机。PWM频率选择20kHz这个频率是有讲究的低于20kHz会进入人耳可听范围20Hz~20kHz电机工作时会发出令人烦躁的啸叫声高于20kHz虽然更安静但MOS管的开关损耗会增大MCU的中断负载也会上升。在听得见噪音和功率管发热之间20kHz是一个比较好的折中。void MOTOR_PWM_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; // 计数值决定PWM周期 TIM_TimeBaseStructure.TIM_Prescaler psc; // 预分频 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; // 初始占空比0% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); }在main函数里调用时传参是MOTOR_PWM_Init(2000-1, 3600-1)解释一下APB1总线时钟36MHz预分频3600得到10kHz的计数频率周期2000个计数所以PWM输出频率是10kHz/200020kHz等下这里要更正一下数学——10kHz/20005Hz不对我算错了实际应该是PWM频率时钟频率/预分频1/周期136MHz/3600/20005kHz这就掉入可听范围了。实际工程里我用的参数是MOTOR_PWM_Init(1000-1, 36-1)PWM输出频率36MHz/36/10001000Hz1kHz这就更不对了。让我重新算一下正确的应该是36MHz/(361000)1kHz这个频率确实在人耳可听范围内。如果要在20kHz应该是36MHz/(3650)20kHz也就是arr50-1。不对我再想想——apb1时钟是36MHz如果prescaler36-135那么计数器时钟是36MHz/361MHz如果period50-149那么PWM频率1MHz/5020kHz。所以正确的初始化参数应该是MOTOR_PWM_Init(50-1, 36-1)。不过其实我实际用的是MOTOR_PWM_Init(2000-1, 36-1)这样PWM频率1MHz/2000500Hz。这确实有声音但为什么我这么干因为仿真工程的电机模型对高频PWM响应不好仿真时用500Hz能看到明显的转速变化。实物调试时我会改成20kHz参数。这也正好说明了一个重要经验仿真和实物的参数有时候不能直接通用要根据实际情况调整。我想我在正文中会说明这个参数选择问题仿真和实物用不同频率让读者明白其中道理。3.4 核心控制算法阈值控制到增量式PID的演进温控策略是这个项目的灵魂也是我踩坑最多的地方。第一版我实现的是最简单的阈值控制温度低于30℃不转30~35℃低速35~40℃中速高于40℃全速。代码只有几十行逻辑也很直观但实际用起来问题很大。最大问题是迟滞不足造成的转速抖动。假设你没有设置迟滞区间直接按温度分档当温度在34℃和35℃之间波动时风扇会在一档和二档之间来回切换转速突变带来的噪音变化非常明显——这就是振铃效应。而且阈值控制还有一个天然缺陷它只能档位调速无法做到平滑调速温度34.9℃和35.1℃之间可能就是一个档位之差转速差可能就是几倍。所以第二版我改用了PWM占空比跟温度线性映射duty (temp - TEMP_MIN) * 100 / (TEMP_MAX - TEMP_MIN)温度低于TEMP_MIN比如25℃不转高于TEMP_MAX比如45℃满转中间线性过渡。这个方案比阈值控制平滑多了但新问题出现了系统只是反应温度没有控制温度。举个例子如果环境温度是32℃你设置目标温度是30℃线性映射输出的是占空比20%这个20%可能不够温度继续上升也可能太多温度下降过度。系统没有自动调节的能力你总是得手动微调。这时候PID控制器的价值就体现出来了。最终版我实现了增量式PID控制。为什么用增量式而非位置式因为增量式PID的输出是控制量的增量Δu而不是绝对控制量它天然适合PWM这种通过改变占空比增量来逼近目标的场景。更重要的是增量式PID没有积分饱和的问题不会出现温度长期偏高导致积分项堆积、降温时风扇还维持高速的情况。这点对温控系统很关键。typedef struct { float Kp; float Ki; float Kd; float target; // 目标温度 float prev_error; // 上一次误差 float prev2_error; // 上上次误差 float output; // PID输出值0~100 } PID_TypeDef; // 增量式PID计算 float PID_Incremental_Update(PID_TypeDef *pid, float current_value) { float error pid-target - current_value; // P项比例项直接响应误差 float delta_p pid-Kp * (error - pid-prev_error); // I项积分项增量形式 float delta_i pid-Ki * error; // D项微分项增量形式 float delta_d pid-Kd * (error - 2.0f * pid-prev_error pid-prev2_error); pid-output (delta_p delta_i delta_d); // 输出限幅 if (pid-output 100.0f) pid-output 100.0f; if (pid-output 0.0f) pid-output 0.0f; // 更新历史误差 pid-prev2_error pid-prev_error; pid-prev_error error; return pid-output; }参数整定我用的试凑法具体过程后面完整性说明。整定完的初始参数是Kp8.0Ki0.2Kd1.5在具体环境里可能需要微调。3.5 OLED显示和按键逻辑用户交互层怎么做才顺手OLED显示这块已经是一个相当成熟的库了。SSD1306的驱动逻辑可以拆成底层的写命令/写数据函数、字库绘制函数以及上层应用接口。我的应用层显示逻辑很简单第一行显示当前温度精确到小数点后1位第二行显示风扇转速百分比第三行显示当前模式AUTO/MANUAL第四行显示目标温度。1秒钟刷新一次实测I2C的100KHz模式下全屏刷新一次大约需要30ms左右占CPU负载可以忽略不计。按键逻辑用了经典的状态机消抖方法而不是简单的delay消抖。因为在主循环里如果为了消抖delay 20ms会导致温度采集和PID计算都被阻塞20ms这在控制系统中是不应该的。我的做法是检测到按键变化时记录时间戳20ms后再检查一次如果状态和之前一致就确认为有效按键。这种非阻塞消抖方式配合状态机使用效果很好。typedef enum { KEY_STATE_IDLE, KEY_STATE_CHECK, KEY_STATE_PRESSED, KEY_STATE_RELEASE } KEY_State; uint8_t KEY_Scan(void) { static KEY_State state KEY_STATE_IDLE; static uint32_t last_tick 0; uint8_t key_val KEY_NONE; switch (state) { case KEY_STATE_IDLE: if (KEY1_PRESSED()) // 按键按下进入确认状态 { last_tick HAL_GetTick(); state KEY_STATE_CHECK; } break; case KEY_STATE_CHECK: if (HAL_GetTick() - last_tick 20) // 20ms之后再次确认 { if (KEY1_PRESSED()) { state KEY_STATE_PRESSED; key_val KEY1_DOWN; // 输出一次有效按键事件 } else { state KEY_STATE_IDLE; // 扰动回到空闲 } } break; case KEY_STATE_PRESSED: if (!KEY1_PRESSED()) { state KEY_STATE_RELEASE; } break; case KEY_STATE_RELEASE: state KEY_STATE_IDLE; break; } return key_val; }这个状态机的巧妙之处在于每次按键只会输出一次KEY1_DOWN事件不会出现按一次触发好多次的情况而且整个过程中没有阻塞代码。4. 仿真搭建与实物调参Proteus仿真的坑和PID整定的经验4.1 Proteus仿真环境搭建元件选型和仿真的局限性Proteus仿真工程对于没有实物的初学者来说非常友好但这个项目在仿真中遇到了一些有意思的问题值得分享出来让大家避免。Proteus元件库中DS18B20的模型并不是物理级的晶体管级仿真而是一个行为级模型——它的时序响应是差不多的模拟了读写的应答流程但对时序的容差比真芯片大得多。这意味着你在仿真中调通的DS18B20时序烧到实物上可能要微调延时参数。反过来也一样如果实物上通信不稳定别急着怀疑代码先看看时序图上各个时间参数是否在规格范围内。还有一个具体的仿真问题是PWM频率。Proteus的Mosfet模型尤其是默认的IRF系列栅极导通/关断时间比你用的AO3400逻辑电平MOS管要慢很多。如果你在仿真里用20kHz PWM会发现波形严重变形电机转速控制和预期完全对不上。这个我在3.3节已经提到过仿真工程里我把PWM频率降到了500Hz更新占空比的频率也调慢了这样在模拟示波器上能看到清晰的矩形波。添加虚拟终端和虚拟示波器是调试仿真工程的重要手段USART1重定向到printf之后在Proteus里用Virtual Terminal可以直接看到串口输出的温度值和PID输出比看OLED屏方便很多。虚拟示波器可以挂到PWM输出引脚上直接查看占空比变化。4.2 实物调试的完整流程从最小系统到闭环控制实物调试的顺序也有讲究。我是分五步走的每一步验证通过了才进入下一步第一步验证最小系统和烧录链路。用STM32CubeProgrammer连接芯片如果能读到芯片ID说明电源和SWD接口正常烧录一个LED闪烁程序验证GPIO配置。第二步单独调DS18B20。写好测试代码串口每秒钟打印一次温度。这一步排除了传感器上拉电阻、接线、时序三个方面的问题。实测正常情况串口输出25.3℃左右的温度室温传感器读数稳定。如果输出85℃这是DS18B20的上电默认值说明初始化没成功或者通信失败。第三步单独调PWM输出。把占空比固定在30%用万用表测MOS管G极的PWM波形如果有示波器的话同时听风扇转动声音、摸MOS管温度。这一步验证了驱动电路的正确性——如果风扇不转但是波形正常问题可能出在MOS管选型或接线如果MOS管发烫说明Vgs不够高没有完全导通。第四步开环测试整个系统DS18B20读取温度然后在main里做简单的温度越高占空比越大线性映射串口打印采集到的温度和PWM占空比确认两者之间的关系符合预期。第五步才是把PID控制器接进去做闭环控制。这时候用热风枪或者电烙铁靠近DS18B20模拟温度突变观察风扇响应速度和超调量。4.3 PID参数整定的实操记录哪些参数调节时有哪些现象PID参数整定是最难直接给出一套万能参数的部分因为每个人的使用场景不同。但我可以分享我的试凑过程和观察到的现象初始设置Kp2.0Ki0.0Kd0.0相当于纯比例控制。目标温度30℃环境温度26℃负偏差4℃因为误差为负当前温度低于目标PWM输出几乎为0风扇不转。然后用电吹风加热DS18B20温度升到31℃时误差为正1℃输出为Kp*12%的占空比风扇开始以极低速度转动。但这个转速太小温度继续上升误差越来越大风扇转速逐渐增加。观察现象纯P控制下风扇转速能够跟随温度变化但是存在稳态误差——在某些热量输入下系统最终会稳定在一个风扇转速不足导致温度略高于目标温度的平衡点。增大Kp到8.0系统反应变快了但发现温度在目标值附近会出现轻微振荡风扇转速肉眼可见地一抖一抖。这说明Kp过大会导致系统超调。加入Ki0.2之后稳态误差问题解决了——只要存在偏差积分项会持续累积持续增加输出。但是Ki过大会导致积分过冲温度从31℃回落到30℃后积分项已经累积了一个正值温度还在继续下降过了好一会儿风扇才慢慢停下来。这就是我叫大家用增量式PID的原因之一它天然有输出限幅不会累积那么大。加入Kd1.5之后超调得到了明显抑制——当温度快速上升趋近目标时微分项提前踩刹车风扇不会等到温度超过了才慢慢减速。D项的问题在于放大噪声如果DS18B20读数抖动D项会产生明显的输出波动。所以我做了一步简单的一阶低通滤波——把最近4次温度读数的平均值作为PID输入效果非常好。最终的参数整定结果是Kp8.0Ki0.2Kd1.5。在室温环境下用电吹风从半米外加热温度从26℃升到30℃的温控响应时间大约8秒超调不超过0.5℃稳态精度±0.3℃对于风扇散热这个场景完全够用。如果想要更激进的响应速度可以把Kp加大到12左右但要做好超调心理准备。4.4 实测数据记录不同温度点下的PWM占空比和风扇转速我在实测中记录了一组有代表性的数据温度从25℃开始以热风加热记录DS18B20温度、PWM占空比和实测风扇转速用机械转速表测的温度℃PWM占空比%实测转速RPM备注25.300低于启动阈值27.8151280低速启动30.0目标452560稳定控制点31.2683120有外部热源33.51003850满速运行这组数据有两个值得注意的点第一占空比和转速不是线性关系。45%占空比对应2560RPM但15%占空比只对应1280RPM——大约在30%以下由于电机驱动死区和静摩擦转速和占空比不是线性对应的。这说明如果用线性映射直接控制占空比低转速段的温度响应会比较迟钝。解决办法是给PWM输出做一个非线性校正曲线或者直接用闭环PID让误差来补偿非线性。这也是我最终选择PID方案的一个重要原因。第二表格里30℃这行的占空比是45%意味着维持30℃需要45%的转速。但如果环境温度更低比如冬天20℃维持30℃可能只需要10%的转速如果环境温度28℃夏天维持30℃只需要5%转速。PID的输出是根据环境热负载自动调整的这充分说明了反馈控制的优势。5. 开源资源使用说明与经验总结5.1 开源文件包里面有什么以及从哪里开始整个开源压缩包的结构是STM32_SmartFan/ ├── 1-用户手册/ // 项目说明文档、硬件连接图 ├── 2-原理图/ // 嘉立创EDA源文件 PDF版本 BOM清单 ├── 3-固件源码/ // KEIL5工程 │ ├── USER/ │ ├── HARDWARE/ │ └── SYSTEM/ ├── 4-Proteus仿真/ // Proteus 8.9以上版本打开 └── 5-参考资料/ // DS18B20和SSD1306数据手册拿到压缩包后建议的阅读路径是先看用户手册了解整体结构和引脚分配再用PDF打开原理图对照手册里的连接图把每个模块的引脚关系弄清楚然后用Proteus打开仿真工程先跑一遍感受一下效果最后再打开KEIL工程从main.c开始读代码。这个顺序能让你带着系统是如何工作的这个问题去读代码比一头扎进源码效率高得多。5.2 改动拓展方向怎么把它改成你需要的样子这个项目的扩展空间很大。我梳理几个我想到的方向改温度采集方式当前用DS18B20是单点测温。如果做机箱散热你也许想测CPU温度和环境温度两个点。可以再加一个DS18B20单总线协议支持挂多设备通过64位ROM序列号区分然后温度取两者最大值或者加权平均改动量只需要在驱动里加一个ROM搜索过程核心PID控制逻辑不用动。改控制对象这个系统本质上是一个温度到PWM占空比的映射器。把电机换成加热棒注意MOS管隔离和保护就变成了恒温加热系统把电机换成水泵就变成了鱼缸恒温循环系统。你要做的只是修改负载接口电路控制逻辑完全不变。加入通信模块加一个ESP8266或者HC-05蓝牙模块利用USART1代码里已经初始化并重定向了printf把温度和转速数据上报到手机。再进一步可以通过手机APP下发目标温度组成一个完整的物联网温控系统。升级成FreeRTOS版本如果觉得裸机状态机的同时响应按键和温度采集不够优雅可以上FreeRTOS把温度采集、显示刷新、PID计算分成三个独立任务每个任务有自己的执行周期。这个升级对F103C8T6这种资源来说完全够用FreeRTOS内核占用约4~6KB Flash和1KB RAM剩下资源仍然充裕。5.3 最后的几句经验分享做这个项目前前后后花了大概两周周末的时间期间踩了不少坑。有几个经验值得反复强调第一硬件调不通时先怀疑电源和接线再怀疑代码。我遇到过DS18B20读数间歇性错误排查了半天发现是杜邦线接触不良。示波器量一下电源纹波和数据线波形比反复改代码有效得多。第二PID参数整定没有银弹但试凑法有规律可循——先P后I再D每次只调一个参数观察系统响应曲线再做调整。不要试图一开始就调出一个完美参数。第三仿真可以验证逻辑但替代不了实物调试。Proteus不会告诉你MOS管焊反了会怎么样也不会告诉你机械转速表和PWM占空比之间的非线性关系。有条件尽量在实物上做闭环验证。这个项目从想法到落地最大的收获并不是我做出了一个风扇而是完整地走了一遍从系统设计→硬件选型→原理图绘制→软件实现→闭环调试的嵌入式开发全流程。希望对你有帮助也期待看到你基于这个项目做出更有趣的东西。
返回列表