完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
原理 元件 stm32f1核心板、L298N模块(当然用MOS管更好)、led一个、三极管一个、蜂鸣器一个、DHT11一个、LCD1602一个、电阻10欧10K欧、可调电阻10K、加热丝 功能描述 用DHT11检测当前环境温湿度,并将数据显示在LCD1602上,在用设定温度与当前温度相减,通过PID算法计算出当前输出脉宽,并将其加在L298N模块中,使加热丝发热,形成一个闭环,经过一段时间温度稳定在设定值。由于我的初衷是做一个恒温箱孵蛋,所以加了湿度报警。 电路图 DHT11时序图 总体时序图 DHT11与MCU通讯一次时间在4ms左右,数据分小数部分和整数部分,一次完整的数据传输为40bit,高位先出。 数据格式: 8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据+8bit校验和。 数据传送正确时校验和数据等于“ 8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据” 所得结果的末8位。 MCU发送一次开始信后,DHT11从低功耗模式转换到高速模式等待主机开始信号结后,DHT11发送响应信号,送出40bit的数据,并触发一次信号采集,用户可选择读取部分数据。从模式下,DHT11接收到开始信号触发一次温湿度采集,如果没有接收到主机发送的开始信号,DHT11不会主动进行温湿度采集。采集数据后转换到低速模式。 初始化 总线空闲状态为高电平,主机把总线拉低等待DHT11响应,主机把总线拉低必须大于18毫秒,保证DHT11能检测到起始信号。 DHT11接收到主机的开始信号后,等待主机开始信号结束,然后发送80us低电平响应信号.主机发送开始信号结束后,延时等待20-40us后, 读取DHT11的响应信号,主机发送开始信号后,可以切换到输入模式,或者输出高电平均可, 总线由上拉电阻拉高。 信号“0” 总线为低电平,说明DHT11发送响应信号,DHT11发送响应信号后,再把总线拉高80us,准备发送数据,每一bit数据都以50us低电平时隙开始,高电平持续的时间在26-28us之间表示0。 信号“1” 总线为低电平,说明DHT11发送响应信号,DHT11发送响应信号后,再把总线拉高80us,准备发送数据,每一bit数据都以50us低电平时隙开始,高电平持续的时间达到70us表示1。 PWM脉宽调制 我们要控制温度就要控制电热丝的发热量,也就是控制加在电热丝两端的端电压,这就要用到D/A转换,那么PWM是不二选择。我们知道PWM波是高低电平变化的等周期方波,通过调节高电平时间来改变占空比(高电平时间占周期的百分比)。比如说,某一PWM波的周期是20ms,高电平对应的电压是12V,持续的时间5ms,则低电平时间也是15ms,其占空比就是25%,那么我们可以得到一个3V(12 x 25%)的电压。 从上述中,我们可以发现,输出电压只与占空比有关,那么频率有什么作用呢?频率是周期的倒数,也可以理解成1s内有多少个周期。在以呼吸灯为例,如果PWM信号的频率很低,为2Hz,占空比为25%,那么这个小灯会在1s内闪烁2次,是闪烁并不是暗淡的光,当我们把频率升高,升到100Hz,此时便不再出现闪烁,而是发出较为暗淡的光,由此例,我们可以理解为PWM的频率越高,输出的响应速度就越快,PWM的频率越低,输出的响应速度越慢,也就是小灯出现闪烁而不是发出黯淡的光(形象比喻)。当然PWM的频率并不是越高越好,频率越高,开关损耗也就越大,耗电量就越大,所以选择一个合适的频率很重要。对于频率有固定要求的,比如步进电机,频率选择相对较容易,根据电机相关参数就能得出。对于本文所做的温度控制来说,我选择的是低频,当然频率在提高一些也是可以的。 PID算法 框图 比例控制 用户设定值Sv表示最终将温度稳定在Sv,从系统运行开始每隔一段时间就采集当前环境内的温度,得样本如下: 这些样本数据也就是程序中的Pv,通过这些样本数据我们可以知道当前环境温度与用户设定值之间的差值Ek,即Ek=Sv-Xk,Ek有三种情况,如表所示: [tr]Ek说明[/tr]
这种算法称为比例控制算法,out0是一个常数,可设置为1,以避免Ek = 0时Out也等于0,Kp表示比例系数,其大小将直接影响系统的响应速度,不难理解,如果Kp很大,那么一个小的差值Ek也会得到一个较大的数值Pout,那么系统将会出现剧烈震荡,很难达到稳定,同样的道理,Kp过小则系统的响应速度太慢,尽管有一个很大的差值Ek也只能得到一个较小的数值Pout,故而系统需要很长一段时间才能达到稳定状态附近。 积分控制 通过用户设定值Sv与我们采集的环境温度数据做差,我们得到了差值Ek,由于连续的温度采集,于是有了一系列的差值样本: 其中: 将这些差值进行累加,得: 同样,SE也有三种情况,如表: [tr]SE说明[/tr]
在单片机中,对于积分运算可近似变换,即: 我们知道求积分其实就是求面积,如下图所示,abcd所围城图形的面积就是积分所求的面积,矩形abce的面积 Ek * T 就是近似变换球的的面积,其中 T 表示采样时间。 由上式可得积分控制算法可写为: Kp表示比例系数,T表示采样时间,out0是一个常数,避免历史差值积分为0时无输出值,Ti表示积分时间,其大小会影响 Iout 的大小,从而影响OUT的大小,当Ti的值很大时,环境温度需要较长的时间才能回到设定值,无论当前环境温度大于还是小于设定值,当Ti较小时,环境温度波动会比较大,且震荡衰减小。 微分控制 前面我们获得了差值样本,那么最近两次差值之差可表示为: 同样,Dk也有三种情况,如下表: [tr]Dk说明[/tr]
在单片机中,微分可做近似变换,即: 从而有: Kp表示比例系数,out0是一个常数,避免Dk为0时取输出信号,T表示采样时间,Td表示微分时间,Td越大微分作用越强,即抑制效果越明显。 整合后的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=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 != ' |