Cyfrowy termometr z wykorzystaniem płytki atmel_display
O wykorzystaniu płytki atmel_display do pomiaru i wyświetlania temperatury pisałem już bardzo dawno temu. Jednak dopiero ostatnio zmobilizowałem się i w jedno popołudnie powstał prosty termometr o dość szerokim zakresie mierzonych temperatur. Program napisałem w C, korzystając z gotowych bilbliotek, bo ciężko na nowo odkrywać obsługę magistrali 1-wire czy multipleksowanie siedmiosegmentowego wyświetlacza.
Termometr – baza sprzętowa
Osoby, które nie wiedzą czym jest płytka atmel_display, zapraszam do kilku krótkich wpisów z 2012 i 2013 roku:
- Mini płytka testowa AVR z odzysku
- Usprawnienie mini płytki testowej AVR z dekodera Cyfrowego Polsatu
- Test mini płytki AVR z ATtiny2313 – pierwszy program
Do budowy termometry wykorzystałem przerobioną płytkę (z wyprowadzonymi na gniazdo dodatkowymi portami), ale równie dobrze będzie działać wersja oryginalna, zdemontowana z dekodera. Jako czujnik temperatury użyłem tani i popularny układ DS18B20 wraz z rezystorem 4k7Ω, którym linia DATA została podciągnięta do plusa zasilania. Schemat całości wygląda tak:
Posiadacze płytki bez przeróbek linię DQ czujnika powinny podpiąć do portu PB5, PB6 lub PB7, gdyż w oryginale PB4 nie jest wyprowadzony na goldpiny. Taka zmiana będzie wymagała tylko niewielkiej korekty w oprogramowaniu o czym niżej.
Termometr na attiny2313 – kod programu w C
Pisząc program skorzystałem z opisu obsługi czujnika, którego autorem jest Gerard Marull Paretas. Dokument pochodzi z 2007 roku, ale kod praktycznie bez problemu skompilował się na obecnej wersji gcc. Wart przejrzenia dokument można pobrać stąd:
Biblioteka ds18b20.h – definicja portu i pinu pod który jest podpięty port DQ została opatrzona stosownym komentarzem:
/* * ds18b20.h * * Created on: 20 sie 2015 * Author: wojtek */ #ifndef DS18B20_H_ #define DS18B20_H_ // Definiujemy pod jaki pin jest podpieta magistrala 1-wire #define DS18B20_PORT PORTB #define DS18B20_DDR DDRB #define DS18B20_PIN PINB #define DS18B20_DQ PB4 //Pin jako wejscie i wyjscie #define DS18B20_INPUT_MODE DS18B20_DDR &= ~(1<<DS18B20_DQ) #define DS18B20_OUTPUT_MODE DS18B20_DDR |= (1<<DS18B20_DQ) //Ustawianie stanu wysokiego i nieskiego #define DS18B20_LOW DS18B20_PORT &= ~(1<<DS18B20_DQ) #define DS18B20_HIGH DS18B20_PORT |= (1<<DS18B20_DQ) //Komendy dla DS18B20 #define DS18B20_CMD_SKIPROM 0xCC #define DS18B20_CMD_CONVERTTEMP 0x44 #define DS18B20_CMD_RSCRATCHPAD 0xBE #define DS18B20_CMD_WSCRATCHPAD 0x4E //Deklaracje funkcji uint8_t ds18b20_reset(void); void ds18b20_write_bit(uint8_t bit); uint8_t ds18b20_read_bit(void); void ds18b20_write_byte (uint8_t byte); uint8_t ds18b20_read_byte(void); void ds18b20_config (void); uint8_t ds18b20_read_temperature(void); #endif /* DS18B20_H_ */
Plik ds18b20.c – kod jest prosty, nie sprawdzamy tu sumy kontrolnej wyniku, ani adresu czujnika ds18b20. Stąd też wynika ograniczenie obsługi tylko jednego układu 1-wire:
/*
* ds18b20.c
*
* Created on: 20 sie 2015
* Author: wojtek
*/
#include <avr/io.h>
#include <util/delay.h>
#include "ds18b20.h"
//Reset magistrali i odpowiedz ukladu 1-wire
uint8_t ds18b20_reset(void)
{
uint8_t i;
//ustawianie 0 linii 1-wire
DS18B20_LOW;
DS18B20_OUTPUT_MODE;
_delay_us(480);
//oczekiwanie na odpowiedz z ds18b20
DS18B20_INPUT_MODE;
_delay_us(60);
//sprawdzanie czy ds18b20 odpowiedzial (ustawil 0)
i = (DS18B20_PIN & (1<<DS18B20_DQ));
_delay_us(420);
// zwracamy wynik resetu 1 - blad, ds18b20 nie odpowiedzial, 0 - OK
return i;
}
//zapis bitu na magistrale
void ds18b20_write_bit(uint8_t bit)
{
DS18B20_LOW;
DS18B20_OUTPUT_MODE;
_delay_us(1);
if (bit) DS18B20_INPUT_MODE;
_delay_us(60);
DS18B20_INPUT_MODE;
}
//odczyt bitu z magistrali
uint8_t ds18b20_read_bit(void)
{
uint8_t bit = 0;
DS18B20_LOW;
DS18B20_OUTPUT_MODE;
_delay_us(1);
DS18B20_INPUT_MODE;
_delay_us(14);
if (DS18B20_PIN & (1<<DS18B20_DQ)) bit=1;
_delay_us(45);
return bit;
}
//zapis bajtu na magistralę
void ds18b20_write_byte (uint8_t byte)
{
uint8_t i=8;
while(i--)
{
ds18b20_write_bit(byte&1);
byte>>=1;
}
}
//odczyt bajtu z magistrali
uint8_t ds18b20_read_byte(void)
{
uint8_t i=8, n=0;
while(i--)
{
//przesunięcie w prawo o jedną pozycję i zapisanie odczytanej wartości
n>>=1;
n |= (ds18b20_read_bit()<<7);
}
return n;
}
// ustawienie rozdzielczosci 9-bitowej
void ds18b20_config (void)
{
ds18b20_reset();
ds18b20_write_byte(DS18B20_CMD_SKIPROM);
ds18b20_write_byte(DS18B20_CMD_WSCRATCHPAD);
ds18b20_write_byte(0); //zapis do TH Register
ds18b20_write_byte(0); //zapis do TL Register
ds18b20_write_byte(0x1F); //zapis do conf. register rozdzielczosci 9 bit
}
//odczyt temperatury
uint8_t ds18b20_read_temperature(void)
{
uint8_t temperature[2];
uint8_t digit;
ds18b20_reset();
ds18b20_write_byte(DS18B20_CMD_SKIPROM);
ds18b20_write_byte(DS18B20_CMD_CONVERTTEMP);
while(!ds18b20_read_bit());
//Reset, pominięcie odczytu adresu, komenda odczytu Sratchpada
ds18b20_reset();
ds18b20_write_byte(DS18B20_CMD_SKIPROM);
ds18b20_write_byte(DS18B20_CMD_RSCRATCHPAD);
//Odczyt dwóch bajtów scratchpada i zapis do tablicy temperature
temperature[0] = ds18b20_read_byte(); // mlodsze bity LSB
temperature[1] = ds18b20_read_byte(); // starsze bity MSB
ds18b20_reset();
digit = temperature[0]>>4; //pozbywam sie czesci ulamkowych
digit |= (temperature[1]&0xF)<<4; //przesuwam wartosc w lewo o 4 pozycje, sumuje z mlodszym bajtem, mam znak na najstarszym bicie
//digit = (temperature[0]&0xF);
return digit; //zwracam temperaturę w liczbie całkowitej, znak na najstarszym bicie
}
Do obsługi wyświetlacza wykorzystałem kod z książki Mirosława Kardasia – Mikrokontrolery AVR Język C – podstawy programowania. Ze względu na całkowicie odmienny sprzęt, w kodzie musiałem wprowadzić małe zmiany. W obsłudze przerwania inkrementuję dodatkową zmienną check_temp, która jest sprawdzana w pętli głównej – jeśli jest większa niż 200, następuje odczyt z DS18B20. Dlaczego akurat 200? 200*5ms (co tyle jest generowane przerwanie) = 1 sekunda – jest to czas odświeżania wyniku. Wyświetlacz jest multipleksowany z częstotliwością 200Hz. Biblioteka led_7seg.h:
/* * led_7seg.h * * Created on: 20 sie 2015 * Author: wojtek */ #ifndef LED_7SEG_H_ #define LED_7SEG_H_ //definicje portow i pinow #define LED_DATA PORTD //segmenty dolaczone do portu D #define LED_DATA_DIR DDRD //kierunek portu #define ANODY_PORT PORTB //anody podlaczone do portu B #define ANODY_DIR DDRB // kierunek portu B #define CA0 (1<<PB0) //anoda pierwszej cyfry sterowana z PB0 #define CA1 (1<<PB1) #define CA2 (1<<PB2) #define CA3 (1<<PB3) #define SEG_A (1<<0) //przyporzadkowanie numeru portu do segmentu #define SEG_B (1<<1) #define SEG_C (1<<2) #define SEG_D (1<<3) #define SEG_E (1<<4) #define SEG_F (1<<5) #define SEG_G (1<<6) //extern - zmienne beda dostepne w kazdym pliku, ktory dolaczy ten plik naglowkowy, volatile - nie beda odkladane do rejestru extern volatile uint8_t cy0; extern volatile uint8_t cy1; extern volatile uint8_t cy2; extern volatile uint8_t cy3; extern volatile uint8_t check_temp; void d_led_init(void); #endif /* LED_7SEG_H_ */
Zawartość pliku led_7seg.c:
/*
* led_7seg.c
*
* Created on: 20 sie 2015
* Author: wojtek
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include "led_7seg.h"
volatile uint8_t cy0; //bufory cyfr
volatile uint8_t cy1;
volatile uint8_t cy2;
volatile uint8_t cy3;
volatile uint8_t check_temp; //do opoznienia odczytu temperatury
const uint8_t cyfry[15] PROGMEM = {
~(SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F), //0
~(SEG_B | SEG_C), //1
~(SEG_A | SEG_B | SEG_D | SEG_E | SEG_G), //2
~(SEG_A | SEG_B | SEG_C | SEG_D | SEG_G), //3
~(SEG_B | SEG_C | SEG_F | SEG_G), //4
~(SEG_A | SEG_C | SEG_D | SEG_F | SEG_G), //5
~(SEG_A | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G), //6
~(SEG_A | SEG_B | SEG_C | SEG_F), //7
~(SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G), //8
~(SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G), //9
~(SEG_A | SEG_B | SEG_F | SEG_G), //stopien
~(SEG_G), //-
~(SEG_A | SEG_D | SEG_E | SEG_F | SEG_G), //E
~(SEG_E | SEG_G), //r
0xFF //pusty
};
void d_led_init(void)
{
LED_DATA_DIR = 0xFF; //wszystkie porty jako wyjscia - sterowanie katodami
LED_DATA = 0xFF; //wygaszenie wszystkich katod
/* 4 piny portu B jako wyjscia (sterowanie anodami wyswietlaczy) */
ANODY_DIR |= CA0 | CA1 | CA2 | CA3;
/* wygaszenie wszystkich wyswietlaczy */
ANODY_PORT &= 0xF0;
// ustawienie TIMER0
TCCR0A |= (1<<WGM01); //tryb CTC
TCCR0B |= (1<<CS01) | (1<<CS00); //preskaler 64
OCR0A = 77; //dodatkowy podział‚ przez 77, dający przerwanie co ~200Hz
TIMSK |= (1<<OCIE0A); //zezwolenie na przerwanie Compare Match
}
// =================== PROCEDURA OBSŁUGI PRZERWANIA ==============================
ISR (TIMER0_COMPA_vect)
{
static uint8_t licznik = 1; //zmienna do przelaczania kolejno anod wyswietlaczy
ANODY_PORT &= 0xF0; //wygasza cyfry, zeby nie bylo podswietlania segmentow
if(licznik == 1) LED_DATA = pgm_read_byte( &cyfry[cy0] ); //na port wystaw dane zapalajace segmenty
else if (licznik == 2) LED_DATA = pgm_read_byte( &cyfry[cy1] );
else if (licznik == 4) LED_DATA = pgm_read_byte( &cyfry[cy2] );
else if (licznik == 8) LED_DATA = pgm_read_byte( &cyfry[cy3] );
//cykliczne przelaczanie w kazdym przerwaniu, podanie zasilania na anodę 0001-0010-0100-1000
ANODY_PORT = (ANODY_PORT & 0xF0) | (licznik & 0x0F); //licznik na port anod
licznik <<= 1; //przesun zawartosc licznika o 1 w lewo i wpisz w licznik
if(licznik>8) licznik = 1;
check_temp++; //zwiększ o 1 wartość check_temp
}
W pętli głównej programu jest odpytywany czujnik temperatury wraz ze sprawdzeniem jego podłączenia oraz przetworzenie odczytanej wartości – przekształcenie, jeśli temperatura jest ujemna i rozbicie wyniku na cyfry.
/*
* main.c
*
* Created on: 20 sie 2015
* Author: wojtek
* : v.1.3
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "led_7seg.h"
#include "ds18b20.h"
int main(void)
{
d_led_init(); //inicjalizacja wyswietlacza 7-seg
//wyswietla wersje softu
cy0 = 3;
cy1 = 1;
sei(); //wlaczenie globalnego zezwolenia na przerwania
ds18b20_config(); //ustawia rodzielczosc 9-bitow
_delay_ms(500);
//petla glowna
while(1)
{
if (check_temp > 200) //pomiar co ~sekunde, check_temp zwiekszane w przerwaniu
{
if(ds18b20_reset()) // sprawdzam obecnosc ds18b20, jesli nie podlaczony, Err na LED
{
cy0 = 14;
cy1 = 13; //r
cy2 = 13; //r
cy3 = 12; //E
}
else
{
//odczyt temperatury z ds18b20
uint8_t temp = ds18b20_read_temperature();
uint8_t sign = (temp & 0x80) >> 7; //sprawdzanie, czy temp ujemna
if (sign) //jesli ujemna
{
temp = ~temp; //konwersja liczby ujemnej
temp ++; //na dodatnia
}
cy0 = 10; //symbol stopien
cy1 = temp%10; //jednosci
/* wariant 1, temp + i -, znak - na cy3, brak nieznaczacych '0'
cy2 = (temp < 10 ? 14 : temp/10); //dziesiatki lub pusto gdy temp<10
cy3 = (sign == 0 ? 14 : 11); //pusto lub '-' gdy temp < 0
*/
// wariant 2, - na cy2 gdy temp <10
if (temp < 10) //jesli mniejsza od 10
{
cy2 = (sign == 1 ? 11 : 14); //i ujemna, to minus, jak dodatnia, to pusto
}
else
{
cy2 = temp/10; //jesli > 10, to cyfra dzisiatek
}
cy3 = ((sign == 1 && temp>9) ? 11 : 14);//jestli ujemna i > 10 to '-', inaczej pusto
}
check_temp = 0; //wyzeruj kontrolke odczytu z ds18b20
}
}
}
Napis Err na wyświetlaczu oznacza, że nie został podłączony czujnik DS18B20, jego linia DQ lub masa. Jeśli wyświetlany wynik to 85ºC i nie zmienia się, to czujnik 1-wire nie ma podłączonego zasilania +5V. Termometr sprawdziłem w temperaturach od -20ºC do ponad +100ºC, kod programu nie uwzględnia temperatur wyższych niż 99ºC.
Program napisałem w linuksie, korzystając z IDE Eclipse w wersji 4.5.0 (Mars). Jeśli ktoś jest zainteresowany instalacją i konfiguracją środowiska programistycznego dla mikrokontrolerów AVR, to polecam ten wpis: Instalacja narzędzi dla AVR i IDE Eclipse w Linux Mint.
Firmware zajmuje tylko 33,4% (684 bajty) pamięci flash attiny2313 i 7 bajtów w RAM, można więc dopisać dodatkowe funkcje, które zamienią termometr np. w termostat.
Mikrokontroler AVR attiny2313 musi mieć ustawione fabryczne wartości fusebitów, czyli Fuse High Byte: DFh (1101 1111) i Fuse Low Byte: 64h (0110 0100). Płytka zdemontowana z dekodera Polsatu nie posiada zaprogramowanego bitu CKDIV8 (podział częstotliwości zegara przez 8, czyli faktyczne taktowanie 1MHz), stąd konieczność zmiany w Fuse Low Byte z E4h na 64h.
Udostępniam cały projekt, który można wygodnie zaimportować do Eclipsa: atmel_display_termometr.
Działanie termometru na attiny2313
Termometr po włączeniu przez pół sekundy wyświetla wersję firmware’u w postaci 0013, czyli 1.3. Następnie przez krótką chwilę wyświetli się temperatura 85ºC i dalej zmierzona już wartość. Z uwagi na brak kropek (przecinków) na wyświetlaczu, zrezygnowałem z wyświetlania ułamkowych części temperatury – chyba bez szkody dla całości, biorąc pod uwagę dokładność samego czujnika. Poniżej kilka zdjęć z działania termometru:
Implementację wszystkich funkcjonalności kodu przedstawia poniższy film:
Termometr pobiera około 60mA przy zasilaniu napięciem +5V. Układ pozytywnie przeszedł testy kilkudniowej ciągłej pracy, nie było też problemów z obsługą czujnika DS18B20 – podłączałem w sumie 5 sztuk pochodzących z różnych źródeł.
Po publikacji wpisu o atmel_display, Grzegorz podesłał link do swojego programu termometru, który można znaleźć tutaj: https://bitbucket.org/grzechu81/dsb616_thermometer/src/. Nie testowałem tego kodu w mikrokontrolerze, ale widać tu m.in. funkcję sprawdzania crc. Inne przykłady realizacji termometru na tej płytkce (głównie Bascom) można znaleźć na elektrodzie. Napiszcie, jeśli wykorzystujecie atmel_display w inny sposób, lub macie pomysł na jakiś układ.



Mam pomysł prosty licznik do nawijarki +1 DO 9999
Do tego ta płytka nada się idealnie.