基于MSP430F5529LP制作的恒温自动控制系统
使用PID控制加热电阻的功率,以温度传感器读取的温度作为反馈,最终实现恒温自动控制功能。
标签
嵌入式系统
MCU
自动控制
2023寒假在家练
PID
salti-fish
更新2023-03-27
西安电子科技大学
470

一、项目介绍:

1、本项目开发板使用MSP-EXP430F5529LP ,这是一款针对MSP430F5529 USB微控 制器的廉价而简单的开发套件。它为MSP430 MCU提供了一种简单的方法,具有用于编程和调试的板载仿真,以及用于简单用户界面的按钮和LED。

 

2、本项目还使用了硬禾学堂的拓展板(包含如下功能):

  • 按键、旋转编码器输入 - 以模拟信号的方式
  • 双电位计控制输入 - 以数字信号的方式
  • RGB三色LED显示
  • 1.44寸128*128 LCD,SPI总线访问
  • MMA7660三轴姿态传感器
  • 电阻加热
  • 温度传感器
  • 与MSP430 Launch Pad开发板的接口

3、本项目的目标为平台(4)的:

项目4 - 实现一个恒温自动控制系统

  • IO扩展板上有一处加温电阻,将加热区域用物体(纸巾等)包裹起来,通过电流给电阻加热,并通过温度传感器感知板上温度的变化,测温以及在LCD屏上的温度显示。

要求:使用按键设定目标温度,并且通过程序控制加热功率,使得温度尽快尽量稳定的维持在目标温度。温度偏离设定温度±3°C彩灯变为红色。

已完成全部目标,其中:LCD屏幕可以实时显示设定温度曲线,实时测量温度曲线以及实时输出功率曲线;两个按键可以分别调高(低)目标温度5℃。

NXiGP.png

二、设计思路

1、使用ADC读取拓展版上的按键,获取目标温度;

2、使用PWM输出控制mos管的导通时间占比,从而控制实时加热功率;

3、使用I2C读取NST112温度传感器的数据,获得实时温度;

4、使用TFT-LCD屏幕显示目标温度,实时温度以及实时加热功率;

5、使用PID算法对输出功率进行控制,使得温度尽快尽量稳定的维持在目标温度,并在温度偏离设定温度±3°C彩灯变为红色。

 

三、框图和软件流程图

1、硬件连接

2、软件流程(以main函数为例)

四、硬件介绍

1、NST112采用I2C通信,可在 -20℃到85℃的范围内实现高达±0.5℃的精度;

2、1.44寸LCD分辨率为128*128,有16位色彩显示,采用ST7735控制,以三线SPI进行通信;

3、拓展版上的按键以如图方式连接到ADC引脚进行读取;

4、使用mos控制实时加热功率;

五、实现的功能及图片展示(可参考视频)

1、使用按键设定目标温度;

2、并且通过程序控制加热功率,使得温度尽快尽量稳定的维持在目标温度;

3、温度偏离设定温度±3°C彩灯变为红色;

其中:LCD屏幕可以实时显示设定温度曲线,实时测量温度曲线以及实时输出功率曲线;两个按键可以分别调高(低)目标温度5℃。

 

六、主要代码片段及说明

1、主函数部分

#include <msp430.h> 
#include "driverlib.h"
#include "delay.h"
#include "sys.h"
#include "pid.h"
#include "lcd_init.h"
#include "lcd.h"
#include "pic.h"
#include "MSP430F5529_I2C.h"
#include "NST112.h"

char ADC_Key=0;
char Goal_Temp = 65;

#define heat_on //!!!加热开关!!!小心!!!
#define Plot_Mode //使能绘图功能

int main(void)
{

	WDT_A_hold(WDT_A_BASE);//初始化
    SystemClock_Init();
    I2C_Init();
    Led_Init();

#ifndef heat_on
    Heat_Off();
#else
    Timer_A0_PWM_Init();
    Timer_A0_PWM_Compare(25);
#endif

#ifdef Plot_Mode
    LCD_Plot_Data Curr_Temp_Data={0}, Goal_Temp_Data={0}, Curr_Pwr_Data={0};//绘图结构体
#endif

	float Curr_Temp = 50;
	float Duty=0;

    Led_Init();
    LCD_Init();//液晶屏初始化,显示标题和画框
    LCD_Fill(0,0,LCD_W,LCD_H,WHITE);
    LCD_ShowString(4,0,"Salti-fish",RED,CYAN,24,0);
    LCD_DrawRectangle(0,0,LCD_W-1,LCD_H-1,MAGENTA);
    LCD_DrawRectangle(1,1,LCD_W-2,LCD_H-2,MAGENTA);
    ADC_Init();
    PID_init();

#ifndef Plot_Mode
    LCD_ShowString(24,30,"Goal_T:",RED,WHITE,16,0);
    LCD_ShowString(24,50,"Curr_T:",RED,WHITE,16,0);
    LCD_ShowPicture(70,70,60,60,gImage_slime);
#endif

    while(1)
    {
        Curr_Temp = Read_NST112_Temp();
        Check_Temp((char)Curr_Temp,Goal_Temp);//检查是否点灯
        Duty=PID_realize((float)Goal_Temp, Curr_Temp);
        Timer_A0_PWM_Compare(Duty);

#ifndef Plot_Mode//数显模式
        LCD_ShowIntNum(24,80,(int)Duty,2,RED,CYAN,24);
        LCD_ShowIntNum(80,30,Goal_Temp,2,RED,WHITE,16);
        LCD_ShowFloatNum1(80,50,Curr_Temp,4,RED,WHITE,16);
#else
        //绘图模式
        //LCD_Fill(0,48,LCD_W,LCD_H,WHITE);本部分用白色曲线覆盖先前曲线,代替fill,更快
        LCD_Draw_Plot(&Curr_Pwr_Data , WHITE);
        LCD_Draw_Plot(&Goal_Temp_Data, WHITE);
        LCD_Draw_Plot(&Curr_Temp_Data, WHITE);

        LCD_ShowIntNum(4 ,24,(int)Goal_Temp,2,RED  ,WHITE,24);
        LCD_ShowIntNum(32,24,(int)Curr_Temp,3,BLUE ,WHITE,24);
        LCD_ShowIntNum(72,24,(int)Duty     ,3,GREEN,WHITE,24);
        Plot_Data_Add(&Curr_Temp_Data, (unsigned int)Curr_Temp*0.8);
        Plot_Data_Add(&Goal_Temp_Data, Goal_Temp*0.8);
        Plot_Data_Add(&Curr_Pwr_Data , Duty*0.8     );
        LCD_Draw_Plot(&Curr_Pwr_Data , GREEN);
        LCD_Draw_Plot(&Goal_Temp_Data, RED  );
        LCD_Draw_Plot(&Curr_Temp_Data, BLUE );
#endif

        ADC12_A_startConversion(ADC12_A_BASE, ADC12_A_MEMORY_0, ADC12_A_SINGLECHANNEL);//启动一次ADC
        __bis_SR_register(LPM0_bits + GIE);
        delay_ms(20);
    }

}



#pragma vector=ADC12_VECTOR
__interrupt
void ADC12_A_ISR (void)
{
    static int ADC_Buffer[6]={0};
    unsigned int Measured = 0;
    switch (__even_in_range(ADC12IV,34))
    {
        case  0: break;   //Vector  0:  No interrupt
        case  2: break;   //Vector  2:  ADC overflow
        case  4: break;   //Vector  4:  ADC timing overflow
        case  6:          //Vector  6:  ADC12IFG0
                 //Is Memory Buffer 0 = A5 > 0.5AVcc?
                 Measured = ADC12_A_getResults(ADC12_A_BASE, ADC12_A_MEMORY_0);//MAX:4095
                 int iadc;
                 for(iadc=0;iadc<5;iadc++)
                 {
                     ADC_Buffer[iadc]=ADC_Buffer[iadc+1];//读取到的ADC值加入缓存等待判断
                 }
                 ADC_Buffer[iadc]=Measured;
                 Check_Key(ADC_Buffer);//ADC按键判断函数
                 __bic_SR_register_on_exit(LPM0_bits);
        case  8: break;   //Vector  8:  ADC12IFG1
        case 10: break;   //Vector 10:  ADC12IFG2
        case 12: break;   //Vector 12:  ADC12IFG3
        case 14: break;   //Vector 14:  ADC12IFG4
        case 16: break;   //Vector 16:  ADC12IFG5
        case 18: break;   //Vector 18:  ADC12IFG6
        case 20: break;   //Vector 20:  ADC12IFG7
        case 22: break;   //Vector 22:  ADC12IFG8
        case 24: break;   //Vector 24:  ADC12IFG9
        case 26: break;   //Vector 26:  ADC12IFG10
        case 28: break;   //Vector 28:  ADC12IFG11
        case 30: break;   //Vector 30:  ADC12IFG12
        case 32: break;   //Vector 32:  ADC12IFG13
        case 34: break;   //Vector 34:  ADC12IFG14
        default: break;
    }
}

这是主要程序框架,进行项目整体框架的安排,分别实现:所用外设初始化,温度采集,LED逻辑判断,按键ADC信号采集与中断,按键判断,pid控制计算加热功率,温度数据统计与LCD绘制功能;

可以在此调整显示模式:可选数值直接显示或作图显示;

也可以开关加热功能,在pid未完善时以及调试其他功能时保护元器件。

2、NST112驱动

#include "NST112.h"
#include <stdlib.h>
#include "MSP430F5529_I2C.h"

float Read_NST112_Temp(void)
{
    int Origin_Temp = I2C_ReadReg(NST112_ADDRESS,NST112_REG_Temp);
    float Temp;
    Temp = (char)(Origin_Temp>>8);
    unsigned char decimal=(unsigned char)Origin_Temp;
    if(Temp>=0)
        Temp += (float)(decimal>>4)/16;
    else
        Temp -= (float)(((~decimal)>>4)+1)/16;
    return Temp;
}

这是读取NST112的函数部分,根据项目需要,没有编写完整驱动来对NST112进行参数配置,只完成了读取寄存器的部分,可以获取实时温度的原始数据,并且对其进行格式转换,输出直接可用的有符号浮点数。

3、PID算法

#include "pid.h"
typedef struct
{
    float SetT;	  	//定义设定值
    float ActualT;	//定义实际值
    float err;			    //定义偏差值
    float err_last;		  	//定义上一个偏差值
    float Kp,Ki,Kd;		  	//定义比例、积分、微分系数
    float result,result_limit;		    //pid计算结果
    float Pwr;		  	//定义电压值(控制执行器的变量)0-5v右转 5-10v左转
    float integral,integral_limit;		  	//定义积分值
    float Times;				//周期对应次数
}pid_p; 

pid_p pid_T;

//pid位置式
void PID_init()
{
    pid_T.SetT= 0.0;         // 设定的预期
    pid_T.ActualT= 0.0;          // adc实际
    pid_T.err= 0.0;                   // 当前次实际与理想的偏差
    pid_T.err_last=0.0;               // 上一次的偏差
    pid_T.Pwr= 0.0;                // 控制值
    pid_T.integral= 0.0;              // 积分值
    pid_T.Kp= 1.2;                    // 比例系数
    pid_T.Ki= 2.8;                   // 积分系数
    pid_T.Kd= 3.5;                    // 微分系数
    pid_T.Times=3;
    pid_T.integral_limit=25;
    pid_T.result_limit=100;
}

float PID_realize( float v, float v_r)
{
    pid_T.SetT = v;
    pid_T.ActualT = v_r;   // 实际传入 = ADC_Value * 3.3f/ 4096
    pid_T.err = pid_T.SetT - pid_T.ActualT;   //计算偏差
    pid_T.integral = (pid_T.integral*(pid_T.Times-1) + pid_T.err)/pid_T.Times;                        //积分求和
    LIMIT(pid_T.integral,-pid_T.integral_limit,pid_T.integral_limit);
    pid_T.result = pid_T.Kp * pid_T.err + pid_T.Ki * pid_T.integral + pid_T.Kd * ( pid_T.err - pid_T.err_last);//位置式公式
    pid_T.err_last = pid_T.err;             //留住上一次误差
    pid_T.Pwr=25+pid_T.result;
    LIMIT(pid_T.Pwr,10,pid_T.result_limit);
    return pid_T.Pwr;
}

根据需要使用了位置式PID,进行调参与限幅后可以很好地完成功能。

4、曲线绘制功能实现

typedef struct//曲线绘制结构体
{
    unsigned int index;
    unsigned int value[64];
}LCD_Plot_Data;

void Plot_Data_Add(LCD_Plot_Data *Plot_Data, unsigned int Data)
{
    Plot_Data->value[Plot_Data->index] = Data;
    Plot_Data->index++;
    Plot_Data->index %= 64;
}

void LCD_Draw_Plot(LCD_Plot_Data *Plot_Data, unsigned int color)
{
    static unsigned char i;
    for(i=0;i<62;i++)
    {
        LCD_DrawLine(2*i+2,128-(Plot_Data->value[(Plot_Data->index+1+i)%64]),2*(i+1)+2,128-(Plot_Data->value[(Plot_Data->index+2+i)%64]),color);
    }
}

首先定义绘图用到的结构体,采用记录自变量与变量数据与目前指针的方式,而非128*128数组的形式,能够减少空间复杂度;单独用一个函数对数据结构体进行操作,简化数据添加过程;建立独立绘制函数,传入数据结构体进行绘制;模块化设计,减少了代码的依赖性,提高了复用性。

5、针对此开发板和此项目的专门功能函数

#include <msp430.h>
#include "driverlib.h"
#include "sys.h"

char almost(int a,int b)//约等于,用于ADC按键模糊判断
{
    int c;
    if(a<b)
    {
        c=a;
        a=b;
        b=c;
    }
    c=a-b;
    if(c<=150)
        return 1;
    else
        return 0;
}

void SystemClock_Init(void)
{
    PMM_setVCore(PMM_CORE_LEVEL_3);     //高主频工作需要较高的核心电压

    //XT1引脚复用
    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P5, GPIO_PIN4);
    GPIO_setAsPeripheralModuleFunctionOutputPin(GPIO_PORT_P5, GPIO_PIN5);

    //起振XT1
    UCS_turnOnLFXT1(UCS_XT1_DRIVE_3,UCS_XCAP_3);

    //XT2引脚复用
    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P5, GPIO_PIN2);
    GPIO_setAsPeripheralModuleFunctionOutputPin(GPIO_PORT_P5, GPIO_PIN3);

    //起振XT2
    UCS_turnOnXT2(UCS_XT2_DRIVE_4MHZ_8MHZ);

    //XT2作为FLL参考时钟,先8分频,再50倍频 4MHz / 8 * 50 = 25MHz
    UCS_initClockSignal(UCS_FLLREF, UCS_XT2CLK_SELECT, UCS_CLOCK_DIVIDER_8);
    UCS_initFLLSettle(25000, 50);

    //XT1作为ACLK时钟源 = 32768Hz
    UCS_initClockSignal(UCS_ACLK, UCS_XT1CLK_SELECT, UCS_CLOCK_DIVIDER_1);

    //DCOCLK作为MCLK时钟源 = 25MHz
    UCS_initClockSignal(UCS_MCLK, UCS_DCOCLK_SELECT, UCS_CLOCK_DIVIDER_1);

    //DCOCLK作为SMCLK时钟源 = 25MHz
    UCS_initClockSignal(UCS_SMCLK, UCS_DCOCLK_SELECT, UCS_CLOCK_DIVIDER_1);

    //设置外部时钟源的频率,使得在调用UCS_getMCLK, UCS_getSMCLK 或 UCS_getACLK时可得到正确值
    UCS_setExternalClockSource(32768, 4000000);
}

#define TIMER_PERIOD 12500
void Timer_A0_PWM_Init(void)
{
    Timer_A_outputPWMParam htim = {0};
    //P1.4复用输出
    GPIO_setAsPeripheralModuleFunctionOutputPin(GPIO_PORT_P1, GPIO_PIN4);
    //时钟源选为SMCLK = 25MHz
    htim.clockSource = TIMER_A_CLOCKSOURCE_SMCLK;
    //分频系数设为20
    htim.clockSourceDivider = TIMER_A_CLOCKSOURCE_DIVIDER_20;
    //装载值设为12500 - 1
    htim.timerPeriod = TIMER_PERIOD - 1;
    //频率为25m/20/12500=100Hz

    //P1.4 对应 TA0.3 故设为TIMER_A_CAPTURECOMPARE_REGISTER_3
    htim.compareRegister = TIMER_A_CAPTURECOMPARE_REGISTER_3;
    htim.compareOutputMode = TIMER_A_OUTPUTMODE_RESET_SET;
    //占空比5%,可调为TIMER_PERIOD的百分比
    htim.dutyCycle = TIMER_PERIOD / 20 ;
    //P1.4 对应 TA0.3 为TIMER_A0_BASE
    Timer_A_outputPWM(TIMER_A0_BASE, &htim);
}

void Timer_A0_PWM_Compare(float percentage)//占空比调整
{
Timer_A_setCompareValue
    (
        TIMER_A0_BASE,
        TIMER_A_CAPTURECOMPARE_REGISTER_3,
        TIMER_PERIOD * percentage / 100
    );
}

void ADC_Init(void)//初始化ADC,可调整IO口after3,after44。
{
    //P6.0 ADC option select
    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P6,GPIO_PIN0);

    //Initialize the ADC12_A Module
    /*
     * Base address of ADC12_A Module
     * Use internal ADC12_A bit as sample/hold signal to start conversion
     * USE MODOSC 5MHZ Digital Oscillator as clock source
     * Use default clock divider of 1
     */
    ADC12_A_init(ADC12_A_BASE,
            ADC12_A_SAMPLEHOLDSOURCE_SC,
            ADC12_A_CLOCKSOURCE_ADC12OSC,
            ADC12_A_CLOCKDIVIDER_1);

    ADC12_A_enable(ADC12_A_BASE);

    /*
     * Base address of ADC12_A Module
     * For memory buffers 0-7 sample/hold for 64 clock cycles
     * For memory buffers 8-15 sample/hold for 4 clock cycles (default)
     * Disable Multiple Sampling
     */
    ADC12_A_setupSamplingTimer(ADC12_A_BASE,
            ADC12_A_CYCLEHOLD_64_CYCLES,
            ADC12_A_CYCLEHOLD_4_CYCLES,
            ADC12_A_MULTIPLESAMPLESDISABLE);

    //Configure Memory Buffer
    /*
     * Base address of the ADC12_A Module
     * Configure memory buffer 0
     * Map input A0 to memory buffer 0
     * Vref+ = AVcc
     * Vr- = AVss
     * Memory buffer 0 is not the end of a sequence
     */
    ADC12_A_configureMemoryParam param = {0};
    param.memoryBufferControlIndex = ADC12_A_MEMORY_0;
    param.inputSourceSelect = ADC12_A_INPUT_A0;
    param.positiveRefVoltageSourceSelect = ADC12_A_VREFPOS_AVCC;
    param.negativeRefVoltageSourceSelect = ADC12_A_VREFNEG_AVSS;
    param.endOfSequence = ADC12_A_NOTENDOFSEQUENCE;
    ADC12_A_configureMemory(ADC12_A_BASE ,&param);

    //Enable memory buffer 0 interrupt
    ADC12_A_clearInterrupt(ADC12_A_BASE,
            ADC12IFG0);
    ADC12_A_enableInterrupt(ADC12_A_BASE,
            ADC12IE0);
}

void Heat_Off(void)
{
    P1SEL &= ~0x10;
    P1DIR |=  0x10;
    P1OUT &= ~0x10;
}

//RGB:2.5,2.4,1.5
void Led_Init(void)
{
    P2SEL &=~0x30;
    P2DIR |= 0x30;
    P2OUT |= 0x30;
    P1SEL &=~0x20;
    P1DIR |= 0x20;
    P1OUT |= 0x20;
}

void Check_Temp(char T1,char T2)//检查是否应该亮灯
{
    char T;
    if(T1<T2)
    {
        T=T1;
        T1=T2;
        T2=T;
    }
    if(T1-T2<=3)
        LED_R_ON;
    else
        LED_R_OFF;
}

void Check_Key(int arr[6])//读取ADC的缓存数据,判断按键情况
{
    extern char Goal_Temp;
    if((almost(arr[4],ADC_K0))&&(almost(arr[5],ADC_K1)))
        Goal_Temp-=5;
    else if((almost(arr[4],ADC_K0))&&(almost(arr[5],ADC_K2)))
        Goal_Temp+=5;
}



控制单片机各功能初始化的函数,以及各种针对此项目的基本判断与引脚控制函数。

 

七、遇到的主要难题及解决方法

1、未找到支持MSP430的ST7735LCD例程与驱动;

解决:移植基于STM32的例程与驱动;

2、NST112没有找到任何例程;

解决:查看数据手册编写驱动;

3、MSP430硬件I2C过于复杂,多次尝试读写失败;

解决:使用软件I2C自己编写程序读取;

4、从未接触过LCD绘图;

解决:基于绘制线条的功能编写函数与相关结构体,实现图像绘制;

 

八、未来的计划

1、研究ADC功能,优化按键读取程序,提高速度与准确率,尝试读取旋转编码器;

2、研究LCD显示屏,与相关函数,尝试更好的绘图效果与更高的刷新率;

3、完善I2C程序,尝试硬件I2C,实现温度传感器的参数配置;

4、尝试串级PID,测试是否有更好的效果;

5、尝试读取加速度传感器数值,做一款平衡球小游戏;

6、尝试使用定时器读取摇杆位置;

7、尝试本平台上的其他项目,利用此开发板练习,更好地掌握MSP430;

 

附件下载
5529_temp_Fin.rar
项目工程文件
团队介绍
朱泓宇,西安电子科技大学
团队成员
朱泓宇
西安电子科技大学本科在读
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号