一、项目介绍:
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℃。
二、设计思路
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 ,¶m);
//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;