一、项目简介
本项目是基于AVR64DD32平台的加热与温度监控系统,主要功能包括加热功率调控、温度监测、实时温度变化曲线、温度曲线截图的保存与显示等。
本项目主控为Microchip公司出品的新品AVR单片机开发板AVR64DD32 Curiosity Nano。此开发板板载了一个调试器,从而无需外部的调试工具即可对MCU进行编程和调试,并且该调试器还有虚拟串口(CDC),只需一根USB线即可完成编译、下载、在线debug、串口日志打印与调试等诸多功能。另外此开发板还板载了一个机械按键(可复用为复位按键或者用户按键)和一个用户LED,可用于提示程序运行状态。
本项目的另一块主体电路板为硬禾学堂出品的IO扩展板,此扩展板包含了诸多外设,本项目使用到的外设包含:1.44吋LCD屏幕(像素128*128,驱动芯片ST7735)、IIC接口的温度测量芯片NST112、三色RGBLED、可通过PWM调节加热功率的加热电路、xy轴摇杆、带按键的旋转编码器以及两个机械按键。
二、硬件方案
整体硬件连接框图如下图所示,以AVR64DD32 Curiosity Nano评估板为核心,以IO扩展板上的LCD和按键、旋转编码器及摇杆搭建人机交互环境,用来选择与查看相应功能展示。IO扩展板上的加热电路、RGBLED和温度计用来完成主要功能。
具体硬件连接如下表所示
UART_TX |
UART_RX |
IIC_ SDA |
IIC_SCL |
LED_Blue |
LED_Green |
LED_Red |
AIN1 |
PD4 |
PD5 |
PA2 |
PA3 |
PF4 |
PF3 |
PF2 |
PD1 |
LCD_MOSI |
LCD_SCK |
LCD_CS |
LCD_DC |
LCD_RES |
PWM_OUT |
PWM_IN |
|
PA4 |
PA6 |
PA7 |
PD3 |
PC0 |
PD2 |
PD7 |
|
三、软件模块介绍
软件完成的功能为:使用摇杆来切换当前菜单界面,点击旋转编码器按键作为确认按键,以选择当前展示功能,两个机械按键分别作为设置和返回按键,旋转编码器的正、反转用来调节部分页面的数值大小。目前完成的共有“温升曲线”、“功率调节”、“相册”等数个页面。软件可分为以下五个功能模块。Funpack2-4基于AVR64DD32的加热和温度监控系统
1、LCD显示模块
LCD显示模块直接由AVR64DD32评估板片内硬件SPI驱动,在使用摇杆、按键等切换菜单、进出菜单时执行页面初始化pageinit(page),初始化后则在主循环内执行页面刷新pagerefresh(page).此外还有规划行变化时的刷新,但暂时使用不到所以未实装。
2、加热模块
加热模块使用片内定时器TCA0产生PWM波来控制加热功率,软件每次复位时会自动将加热功率调节为1% ,防止温度过热(加热模块5V供电的情况下实测加热功率调节到25%以上时温度会迅速飙升至120度以上,这已经超出了温度计量程)。加热功率在“功率调节”页面内调节,旋转编码器顺时针旋转时增加功率,逆时针旋转时则减小,按下旋转编码器按钮确认功率并自动退出页面,如果不确定直接按下返回按键则功率保持之前的数值不变。
3、温度检测模块
温度监测模块使用片内硬件IIC直接读取温度。温度监测芯片NST112可配置高低温警报,超出温度范围是INT脚产生中断信号,检测到报警变化后还需判断当前温度是高温报警还是低温报警,然后点亮相应的LED灯,高温亮红灯、低温亮蓝灯,正常时则亮绿灯。
4、按键与编码器响应模块
IO扩展板上的按键与编码器响应模块搭建了一个电阻网络,在按下不同按键或者转动编码器的时候检测点的电压会有不同的变化,软件上只需使用片内ADC检测当前ADC转化值的大小变化即可响应相应动作。
5、摇杆响应模块
IO扩展板上使用了一颗四运放芯片搭建了一个PWM波生成电路,摇杆的x轴与y轴的阻值变化分影响PWM波的周期与占空比,软件上使用GPIO中断配合定时器实时计算当前PWM波的周期与占空比即可响应摇杆动作。
四、主要模块代码说明
1、TFT显示模块
切换的时候会有一个单箭头变双箭头的动画效果,用下边这个函数即可完成
void ShowArrow(uint8_t sta)
{
switch(sta)
{
case ARROWINIT://init
LCD_ShowBMP(0,54, WHITE, BLACK,gImage_arrow_left);
LCD_ShowBMP(105,54, WHITE, BLACK,gImage_arrow_right);
break;
case ARROWLEFT:
LCD_ShowBMP(0,54, WHITE, BLACK,gImage_arrow_left);
break;
case ARROWRIGHT:
LCD_ShowBMP(105,54, WHITE, BLACK,gImage_arrow_right);
break;
case ARROWDOUBLELEFT:
LCD_ShowBMP(0,54, WHITE, BLACK,gImage_arrow_doubleleft);
break;
case ARROWDOUBLERIGHT:
LCD_ShowBMP(105,54, WHITE, BLACK,gImage_arrow_doubleright);
break;
default:break;
}
}
菜单切换的时候则使用下面这个函数
void ShowMenu(uint8_t index)
{
switch(index)
{
case 0:
LCD_ShowBMP(23,23, GREEN, BLACK,gImage_data);
LCD_ShowZh16(32,104,zhTempData,4, YELLOW, BLACK);
break;
case 1:
LCD_ShowBMP(23,23, RED, BLACK,gImage_heat);
LCD_ShowZh16(32,104,zhHeatPower,4, YELLOW, BLACK);
break;
case 2:
LCD_ShowBMP(23,23, YELLOW, BLACK,gImage_set);
LCD_ShowZh16(48,104,zhSet,2, YELLOW, BLACK);
break;
case 3:
LCD_ShowBMP(23,23, YELLOW, BLACK, gImage_warning);
LCD_ShowZh16(32,104,zhSysinfo,4, YELLOW, BLACK);
break;
case 4:
LCD_ShowBMP(23,23, GREEN, BLACK,gImage_view);
LCD_ShowZh16(48,104,zhPicture,2, YELLOW, BLACK);
break;
case 5:
LCD_ShowBMP(23,23, GREEN, BLACK,gImage_smile);
LCD_ShowZh16(48,104,zhExm,2, YELLOW, BLACK);
break;
default:break;
}
}
main()
{
init();
while(1)
{
//do something
if(flag.menuChanged==1)
{
flag.menuChanged=0;
LCD_Fill(0, 0, LCD_W, LCD_H, BLACK);
ShowArrow(ARROWINIT);
ShowMenu(g_page);
}
//do anything
}
}
进入菜单后需要对各个当前菜单项进行初始化与数据刷新,初始化只需在切换时执行一次,数据刷新需要在主循环中周期调用
void LCD_polt_init(uint16_t c)
{
LCD_DrawLine(8,0,8,119,c);//y轴
LCD_DrawLine(8,119,127,119,c);//x轴
LCD_DrawLine(2,6,8,0,c);
LCD_DrawLine(8,0,15,7,c);//y轴箭头
LCD_DrawLine(122,114,127,119,c);
LCD_DrawLine(122,124,127,119,c);//x轴箭头
for(u8 i=1;i<8;i++)
{
LCD_DrawLine(8,15*i,15,15*i,c);//y轴刻度
LCD_DrawLine(8+15*i,113,8+15*i,119,c);//x轴刻度
}
LCD_ShowStrx6y8(0,120,"0",c,BLACK);//0
LCD_ShowStrx6y8(15,120,"15",c,BLACK);//x 15
LCD_ShowStrx6y8(30,120,"30",c,BLACK);//30
LCD_ShowStrx6y8(45,120,"45",c,BLACK);//45
LCD_ShowStrx6y8(60,120,"60",c,BLACK);//60
LCD_ShowStrx6y8(75,120,"75",c,BLACK);//75
LCD_ShowStrx6y8(90,120,"90",c,BLACK);//90
LCD_ShowStrx6y8(105,120,"105",c,BLACK);//105
LCD_ShowStrx8y6(0,7,"105",c,BLACK);//y 15
LCD_ShowStrx8y6(0,24,"90",c,BLACK);//30
LCD_ShowStrx8y6(0,39,"75",c,BLACK);//45
LCD_ShowStrx8y6(0,54,"60",c,BLACK);//60
LCD_ShowStrx8y6(0,69,"45",c,BLACK);//75
LCD_ShowStrx8y6(0,84,"30",c,BLACK);//90
LCD_ShowStrx8y6(0,99,"15",c,BLACK);//105
}
void plot_refresh(uint8_t t)
{
if(data_len<120)
{
LCD_ShowIntNum(64, 0, t, 3, RED, BLACK, 16);
data_fifo[data_len] = t;
LCD_DrawPoint(8+data_len,120-t,RED);
data_len++;
}
//TO DO : 超过120后自动左移刷新
}
void Page0Init(void)
{
data_len=0;
LCD_polt_init(WHITE);
LCD_ShowBMP(112,0, YELLOW, BLACK,gImage_setting);
LCD_ShowBMP(112,16, YELLOW, BLACK,gImage_cundo);
LCD_DrawRectangle(112,0,127,16,GREEN);
LCD_DrawRectangle(112,16,127,32,GREEN);
LCD_ShowString(40, 0, "T=:", RED, BLACK, 16, 0);
LCD_ShowString(40, 16, "P=:", RED, BLACK, 16, 0);
LCD_ShowIntNum(64, 16, heat_power, 2, RED, BLACK, 16);
}
void Page1Init(void)
{
LCD_ShowBMP(112,96, YELLOW, BLACK,gImage_setting);
LCD_ShowBMP(112,112, YELLOW, BLACK,gImage_cundo);
LCD_DrawRectangle(112,96,127,111,GREEN);
LCD_DrawRectangle(112,112,127,127,GREEN);
LCD_ShowZh16(0,0,zhHeatPower,4, WHITE, BLACK);
LCD_ShowString(64, 0, ": %", WHITE, BLACK, 16, 0);
ProgressUpdate(heat_power);
}
void ProgressUpdate(uint8_t p)
{
if(p>99)return;
LCD_ShowIntNum(72, 0, p, 2, RED, BLACK, 16);
LCD_Fill(10, 30, 110, 40, BLACK);
LCD_Fill(10, 30, 10+p, 40, RED);
LCD_DrawRectangle(10,30,110,40,RED);
}
void Page3Init(void)
{
LCD_ShowString(0, 0, "TEMP:", RED, BLACK, 16, 0);
LCD_ShowString(0, 20, "T_Hi:", RED, BLACK, 16, 0);
LCD_ShowString(0, 40, "T_Lo:", RED, BLACK, 16, 0);
LCD_ShowString(0, 60, "Freq:", RED, BLACK, 16, 0);
LCD_ShowString(0, 80, "Duty:", RED, BLACK, 16, 0);
LCD_ShowString(0, 100, "ADC:", GREEN, BLACK, 16, 0);
LCD_ShowBMP(112,96, YELLOW, BLACK,gImage_setting);
LCD_ShowBMP(112,112, YELLOW, BLACK,gImage_cundo);
LCD_DrawRectangle(112,96,127,111,GREEN);
LCD_DrawRectangle(112,112,127,127,GREEN);
}
void Page4Init(void)
{
LCD_ShowZh16(0,0,zhShot,2, BLACK, WHITE);
LCD_ShowString(32, 0, "1.BMP ", BLACK, WHITE, 16, 0);
LCD_ShowZh16(0,16,zhNice,2, YELLOW, BLACK);
LCD_ShowString(32, 16, ".avi", YELLOW, BLACK, 16, 0);
LCD_ShowString(0, 32, "smile.jpg", YELLOW, BLACK, 16, 0);
LCD_ShowBMP(112,96, YELLOW, BLACK,gImage_setting);
LCD_ShowBMP(112,112, YELLOW, BLACK,gImage_cundo);
LCD_DrawRectangle(112,96,127,111,GREEN);
LCD_DrawRectangle(112,112,127,127,GREEN);
}
void PageDefaultInit(void)
{
LCD_ShowBMP(112,96, YELLOW, BLACK,gImage_setting);
LCD_ShowBMP(112,112, YELLOW, BLACK,gImage_cundo);
LCD_DrawRectangle(112,96,127,111,GREEN);
LCD_DrawRectangle(112,112,127,127,GREEN);
}
void Page2Refresh(void)
{
//uint8_t t=0;
//LCD_ShowIntNum(40, 20, time_hi, 5, RED, BLACK, 16);
//LCD_ShowIntNum(40, 40, time_lo, 5, RED, BLACK, 16);
//LCD_ShowIntNum(40, 60, 1000000/(time_hi+time_lo), 4, YELLOW, BLACK, 16);
//LCD_ShowIntNum(40, 80, time_hi*100/(time_hi+time_lo), 4, YELLOW, BLACK, 16);
//if(GetTemperature(&t))
//LCD_ShowFloatNum1(40, 0, t, 4, RED, BLACK, 16);
//LCD_ShowIntNum(32, 100, ADC0_GetConversion(ADC_MUXPOS_AIN1_gc)>>4, 4, GREEN, BLACK, 16);
}
void ShowPageInit(uint8_t page)
{
LCD_Fill(0, 0, 128, 128, BLACK);
switch(page)
{
case 0:
Page0Init();
break;
case 1:
Page1Init();
break;
case 3:
Page3Init();
break;
case 4:
Page4Init();
break;
default:
PageDefaultInit();
break;
}
}
截图功能由一下几个函数完成
void data_save(void)//保存数据
{
while(EEPROM_IsBusy());
EEPROM_Write(0x1400,data_len);
for(uint8_t i=0;i<data_len;i++)
{
while(EEPROM_IsBusy());
EEPROM_Write(0x1401+i,data_fifo[i]);
}
}
void ShowData(void)//相册--截图展示
{
LCD_Fill(0, 0, 128, 128, BLACK);
LCD_polt_init(WHITE);
LCD_ShowBMP(112,0, YELLOW, BLACK,gImage_setting);
LCD_ShowBMP(112,16, YELLOW, BLACK,gImage_cundo);
LCD_DrawRectangle(112,0,127,16,GREEN);
LCD_DrawRectangle(112,16,127,32,GREEN);
uint8_t len = EEPROM_Read(0x1400);
if(len>120)
{
printf("eeprom error!\r\n");
return;
}
uint8_t data = 0;
for(uint8_t i=0;i<len;i++)
{
data = EEPROM_Read(0x1401+i);
LCD_DrawPoint(8+i,120-data,RED);
}
}
void ShowSaveOK(void)//截图成功
{
LCD_Fill(28, 28, 100, 100, BLACK);
LCD_DrawRectangle(28,28,100,100,GREEN);
LCD_ShowBMP(40,36, GREEN, BLACK,gImage_check);
LCD_ShowZh16(32,84,zhSuccess,4, GREEN, BLACK);
}
2、加热模块
加热模块比较简单,定时器配置好后只需按设定调节占空比即可
void set_heat_power(uint8_t p)//2399为满占空比时,实测335以上就加热速度飙升
{
if(p>100)return;
TCA0.SINGLE.CMP2 = 600*p/100;
}
3、温度检测模块
温度检测也比较简单,配置好后直接从芯片读取寄存器0即可
bool GetTemperature(float* temperature)
{
bool ret=false;
uint8_t tx=0;
uint8_t rx[2]={0};
uint16_t temp=0;
// ret = TWI0_Write(ADDR_NST112,&tx,1);
// do
// {
// TWI0_Tasks();
// }while(TWI0_IsBusy());
// if(!ret)
// {
// printf("write failed!!\r\n");
// return ret;
// }
// ret = TWI0_Read(ADDR_NST112,rx,2);
// do
// {
// TWI0_Tasks();
// }while(TWI0_IsBusy());
// if(!ret)
// {
// printf("read failed!!\r\n");
// return ret;
// }
ret = TWI0_WriteRead(ADDR_NST112,&tx,1,rx,2);
do
{
TWI0_Tasks();
}while(TWI0_IsBusy());
if(ret)
{
temp = ((rx[0]<<8)|rx[1])>>4;
*temperature = 0.0625 * temp;
}
return ret;
}
4、按键与编码器响应模块
按键与编码器动作检测需要检测ADC转换值的变化来得出,主要是编码器的正转与反转区分需要思考一下,因为编码器输出的两路正交周期相同占空比50%的方波,如果直接使用GPIO中断来检测编码器旋转的话只需判断沿变化时另一相的高低电平即可。使用ADC的话需要判断转换值的顺序
#define AllRelease 0
#define Key1Pressed 1
#define Key2Pressed 2
#define Key3Pressed 3
#define Key_CD 4//顺时针
#define Key_ACD 5//逆时针
#define AllRelease_Value 3935
#define Key1Pressed_Value 1915
#define Key2Pressed_Value 2915
#define Key3Pressed_Value 3424
#define Key_CD_Value 3813 //顺时针
#define Key_ACD_Value 3680 //逆时针
#define Key_Both_Value 3550
uint16_t keyValueList[7]={ AllRelease_Value,
Key1Pressed_Value,
Key2Pressed_Value,
Key3Pressed_Value,
Key_CD_Value,
Key_ACD_Value,
Key_Both_Value};
uint8_t KeyScan(void)
{
static bool pressed = false;
static uint8_t t=0;
uint8_t keyValue=0;
static uint8_t keyValueLeast=0;
uint16_t adcVal=0;
adcVal = ADC0_GetConversion(ADC_MUXPOS_AIN1_gc) >> 4;
for(keyValue=0;keyValue<7;keyValue++)
{
if(adcVal>keyValueList[keyValue]-DIFF && adcVal<keyValueList[keyValue]+DIFF)
break;
}
if(keyValue==7)keyValue=0;
if(keyValue>0)t++;
else
{
pressed = false;//标记为未按下
t=0;
}
if(t>20 && (!pressed))//消抖
{
pressed = true;//标记为已按下
if(keyValue==4||keyValue==5)//编码器方向判断 顺时针键值为0->4->6-5, 逆时针为0->5->6>4
{
if(keyValueLeast==6)keyValue=0;
}
//其他情况键值不变
}
else keyValue = 0;
keyValueLeast = keyValue;
return keyValue==6?0:keyValue;
}
void KeyPro(uint8_t keyValue)
{
static uint8_t power=0;
switch(keyValue)
{
case AllRelease:
break;
case Key1Pressed://cundo
if(flag.pageEnter==1)
{
LED1_SetHigh();
LED3_SetHigh();
LED2_SetHigh();//关灯
flag.menuChanged=1;
flag.pageEnter=0;
}
break;
case Key2Pressed://setting
break;
case Key3Pressed://确认按键
if(flag.pageEnter==0)//确认进入菜单
{
ShowPageInit(g_page);
if(g_page==1)power=heat_power;
flag.pageEnter=1;
}
else if(g_page==1)//加热功率修改确认
{
heat_power = power;
set_heat_power(heat_power);
flag.menuChanged=1;
flag.pageEnter=0;
}
else if(g_page==0)//截图功能
{
data_save();
ShowSaveOK();
flag.shotSuccess = 1;
}
else if(g_page==4)//显示截图
{
ShowData();
}
break;
case Key_CD://顺时针
if(flag.pageEnter==1)
{
if(g_page==1)
{
power++;
if(power>99)power=99;
ProgressUpdate(power);
}
}
break;
case Key_ACD://逆时针
if(flag.pageEnter==1)
{
if(g_page==1)
{
power--;
if(power<1)power=1;
ProgressUpdate(power);
}
}
break;
default:break;
}
}
5、摇杆响应模块
摇杆检测也比较简单,使用一个GPIO中断和定时器计时实时计算输入PWM的频率和占空比,x轴改变频率,y轴改变占空比,先分别测量出两个零位值和四个极值,再根据两个中位值判断摇杆操作即可
#define Freq_Default 302//234~435
#define Freq_Left 268
#define Freq_Right 368
#define Duty_Default 57//36~79
#define Duty_Up 46
#define Duty_Down 68
void JoyScanX(uint16_t freq)//页切换
{
if(freq<Freq_Left&&flag.menuChanging==0)
{
flag.menuChanging = 1;
if(g_page==0)g_page=MAXPAGE;
g_page--;
ShowArrow(ARROWDOUBLELEFT);
}
else if(freq>Freq_Right&&flag.menuChanging==0)
{
flag.menuChanging = 1;
g_page++;
if(g_page>=MAXPAGE)g_page=0;
ShowArrow(ARROWDOUBLERIGHT);
}
else if(freq>Freq_Left&&freq<Freq_Right&&flag.menuChanging==1)
{
flag.menuChanging = 0;
flag.menuChanged = 1;
ShowArrow(ARROWINIT);
}
}
void JoyScanY(uint8_t duty)//行切换
{
//TO DO
}
五、主要功能展示
1、各个菜单页面展示
2、进入温升曲线菜单,点击确认键(旋转编码器按键)截图当前页面,截图保存后可在相册菜单内查看,掉电保存。
3、进入功率调节菜单,转动编码器调节功率大小,按下确认键加载当前功率值并自动退出,直接返回则保持进入菜单之前的功率。
4、进入相册菜单,上下行切换暂未实装,点击确认显示截图。
六、总结
总的来说本项目还是比较简单的,前后花的时间不多,笔者相对而言会更喜欢GUI相关的部分,所以多做了一些花样。AVR64DD32虽然是8位机,但是性能表现却意外的不错,开发体验也还可以,配合Microchip官方IDE MPLAB开发,大概只用了两个晚上就把驱动弄好了,唯一值得吐槽的就是内置的MCC插件启动实在是太卡了,启动慢,稍微多点一下鼠标IDE就直接卡死无响应了,只能任务管理器关闭然后重启软件。另外开发过程中比较有意思的就是硬禾学堂出品的IO扩展板了,明明可以直接检测编码器的正交的方波来检测正转与反转的,他缺硬是放到电阻网络里使用ADC来检测,明明可以直接使用ADC来检测摇杆xy轴的阻值变化的缺非要使用运放搭个方波生成电路来检测。