stm32f103核心板、L298N模块(当然用MOS管更好)、led一个、NPN三极管一个、蜂鸣器一个、DHT11一个、LCD1602一个、电阻200欧两个、可调电阻10K一个、加热丝一个
用DHT11检测当前环境温湿度,并将数据显示在LCD1602上,在用设定温度与当前温度相减,通过PID算法计算出当前输出脉宽,并将其加在L298N模块中,使加热丝发热,形成一个闭环,经过一段时间温度稳定在设定值。由于我的初衷是做一个恒温箱孵蛋,所以加了湿度报警。
我们要控制箱内温度就要控制电热丝的发热量,通电时电热丝发热,其向箱内输入的热量大于箱子向外散失的热量,箱内温度升高;断电时,电热丝不在产热,但其仍有余温,其依然能向箱内输入热量,如果在断电前电热丝的温度已经很高了,则在断电后的前一段时间内依然会向箱子输入大量的热量,箱内温度还会上升,但是一段时间后,电热丝自身温度降低,向箱内输入的热量小于箱子向外散失的热量,箱内温度就会降低。故而通过调节通电时间和断电时间就可以控制电热丝的发热量和箱子散热量。以PWM控制开关器件从而控制电热丝能够满足上述要求,调节占空比就可以达到调节通电时间和断电时间的目的。
PWM中还有一个重要的参数就是频率,尽管不能计算出这个值到底是多少,但也不能随意设置。从控制系统的角度来说,频率越高则越接近连续系统,控制的效果也会越好,但是这必须考虑到实际的被控对象,比如一个电感比较大的直流电机,如果选取的pwm频率过高,那么此时电机的感抗会很强,则流入的电流很小,那么电机转速就很低甚至不转;另外频率过高也会导致开关器件的开关损耗增大,发热较为严重;然而频率过低又会导致输出响应速度变慢,系统调节时间增长,因为在频率很低的情况下,电热丝的通电时间或者断电时间就会变得很长,如果通电时间长则断电时间就会相应的缩短,那么电热丝产生的热量就会很多,甚至温度已经超过设定值电热丝仍在发热,而即使此时电热丝断电,在自身的高温下箱内温度还会持续上升,甚至电热丝温度还未下降多少又开始加热了;同理,如果断电时间很长的情况下,箱内温度已经低于设定值很多了但断电时间还没过去,温度还会持续下降,即使此时电热丝开始加热,温度还未回升可能电热丝就断电了。如此往复,温度值震荡的会很厉害,调节时间会变得很长。或许你会问,系统及时做出反应,修正占空比不就可以避免温度过高或者过低了吗?系统的及时性是有限度的,系统每采样一次就会做出一次修正,但是采样周期不能小于PWM周期,过于频繁的更改占空比不仅会导致控制器输出波形变形,还会使电热丝来不及做出反应(箱内温度变化较慢)。
在PWM频率很低时可以形象的按照上述所说来理解,在中高频时,PWM控制电热丝发热原理并不是上述那般,而是通过改变占空比达到改变输出电压的原理。
注:通电时间和断电时间可以理解为高电平持续时间和低电平持续时间。
后来有朋友问我MOS管驱动电路的问题,于是就在做些补充,介绍几种MOS驱动电路。当然如果用MOS管驱动芯片那是最好的。之所以需要MOS管驱动电路是因为单片机IO口输出电压电流都比较小,驱动能力低,如果单片机输出PWM信号来控制MOS管,驱动波形有可能会有所形变,最重要的是栅极电压低不能使MOS管完全导通,其自身内阻较大,发热严重。
用户设定值Sv表示最终将温度稳定在Sv,从系统运行开始每隔一段时间就采集当前环境内的温度,得样本如下:
Ek | 说明 |
---|---|
Ek > 0 | 当前环境温度未达标 |
Ek = 0 | 当前环境温度满足要求 |
Ek < 0 | 当前环境温度已超标 |
由于我们是通过改变PWM的占空比来调节电热丝输出功率的,PID算法所计算出的数值OUT(最终输出值,OUT=Pout+Iout+Dout) 就是PWM的脉宽。故而在Ek > 0时我们需要加大Pout从而加大OUT来提高电热丝输出功率,Ek < 0时则降低Pout从而降低电热丝输出功率,得:
通过用户设定值Sv与我们采集的环境温度数据做差,我们得到了差值Ek,由于连续的温度采集,于是有了一系列的差值样本:
SE | 说明 |
---|---|
SE > 0 | 历史数据大多数未达标甚至从未达标 |
SE = 0 | 控制效果较理想 |
SE < 0 | 历史数据大多数超标甚至是一直在超标 |
由此可知,SE的正负可以反映出历史温度处于哪种阶段,对于SE > 0,历史温度大多数未达标则将加大Iout从而加大OUT来提高电热丝输出功率,同理,SE < 0时,历史温度大多数超标则将降低Iout从而*降低电热丝输出功率。这种算法称为积分控制算法。公式如下:
前面我们获得了差值样本,那么最近两次差值之差可表示为:
Dk | 说明 |
---|---|
Dk > 0 | 差值有增大趋势 |
Dk = 0 | 差值趋势平稳 |
Dk< 0 | 差值有减小趋势 |
Dk能反应最近两次采样的温度的状态变化趋势,Dk的绝对值越大表明温度变化速率越大,由Dk的公式可知,温度呈上增长时,Dk为负数,增长速率越大,Dk越小,当温度呈下降低时,Dk为正数,下降速率越大,Dk越大。由此可知Dk具有抑制温度变化的功能,并使其趋于稳定。数学模型可表达为:
整合后的PID算法公式如下:
比例系数与积分时间的大小对曲线的影响如下:
LedAndBeep.h
#ifndef _LEDANDBEEP_H
#define _LEDANDBEEP_H
#include "sys.h"
#include "DHT11.h"
#define led_1 GPIO_SetBits(GPIOB,GPIO_Pin_0)
#define led_0 GPIO_ResetBits(GPIOB,GPIO_Pin_0)
#define beep_1 GPIO_SetBits(GPIOB,GPIO_Pin_1)
#define beep_0 GPIO_ResetBits(GPIOB,GPIO_Pin_1)
void GPIO_init_Alert(void);
void Delay_ms(int k);
void Alert(void);
#endif
LedAndBeep.c
#include "LedAndBeep.h" #include "PID.h" void GPIO_init_Alert() { GPIO_InitTypeDef Alert_GPIO; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); Alert_GPIO.GPIO_Mode = GPIO_Mode_Out_PP; Alert_GPIO.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; Alert_GPIO.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &Alert_GPIO); led_0; beep_0; } void Alert() { if((DHT_Data[0]>70)||(DHT_Data[0]==70)||(DHT_Data[0]<45)||(DHT_Data[0]==45))//湿度不在45~70之间就报警 { led_1; if(pid.C10ms<(pid.T/2))//pid.C10ms在中断函数中,蜂鸣器响的时间小于250ms beep_1; else beep_0; } else { led_0; beep_0; } }
DHT11.h
#ifndef __DHT11_H #define __DHT11_H #include "sys.h" extern char DHT_Data[5]; //IO方向设置 #define DHT11_IO_IN() {GPIOB->CRH&=0XFFFF0FFF;GPIOB->CRH|=8<<12;} #define DHT11_IO_OUT() {GPIOB->CRH&=0XFFFF0FFF;GPIOB->CRH|=3<<12;} IO操作函数 #define DHT11_DQ_OUT PBout(11) //数据端口 PB11输出 #define DHT11_DQ_IN PBin(11) //数据端口 PB11输入 u8 DHT11_Init(void);//初始化DHT11 u8 DHT11_Read_Data(void);//读取温湿度 u8 DHT11_Read_Byte(void);//读出一个字节 u8 DHT11_Read_Bit(void);//读出一个位 u8 DHT11_Check(void);//检测是否存在DHT11 void DHT11_Rst(void);//复位DHT11 #endif
DHT11.c
#include "DHT11.h" #include "delay.h" #include "PID.h" char DHT_Data[5]={0}; // DHT_Data[0]、DHT_Data[1]存储湿度数据 //DHT_Data[2]、DHT_Data[3]存储温度数据 void DHT11_Rst(void) { DHT11_IO_OUT(); //SET OUTPUT DHT11_DQ_OUT=0; //拉低DQ delay_ms(20); //拉低至少18ms DHT11_DQ_OUT=1; //DQ=1 delay_us(30); //主机拉高20~40us } u8 DHT11_Check(void) { u8 retry=0; DHT11_IO_IN();//SET INPUT while (DHT11_DQ_IN&&retry<100)//DHT11会拉低40~80us { retry++; delay_us(1); }; if(retry>=100) return 1; else retry=0; while (!DHT11_DQ_IN&&retry<100)//DHT11拉低后会再次拉高40~80us { retry++; delay_us(1); } if(retry>=100) return 1; return 0; } u8 DHT11_Read_Bit(void) { u8 retry=0; while(DHT11_DQ_IN&&retry<100)//等待变为低电平 { retry++; delay_us(1); } retry=0; while(!DHT11_DQ_IN&&retry<100)//等待变高电平 { retry++; delay_us(1); } delay_us(40);//等待40us if(DHT11_DQ_IN) return 1; else return 0; } u8 DHT11_Read_Byte(void) { u8 i,dat; dat=0; for (i=0;i<8;i++) { dat<<=1; dat|=DHT11_Read_Bit(); } return dat; } u8 DHT11_Read_Data(void) { u8 i; DHT11_Rst(); if(DHT11_Check()==0) { for(i=0;i<5;i++)//读取40位数据 { DHT_Data[i]=DHT11_Read_Byte(); } if((DHT_Data[0]+DHT_Data[1]+DHT_Data[2]+DHT_Data[3])==DHT_Data[4]) { pid.Pv=DHT_Data[2]+(DHT_Data[3]/10); return 0; } } else return 1; return 0; } u8 DHT11_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能PG端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; //PG11端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化IO口 GPIO_SetBits(GPIOB,GPIO_Pin_11); //PG11 输出高 DHT11_Rst(); //复位DHT11 return DHT11_Check();//等待DHT11的回应 }
LCD1602.h
#ifndef LCD1602_H #define LCD1602_H #include "sys.h" #define RS GPIO_Pin_8 //设置PB8为RS #define RW GPIO_Pin_6 //PB6为RW #define EN GPIO_Pin_7 //PB7为EN使能 void ReadBusy(void); void LCD_WRITE_CMD( char CMD ); void LCD_WRITE_StrDATA( char *StrData, char row, char col ); void LCD_WRITE_ByteDATA( char ByteData ); void LCD_INIT(void); void GPIO_INIT(void); #endif
LCD1602.c
#include "LCD1602.h" #include "delay.h" void GPIO_INIT(void) { //GPIO初始化 GPIO_InitTypeDef GPIO; GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); //禁用jtag RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC, ENABLE ); GPIO.GPIO_Pin = EN|RW|RS; GPIO.GPIO_Mode = GPIO_Mode_Out_PP; GPIO.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO); GPIO.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7; GPIO.GPIO_Mode = GPIO_Mode_Out_PP; GPIO.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO); } void LCD_INIT(void) { //初始化 GPIO_INIT(); GPIO_Write(GPIOA, 0x0000); GPIO_Write(GPIOB, 0x0000); delay_us(500); LCD_WRITE_CMD(0x38); LCD_WRITE_CMD(0x0d); //开启光标和闪烁 LCD_WRITE_CMD(0x06); LCD_WRITE_CMD(0x01); } void LCD_WRITE_CMD(char CMD) { //写入命令函数 ReadBusy(); GPIO_ResetBits(GPIOB, RS); GPIO_ResetBits(GPIOB, RW); GPIO_ResetBits(GPIOB, EN); GPIO_Write(GPIOA, CMD); // GPIO_SetBits(GPIOB, EN); GPIO_ResetBits(GPIOB, EN); } void LCD_WRITE_ByteDATA(char ByteData ) { //写入单个Byte函数 ReadBusy(); GPIO_SetBits(GPIOB, RS); GPIO_ResetBits(GPIOB, RW); GPIO_ResetBits(GPIOB, EN); GPIO_Write(GPIOA, ByteData); GPIO_SetBits(GPIOB, EN); GPIO_ResetBits(GPIOB, EN); } void LCD_WRITE_StrDATA(char *StrData,char row, char col) {//写入字符串 char baseAddr = 0x00; //定义256位地址 if (row) { baseAddr = 0xc0; } else { baseAddr = 0x80; } baseAddr += col; while (*StrData != '\0') { LCD_WRITE_CMD( baseAddr ); LCD_WRITE_ByteDATA( *StrData); baseAddr++; StrData++; } } void ReadBusy(void) { //读忙函数,读忙之前记得更改引脚的工作方式!!!因为STM32的IO不是准双向IO GPIO_InitTypeDef GPIO; GPIO_Write(GPIOA, 0x00ff); GPIO.GPIO_Pin = GPIO_Pin_7; //选定GPIOA的第七Pin GPIO.GPIO_Mode = GPIO_Mode_IN_FLOATING; //第七Pin的工作方式为浮空输入模式,用于检测LCD1602的忙状态 GPIO.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO); GPIO_ResetBits(GPIOB, RS);//RS拉低 GPIO_SetBits(GPIOB, RW);//RW拉高 GPIO_SetBits(GPIOB, EN); //使能开 while( GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7 )); //读第七Pin状态,如果一直为1则循环等待 GPIO_ResetBits(GPIOB, EN);//使能关 GPIO.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7; //使GPIOA的状态还原成推挽模式 GPIO.GPIO_Mode = GPIO_Mode_Out_PP; GPIO.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO); }
PID.h
#ifndef PID_H_ #define PID_H_ typedef struct Pid { float Sv;//用户设定值 float Pv; float Kp; int T; //PID计算周期--采样周期 float Ti; float Td; float Ek; //本次偏差 float Ek_1;//上次偏差 float SEk; //历史偏差之和 float Iout; float Pout; float Dout; float OUT0; float OUT; int C1ms; int pwmcycle;//pwm周期 int times; }PID; extern PID pid; void PID_Init(void); void PID_Calc(void); #endif
PID.c
#include "PID.h" PID pid; void PID_Init() { pid.Sv=38;//用户设定温度 pid.Kp=30; pid.T=400;//PID计算周期 pid.Ti=4000000;//积分时间 pid.Td=1000;//微分时间 pid.pwmcycle=200;//pwm周期200 pid.OUT0=1; pid.C1ms=0; } void PID_Calc() //pid计算 { float DelEk; float ti,ki; float td; float kd; float out; if(pid.C1ms<(pid.T)) //计算周期未到 { return ; } pid.Ek=pid.Sv-pid.Pv; //得到当前的偏差值 pid.Pout=pid.Kp*pid.Ek; //比例输出 pid.SEk+=pid.Ek; //历史偏差总和 DelEk=pid.Ek-pid.Ek_1; //最近两次偏差之差 ti=pid.T/pid.Ti; ki=ti*pid.Kp; pid.Iout=ki*pid.SEk; //积分输出 td=pid.Td/pid.T; kd=pid.Kp*td; pid.Dout=kd*DelEk; //微分输出 out= pid.Pout+ pid.Iout+ pid.Dout; if(out>pid.pwmcycle) { pid.OUT=pid.pwmcycle; } else if(out<=0) { pid.OUT=pid.OUT0; } else { pid.OUT=out; } pid.Ek_1=pid.Ek; //更新偏差 pid.C1ms=0; }
PWMOUT.h
#ifndef PWMOUT_H
#define PWMOUT_H
#include "sys.h"
void Time_init(void);
void TimePwm_init(int arr,int psc);
#endif
PWMOUT.c
#include "PWMOUT.h" #include "PID.h" #include "LedAndBeep.h" void Time_init(void) { NVIC_InitTypeDef NVIC_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 自动装载的计数值 1ms TIM_TimeBaseStructure.TIM_Period = 1000; // 10KHz TIM_TimeBaseStructure.TIM_Prescaler = (72 - 1); // 1MHz TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_ClearITPendingBit(TIM2, TIM_IT_Update); TIM_Cmd(TIM2,ENABLE); } void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2,TIM_IT_Update)) { pid.C1ms++; Alert(); TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除中断标志 } } void TimePwm_init(int arr,int psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //TIM_CH2 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler =psc; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; TIM_OC2Init(TIM3, &TIM_OCInitStructure); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }
main.c
#include "LCD1602.h" #include "DHT11.h" #include "LedAndBeep.h" #include "PID.h" #include "PWMOUT.h" #include "delay.h" #include <string.h> #include <stdio.h> #define PERIOD 400 #define PRESCALER 36000 void Situation() { char hum[5]={0},temp[5]={0},PWM[10]={0},arr[5]={0x20,0x20,0x20,0x20,0x20}; sprintf(hum,"%d.%d",DHT_Data[0],DHT_Data[1]); sprintf(temp,"%d.%d",DHT_Data[2],DHT_Data[3]); //显示湿度 LCD_WRITE_StrDATA( hum,0,5 ); LCD_WRITE_StrDATA("%",0,9 ); //显示温度 LCD_WRITE_StrDATA( temp,0,11); LCD_WRITE_StrDATA("C",0,15 ); //显示pid.out LCD_WRITE_StrDATA("pid.out:",1,0); sprintf(PWM,"%f",pid.OUT); PWM[6]='\0'; LCD_WRITE_StrDATA(PWM,1,9); } int main() { unsigned int num=0; GPIO_init_Alert(); Time_init(); DHT11_Init(); PID_Init(); LCD_INIT(); LCD_WRITE_CMD( 0x80 ); LCD_WRITE_CMD(0x0C); LCD_WRITE_StrDATA( "situ:",0,0 ); TimePwm_init(PERIOD-1,PRESCALER); while(1) { while(DHT11_Read_Data()); PID_Calc(); num=(((pid.OUT*PERIOD)/pid.pwmcycle)-1); TIM_SetCompare2(TIM3,num); Situation(); } }
联系客服