一、项目介绍
本项目为参加Funpack2-4活动基于AVR64DD32和搭配数字系统的输入、输出扩展板实现的的温度采集与恒温控制系统。本项目使用到的扩展板上的资源为1.44寸ST7735屏幕、NST112温度传感器、SI2302 N型MOS管、RGB LED灯以及模拟输入的三个按键。
项目任务: 任务1 - 实现一个加热和温度采集系统
·IO扩展板上有一处加温电阻,通过电流给电阻加热,并通过温度传感器感知板上温度的变化,将测量到的温度信息显示在LCD屏幕上,绘制一个1分钟的温升曲线。并且每隔1分钟改变加热电阻的占空比,重复温度测量和绘制的过程。
·板上有一处RGB彩灯,当温度超过50°C时转为红色,低于20°C时转为蓝色,正常状态下为绿色。 要求:按下按键时,截图当前的温升曲线。(注意,加热电阻满占空比开启后温度较高)
任务2 - 实现一个恒温自动控制系统
·IO扩展板上有一处加温电阻,将加热区域用物体(纸巾等)包裹起来,通过电流给电阻加热,并通过温度传感器感知板上温度的变化,测温以及在LCD屏上的温度显示。
要求:使用按键设定目标温度,并且通过程序控制加热功率,使得温度尽快尽量稳定的维持在目标温度。温度偏离设定温度±3°C彩灯变为红色。(注意,加热电阻满占空比开启后温度较高)
二、硬件电路分析
1.按键输入的处理
如上图所示,在这块扩展板上面的按键输入并不是采用常见的数字高低电平表示,应该是因为本身的扩展板需要连接ESP32_IF,IO数量不太够。我使用到了右边的两个弹片按键和旋转编码器的按键(没使用到旋转编码器的旋转输入),这里主要就是不同按键按下时,A_Out位置的电位发生变化,为了省事我直接测量了不同状态时的电位(其实是懒的分析电路)。 按键状态如下:
按键状态 | 电位电压 |
无按键按下 | 3.18V |
上键按下 | 2.35V |
下键按下 | 1.54V |
编码器按键按下 | 2.76V |
2.加热电路分析
如上图所示,这块扩展板上R29焊上了,R30没有焊。使用的是5V电源对板子加热,该MOS管为N型,我们在使用时基本工作在截至区或饱和区,所以,我们只需要把他当作开关就行了。当V_HEAT信号为高电平时,MOS管导通,V_HEAT信号为低电平时关闭。但我感觉这块电路有点小问题,因为我在下载程序时单片机引脚默认是高电平,会让MOS管导通。感觉可以给MOS管的栅极加个下拉电阻会更好些。
3.屏幕与温度传感器
原理图中的屏幕没有做背光可调电路,应该也是出于节约引脚的考虑。
扩展板上使用的温度传感器NST112已经高度集成了,我们只需要通过I2C通讯协议配置模式和读取温度就行了,值得一提的是该传感器芯片还有个Alert引脚用于超过设置的温度区间进行报警,但因为本次项目不需要该功能,故没有连接。
三、软件设计
对于本次的AVR64DD32我没有使用官方的开发环境(因为太过陌生,以后有时间再看看),我使用的是在VS code中的platformio环境ARDUINO框架。在此感谢乔楚大佬的环境搭建教程,链接为:FunPack4 AVR64DD32 使用入门。
1.驱动部分
因为在ARDUINO框架下适配了极多的驱动库,我便愉快的使用了Adafruit ST7735 and ST7789 Library。然后当我想再次找现成的NST112驱动库时。。。我没找到。好吧,既然没有,那咱就自己翻手册写吧,因为基础的I2C通信我可以直接使用,再加上它内部的寄存器极少,且操作很简单,我很快就搞定了(使用了默认的连续转换 12bit分辨率 4HZ采样率 ,为了方便操作,读取温度为正常温度100倍)。
一开始读到的温度始终为乱码,于是我用逻辑分析仪(上位机为pulse view),来查看我写入和读取是否正确,查看后发现符合预期。后尝试直接12位输出数字量正确,让我怀疑是不是软串口打印的问题,发现果然是软串口打印浮点数有问题,然后换成模拟量温度放大100倍,再打印整数,乱码问题解决。
驱动程序如下
//NST112.h
#ifndef __NST112_H__
#define __NST112_H__
#define NST112_ADDR 0x48 //1001000
#define NST112_temperature_Register 0x00
#define NST112_config_Register 0x01
#define NST112_temp_Low_Register 0x02
#define NST112_temp_High_Register 0x03
class NST112
{
private:
/* data */
void write(uint8_t _register, uint16_t _data);
uint16_t read(uint8_t _register);
public:
NST112(/* args */);
~NST112();
void init();
void init(uint8_t sda,uint8_t scl);
bool getTemperature(int16_t *temp);
bool set_temp_alert_high(int16_t *temph);
bool set_temp_alert_low(int16_t *templ);
bool get_temp_alert_high(int16_t *temph);
bool get_temp_alert_low(int16_t *templ);
uint16_t test();
};
#endif
//NST112.c
#include <Arduino.h>
#include <Wire.h>
#include "NST112.h"
NST112::NST112(/* args */)
{
}
NST112::~NST112()
{
}
void NST112::write(uint8_t _register, uint16_t _data)
{
uint8_t data_array[2]={_data>>8,_data&0xFF};
Wire.begin();
Wire.beginTransmission(NST112_ADDR);
Wire.write(_register);
Wire.write(data_array,2);
Wire.endTransmission();
}
uint16_t NST112::read(uint8_t _register)
{
uint16_t data_read;
uint8_t data_array[2];
Wire.begin();
Wire.beginTransmission(NST112_ADDR);
Wire.write(_register);
Wire.endTransmission();
Wire.beginTransmission(NST112_ADDR);
Wire.requestFrom(NST112_ADDR,2);
while(Wire.available())
{
Wire.readBytes(data_array,2);
}
Wire.endTransmission();
data_read=data_array[0]*256 +data_array[1];
return data_read;
}
uint16_t NST112::test()
{
uint16_t data_read;
uint8_t data_array[2];
// Wire.begin();
Wire.beginTransmission(NST112_ADDR);
Wire.write(NST112_temperature_Register);
Wire.endTransmission();
Wire.beginTransmission(NST112_ADDR);
Wire.requestFrom(NST112_ADDR,2);
while(Wire.available())
{
Wire.readBytes(data_array,2);
}
Wire.endTransmission();
data_read=data_array[0]*256 +data_array[1];
return data_read;
}
void NST112::init()
{
// Wire.begin();
write(NST112_config_Register,0b0110000010100000); //0110_0001_1100_0000 0x61C0
}
void NST112::init(uint8_t sda,uint8_t scl)
{
Wire.pins(sda,scl);
write(NST112_config_Register,0x61C0); //0110_0001_1100_0000 0x61C0
}
bool NST112::getTemperature(int16_t *temp)
{
uint16_t _data=read(NST112_temperature_Register) >>4;
float val=0;
if(_data&0x800)
{
val=(0xFFF-_data +1)*-6.25f;
}
else
{
val=6.25f*_data;
}
// val=0.0625f*_data;
*temp=int16_t(val);
return 1;
}
bool NST112::set_temp_alert_high(int16_t *temph)
{
uint8_t temp_Set[2];
uint16_t temp_data=0;
int16_t temp_val=*temph;
temp_data=temp_val / 6.25;
temp_data <<=4;
write(NST112_temp_High_Register,temp_data);
}
bool NST112::set_temp_alert_low(int16_t *templ)
{
uint8_t temp_Set[2];
uint16_t temp_data=0;
int16_t temp_val=*templ;
if(temp_val<0)
temp_val= -1*temp_val;
temp_data=temp_val / 6.25;
temp_data <<=4;
temp_data =0x0FFF +1-temp_data;
write(NST112_temp_High_Register,temp_data);
}
bool NST112::get_temp_alert_high(int16_t *temph)
{
uint16_t _data=read(NST112_temp_High_Register) >>4;
int16_t val=0;
if(_data&0x800)
{
val=(0xFFF-_data +1)*6.25;
}
else
{
val=6.25*_data;
}
*temph=val;
return 1;
}
bool NST112::get_temp_alert_low(int16_t *templ)
{
uint16_t _data=read(NST112_temp_High_Register) >>4;
int16_t val=0;
if(_data&0x800)
{
val=(0xFFF-_data +1)*-6.25;
}
else
{
val=6.25*_data;
}
*templ=val;
return 1;
}
对于按键输入的识别使用的是常见的延时20ms再判断的思路,只是把单纯的高低电平判断变成了范围(因为电阻精度的问题,肯定会有抖动的)
uint8_t key_scan(void)
{
static uint8_t key_up = 1;
uint16_t adc_read = 0;
adc_read = analogRead(KEY_Analog);
if (key_up && adc_read < 950)
{
delay(10);
key_up = 0;
adc_read = analogRead(KEY_Analog);
if (700 < adc_read && adc_read < 760)
return KEY_UP;
else if (450 < adc_read && adc_read < 510)
return KEY_DOWN;
else if (830 < adc_read && adc_read < 910)
return KEY_ENC;
else
return KEY_NONE;
}
else if (adc_read > 950)
key_up = 1;
return KEY_NONE;
}
2.IO连接与实现思路
io连接如下
外设信号 | MCU引脚 | Arduino引脚编号 |
LCD_SCL | PA6 | 6 |
LCD_SDA | PA4 | 4 |
LCD_RESn | PC0 | 8 |
LCD_DC | PC1 | 9 |
LCD_CSn | PA7 | 7 |
LED_R | PD2 | 14 |
LED_G | PD3 | 15 |
LED_B | PD6 | 18 |
V_HEAT | PD1 | 13 |
A_Out | PD7 | 19 |
I2C_SCL | PA3 | 3 |
I2C_SDA | PA2 | 2 |
其中屏幕用到了SPI,温度传感器用到I2C,MOS管控制用到了PWM,按键输入用到了ADC,以及其他的普通IO。
实现思路
1、因为任务要求绘制一个1分钟的温升曲线,所以我用了个软件定时器进行1S的定时采集温度和曲线绘制,同时还要按下按键具有截图功能,所以还有两个缓存用于波形曲线缓存(使用两个的原因是为了在59S刷新时不会丢掉之前缓存的波形)。
2、恒温控制使用的是最经典的PID控制,详细的理论不再赘述,相信随便搜个视频都比我讲的好,哈哈。 自己随手写的PID控制如下(PID的参数只是自己感觉差不多了就没调了):
uint8_t ACTC_process(double set_value, double in_value)
{
static double actc_kp = 15.0, actc_ki = 1.0, actc_kd = 2.0;
static double now_value = 0, last_value = 0;
static double error_add = 0.0;
double out_value = 0;
now_value = in_value;
out_value = actc_kp * (set_value - now_value) + actc_ki * error_add + actc_kd * (now_value - last_value);
error_add += set_value - now_value;
last_value = now_value;
if (error_add > 80)
error_add = 80;
if (out_value < 0)
out_value = 0;
else if (out_value > 254)
out_value = 254;
return out_value;
}
实现代码如下:
#include <Adafruit_GFX.h> // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library for ST7735
// #include <Adafruit_ST7789.h> // Hardware-specific library for ST7789
#include <SPI.h>
#include <SoftwareSerial.h>
#include "NST112.h"
#include <Wire.h>
#include <arduino-timer.h>
// LCD
#define TFT_CS 7
#define TFT_RST 8 // Or set to -1 and connect to Arduino RESET pin
#define TFT_DC 9
#define SerialDebugging true
#define LCD_SDA 4
#define LCD_SCL 6
// RGB_LED
#define LED_R 14 // G
#define LED_G 15 // B
#define LED_B 18 // R
// 模拟按键
/*
NONE 990
UP 734 700-760
DOWM 482 450-510
ENC 860 830-910
*/
#define KEY_Analog 19
// N MOS PWM
#define Heat_PWM 13
// I2C temperature
#define NST_SCL 3
#define NST_SDA 2
#define KEY_NONE 0
#define KEY_UP 1
#define KEY_DOWN 2
#define KEY_ENC 3
uint8_t key_scan(void);
NST112 m_nst112;
SoftwareSerial softSerial1(PIN_PD5, PIN_PD4);
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, LCD_SDA, LCD_SCL, TFT_RST);
int16_t nst112_temp;
uint8_t pwm_duty = 0;
uint8_t sys_mode = 0;
/*
0- chang pwm duty
1= up :save graphics dowm:show
2= auto constan temp control
*/
uint8_t ACTC_temp = 50;
uint8_t ACTC_pwm_duty = 0;
void Draw_string(int16_t x, int16_t y, uint8_t *strings, uint8_t size, uint16_t color);
void GUI_Init(void);
void rect_clear(void);
void Draw_Graph(uint8_t index);
void Screen_Shot_Show(void);
void Temp_to_Led(uint8_t temperature);
void ACTC_show(void);
uint8_t ACTC_process(double set_value, double in_value);
auto timer = timer_create_default(); // create a timer with default settings
uint8_t temp_graph_buff[60];
uint8_t temp_graph_buff_1[60];
uint8_t temp_graph_index = 0;
uint8_t screen_shot_en = 0;
bool period_1s_event(void *)
{
static uint8_t time_counter = 0;
if (sys_mode == 0)
{
m_nst112.getTemperature(&nst112_temp);
softSerial1.printf("%d:Temperature=%d.%d 'c\n", time_counter, nst112_temp / 100, nst112_temp % 100);
if (time_counter == 1)
{
rect_clear();
}
Draw_Graph(time_counter);
if (time_counter >= 59)
{
if (pwm_duty < 200)
pwm_duty += 100;
else
pwm_duty = 0;
analogWrite(Heat_PWM, pwm_duty);
time_counter = 0;
}
else
time_counter++;
Temp_to_Led(nst112_temp / 100);
// digitalWrite(LED_G, !digitalRead(LED_G)); // toggle the LED
}
else if (sys_mode == 1)
{
time_counter = 0;
analogWrite(Heat_PWM, 0);
digitalWrite(LED_R, HIGH);
digitalWrite(LED_G, HIGH);
digitalWrite(LED_B, HIGH);
}
else if (sys_mode == 2)
{
ACTC_show();
}
// softSerial1.printf("ADC READ: %d\n", analogRead(KEY_Analog));
return true; // repeat? true
}
void setup(void)
{
softSerial1.begin(9600);
pinMode(KEY_Analog, INPUT);
pinMode(Heat_PWM, OUTPUT);
analogWrite(Heat_PWM, 0);
pinMode(LED_R, OUTPUT);
pinMode(LED_G, OUTPUT);
pinMode(LED_B, OUTPUT);
timer.every(1000, period_1s_event);
m_nst112.init();
tft.initR(INITR_144GREENTAB); // Init ST7735R chip, green tab
GUI_Init();
}
void loop()
{
timer.tick(); // tick the timer
uint8_t key_state = key_scan();
switch (key_state)
{
case KEY_UP:
if (sys_mode == 0)
{
if (pwm_duty <= 110)
pwm_duty += 10;
else
pwm_duty = 0;
analogWrite(Heat_PWM, pwm_duty);
softSerial1.printf("pwm_duty=%d 'c\n", pwm_duty);
}
else if (sys_mode == 1)
{
Screen_Shot_Show();
}
else if (sys_mode == 2)
{
ACTC_temp += 10;
}
break;
case KEY_DOWN:
if (sys_mode == 0)
{
screen_shot_en = 1;
}
else if (sys_mode == 1)
{
}
else if (sys_mode == 2)
{
// PID_Setpoint = ACTC_temp;
}
break;
case KEY_ENC:
sys_mode = sys_mode < 2 ? sys_mode + 1 : 0;
softSerial1.printf("KEY ENC\n");
default:
break;
}
if (sys_mode == 2)
{
m_nst112.getTemperature(&nst112_temp);
ACTC_pwm_duty = ACTC_process(ACTC_temp, nst112_temp / 100);
analogWrite(Heat_PWM, ACTC_pwm_duty);
softSerial1.println(ACTC_pwm_duty);
delay(100);
}
}
uint8_t key_scan(void)
{
static uint8_t key_up = 1;
uint16_t adc_read = 0;
adc_read = analogRead(KEY_Analog);
if (key_up && adc_read < 950)
{
delay(10);
key_up = 0;
adc_read = analogRead(KEY_Analog);
if (700 < adc_read && adc_read < 760)
return KEY_UP;
else if (450 < adc_read && adc_read < 510)
return KEY_DOWN;
else if (830 < adc_read && adc_read < 910)
return KEY_ENC;
else
return KEY_NONE;
}
else if (adc_read > 950)
key_up = 1;
return KEY_NONE;
}
void Draw_string(int16_t x, int16_t y, const char *strings, uint8_t size, uint16_t color)
{
uint8_t temp = 0;
while (strings[temp] != '\0')
{
tft.drawChar(x + temp * size * 6, y, strings[temp], color, ST77XX_WHITE, size);
temp++;
}
}
void GUI_Init(void)
{
tft.fillScreen(ST77XX_WHITE);
tft.drawRect(1, 10, 60, 100, ST77XX_BLACK);
tft.drawFastHLine(1, 60, 10, ST77XX_BLACK);
tft.drawFastVLine(31, 100, 10, ST77XX_BLACK);
Draw_string(61, 10, "temp:", 2, ST77XX_BLUE);
Draw_string(61, 50, "duty:", 2, ST77XX_BLUE);
Draw_string(0, 0, "100`C:", 1, ST77XX_BLUE);
Draw_string(0, 110, "0", 1, ST77XX_BLUE);
Draw_string(61, 110, "60s", 1, ST77XX_BLUE);
Draw_string(100, 80, "/255", 1, ST77XX_BLUE);
}
void rect_clear(void)
{
tft.fillRoundRect(1, 10, 60, 100, 10, ST77XX_WHITE);
tft.drawRect(1, 10, 60, 100, ST77XX_BLACK);
tft.drawFastHLine(1, 60, 10, ST77XX_BLACK);
tft.drawFastVLine(31, 100, 10, ST77XX_BLACK);
}
void Draw_Graph(uint8_t index)
{
static char show_buff[20];
static uint8_t last_temp = 0, now_temp = 0;
sprintf(show_buff, "%d.%d", nst112_temp / 100, nst112_temp % 100);
Draw_string(61, 30, show_buff, 2, ST77XX_BLUE);
sprintf(show_buff, "%3d", pwm_duty);
Draw_string(61, 70, show_buff, 2, ST77XX_BLUE);
now_temp = nst112_temp / 100;
if (index)
tft.drawLine(index, 110 - last_temp, 1 + index, 110 - now_temp, ST77XX_RED);
temp_graph_buff[index] = now_temp;
if (screen_shot_en && temp_graph_index + 1 == index)
{
screen_shot_en = 0;
tft.fillRoundRect(10, 120, 80, 10, 10, ST77XX_WHITE);
}
if (screen_shot_en)
{
Draw_string(10, 120, "screenshoted!", 1, ST77XX_ORANGE);
memcpy(temp_graph_buff_1, temp_graph_buff, 60);
}
temp_graph_index = screen_shot_en ? index : temp_graph_index;
sprintf(show_buff, "%2d", index);
Draw_string(90, 100, show_buff, 2, ST77XX_ORANGE);
last_temp = now_temp;
}
void Screen_Shot_Show(void)
{
tft.fillRoundRect(1, 10, 60, 100, 10, ST77XX_WHITE);
tft.drawRect(1, 10, 60, 100, ST77XX_BLACK);
tft.drawFastHLine(1, 60, 10, ST77XX_BLACK);
tft.drawFastVLine(31, 100, 10, ST77XX_BLACK);
for (uint16_t i = 0; i < temp_graph_index; i++)
{
tft.drawLine(i + 1, 110 - temp_graph_buff_1[i], 2 + i, 110 - temp_graph_buff_1[i + 1], ST77XX_GREEN);
}
}
void Temp_to_Led(uint8_t temperature)
{
if (temperature >= 50)
{
digitalWrite(LED_R, LOW);
digitalWrite(LED_G, HIGH);
digitalWrite(LED_B, HIGH);
}
else if (temperature < 20)
{
digitalWrite(LED_R, HIGH);
digitalWrite(LED_G, HIGH);
digitalWrite(LED_B, LOW);
}
else
{
digitalWrite(LED_R, HIGH);
digitalWrite(LED_G, LOW);
digitalWrite(LED_B, HIGH);
}
}
void ACTC_show(void)
{
static char show_buff[20];
static uint8_t index = 0, last_temp = 0, now_temp = 0;
// static uint8_t temp_graph_flag=1;
sprintf(show_buff, "%d.%d", nst112_temp / 100, nst112_temp % 100);
Draw_string(61, 30, show_buff, 2, ST77XX_BLUE);
sprintf(show_buff, "%3d", ACTC_pwm_duty);
Draw_string(61, 70, show_buff, 2, ST77XX_BLUE);
now_temp = nst112_temp / 100;
if (index)
tft.drawLine(index, 110 - last_temp, 1 + index, 110 - now_temp, ST77XX_RED);
sprintf(show_buff, "set-temp");
Draw_string(70, 100, show_buff, 1, ST77XX_ORANGE);
sprintf(show_buff, "%2d", ACTC_temp);
Draw_string(80, 110, show_buff, 2, ST77XX_ORANGE);
if (index == 59)
{
rect_clear();
}
if (index < 59)
index++;
else
index = 0;
if (abs(now_temp - ACTC_temp) >= 3)
{
digitalWrite(LED_R, LOW);
digitalWrite(LED_G, HIGH);
digitalWrite(LED_B, HIGH);
}
else
{
digitalWrite(LED_R, HIGH);
digitalWrite(LED_G, HIGH);
digitalWrite(LED_B, HIGH);
}
last_temp = now_temp;
}
uint8_t ACTC_process(double set_value, double in_value)
{
static double actc_kp = 15.0, actc_ki = 1.0, actc_kd = 2.0;
static double now_value = 0, last_value = 0;
static double error_add = 0.0;
double out_value = 0;
now_value = in_value;
out_value = actc_kp * (set_value - now_value) + actc_ki * error_add + actc_kd * (now_value - last_value);
error_add += set_value - now_value;
last_value = now_value;
if (error_add > 80)
error_add = 80;
if (out_value < 0)
out_value = 0;
else if (out_value > 254)
out_value = 254;
return out_value;
}
四、效果展示
1、温度采集与绘制
图4-1关断MOS管不加热时
图4-2开启MOS管占空比为100/255时
在按下按键时,会显示”screenshooted!“ 1秒的时间,表示已截取当前曲线。
图4-2截屏曲线读取
按下按键读取截屏曲线,且换用绿色绘制。
2、恒温控制
图4-3升温接近设定温度时,未达到正负3度内亮红灯
图4-3升温接近设定温度时,达到正负3度内红灯熄灭
五、心得体会
之前忙着毕业设计和论文这些杂七杂八的事,直到Lucia老师在群里提醒,才想起来还有这个活动没做。匆匆忙忙搞了几天,总算搞完了,但因为时间仓促,都没去好好的了解这款8位MCU,得长个教训。不然用了这个单片机,连他的特点和不足都说不出来多少也太丢脸了。
总的来说,电子森林举办Funpack这种活动让我们开阔了不少视野,不再只是盯着ST的32系列和TI这些我们耳熟能祥的单片机看,知道了,原来还有这么多的单片机,可玩性还可以。
上次我参加的寒假训练营也是第一次知道了国外的FPGA除了ALTERA和XILINX还有LATTICE这种还不错的。很感谢电子森林让我开阔了视野,参加了有趣的活动。