Funpack第二季第四期:基于AVR64DD32的加热和温度采集系统
Funpack2-4活动的开发板为AVR64DD32 Curiosity Nano,基于AVR64DD32设计了一个加热和温度采集系统
标签
嵌入式系统
Funpack活动
氢化脱氯次氯酸
更新2023-05-05
中国科学技术大学
638

1 AVR64DD32 Curiosity Nano平台介绍

AVR DD系列是Microchip公司AVR单片机产品线中的最新的一员,它的内部结构灵活、性能强大,有着比较丰富的外设和存储资源。该系列采用了最新的不依赖内核的外设,能够以非常低的功耗、在多种电压输入输出之间进行安全的双向通信,事件系统、可配置逻辑CCL、智能模拟外设,比如12位的ADC和过零检测都使得AVR DD系列非常适用于传感、IoT终端节点以及其它需要信号调理及电平转移的应用。

AVR64DD32板可轻松访问AVR64DD32的特性,由MPLAB® X IDE和Microchip Studio提供支持,从而将该器件集成到定制设计中。Curiosity Nano评估板包括板载调试器,无需外部工具即可对AVR64DD32 MCU进行编程和调试。

Fvv_dIu69pPauSr9rbuBf01Br7FE

 

2 任务及实现方案

2.1 本期任务

任务1 - 实现一个加热和温度采集系统

IO扩展板上有一处加温电阻,通过电流给电阻加热,并通过温度传感器感知板上温度的变化,将测量到的温度信息显示在LCD屏幕上,绘制一个1分钟的温升曲线。并且每隔1分钟改变加热电阻的占空比,重复温度测量和绘制的过程。

板上有一处RGB彩灯,当温度超过50°C时转为红色,低于20°C时转为蓝色,正常状态下为绿色。

要求:按下按键时,截图当前的温升曲线。(注意,加热电阻满占空比开启后温度较高)

2.2 功能展示

开机后,屏幕上方显示当前的温度与加热电阻PWM波的占空比,屏幕中间绘制近100秒时间内的温度曲线(从右向左绘制,滚动刷新),上述信息每1秒刷新一次。

扩展板上右下角有两个按键开关,上边的开关是截图按键,按下即可对当前的温度曲线进行截图,再按即可继续绘制曲线;下边的开关是加热电阻PWM波的占空比调节,可以在0%、10%和20%三档之间切换。

扩展板上的LED灯的颜色也会随温度变化,小于20度时为蓝色,20度到50度之间为绿色,大于50度为红色。

wEvtfmckPlk1AAAAABJRU5ErkJggg==

 

3 实现过程

3.1 总览

本项目利用到了LCD显示屏(SPI驱动)、NST112温度传感器(IIC驱动)、按键开关(ADC检测)、加热电阻(PWM控制)、LED灯等模块,工作流程如下:

D6HETVsLkV0EAAAAAElFTkSuQmCC

 

3.2 获取温度与点亮LED

本项目使用的温度传感器为扩展板上的NST112-DSTR,通过IIC实现与开发板的通信, SDA与SCL分别连接至开发板的PA2与PA3,开发板的IIC实现使用官方提供的例程(https://github.com/microchip-pic-avr-examples/avr64dd32-getting-started-with-i2c-mplabx)。

开发板获取温度包含配置传感器的IIC地址、IIC读取温度和温度格式转换三步。根据NST112-DSTR的数据手册得知,A0脚的连接状况决定了传感器的IIC地址。扩展板上的A0脚接地,因此从机地址为1001000,即0x48。

表格  描述已自动生成

随后利用例程提供的I2C_Read()函数,即可得到原始温度信息。原始温度信息包含16位数据,其中低4位恒为0,第4至第7位为小数部分,第8位至第15位为整数部分。利用移位等操作,即可获得浮点型的温度信息。

float NST112_Get_Temp_Float(int16_t rawData)
{
    return (rawData >> 4) / 16.0;
}

AcJoktNVyVwXAAAAAElFTkSuQmCC

再根据当前温度值,点亮不同颜色的LED,同时将温度信息显示在屏幕上。扩展板上的三色LED分别由开发板的PD1~3控制。

if (temperature < 20)
{
    LED_B_SetLow();
    LED_G_SetHigh();
    LED_R_SetHigh();
}
else if (temperature >= 20 && temperature < 50)
{
    LED_B_SetHigh();
    LED_G_SetLow();
    LED_R_SetHigh();
}
else
{
    LED_B_SetHigh();
    LED_G_SetHigh();
    LED_R_SetLow();
}

3.3 温度曲线绘制

开发板可以保存近100秒的温度信息,并绘制在LCD屏幕上,从右至左滚动刷新,即最右侧的温度为当前温度,最左侧的温度为100秒前的温度。

由于每次温度的更新需要在末尾添加最新的温度值,并删除底部最早的温度值,这种先入先出的特性可以由队列实现,且由于长度固定(100个温度值),所以绘制曲线所需要的温度信息被储存在由静态数组实现的循环队列中。队列的长度为101,需要一个额外元素来判断循环队列是否已满。此外绘制曲线时需要查询所有时刻的温度值,针对该需求额外实现了一个队列索引的方法。

#define MAX_QUEUE_SIZE 101

typedef struct {
    int8_t data[MAX_QUEUE_SIZE];
    uint8_t front;
    uint8_t rear;
} Queue;

void initQueue(Queue *q)
{
    q->front = 0;
    q->rear = 0;
}

int isQueueEmpty(Queue *q)
{
    return q->front == q->rear;
}

int isQueueFull(Queue *q)
{
    return (q->rear + 1) % MAX_QUEUE_SIZE == q->front;
}

void enQueue(Queue *q, int8_t value)
{
    if (isQueueFull(q)) {
        deQueue(q);
    }
    q->data[q->rear] = value;
    q->rear = (q->rear + 1) % MAX_QUEUE_SIZE;
}

int deQueue(Queue *q)
{
    if (isQueueEmpty(q)) {
        return 1;
    } else {
        q->front = (q->front + 1) % MAX_QUEUE_SIZE;
        return 0;
    }
}

int getQueueElem(Queue *q, uint8_t index, int8_t *value)
{
    uint8_t position;
    if (index >= MAX_QUEUE_SIZE - 1 || index < 0)
        return -1;
    
    if (index >= (q->rear + MAX_QUEUE_SIZE - q->front) % MAX_QUEUE_SIZE)
        return 1;
    
    position = (q->front + index) % MAX_QUEUE_SIZE;
    
    *value = q->data[position];
    
    return 0;
}

之后将这些温度值线性映射至LCD上的像素点,图像最顶端为60度,最低端为10度,并返回映射后曲线的长度。

uint8_t generate_wave(Queue *i_temperature, uint8_t y[GRAPH_WIDTH])
{
	int8_t i_temp_max = 60;
	int8_t i_temp_min = 10;
	uint8_t i;
    int8_t temp;

    for (i = 0; i < GRAPH_WIDTH; i++)
    {
        if (getQueueElem(i_temperature, i, &temp))
            break;
        if (temp <= i_temp_max && temp >= i_temp_min)
            y[i] = (GRAPH_HEIGHT - 1) * (i_temp_max - temp) / (i_temp_max - i_temp_min) + GRAPH_START_Y;
        else if (temp > i_temp_max)
            y[i] = GRAPH_START_Y;
        else if (temp < i_temp_min)
            y[i] = GRAPH_HEIGHT + GRAPH_START_Y - 1;
    }
    
    return i;
}

最后在LCD屏幕上绘制曲线,包含擦除上一次的曲线和画出当前曲线两步。

void display_wave(const uint8_t *y, const uint8_t *y_prev, uint8_t y_wave_length, uint8_t y_prev_wave_length)
{
	uint8_t x;
    uint8_t start_point =  GRAPH_START_X + GRAPH_WIDTH - y_prev_wave_length;
	for (x = start_point; x < GRAPH_START_X + GRAPH_WIDTH - 1; x++)
	{
	    LCD_DrawLine(x, y_prev[x-start_point], x + 1, y_prev[x-start_point+1], BACKGROUND_COLOR);
	}
    LCD_DrawPoint(x, y_prev[x-start_point], BACKGROUND_COLOR);
    
    start_point =  GRAPH_START_X + GRAPH_WIDTH - y_wave_length;
    
    for (x = start_point; x < GRAPH_START_X + GRAPH_WIDTH - 1; x++)
	{
		LCD_DrawLine(x, y[x-start_point], x + 1, y[x-start_point+1], WAVE_COLOR);
	}
    LCD_DrawPoint(x, y[x-start_point], WAVE_COLOR);
}

3.4 读取按键状态

开发板上的两个按键开关接入了电阻网络中,通过ADC检测电阻网络输出的电压即可判断哪个开关被按下。开发板的PF3端口(AIN19)接入扩展板上的电阻网络输出Aout,ADC精度为10bit,默认状态下ADC输出值在900以上,当按下下边的开关时,ADC输出值为480左右,按下上边的开关时,ADC输出值在735左右。因此间隔20ms读取ADC输出值,检测下降沿即可判断哪个按键被按下。

adcVal1 = ADC0_GetConversion(ADC_MUXPOS_AIN19_gc);
_delay_ms(20);
adcVal2 = ADC0_GetConversion(ADC_MUXPOS_AIN19_gc);

if (adcVal1 > 900 && adcVal2 < 500 && adcVal2 > 460)
{
    duty_list_idx = (duty_list_idx + 1) % 3;
    TCB2.CCMPH = duty_list[duty_list_idx];
}
else if (adcVal1 > 900 && adcVal2 > 710 && adcVal2 < 750)
{
    TCA0.SINGLE.CTRLA ^= TCA_SINGLE_ENABLE_bm;
}

3.5 定时器配置

本项目使用了开发板的TCA0和TCB2两个定时器,分别控制温度的读取和加热电阻PWM波的输出。定时器部分的代码参考了官方例程(https://github.com/microchip-pic-avr-examples/avr64dd32-getting-started-with-tca-mplabxhttps://github.com/microchip-pic-avr-examples/avr64dd32-getting-started-with-tcb-mplabx)。

TCA0定时器的溢出时间为1秒,因此对输入的4MHz进行64分频,并配置计数周期为62500(4M/64),并使能溢出中断。在中断服务函数中设置刷新标志位,实现温度的读取与屏幕刷新。注意这里的标志位变量必须使用volatile修饰,否则不会刷新。

#define PERIOD_EXAMPLE_VALUE   62499

void TCA0_init(void)
{
    /* enable overflow interrupt */
    TCA0.SINGLE.INTCTRL = TCA_SINGLE_OVF_bm;
    
    /* set Normal mode */
    TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc;
    
    /* disable event counting */
    TCA0.SINGLE.EVCTRL &= ~TCA_SINGLE_CNTAEI_bm;
    
    /* set the period */
    TCA0.SINGLE.PER = PERIOD_EXAMPLE_VALUE;  
    
    TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV64_gc         /* set clock source (sys_clk/64) */
                      | TCA_SINGLE_ENABLE_bm;                /* start timer */
}

volatile uint8_t refresh;
ISR(TCA0_OVF_vect)
{
    refresh = 1;
    
    /* Clear the interrupt flag */
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm;
}

当截屏按键被按下时,停止TCA0的计数,再次按下时继续计数,可以通过翻转寄存器的对应标志位实现。

TCA0.SINGLE.CTRLA ^= TCA_SINGLE_ENABLE_bm;

TCB2使能PWM波的输出(端口PC0),计数周期为100,可以通过按键来切换占空比大小。

#define TCB_EXAMPLE_PERIOD         99
#define TCB_EXAMPLE_DUTY_CYCLE     0

void TCB2_init(void)
{
    /* Enable TCB2 and Divide CLK_PER by 2 */
    TCB2.CTRLA = TCB_ENABLE_bm | TCB_CLKSEL_DIV2_gc;
    
    /* Enable Pin Output and configure TCB in 8-bit PWM mode */
    TCB2.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_PWM8_gc;
    
    /* Duty cycle register */
    TCB2.CCMPH = TCB_EXAMPLE_DUTY_CYCLE;
    /* Period Register */
    TCB2.CCMPL = TCB_EXAMPLE_PERIOD;
}

uint8_t duty_list[3] = {0, 20, 40}, duty_list_idx = 0;

duty_list_idx = (duty_list_idx + 1) % 3;
TCB2.CCMPH = duty_list[duty_list_idx];

4 心得体会

本次活动我使用MPLAB作为开发软件,MPLAB可以提供图形化的外设配置界面并自动生成对应的初始化代码,免去了手动配置的繁琐过程。不过其对于需要配置的外设参数并没有详细的说明,经常看着一堆参数一头雾水。因此我只使用了图形化界面配置了GPIO与ADC,对于定时器与IIC等外设则直接操作AVR的寄存器来实现。

 

附件下载
funpack_s2e4.X.production.elf
可编译下载的二进制文件
funpack_s2e4.X.zip
工程文件
团队介绍
中国科学技术大学学生
团队成员
氢化脱氯次氯酸
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号