AVR-GCC. Ένα πολύ απλό θερμόμετρο.

Πριν λίγο καιρό έπρεπε να καταγράψω για μεγάλο χρονικό διάστημα την θερμοκρασία μιας συσκευής. Έφτιαξα λοιπόν ένα πολύ απλό θερμόμετρο με έναν AVR και ένα NTC Thermistor. Πρόσθεσα τώρα ένα LCD και αποθήκευση μέγιστης και ελάχιστης θερμοκρασίας στην eeprom ώστε να διατηρούνται σε περίπτωση απώλειας της τροφοδοσίας. Ο χρήστης μπορεί να πιέσει ένα πλήκτρο (Min/Max reset) και η μέγιστη και ελάχιστη θερμοκρασία να γίνουν βάση της τρέχουσας θερμοκρασίας. Σε σύγκριση με 2~3 καλά εμπορικά θερμόμετρα που έγινε σύγκριση, η διαφορά στην θερμοκρασία ήταν από 0.1 έως 0.5 βαθμοί Κελσίου.

Άρθρο της WikiPedia για τα Thermistor.
Τεκμηρίωση του NTC Thermistor.
Τεκμηρίωση του ATmega8.

Σε ένα NTC (Negative Temperature Coefficient) thermistor η τιμή της αντίστασης πέφτει όσο ανεβαίνει η θερμοκρασία. Η σχέση της θερμοκρασίας με την αντίσταση του NTC δίνεται από την εξίσωση Steinhart-Hart.

Όπου T η θερμοκρασία σε βαθμούς Κέλβιν, R η τιμή της αντίστασης του NTC και Rn η τιμή της αντίστασης του NTC στους 25 βαθμούς Κελσίου. Τα a, b, c και d είναι σταθερές που εξαρτώνται από τον τύπο του NTC που χρησιμοποιούμε και θα τις βρούμε στην τεκμηρίωσή του. Για τον υπολογισμό των λογαρίθμων χρειάζεται η εισαγωγή του αρχείου κεφαλίδας math.h.

Για την ανάγνωση των δεδομένων από την μεριά του υπολογιστή έγραψα ένα μικρό script σε Python (terminal.py). Η αποθήκευση των θερμοκρασιών σε ένα αρχείο γίνεται με την εντολή ανακατεύθυνσης python2 terminal.py >> temperatures.txt.

Τα αρχεία.

Ο κώδικας.

#include <avr/io.h>
#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <avr/wdt.h>
#include <stdio.h>
#include <math.h>
#include "lcd.h"
#include "uart.h"


/*Η παρακάτω δήλωση ελέγχει αν στον compiler έχουμε βάλει την παράμετρο F_CPU.
  Αν όχι, την περνά. */

#ifndef F_CPU
    #warning "H F_CPU den exei oristei 8a parei thn timh 4000000"
    #define F_CPU 4000000UL  //  Use unsigned long
#endif


#define LOW  0
#define HIGH 1

#define FALSE 0
#define TRUE  1


#define BAUD 9600UL //Ρυθμός Baud

// Υπολογισμοί.
#define UBRR_VAL ((F_CPU+BAUD*8)/(BAUD*16)-1) // ”Έξυπνη” στρογγυλοποίηση
#define BAUD_REAL (F_CPU/(16*(UBRR_VAL+1)))   // Πραγματικός Ρυθμός Baud
#define BAUD_ERROR ((BAUD_REAL*1000)/BAUD)    // Σφάλμα ανά 1000 μέρη.

#if ((BAUD_ERROR<990) || (BAUD_ERROR>1010))
    #error To sfalma ston ry8mo Baud einai megalytero toy 1%, mataiosh!
#endif



//adjust UARTsendChar() function for stream
static int UARTsendstream(char c, FILE *stream);
//----set output stream to UART----
static FILE uart_str = FDEV_SETUP_STREAM(UARTsendstream, NULL, _FDEV_SETUP_WRITE);

static int UARTsendstream(char c , FILE *stream){
    uart_putc(c);
    return 0;
}

// Η ακριβής τιμή της R2.
#define R2 9988.0
// Τιμή του NTC στους 25 βαθμούς C.
#define Rn 10000.0

/*
    Vcc
     |
     |
     /
     \
     /  NTC_Res
     \
     /
     |
     |--------ADC_in
     |
     /
     \
     /  R2
     \
     /
     |
     |
    GND

    NTC_Res = (1024*R2)/ADC - R2

*/


// A, B, C και D σταθερές του NTC.
// Σελίδα 4 (78) της τεκμηρίωσης του
// NTC.
#define A 0.003354016
#define B 0.000256985
#define C 0.000002620131
#define D 0.00000006383091


#define   EEPReadWord(addr)         eeprom_read_word((uint16_t *)addr)
#define   EEPWriteWord(addr, val)   eeprom_write_word((uint16_t *)addr, val)
#define   EEPReadByte(addr)         eeprom_read_byte((uint8_t *)addr)
#define   EEPWriteByte(addr, val)   eeprom_write_byte((uint8_t *)addr, val)

void adc_init(void);
uint16_t adc_read(uint8_t);
double adc2temp(int);
void int10_to_ascii(int, char *);
uint8_t debounce(uint8_t);


int main(void){

    char s[20];
    int tmp=0,dec=0,c;
    int min_t, max_t;
    uint8_t i=0,rst=0,test_mem;

    uint8_t lastButton = LOW;
    uint8_t currentButton = LOW;

    wdt_enable(WDTO_120MS);  // Ενεργοποίηση του WatchDog
                             // με χρόνο 120 msec.

    DDRB &= ~(1<<PB0);  // PB0 ως είσοδος.
    PORTB |= (1<<PB0);  // PB0 Pull-Up.

    DDRB |= (1<<PB1);   // PB1 ως έξοδος.


    // Αν το WDRF bit στον MCUCSR είναι 1, που
    // σημαίνει επανεκκίνηση από τον WatchDog,
    // άναψε το LED στο pin PB1.
    if(bit_is_set(MCUCSR,WDRF)) PORTB |= (1<<PB1);

    min_t = EEPReadWord(2);
    max_t = EEPReadWord(5);

    test_mem = EEPReadByte(10);



    // Αρχικοποίηση του UART.
    uart_init(UART_BAUD_SELECT(BAUD,F_CPU));

    sei(); // Ενεργοποίηση των interrupt;

    stdout = &uart_str;                           // Stream initialization.
    uart_init( UART_BAUD_SELECT(BAUD,F_CPU) );    // Αρχικοποίηση του UART

    wdt_reset();

    lcd_init();
    lcd_clear();

    adc_init();

    wdt_reset();

    // Έλεγχος για να δούμε αν έχει διαγραφεί η eeprom
    // Π.χ. μετά από προγραμματισμό. Αν έχει διαγραφεί
    // test_mem = 0xFF αν όχι ) 0xAA (τυχαία επιλέχθηκε
    // η τιμή 0xAA).
    if (test_mem != 0xAA){
        tmp = adc_read(0);
        dec = (int)10.0*adc2temp(tmp);
        min_t = dec;
        max_t = dec;
        EEPWriteWord(2,min_t);
        EEPWriteWord(5,max_t);
        test_mem = 0xAA;
        EEPWriteByte(10,test_mem);
    }

    i=200;


    while(1){

        if (i>=200) {  // Το i γίνεται 200 κάθε 2 δευτερόλεπτα (200*10 msec).

            tmp = adc_read(0);
            dec = (int)10.0*adc2temp(tmp);

            // Αν φτάσαμε στα min/max όρια ή πατήθηκε το
            // μπουτόν min/max reset όρισε τα νέα min/max.
            if (dec<min_t || rst==1) {
                min_t = dec;
                EEPWriteWord(2,min_t);
            };

            if (dec>max_t || rst==1) {
                max_t = dec;
                EEPWriteWord(5,max_t);
            };

            // H συνάρτηση int10_to_ascii μετατρέπει έναν
            // ακέραιο της μορφής xxx σε xx.x (π.χ. το
            // 218 σε 21.8) για να τυπώσουμε τον δεκαδικό
            // αριθμό χωρίς την χρήση printf για float που
            // απαιτεί αρκετούς πόρους.
            int10_to_ascii(dec,s);

            lcd_home();
            lcd_str("Temp:");
            lcd_str(s);
            lcd_str(" C  ");
            printf("Temp:%s ",s);

            lcd_setcursor(0,2);
            int10_to_ascii(min_t,s);
            lcd_str("Mn:");
            lcd_str(s);
            printf("Min_Temp:%s ",s);

            int10_to_ascii(max_t,s);
            lcd_str(" Mx:");
            lcd_str(s);
            lcd_data(' ');
            printf("Max_Temp:%s\n",s);

            i=0;
            rst=0;

        };

        // Χρήση debounce.
        currentButton = debounce(lastButton);

        if (lastButton == LOW && currentButton == HIGH) {
            min_t=dec;
            max_t=dec;
            i=150;
            rst=1;
        };

        lastButton = currentButton;


         _delay_ms(10);
         i++;

         wdt_reset();

    };

    return 0;
}

uint8_t debounce(uint8_t last){

    uint8_t current = bit_is_set(PINB,PB0);

    if (last != current){
    _delay_ms(25);
    current = bit_is_set(PINB,PB0);
  };

  return current;
}


void int10_to_ascii(int inum,char *outbuf){

	unsigned char i,l,j,minus;
	char chbuf[7];

	minus=0;
	j=0;

	if (inum<0){
		minus=1;
		inum=inum * -1;
	};

	while(inum>9 && j<7){
		// το μηδέν σε ascii είναι το 48:
		chbuf[j]=(char)48+ inum-((inum/10)*10);
		inum=inum/10;
		j++;
	};


	chbuf[j]=(char)48+inum; //Το ποιο σημαντικό ψηφίο.


	// Αντιστροφή του outbuf και εισαγωγή  της τελείας (.).
	i=0;
	l=0;

	if(minus){
		outbuf[i]='-';
		i++;
	};

	while(j>0){
		outbuf[i]=chbuf[j];
		j--;
		i++;
		l++;
	}

	// Αν το inum είναι μικρότερο του 10 θα έχει μορφή  "0.inum".
	if (l==0){
		outbuf[i]='0';
		i++;
	}
	outbuf[i]='.';
	i++;
	outbuf[i]=chbuf[j];
	i++;
	outbuf[i]='\0';
}

void adc_init(){

    ADMUX |= (1<<REFS0);              // AVCC with external capacitor at AREF pin.

    ADCSRA |= (1<<ADPS2)|(1<<ADPS0);  // Prescaler 32 (4 MHz/32=125KHz).

    ADCSRA |= (1<<ADEN);              // Enable ADC.

    ADCSRA |= (1<<ADSC);              // Start ADC conversion

}

uint16_t adc_read(uint8_t ADC_channel){

    int16_t adc_val;


    ADMUX = (ADMUX & 0xF0) | (ADC_channel & 0x0F);

    ADCSRA |= (1<<ADSC);

    while( ADCSRA & (1<<ADSC) ); // Αναμονή για την ολοκλήρωση της μέτρησης

    adc_val = ADC;

    return adc_val;
}

// Συνάρτηση μετατροπής τις τιμής το ADC σε βαθμούς Κελσίου
// με την χρήση της εξίσωσης Steinhart-Hart.
double adc2temp(int RawADC) {

    double NTC_Res, celsius;
    double tmp;                 // Προσωρινή μεταβλητή.

    // Υπολογισμός της αντίσταση από την τιμή του ADC.
    NTC_Res=((1024 * R2 / RawADC) - R2);

    tmp = log(NTC_Res/Rn);

    tmp = (A+(B*tmp)+(C*tmp*tmp)+(D*tmp*tmp*tmp));

    celsius = (1.0/tmp)-273;    // Μετατροπή βαθμών Κέλβιν σε Κελσίου.


    return celsius;
}

To Python script.

import signal
import sys
import time
import serial

# configure the serial connections (the parameters differs on the device you are connecting to)
ser = serial.Serial(
    port='/dev/ttyUSB1',
    baudrate=9600
)

sys.stderr.write('Press Ctrl+C to exit.\n\n')

ser.close()
time.sleep(1)
ser.open()
ser.isOpen()

def signal_handler(signal, frame):
        sys.stderr.write('\n\nYou pressed Ctrl+C!\n\n')
	ser.close()
        sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)


while 1:
	data = ser.readline()
	print time.strftime("%d/%m/%y %H:%M:%S", time.localtime()), data,
Advertisements
This entry was posted in AVR, Electronics and tagged , , , , , , , . Bookmark the permalink.

3 Responses to AVR-GCC. Ένα πολύ απλό θερμόμετρο.

  1. Pingback: AVR-GCC. 7-Segment Displays. | My humble Blog.

  2. Cosma Geo says:

    Πολύ ωραίο το tutorial. Μπράβο!
    Μία ερώτηση: Έχω υλοποιήσει και εγώ ένα παρόμοιο θερμόμετρο αλλά η τιμή της θερμοκρασίας αλλάζει συνέχεια. Δηλαδη είναι 28.2, μετα πάει 28.4, μετα 28.6, μετα 28.2 κοκ. Ξέρω οτι επειδη αλλάζει η αντισταση αλλαζει και η τιμη. Υποθέτω πως ακομα και σε σταθερη θερμοκρασια, η αντισταση παρουσιαζει καποιες αυξομειωσεις( διορθωσε με αν κανω λαθος). Ρωτάω λοιπον, πως μπορω να κρατάω την τιμη σταθερη οταν εχω πολυ μικρες μεταβολες στην αντισταση;

    • alexkaltsas says:

      Εξαρτάται από την υλοποίηση σου. Γι παράδειγμα κάθε πότε ανανεώνεται η ένδειξη; Θα μπορούσε να μαζεύεις π.χ. 5 μετρήσεις και να βγάζεις μέσο όρο. Θα μπορούσε να βάλεις και έναν πυκνωτή των 100 nF για παράδειγμα μεταξύ γείωσης και του καναλιού του ADC. Μπορεί αν είναι και θόρυβος όμως. Αν για παράδειγμα ο αισθητήρας και η αντίσταση πόλωσης του έχουν μεγάλες τιμές, μπορεί ο ADC “να πιάνει” θόρυβο.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s