C51_PID 水温控制系统

C51_PID 水温控制系统

前言

通过C语言程序写入51单片机实现水的温度的采集,并通过控制器控制加热器给水体加热,对水体的温进行PID控制,保证温度在设定值范围内波动
最终包括C51的视频的内容以及部分参考资料都可以在 百度分享链接内下载

链接: https://pan.baidu.com/s/1jpawh31O1gqv9CU_0LXFZQ 提取码: 2333

包含店家赠送的51的资料以及使用过程中需要用到的部分参考文件以及代码


1. 系统设计

系统硬件部分主要包含主控制器(89C52RC),温度传感器(DS18B20),加热器,数码管显示,AD采集以及DA输出

系统总体结构
系统总体结构

51系统板接线
51系统板接线

1.1 主控制器

主控制器是89C52 使用手册下载 也可以在pdf 文件中找到使用手册
本次实验使用的设备是 德飞莱LY-51S V3.0 可以参考附件中V2.32版本的使用手册,实物可以参考京东链接介绍页面[1], 基本操作视频在最上方发出的百度分享链接中(2.7G),以及店家附带的关于系统板的参考资料(600M)

系统控制器的原理图如图1.1所示

图1.1 51系统板原理图
图1.1 51系统板原理图

图1.2 所示是51 系统板原始图像,跟本版本几乎一致, 可以参考

图1.2 51系统板裸板图像
图1.2 51系统板裸板图像

最终只使用到了:数码管显示,只是LED小灯,串口模块,AD-DA模块,循环会一一介绍

1.2 LED小灯

程序里面设计了四个小灯来标识程序执行情况,对应的使用P10,P12,P14,P16四个端口,这样便于小灯之前区分开,避免识别出错, LED只需要在程序中将LED进行赋值1 0 即可控制小灯亮灭,由于小灯是共阳极, 所以 设定LED=1 小灯是灭的 LED=0 实现小灯亮起

//  定义小灯表示输出
sbit LED0 = P1^0; // 程序主循环函数
sbit LED1 = P1^2;   // 读取温度闪灯, 每次读取温度 闪灯一次
sbit LED2 = P1^4;   // 小灯,串口接收信号 闪灯
sbit LED3 = P1^6;   // 小灯,串口接收信号 闪灯

虽然原理图里面显示小灯是不同颜色, 但是在实际显示的时候全部是红色 所以间隔取了4个小灯来标识程序执行

小灯原理图
小灯原理图

LED0 是程序主循环程序指示灯,亮灭代表程序进入主程序的while 循环中循环执行, 主循环 控制小灯闪灭,同时循环判断读取温度标识位ReadTempFlag 只有满足的情况下会进入执行否则不执行
LED1 是温度读取小灯, 标识 程序进入中断函数执行, 设定值是中断函数每2ms 进入一次中断, 每次中断执行数码管刷新任务,同时每中断250次 0.5s 将程序执行标志位ReadTempFlag 置位 便于主程序执行,所以程序读取温度周期和程序执行周期都是0.5s
LED2 标识串口输出数据的小灯,每次串口接收到一个数据会将LED2 进行取反操作 如果输入两个数据则小灯会快速亮灭一次完成操作
LED3 标识命令执行小灯,程序里面目前设定的是5个字符标识一条指令, 每次满足5个字符之后LED3小灯会亮起,标识指令输入完成,当不满足的时候灯会灭掉,可以通过此等判断当前状态

1.3 数码管显示

数码管显示包含8个数据口P0的全部端口P00-P07以及两个控制控制口P22,P23

#define DataPort P0 //定义数据端口 程序中遇到DataPort 则用P0 替换
sbit LATCH1=P2^2;//定义锁存使能端口 段锁存
sbit LATCH2=P2^3;//                 位锁存

共阴极数码管原理图 对应的程序可以见程序工程display.c 在头文件display.h中定义了程序接口,对应的需要将P0口的线接到数码管的数据口上J3 , 同时需要将J50接口短接,否则数码管会很暗,几乎不可见同时需要将控制线 LATCH1 接到J2的B口, LATCH2接到J2的A口

程序会每次2ms循环显示数码管,showTemp 显示前面四个字,(-)25.1 的格式显示温度, showOuts 显示后面资格字 会显示-999 到999 的输出值, 显示计算得到的输出设定值, 输出值的显示会限定在-999 到999 之间,

数码管原理图
数码管原理图

// 千四个数码管 显示温度 _00.0 格式
float showTemp(unsigned int temp)
{
    unsigned int TempH,TempL;
    uchar b=0,s=0,g=0,d=0;  // 百 十 个位显示 
    float t_ = 0.0f;
    // 显示温度值  
    if(temp&0x8000)
    {
        TempData[0]=0x40;//负号标志
        temp=~temp;  // 取反加1
        temp +=1;
    }
    else
    {
        TempData[0]=0;
    }

    TempH=temp>>4;
    TempL=temp&0x0F;
    TempL=TempL*6/10;//小数近似处理

    b = TempH/100;          // 百位
    s = (TempH%100)/10;     // 十位
    g = (TempH%10);         // 个位
    d = TempL;              // 十分位

    if(TempH/100==0)
        TempData[1]=0;
    else
        TempData[1]=dofly_DuanMa[b]; //十位温度

    if((b==0)&&(s==0))//消隐
        TempData[1]=0x00;
    else
        TempData[1]=dofly_DuanMa[s]; //十位温度

    TempData[2]=dofly_DuanMa[g]|0x80; //个位温度,带小数点
    TempData[3]=dofly_DuanMa[d];

    t_ = b*100+s*10+g+d*0.1;
    return t_;
}

// 显示输出值,  取整 最大 999
void showOuts(int num)
{
    // 限定在-999~999
    num = num>999?999:num;
    num = num<-999?-999:num;

    if(num<0)
    {
        TempData[4]=0x40;//负号标志
        num = -num;
    }
    else
        TempData[4]=0x00;//正号不显示


    TempData[5]=dofly_DuanMa[num/100]; //十位温度
    TempData[6]=dofly_DuanMa[num%100/10]; //个位温度,带小数点
    TempData[7]=dofly_DuanMa[num%10];
}

1.4 温度传感器

温度传感器使用的集成的DS18B20 模块,有三个接线, 白色是5V-VCC黑色是GND.黄色是信号线,在程序中设定AD采集接口是P36接口 以便进行AD采集定义在18b20.h文件中,对应的实现包含了传感器的初始初始化以及温度采集函数,通过调用采集函数ReadTemperature可以返回uint型的温度数据, 其中在display.c中包含了showTemp 函数中可以将uint型的数据进行解码得到float型的数据 并显示在数码管的前面四个数字中 按照(-)25.0的格式显示总共四个格子, 标识当前的温度, 同时串口会实时的将温度输出到串口处, 输出格式为 025的格式输出,实际上每次输出之后 会用一个空格分割每次的值,在串口部分详细介绍

温度传感器的温度输出值,可以参考pdf 文件中的温度传感器的中文资料,其中数据与真实温度设定值可以参考下面的标识方式,实际上需要将uint16位的数据转换成float型的数据
温度传感器需要进行温度标定,将输出结果与真实温度进行数据标定成一个标准数据,,否则数据可能存在一个误差,

温度传感器温度值与数据的关系
温度传感器温度值与数据的关系

温度传感器的温度读取只在18b20文件中, 只需要将P36接口接到温度传感器的信号线上便能够读取温度, 不需要使用i2c来实现,似乎也不要使用AD采集就能得到结果

1.5 串口UART

51单片机的串口部分包含比较简单,但是硬件部分存在大坑, 建议不要改动相应的程序 可以备份之后再进行修改,需要分开部分进行介绍 包括串口基本部分,以及串口输出部分以及串口接收指令部分
由于51单片机支持串口下载,可以直接通过STC下载器将hex下载进51中,同时可以打开串口与单片机进行通讯

1.51 串口基本部分

51 单片机的串口部分操作比较类似, 目前程序里面使用的是2400 波特率baudrate,可以基本部分进行串口初始化以及实现串口发送单个字符以及发送字符串的功能 由于程序存在 串口初始化和定时器初始化, 所以在初始化过程中可以打开串口中断和定时器中断,初始化成功之后在主程序中使用EA=1 开启总中断,使能程序的中断过程避免由于中断导致初始化失败引发的程序假死
串口部分的基本部分可以参考程序的注释部分的内容, 程序附带文件夹里面包括了其他的串口的使用介绍例程,部分介绍了uart 的缓冲区扩充机制,以及一个环形缓冲区的结构设计,以及一个使用uart的modbus协议的实现,之后可能会用到的内容,


/*------------------------------------------------
                    串口初始化
------------------------------------------------*/
void InitUART  (void)
{

    SCON  = 0x50;		        // SCON: 模式 1, 8-bit UART, 使能接收  
    TMOD &= 0x0F;               //清零T1的控制位
    TMOD |= 0x20;               // TMOD: timer 1, mode 2, 8-bit 重装
    TH1   = 0xF3;               // TH1:  重装值 2400 波特率 晶振 11.0592MHz     
    ET1  =0 ;                    //禁止T1中断
    ES    = 1;                  //打开串口中断
    //EA    = 1;                  //打开总中断
    TR1   = 1;                  // TR1:  timer 1 打开      
}                            

/*------------------------------------------------
                    发送一个字节
------------------------------------------------*/
void uartSendByte(unsigned char dat)
{
    SBUF = dat;
    //while(!TI) ;
    // 延时一定时间 保证发送成功 如果使用上面的循环 等待硬件TI触发 很容易造成程序卡死,建议设定成
    DelayMs(10);
        
    TI = 0;
}
/*------------------------------------------------
                    发送一个字符串
------------------------------------------------*/
void SendStr(unsigned char *s)
{
 while(*s!='')//  表示字符串结束标志,通过检测是否字符串末尾
  {
	  uartSendByte(*s);
	  s++;
  }
}

1.52 串口输出部分

串口缓冲器SBUF只有一个字节,每次只能存储一个字节, 每次会将接收到的数据存放在SBUF中,如果将数据存放在SBUF中之后就能够实现输出字节了,下面两个函数 是实现发送一个三位的温度值的实现方法,即将每一位输出出去,显示在串口部分,

// 发送 整型数据
void SendNum(int num)
{
	// xdata 关键字会将数据存在 扩展RAM中 避免主data数据空间不够导致的出错
    xdata unsigned char b1=0;
	xdata unsigned char s1=0;
	xdata unsigned char g1=0;

  if(num<0)
  {
    uartSendByte(0x2D);  //发送负号 -
    num=-num;
  }

  b1=(num/100)+0x30;
  s1=(num%100)/10+0x30;
  g1=num%10+0x30;
	
	// 将数据直接发送回去  然后 结束 
	uartSendByte(b1);
	uartSendByte(s1);
	uartSendByte(g1);
	SendStr(" "); // 发送空格分割

}

// 发送 float 数据  3.1f
void SendNumF(float num)
{
	uint i_num = 0;
	uint f_num = 0.0f;	
	i_num = (int)num;		//获取整型
	f_num = 10.0f*(num-i_num);	//获取小数点的值
	
	SendNum(i_num);
	uartSendByte(0x2E); // 发送给一个点号 
	SendNum(f_num);
	
	SendStr(" ");

}

1.53 串口服务函数

串口中断使用的是中断4 每次接收到数据之后触发中断,进入中断服务函数中每次接受到数据之后会将串口接收灯置位, 亮灭一次,同时将接受到的字符返回到显示界面, 然后将接收到的字符使用串口处理函数进行处理,处理函数会进行累加,记录当前字符的数目, 设定的存储区域位5位的数组,每次会循环覆盖掉之前的数据,当接收满了5个字符之后能够将LED3电量,表示接受到了一条指令,目前程序下载之后可能需要手动输入空字符等到灯亮起来之后再输入完整指令 目前只支持T0350格式的指令,程序会判断第一个字符是否为 T, 如果满足则会将数组的RE_STR的 RE_STR[2],RE_STR[3]里面的数据组合成温度值, 表示设定的温度,其余部分暂时没有考虑添加,之后可能会进行处理


#define MAX_NUM_b 5			// 设定存在值就5个字符

unsigned char xdata RE_STR[MAX_NUM_b]; //存储显示值的全局变量

/*------------------------------------------------
                     串口中断程序
------------------------------------------------*/
void UART_SER (void) interrupt 4 //串行中断服务程序
{
    unsigned char Temp;          //定义临时变量 
    if(RI)                        //判断是接收中断产生
    {
      RI=0;                      //标志位清零
      Temp=SBUF;                 //读入缓冲区的值        	
      LED2 = !LED2; 			// 亮灭串口接收灯 
      SBUF=Temp;                 //把接收到的值 再发回电脑端
      handleRE(Temp);
    }
    if(TI)                        //如果是发送标志位,清零
    {
        TI=0;
    }

} 

// 处理接收串口事件  每次接收会 出发函数 
void handleRE(uchar t)
{
    static uchar count_r=0;
    // 每次接收数据加1 
    // 如果不是 结束符号
    if (t != '')
    {
        RE_STR[count_r] = t;
    }
    count_r++;

    // 灯灭 每次程序将灯灭掉
    LED3 = 1; 

    if(count_r == MAX_NUM_b)
    {
        count_r = 0;
        SendStr("!");
				
		// 如果首位是T 则  后三位标识 温度 x10
		if(RE_STR[0] == 't' || RE_STR[0] == 'T')
		{
			temp_set = (RE_STR[2]-0x30)*10+(RE_STR[3]-0x30);
			SendStr("T:");
			SendNum(temp_set);
		}
        LED3 = 0; 	// 灯亮 
    }


}

1.6 AD-DA模块

AD-DA转换模块可以参考附件文件夹pdf中AD模块内容, 实际过程中AD采集模块 并没有用到,温度采集的部分使用的是P36接口, 使用 在ADC.c 中进行了定义 但是程序中使用了DA模块实现了温度的输出值, 此处可以更改温度读取 设定,改用AD采集获取信号信号,目前使用的是直接读取结果,

AD-DA模块
AD-DA模块

1.61 I2C接口实现

温度传感器是是DS18b20 的格式, 器件的初始化 使用I2C协议进行操作,参考程序内部的i2c.c函数实现,内容比较多,可以直接区程序里面进行参考,数字I2c的实现 都差不多,注意时序就好, I2C的接口需要接到P20,P21接口在板上J8 接口处,使用I2c可以控制 系统板上的4路AD采集以及一路AD输出, 函数头定义了I2C器件地址, 可以参考附件中介绍,完成数据采集过程.

sbit SDA=P2^1;
sbit SCL=P2^0;

#define AddWr 0x90   //写数据地址 
#define AddRd 0x91   //读数据地址

1.62 AD-DA输出实现

AD采集函数可以实现4路AD采集过程,DA输出只支持DA输出, 将J33的接口短接之后可以使用DA输出 的电压值控制小灯LED9 的亮亮度,此小灯是输出到了阴极,如果输出5V则小灯是灭的 输出0 则会亮起来,但是LED存在当输出的两端电压差值在1.5V以内 小灯则不会亮,

/*------------------------------------------------
             读AD转值程序
输入参数 Chl 表示需要转换的通道,范围从0-3
返回值范围0-255
------------------------------------------------*/
unsigned char ReadADC(unsigned char Chl)
 {
   unsigned char Val;
   Start_I2c();               //启动总线
   SendByte(AddWr);             //发送器件地址
     if(ack==0)return(0);
   SendByte(0x40|Chl);            //发送器件子地址
     if(ack==0)return(0);
   Start_I2c();
   SendByte(AddWr+1);
      if(ack==0)return(0);
   Val=RcvByte();
   NoAck_I2c();                 //发送非应位
   Stop_I2c();                  //结束总线
  return(Val);
 }
/*------------------------------------------------
               写入DA转换数值
输入参数:dat 表示需要转换的DA数值,范围是0-255
------------------------------------------------*/
bit WriteDAC(unsigned char dat)
{
   Start_I2c();               //启动总线
   SendByte(AddWr);             //发送器件地址
     if(ack==0)return(0);
   SendByte(0x40);            //发送器件子地址
     if(ack==0)return(0);
   SendByte(dat);             //发送数据
     if(ack==0)return(0);
   Stop_I2c();  
	
	return 1;
}

1.62 PI控制以及输出

主循环过程中设置了读取温度标志位后,会进去温度读取过程,, 然后根据显示温度,串口输出温度,之后进行PI运算, 最终显示输出值之后之后将电压输出到 电压中

if(ReadTempFlag==1)
        {
            ReadTempFlag=0;
            temp=ReadTemperature(); //读取温度
            // num2str(101);

            t_0 = showTemp(temp);   // 显示温度
            SendNum(t_0);           // 串口输出温度

            //===================================
            // //PID
            ek = (temp_set-t_0);
            ei += ek;
            
            tp = ek*P;
            
            ti = ei*I;
            
            out_pi = tp+ti;		

            //===================================
            //out_pi= out_pi;
            showOuts(out_pi);
            out_pi = (out_pi>255?255:out_pi);
            out_pi = (out_pi<0?0:out_pi);

            // 将控制结果输出到灯光的控制中
            WriteDAC(out_pi);			// 最后总控制输出电压
        }	// end of readTempFlag

  1. https://item.jd.com/29638729865.html

原文地址:https://www.cnblogs.com/hugochen1024/p/12704333.html