[PROJEKT] Wielokanałowy termometr i termostat z wyświetlaczem OLED [1]
Inicjalizację można podzielić na kilka etapów:
- wyzerowanie liczników: kolumn, stron pamięci i linii początkowej. Domyślnie po włączeniu zasilania (POR) te liczniki są wyzerowane,
- ustawienie powiązania zawartości pamięci RAM obrazu z pozycją na panelu OLED (orientacja wyświetlania),
- ustawienie kontrastu, przetwornicy DC/DC zasilającej drivery, i częstotliwości taktowania, oraz włączenie wyświetlenia.
Matryca wyświetlacza jest monochromatyczna i jednemu pikselowi odpowiada jeden bit w pamięci obrazu. Pamięć ma rozmiar 132×64 bity, ale w rzeczywistości jest zorganizowana bajtowo. Host wysyła kolejne bajty po magistrali SPI pod lokacje określone przez liczniki kolumn i stron. Jeden bajt w pamięci obrazu odpowiada pionowej linijce o długości 8 pikseli. Najmłodszy bit tego bajta jest pikselem położonym najwyżej w linijce, a bit najstarszy pikselem położonym najniżej. Położenie linijki w poziomie określa licznik kolumn zmieniający się w zakresie 0…131, a położenie w pionie określa licznik stron zmienia się w zakresie 0…7. Po ustaleniu numeru strony kolejne zapisywane bajty tworzą pasek o szerokości 8 pikseli. Każdy zapis danej powoduje inkrementację licznika kolumn i nowa dana jest zapisywana po kolejna lokację. Kiedy licznik kolumn osiągnie wartość 131, to po następnym zapisie danych jest zerowany. Przepełnienia licznika kolumn nie powoduje inkrementacji licznika stron i jeżeli ni zmodyfikujemy go wykorzystując do tego celu komendę „ustawienie licznika stron”, to kolejne wpisy do pamięci będą nadpisywać dane począwszy od pozycji zerowej (wyzerowanie licznika kolumn). To ważna właściwość, o której trzeba pamiętać. Na listingach 6 i 7 pokazano procedury ustawienia licznika kolumn i licznika stron.
List. 6. Ustawienie licznika kolumn
#define COLADDLOW 0 #define COLADDHIGH 0x10 #define PAGEADD 0xB0 #define LINEADD 0x40 #define OLEDON 0xAE unsigned char SetColumn(int column) { if(column>128) return(1); ++column; ++column; OledCmd(COLADDLOW|(column&0x0f)); OledCmd(COLADDHIGH|(column>>4)); return(0); }
List. 7. Ustawienie licznika stron
unsigned char SetPage(unsigned char page) { if(page>7) return(1); OledCmd(PAGEADD|page); return(0); }
Pamięć EEPROM
Pamięć EEPROM typu 24C04 ma wbudowany interfejs I2C. Do komunikacji z pamięcią został wykorzystany sprzętowy interfejs I2C2 wbudowany w mikrokontroler. Dostęp do pamięci jest realizowany przez 2 sekwencje: zapisu pojedynczej komórki pamięci i odczytu pojedynczej komórki pamięci.
Zapisanie komórki pamięci zaczyna się sekwencją START, po niej wysyłany jest adres slave z bitem R/W=0. Kolejne 2 bajty to adres komórki i bajt do zapisania w pamięci i na koniec sekwencja STOP.
Na listingu 8 został pokazana procedura zapisywania danej do komórki pamięci pod konkretny adres. Dla linii adresowych A0=A1=A2=0 adres slave jest równy 0xA0.
List. 8. Zapis danej do komórki pamięci EEPROM
void EEWr(unsigned char addr, unsigned char data) { UINT config1; CloseI2C2(); //zamknij interfejs I2C ConfigIntI2C2(MI2C_INT_OFF); //zablokuj przerwania od I2C2 config1 = (I2C_ON | I2C_7BIT_ADD);//konfiguruj interfejs I2C2 OpenI2C2(config1,39); // IdleI2C2(); StartI2C2(); //sekwencja START while(I2C2CONbits.SEN); //czekaj na zkończenie sekwencji START MI2C2_Clear_Intr_Status_Bit; //Zeruj flagi przerwania IdleI2C2(); MasterWriteI2C2(0xA0); //Zapisz adres SLAVE while(I2C2STATbits.TBF); //Czekaj na wysłanie 8 bitów while(!IFS3bits.MI2C2IF); //Czekaj na 9-ty takt zegara MI2C2_Clear_Intr_Status_Bit; //Zeruj flage przerwania od I2C2 while(I2C2STATbits.ACKSTAT);//czekaj ba bit potwierdzenia IdleI2C2(); MasterWriteI2C2(addr); //wyślij adres komórki eeprom while(I2C2STATbits.TBF); while(!IFS3bits.MI2C2IF); MI2C2_Clear_Intr_Status_Bit; while(I2C2STATbits.ACKSTAT); IdleI2C2(); MasterWriteI2C2(data); //wyślij daną do zapisu while(I2C2STATbits.TBF); while(!IFS3bits.MI2C2IF); MI2C2_Clear_Intr_Status_Bit; while(I2C2STATbits.ACKSTAT); IdleI2C2(); StopI2C2(); //sekwencja STOP while(I2C2CONbits.PEN); //czekaj na zakończenie sekwencji STOP IdleI2C2(); __delay_ms(6); // czekaj na zakończenie zapisu do komórki eeprom CloseI2C2(); //zamknij interfejs I2C2 }
Odczytywanie danej z konkretnej lokalizacji rozpoczyna się od wysłania sekwencji START, adresu slave z bitem RW=0, a po nim bajtu z adresem w pamięci EEPROM. Po tym na magistralę jest ponownie wysyłana sekwencja START, a po niej adres slave z bitem RW=1 informującym pamięć, ze ma odesłać dane. Po wysłaniu tego adresu mikrokontroler wywyła sekwencję odczytania danej z pamięci i kończy wszystko sekwencja STOP.
List. 9. Sekwencja odczytywania danej z pamięci EEPROM
unsigned char EERead(unsigned char addr) { unsigned char buf[15]; IdleI2C2(); StartI2C2(); while(I2C2CONbits.SEN); //Wait till Start sequence is completed MI2C2_Clear_Intr_Status_Bit; //Clear interrupt flag IdleI2C2(); MasterWriteI2C2(0xA0); //Write Slave address and set master for transmission while(I2C2STATbits.TBF); //Wait till address is transmitted while(!IFS3bits.MI2C2IF); //Wait for ninth clock cycle MI2C2_Clear_Intr_Status_Bit; //Clear interrupt flag while(I2C2STATbits.ACKSTAT); IdleI2C2(); MasterWriteI2C2(addr); while(I2C2STATbits.TBF); //Wait till address is transmitted while(!IFS3bits.MI2C2IF); //Wait for ninth clock cycle MI2C2_Clear_Intr_Status_Bit; //Clear interrupt flag while(I2C2STATbits.ACKSTAT); IdleI2C2(); StartI2C2(); while(I2C1CONbits.SEN); //Wait till Start sequence is completed MI2C2_Clear_Intr_Status_Bit; //Clear interrupt flag IdleI2C2(); MasterWriteI2C2(0xA1); //Write Slave address and set master for receive while(I2C2STATbits.TBF); //Wait till address is transmitted while(!IFS3bits.MI2C2IF); //Wait for ninth clock cycle MI2C2_Clear_Intr_Status_Bit; //Clear interrupt flag while(I2C2STATbits.ACKSTAT); IdleI2C2(); MastergetsI2C2(2,buf,1000); //Master recieves from Slave upto 10 bytes IdleI2C2(); //wait for the I2C to be Idle StopI2C2(); //Terminate communication protocol with stop signal while(I2C2CONbits.PEN); //Wait till stop sequence is completed return(buf[0]); //CloseI2C1(); //Disable I2C }
Czujnik temperatury DS18B20
W prezentowanym urządzeniu do jednej magistrali 1-wire dołączamy wiele czujników. Program najpierw musi wykryć ile czujników jest podłączonych, odczytać i zapamiętać ich identyfikatory zapisane w pamięci ROM każdego z czujników. Potem na podstawie tablicy identyfikatorów cyklicznie odczytywać i wyświetlać temperatury z każdego z czujników. Napisanie procedury identyfikacji nie jest zadaniem prostym, ale na szczęście firma Maxim, obecny producent DS18B20 udostępnia notę katalogową numer 162 „Interfacing DS18X20/DS1822 -1wire Temperature Sensor In a Microcontroller Enviroment. W tej nocie są dokładnie opisane czynności, które należy wykonać żeby identyfikacja została wykonana prawidłowo. Oprócz wyczerpującego opisu zamieszczono tam przykładowe fragmenty programów. Procedury obsługi magistrali 1-wire i wyszukiwania termometrów na magistrali są napisane w oparciu o informacje zawarte w tej nocie.
Na listingu 10 pokazano procedury zerowanie magistrali DS._reset, zapisania bitu na magistrali write_bit i odczytanie bitu na magistrali red_bit. Opóźnienia czasowe są generowane przez funkcję biblioteczną kompilatora MPLAB XC16.
List. 10. Procedury zerowania magistrali oraz zapisu i odczytu bitu z magistrali
unsigned char DS_reset(void) { unsigned char presence; DQPORT=0; DQ=OUTPUT; DQ = 0; //pull DQ line low __delay_us(480); // stan niski przez 480us DQ=INPUT; // allow line to return high __delay_us(70); // czakaj na stan presence presence = DQPORT; // odczytaj sygnał presence __delay_us(410); // czekaj na koniec szczeliny czasowej return(presence); // 0=presence, 1 = nie ma układu na magistrali } // WRITE_BIT – zapis bitu bitval na magistrlę 1-wire void write_bit(unsigned char bitval) { __delay_us(1); DQ=OUTPUT; //DQ w stan niski __delay_us(10); if(bitval==1) DQ =1; // DQ w stan wysoki jeżeli zapisujemy jedynkę __delay_us(50); // stan zapisywanego bitu na magistrali przez całą szczelinę czasową DQ=INPUT; // zwolnienie DQ (stan wysoki) } // READ_BIT – odczytanie bitu z magistrali 1-wire unsigned char read_bit(void) { unsigned char retval; __delay_us(1); DQ=OUTPUT;//DQ=0 – start szczeliny czasowej __delay_us(2); DQ=INPUT; // zwolnienie magistrali __delay_us(10); retval=DQPORT;//odczytaj stan linii magistrali __delay_us(48); // dokończenie szceliny czasowej odczytu (min 60usek) return(retval); }
Do realizacji magistrali 1-wire wykorzystano możliwości portów w mikrokontrolerach Microchipa. Kierunek przesyłania danych na linii portu jest określony przez zapisanie odpowiedniego bitu w rejestrze SFR TRISx. Jeżeli na przykład bit TRISB4 dla linii portu RB4 jest wyzerowany, to linia jest wyjściowa, a jeżeli ustawiony to linia jest wejściowa. Drugi rejestr PORT odpowiada za odczytywanie stanu linii ustawionej jako wejściowa, lub zapisywanie stanu linii ustawionej jako wyjściowa. Załóżmy, że do rejestru PORTB4 linii RB4 zapiszemy zero, a linia będzie podciągnięta do plusa zasilania przez rezystor. Po wpisaniu do TRISB zera linia staje się wyjściową i pojawi się na niej stan niski, bo do PORTB4 zostało wpisane zero. Kiedy do TRISB4 wpiszemy jedynkę, to linia staje się wejściową i rezystor wymusza na niej stan wysoki. Manipulowanie stanem jest realizowane przez zapisywanie rejestru TRISB4. Inicjalizacja portów została na listingu 11.
List. 11. Inicjalizacja linii DQ i wyszukiwanie czujników
#define OUTPUT 0 #define INPUT 1 #define DQ TRISBbits.TRISB4 #define DQPORT PORTBbits.RB4 void DS18B20Init(void) { DQ=OUTPUT; DQPORT = 0;//zapisanie do RB4 stanu niskiego DQ=INPUT; DS_reset(); //zerowanie magistrali FindDevices(); //procedura wyszukiwania czujników temperatury }
Procedura Find devices (listing 12) wyszukuje czujniki na magistrali i tworzy tablicę FoundROM z odczytanymi numerami seryjnymi DS18B20.
List. 12. Wyszukiwanie czujników
// FIND DEVICES void FindDevices(void) { unsigned char m; if(!DS_reset()) { // początek wyszukiwania, kiedy wykryto presence if(First()) { numROMs=0; do { numROMs++; for(m=0;m<8;m++) { FoundROM[numROMs][m]=ROM[m]; //Identifies ROM } } while (Next()&&(numROMs<MaxROMs)); //Continues until no additional devices are found } } }
Procedury First i Next używane do wyszukiwania zostały pokazane na listingach 13 i 14.
List. 13. Wyszukiwanie pierwszego czujnika na magistrali
//wyszukiwanie pierwszego czujnika na magistrali unsigned char First(void) { lastDiscrep = 0; doneFlag = FALSE; return Next(); }
List. 14. Wyszukiwanie kolejnych czujników
// NEXT //wyszukiwanie kolejnych czujników na magistrali //jeżeli nie ma czujników, to funkcja zwraca FALSE unsigned char Next(void) { unsigned char m = 1; unsigned char n = 0; unsigned char k = 1; unsigned char x = 0; unsigned char discrepMarker = 0; unsigned char g; unsigned char nxt; int flag; nxt = FALSE; dowcrc = 0; flag = DS_reset(); // reset the 1-Wire if(flag||doneFlag) { //nie ma czujnika – powrót z FALSE lastDiscrep = 0; // return FALSE; } write_byte(0xF0); // wyślij komendę SearchROM do { // dla wszyskich bajtów x = 0; if(read_bit()==1) x = 2; __delay_us(120); if(read_bit()==1) x |= 1; if(x ==3) // nie ma czujników na magistrali 1-wire break; else { if(x>0) g = x>>1; else { if(m<lastDiscrep) g = ((ROM[n]&k)>0); else g = (m==lastDiscrep); if (g==0) discrepMarker = m; } if(g==1) ROM[n] |= k; else ROM[n] &= ~k; write_bit(g); m++; k = k<<1; if(k==0) { ow_crc(ROM[n]); n++; k++; } } } while(n<8); if(m<65||dowcrc) lastDiscrep=0; else { lastDiscrep = discrepMarker; doneFlag = (lastDiscrep==0); nxt = TRUE; // } return nxt; }