一.项目描述
项目需求:
IO扩展板上有一处加温电阻,将加热区域用物体(纸巾等)包裹起来,通过电流给电阻加热,并通过温度传感器感知板上温度的变化,测温以及在LCD屏上的温度显示。
要求:使用按键设定目标温度,并且通过程序控制加热功率,使得温度尽快尽量稳定的维持在目标温度。温度偏离设定温度±3°C彩灯变为红色。(注意,加热电阻满占空比开启后温度较高)
本人已全部实现。
开发板简介:
AVR DD系列是Microchip公司AVR单片机产品线中的最新的一员,它的内部结构灵活、性能强大,有着比较丰富的外设和存储资源。本次申请参加Funpack活动,是为了体验该芯片上差分ADC,过零检测硬件ZCD、逻辑资源等。这些功能在我目前掌握的单片机中很少出现,所以趁此机会学习一下这款AVR的单片机。
二.设计思路
①开发板、拓展板
如上是本次avr单片机的核心板外围引脚电路图
这个是本次搭配的拓展板,二者通过杜邦线进行连接。
②按键检测原理
如上为按键部分原理图。通过一个io口输出数字量从而读取按键组合。该电路图上有100k和200k电阻,让我想到了R2R的dac网络结构。那么根据R2R的原理,五个按键可以当作五个开关,那么就会有5bit分辨率的电压输出。而我们采集的时候是用adc进行采集,adc在单片机中本质也是二进制,所以我选择只用avr单片机中连续采样10位adc单端模式,然后取出高五位,那么每一位的0或1结果就代表着对应的按键,从而可以识别出哪一个按键按下了。因为adc无法使用中断,所以在主程序循环中使用查询法。
static void ADC0_init(void)
{
/* Select ADC voltage reference */
VREF.ADC0REF = VREF_REFSEL_VDD_gc;
/* Disable digital input buffer */
PORTF.PIN2CTRL &= ~PORT_ISC_gm;
PORTF.PIN2CTRL |= PORT_ISC_INPUT_DISABLE_gc;
/* Disable pull-up resistor */
PORTF.PIN2CTRL &= ~PORT_PULLUPEN_bm;
ADC0.CTRLC = ADC_PRESC_DIV4_gc; /* CLK_PER divided by 4 */
/* Select ADC channel */
ADC0.MUXPOS = ADC_MUXPOS_AIN18_gc;
ADC0.CTRLA = ADC_ENABLE_bm /* ADC Enable: enabled */
| ADC_RESSEL_10BIT_gc /* 10-bit mode */
| ADC_FREERUN_bm; /* Enable FreeRun mode *///自动开启下一次转换
}
static uint16_t ADC0_read(void)
{
/* Clear the interrupt flag by writing 1: */
ADC0.INTFLAGS = ADC_RESRDY_bm;
return ADC0.RES;
}
static void ADC0_start(void)
{
/* Start conversion */
ADC0.COMMAND = ADC_STCONV_bm;
}
static bool ADC0_conversionDone(void)
{
return (ADC0.INTFLAGS & ADC_RESRDY_bm);
}
↑ADC的配置
if (ADC0_conversionDone())
{
adcVal = ADC0_read();
a1=adcVal&0x200;
a2=adcVal&0x100;
}
if(a1){set_temp++;}
if(a2){set_temp--;}
在while循环里的检测按键,a1和a2就是取ad的最高位和次高位,从而读取按键
③NST112的读取
通过数据手册可以看到NST112是使用i2c协议进行通信的。那么本次我使用io口模拟i2c的方式,没有使用硬件i2c,因为读温度数据短,不需要很高速度,用软件i2c便于移植。
在板子上A0脚连接GND了,所以设备地址是1001000,所以设备地址为0x48,我程序里定义的需要左移以为,所以是0x90。
寄存器地址只需要0x00配置寄存器,在初始化的时候使用,0x01为读温度寄存器,每次采集温度的时候访问0x01读两个字节
这个就是0x00配置寄存器,在初始化的时候发两个字节配置一下即可,我设置8Hz采样
void NST112_configuration(){
char hh[2]={0x60,0xE0}; //8Hz
I2C_SAND_BUFFER(0x90, 0x01,hh,2);
}
↑初始化程序部分
I2C_READ_BUFFER(0x90,0x00,buf,2);
now_T=buf[0]*256+buf[1];
now_T=now_T>>4;
↑读取温度和进行数据拼接和移位
④加热的控制
由于加热电阻在pcb的下方,温度传感器在pcb的上方,所以检测温度会比实际温度滞后,造成很大的过冲。我采用在pid上增加分段的限幅,比如在60°C以下在目标温度附近时最大加热占空比为40%,在60°C以上在目标温度附近时最大加热占空比为80%,这样就可以稳定在±2°C。否则不加限幅的话温度波动会达到±4°C。
/* TMR_CLK = F_CPU / PRESCALER = 24MHz / 4 = 6MHz */
#define PERIOD_EXAMPLE_VALUE 500 /* fpwm = 6000kHz / 2 / 500 = 6kHz */
#define DUTY_CYCLE_EXAMPLE_VALUE (PERIOD_EXAMPLE_VALUE/2) /* 50% duty cycle */
void TCA0_init(void)
{
/* set waveform output on PORT C */
PORTMUX.TCAROUTEA = PORTMUX_TCA0_PORTC_gc;
TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm /* enable compare channel 0 */
| TCA_SINGLE_WGMODE_DSBOTTOM_gc; /* set dual-slope PWM mode */
/* disable event counting */
TCA0.SINGLE.EVCTRL &= ~TCA_SINGLE_CNTAEI_bm;
/* set PWM frequency and duty cycle (50%) */
TCA0.SINGLE.PERBUF = PERIOD_EXAMPLE_VALUE;
TCA0.SINGLE.CMP0BUF = DUTY_CYCLE_EXAMPLE_VALUE;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV4_gc /* set clock source (sys_clk/4) */
| TCA_SINGLE_ENABLE_bm; /* start timer */
}
void PORT_init(void)
{
/* set pin 0 of PORT C as output */
PORTC.DIRSET = PIN0_bm;
}
TCA0.SINGLE.CMP0BUF=PID_realize((float)(set_temp*16),(float)now_T);
↑pwm配置及应用
float PID_realize(float set_place,float now_place)
{
pid.Set=set_place;
pid.Actual = now_place;
pid.err=pid.Set-pid.Actual;
pid.integral+=pid.err;
pid.voltage=pid.Kp*pid.err+pid.Ki*pid.integral+pid.Kd*(pid.err-pid.err_last);
pid.err_last=pid.err;
if(pid.integral>100)pid.integral=100;
if(pid.integral<-100)pid.integral=-100;
if(set_place-now_place>80){
if(pid.voltage>500)pid.voltage=500;
}
else{
if(set_place>960){
if(pid.voltage>390)pid.voltage=390;//大于60°C
}
else{
if(pid.voltage>200)pid.voltage=200;
}
}
if(pid.voltage<0)pid.voltage=0;
pid.pwm=(int)pid.voltage;
return pid.pwm;
}
↑pid核心部分
⑤屏幕的驱动
屏幕由于需要较高的速度,所以我使用了硬件spi,硬件spi还是比较简单的。之后就把程序接口和屏幕的例程对接好,屏幕驱动就好了。
void SPI0_init(void)
{
/* SPI in host mode setting */
PORTA.PIN7CTRL |= PORT_PULLUPEN_bm; /* Pull-up enable on PIN7 */
//PORTA.OUTSET = PIN7_bm; /* Set SS pin to high state */
PORTA.DIRSET = PIN4_bm; /* Set MOSI pin direction to output */
//PORTA.DIRCLR = PIN5_bm; /* Set MISO pin direction to input */
PORTA.DIRSET = PIN6_bm; /* Set SCK pin direction to output */
// PORTA.DIRSET = PIN7_bm; /* Set SS pin direction to output */
/* SPI Clock speed is set to 2 x 24MHz / 16 = 3 MHz */
SPI0.CTRLA = SPI_CLK2X_bm /* Enable double-speed */
| SPI_ENABLE_bm /* Enable module */
| SPI_MASTER_bm /* SPI module in Host mode */
| SPI_PRESC_DIV16_gc; /* System Clock divided by 16 */
}
uint8_t LCD_Writ_Bus(uint8_t sent_data)
{
SPI0.DATA = sent_data;
while (!(SPI0.INTFLAGS & SPI_IF_bm)); /* waits until data is exchanged */
return SPI0.DATA;
}
↑spi配置部分以及和屏幕例程对接的接口部分,其余显示就是调用库,没什么好说的
⑥串口部分
由于需要进行pid调参,所以使用串口发送温度传感器数据,使用arduino的串口绘图器进行绘制,就可以看到曲线图了。
#define F_CPU 24000000UL
#define BAUD_RATE 115200
#define USART0_BAUD_RATE(BAUD_RATE) (uint16_t)((float)((F_CPU) * 64 / (16*6* (float)(BAUD_RATE))) + 0.5)
void USART0_Init(void)
{
PORTMUX.USARTROUTEA = PORTMUX_USART0_ALT3_gc; /* USART0 routed to PORTD that is connected to CDC */
PORTD.DIRSET = PIN4_bm;
USART0.BAUD = USART0_BAUD_RATE(BAUD_RATE);
USART0.CTRLB |= USART_TXEN_bm;
}
void USART0_sendChar(char c)
{
while (!(USART0.STATUS & USART_DREIF_bm));
USART0.TXDATAL = c;
}
void SendString(const char* pStr)
{
while(*pStr)
USART0_sendChar(*pStr++);
}
↑串口配置部分。
USART0_sendChar(48+now_T/1000);
USART0_sendChar(48+now_T/100%10);
USART0_sendChar(48+now_T/10%10);
USART0_sendChar(48+now_T%10);
USART0_sendChar(',');
USART0_sendChar(48+set_temp*16/1000);
USART0_sendChar(48+set_temp*16/100%10);
USART0_sendChar(48+set_temp*16/10%10);
USART0_sendChar(48+set_temp*16%10);
USART0_sendChar('\n');
USART0_sendChar('\r');
↑串口发送数据,就按照arduino串口绘图器要求的数据格式进行发送。
三、心得体会
通过funpack活动,可以玩一些新的单片机,锻炼了快速学习一款新单片机的能力。由于以前会stm32,stc,esp32等单片机,再加上这学期学校里单片机课程使用微芯pic,所以对mplab的开发有所熟悉,拿到这款avr64dd32配合官方的例程花了大概两三天时间就大致做出来了,确实锻炼了自己快速学习新器件并进行开发的能力。同时avr64dd32这款八位机也很好,有zcd过零检测,以及adc参考选负电源可以测负电压,这些功能是其他很多单片机所没有的,参加funpack的同时也获得了一款开发利器。