Funpack第二季第四期:AVR64DD32 Curiosity Nano开发板
这次购买的是【AVR64DD32核心板+扩展板】套餐,因此选择任务2 - 实现一个恒温自动控制系统:
IO扩展板上有一处加温电阻,将加热区域用物体(纸巾等)包裹起来,通过电流给电阻加热,并通过温度传感器感知板上温度的变化,测温以及在LCD屏上的温度显示。
一、产品介绍
1.AVR64DD32
Microchip Technology AVR64DD32 Curiosity Nano评估套件设计用于评估AVR® DD微控制器。
AVR64DD32 将提供 TQFP 和 VQFN 封装选项,运行速度最高 至 24 MHz,并具有 64 KB 闪存、8 KB SRAM 和 256B EEPROM。
AVR64DD32 产品支持四个多电压 I/O 通道,能够双向 与在高于或低于以下电压下运行的外部设备的通信 MCU 本身。MVIO 系统使 AVR® DD 成为电路板的完美器件 控制、传感器应用、物联网终端节点和其他尺寸受限 应用,以及大型系统中的传感器融合。
2.板卡特点:
- AVR64DD32微控制器
- 一个黄色用户LED
- 一个机械式用户开关
- 一个32.768kHz晶体
- 一个24MHz晶体
- 板载调试器:
- 一个绿色电源及状态指示LED
- 编程和调试
- 虚拟串行端口 (CDC)
- 两个调试GPIO通道 (DGI GPIO)
- USB供电
- 可调目标电压:
- MIC5353 LDO稳压器,由板载调试器控制
- 输出电压范围:1.8V至5.1V(受USB输入电压限制)
- 最大输出电流:500mA(受环境温度和输出电压限制)
3.输入、输出扩展板介绍:
本扩展板包含如下功能:
- 按键、旋转编码器输入 - 以模拟信号的方式
- 双电位计控制输入 - 以数字信号的方式
- RGB三色LED显示
- 1.44寸128*128 LCD,SPI总线访问
- MMA7660三轴姿态传感器
- 电阻加热
- 温度传感器
但由于该扩展板并未设计与AVR64DD32板的接口,故使用杜邦线进行两者之间的连接。
AVR64 扩展板 说明
VBUS——5V 5v电源,用于加热部分供电
VTG——3.3V 3.3v电源,用于显示屏及温度传感器等部分供电
GND——GND 电源地
PA2—— I2C_SDA IIC数据线,接温度传感器
PA3——I2C_SCL IIC时钟线,接温度传感器
PA4——LCD_SDA SPI数据线,接显示屏
PA6——LCD_SCL SPI时钟线,接显示屏
PA7——LCD_CSn SPI片段线,接显示屏
PC2——LCD_DCx SPI数据/命令选择线,接显示屏
PC3——LCD_RESn SPI复位线,接显示屏
PD1——A_OUT ADC通道,接电阻网络分压按键、编码器
PD6——LED3 控制红灯的状态
PD7——V_HEAT PWM输出控制加热部分
二、设计思路
正如扩展板的原理图所示的加热-->控温:首先控制MOS管导通,使得电阻流过电流产生热量,进而使电路板升温,再使用温度传感器将温度值读出,根据当前的温度值不断地进行反馈控制MOS管通断,使电路板温度保持在设定值范围。
在扩展板上,加热部分为4个68Ω电阻并联,其最大加热功率P=U²/R = 5*5/(68//68//68//68) ≈ 1.47W。
根据如上所示的分析,计划出软件流程框架:
三、开发环境准备
1.Platform IO with Visual Studio Code,用于编写代码及下载程序。
2.ComAssistant,用于串口调试
四、项目任务的代码实现
本次项目使用Arduino框架开发,Arduino上手容易,简单快捷。
综合项目的要求,首先确定项目的具体实现需求:
1.读取温度传感器
2.驱动屏幕进行显示
3.产生PWM波形驱动MOS管导通,通过电流给电阻加热
4.解析编码器/按键,进行相应的设置
首先是温度传感器,型号为NST112-DSTR,这是一款超小封装的高精度低功耗温度传感器,通过I2C接口数字进行访问以获取温度。该传感器的用法也较为简单,只需配置两个寄存器即可。
#ifndef NST112_H
#define NST112_H
#include "Arduino.h"
#include "Wire.h"//IIC库
#define NST112_ADDRESS 0x48 //温度传感器地址
#define GET_ADDR 0X00//读寄存器
#define SET_ADDR 0X01//写寄存器
#define VAL_BASE 0.0625//基准读数
#define NUM_BIT 12//数据位数
// #define CFG_CMD_0 0b01100000//数据高
// #define CFG_CMD_1 0b10100000//数据低
#define CFG_CMD_0 0b01100000//数据高
#define CFG_CMD_1 0b11100000//数据低//8HZ
void NST112_Init()
{
Wire.begin();//默认引脚加入
Wire.beginTransmission(NST112_ADDRESS);
Wire.write(SET_ADDR);//进入设置寄存器
Wire.write(CFG_CMD_0);//写寄存器1
Wire.write(CFG_CMD_1 | (NUM_BIT==13?(1<<4):0));//写寄存器2
Wire.endTransmission();//结束
}
float NST112_Read()
{
byte MSB;
byte LSB;
uint16_t value;
float temp;
Wire.beginTransmission(NST112_ADDRESS);
Wire.write(GET_ADDR);//进入温度寄存器
Wire.endTransmission();//结束
Wire.requestFrom(NST112_ADDRESS,2);
while(Wire.available())
{
MSB = Wire.read();
LSB = Wire.read();
value = ((MSB<<8)|LSB) >>(NUM_BIT==13?3:4);
temp = ((float)value)*VAL_BASE;
}
return temp ;
}
#endif
屏幕的驱动IC是ST7735,1.44寸128*128分辨率 ,使用SPI接口驱动屏幕进行显示,为了方便,选用了Adafruit_GFX驱动库,在屏幕上显示出设定温度、当前温度、PWM占空比及加热状态。还绘制了一个模拟仪表盘,这样可以更为直观的看到温度的变化。
由于Adafruit_GFX驱动库对于Float数据显示有问题,故让其扩大10倍,以消除小数点的存在,在屏幕显示时,为了方便观察,再将小数点添加到数字下方。
sprintf(Serial_buff, "%3d,%3d", (int)(Temp_now*10),(int)(Temp_target*10));
tft.setCursor(0, 85);
tft.print(Serial_buff);
产生PWM波形则使用的是AVR64DD32微控制器芯片的定时器功能,产生 PWM 信号用于其他设备的控制。在Arduino使用起来也是非常方便的。只需要确定输出引脚及占空比数值,使用analogWrite()函数即可。
pinMode(PWM_Pin, OUTPUT);
analogWrite(PWM_Pin, (uint16_t)Temp_OUT_PWM);
当PWM波形为高电平时,驱动MOS管开启,此时电阻流过电流开始发热,当PWM波形为低电平时,驱动MOS管关闭,此时电阻没有电流不发热。这样,通过控制PWM波形的占空比,就可以控制电阻的平均功率,进而控制发热温度。
在这个过程中,使用经久不衰的PID算法,计算出在设定温度下,相应的PWM波形占空比,并且随着实际温度自动进行调整,最终使其达到设定的温度。
在PID算法中,还采用了积分分离的方式,防止温度超调,但是在最终的测试中,由于温控系统惯性环节较大,仅使用PD两项的系数就能达到较好的目标。
//单闭环位置PID//pid结构—目标-实际
float SinglePID_Position(Position_PID *pid, float Target, float Measure)
{
float Err; //误差
if (pid == NULL)
{
return 0.0;
}
Err = Target - Measure; //目标值减实际值
/* 积分分离 */
if (abs(Err) > (pid->Integraldead_zone)) //积分绝对值大于 积分盲区
{
pid->index = 0; //积分指数赋值0
}
else
{
pid->index = 1; //积分指数赋值1
}
pid->Output = pid->Kp * Err + pid->Kd * (Err - pid->Last_Err); //pd输出
pid->Integral += pid->Ki * Err * pid->index; //积分累加
constrain(pid->Output, pid->OutputMin, pid->OutputMax);
pid->Integral = constrain(pid->Integral, pid->I_outputMin, pid->I_outputMax); ////积分限幅
pid->Output += pid->Integral; //pid完整的输出
pid->Output = constrain(pid->Output, pid->OutputMin, pid->OutputMax); ////输出限幅
pid->Last_Err = Err; //保存上一次控制量
return pid->Output; //返回pid控制量
}
在该套控制系统中,由于温度传感器自动更新的最大频率为8Hz,为保证系统的一致性,所以PID程序的执行频率也应为8Hz,因此编写一个简易的定时任务,每隔125ms运行一次即可,包含温度读取,PID计算,更新占空比,刷新屏幕显示。
if ((millis() > Time_timeout) || (Time_timeout < 125))
{
Time_tick++;
Temp_now = NST112_Read();
Temp_OUT_PWM += SinglePID_Position(&Position_PID_Temp, Temp_target, Temp_now);
Temp_OUT_PWM = constrain(Temp_OUT_PWM, 0, 255);
tft.setCursor(0, 0);
if (Heating == 0)
{
Temp_OUT_PWM = 0;
digitalWrite(LED_R_Pin, 1);
tft.setCursor(90, 86);
tft.print("Off");
}
else
{
if (abs(Temp_now - Temp_target) < 1.0F)
{
digitalWrite(LED_R_Pin, 0);
tft.setCursor(90, 86);
tft.print(" Ok ");
}
else
{
digitalWrite(LED_R_Pin, 1);
tft.setCursor(90, 86);
tft.print(" On");
}
}
analogWrite(PWM_Pin, (uint16_t)Temp_OUT_PWM);
Time_timeout = millis() + Temp_Timeout; //加上下一次的刷新毫秒输
mySerial.print("{T}"); //当前温度
mySerial.println(Temp_now);
mySerial.print("{Tt}"); //目标温度
mySerial.println(Temp_target);
mySerial.print("{PWM}"); //当前PWM值
mySerial.println(Temp_OUT_PWM);
}
最后是编码器、按键的部分,由于扩展板电路设计比较特殊,按键和编码器使用的是R-2R电阻网络分压的方案,因此使用ADC读取相应引脚的数值,即可判断出是某个按键按下,但是编码器比较特殊,硬件电路将编码器的AB相及中央按键均接入了该电阻网络,单次的读取并不能体现是否旋转,需要高频率地进行ADC采样,根据结果进行判断正/反转
下图为ADC采集高频采样得到的数据。
图1为顺时针旋转,图2为逆时针旋转,图三分别为编码器中央按键、普通按键上、普通按键下
可以看出来编码器在进行不同方向的旋转时,ADC检测到的电压台阶顺序是不一样的,因此根据这个顺序,在符合电压最低处时判断上一台阶的电压值,即可分析出是哪个方向的旋转。
事先使用手动方式测量出各个操作ADC的测量值,再根据此值判断即可
uint16_t ADC_Val[7] = {3961,3832 , 3700, 3573, 3446, 2935, 1923};
#define ADC_error 50 //ADC的上下范围
//编码器变量
int8_t Encoder_Direction = (-1);
int8_t KEY_Encoder = true; //编码器按键
int8_t KEY_Key1 = true; //上按键
int8_t KEY_Key2 = true; //下按键
uint8_t Encoder_Val_last = 0;
float Temp_OUT_PWM = 0.0f;
//设定温度变量
float Temp_target = 45.0f;
float Temp_now = 0.0f;
float Add_Val = 0.1; ///温度一次变更的值
int8_t Heating = 0; //是否在加热
//按键变量
typedef struct
{
bool val;
bool last_val;
} KEY;
KEY key[6] = {false};
//按键信息
typedef struct
{
int8_t id; //按键的号码
bool pressed; //是否按下
} KEY_MSG;
KEY_MSG key_msg = {0};
//得到按键的值
void key_scan()
{
ADC_Val0 = analogRead(ADC_Pin);
// mySerial.print(millis());
mySerial.print("{A}");
mySerial.println(ADC_Val0);
// mySerial.print("{K}");
// mySerial.println(key_msg.id);
if ((ADC_Val0 > (ADC_Val[0] - ADC_error)) && (ADC_Val0 < (ADC_Val[0] + ADC_error))) //未按下
{
for (uint8_t i = 0; i < (sizeof(key) / sizeof(KEY)); ++i)
{
key[i].val = 1;
}
}
else if ((ADC_Val0 > (ADC_Val[1] - ADC_error)) && (ADC_Val0 < (ADC_Val[1] + ADC_error))) //旋转1
{
key[0].val = 0;
}
else if ((ADC_Val0 > (ADC_Val[2] - ADC_error)) && (ADC_Val0 < (ADC_Val[2] + ADC_error))) //旋转2
{
key[1].val = 0;
}
else if ((ADC_Val0 > (ADC_Val[3] - ADC_error)) && (ADC_Val0 < (ADC_Val[3] + ADC_error))) //旋转3
{
key[2].val = 0;
}
else if ((ADC_Val0 > (ADC_Val[4] - ADC_error)) && (ADC_Val0 < (ADC_Val[4] + ADC_error))) //编码器按键
{
key[3].val = 0;
}
else if ((ADC_Val0 > (ADC_Val[5] - ADC_error)) && (ADC_Val0 < (ADC_Val[5] + ADC_error))) //普通按键1
{
key[4].val = 0;
}
else if ((ADC_Val0 > (ADC_Val[6] - ADC_error)) && (ADC_Val0 < (ADC_Val[6] + ADC_error))) //普通按键1
{
key[5].val = 0;
}
else
{
}
for (uint8_t i = 0; i < (sizeof(key) / sizeof(KEY)); ++i) //变更状态
{
if (key[i].last_val != key[i].val) //发生改变
{
key[i].last_val = key[i].val; //更新状态
if (key[i].val == LOW)
{
key_msg.id = i;
// key_msg.pressed = true;
}
}
}
if ((key_msg.id == 0) || (key_msg.id == 1) || (key_msg.id == 2))
{
if ((key_msg.id == 1)) //转到中间1的时候,判断上一次的值
{
switch (Encoder_Val_last)
{
case 2: //右转
Encoder_Direction = 1;
Temp_target += Add_Val;
break;
case 0: //左转
Encoder_Direction = 0;
Temp_target -= Add_Val;
break;
default:
break;
}
}
Encoder_Val_last = key_msg.id;
key_msg.id = (-1);
}
}
void KEY_update()
{
if (key_msg.id != (-1))
{
if ((key_msg.id == 0) || (key_msg.id == 1) || (key_msg.id == 2))
{
if ((key_msg.id == 1)) //转到中间1的时候,判断上一次的值
{
switch (Encoder_Val_last)
{
case 0: //右转
Encoder_Direction = 1;
// Temp_target += Add_Val;
break;
case 2: //左转
Encoder_Direction = -1;
// Temp_target -= Add_Val;
break;
default:
break;
}
}
}
else if (key_msg.id == 3) //编码器按键
{
KEY_Encoder = false;
}
else if (key_msg.id == 4) //上按键
{
KEY_Key1 = false; //被按下为0
}
else if (key_msg.id == 5) //下按键
{
KEY_Key2 = false; //被按下为0
}
Encoder_Val_last = key_msg.id;
key_msg.id = (-1);
}
}
五、功能展示
按下编码器中央按键时,切换开启/关闭加热,按下普通按键上时,切换需要设置的温度位数(10、1、0.1),顺时针旋转编码器增加设定值,逆时针旋转编码器减少设定值。
当温度达到设定温度的±1℃时(原先要求的是±3℃,这里稍微提高了一下控制精度),亮起红灯,并在屏幕上显示“OK”字样。
六、遇到的主要难题及解决方法
感觉该项目的主要难点就在编码器/按键方面,通过ADC读值的方式确定按键的状态,这样的好处也很明显,就是只用一个GPIO的端口即可完成多个按键的读取;,但是缺点就是需要高频率地使用ADC去读取模拟电压值,消耗时间,再加上AVR64DD32微控制器本身频率就不高,还要进行彩屏的驱动。在寻找屏幕可用的驱动库时发现,由于Arduino库太久未更新,也存在一些问题。再有就是关于PID调节的部分,由于温控系统的大惯性环节,当PWM占空比升高后,温度上升并没有那么迅速,因此PID中的微分系数需要比较大,才能使得系统稳定,且使用analogWrite()函数,占空比只能为8位(0-255),对温控的精确度方面有着不小的阻碍。
七、后续完善计划
作为一个基本能够使用的恒温自动控制系统,已基本满足要求,但是还是有许多不足之处可以完善,比如在界面不够精美,按键交互不太完善,抗干扰性能不强等,由于时间关系暂未解决,后续的话有时间再抽空完善一下,这样总体的感觉也会提升。
八、参考资料
1.Arduino https://www.arduino.cc/
2.使用入门https://blog.csdn.net/honestqiao/article/details/129635075
3.PlatformIO https://platformio.org/
4.AVR64DD32https://www.microchip.com/en-us/product/AVR64DD32
5.高精度、低功耗数字温度传感器NST112 https://www.novosns.com/news-center-57
6.积分分离PID控制算法https://blog.csdn.net/songyulong8888/article/details/117389191