- zakres pomiarowy temperatury: -40…125°C
- rozdzielczość pomiaru temperatury: 0,5°C
- dokładność pomiaru temperatury: ±2°C
- obsługiwane rozkazy: SEARCH_ROM, READ_ROM, MATCH_ROM, SKIP_ROM, CONVERT_T oraz READ_SCRATCHPAD
- napięcie zasilania: 2,7…5 V, pobierany prąd: 6 mA
Program sterujący
Cała magia emulatora czujnika temperatury DS1820 dzieje się w oprogramowaniu urządzenia. Jak wiemy, każda operacja mająca miejsce na magistrali 1-Wire inicjowana jest przez układ Master poprzez ściągnięcie tejże magistrali do logicznego „0” (przez czas 1…5 μs).
W związku z tym naturalnym sposobem na obsłużenie protokołu ze strony układu Slave jest wykorzystanie przerwania zewnętrznego (np. INT0) skonfigurowanego w ten sposób aby zachodziło przy opadającym zboczu sygnału. Użycie przerwania zewnętrznego jest o tyle niezbędne, że magistrala 1-Wire wymusza dość rygorystyczne wymogi czasowe, więc zastosowanie typowego portu I/O i tzw. pollingu jest niewystarczająca w przypadku, gdyby układ Slave miał wykonywać jeszcze inne czasowo istotne operacje niezwiązane z obsługą magistrali. Wykorzystanie przerwania zewnętrznego niesie za sobą tą dodatkową zaletę, że cała obsługa protokołu zostanie zamknięta w ramach jednej funkcji ISR a program główny może realizować inną, niezbędną z punktu widzenia konkretnej aplikacji, funkcjonalność.
Zadeklarujmy, zatem kilka podstawowych zmiennych globalnych: numer seryjny naszego urządzenia Slave, scratchpad (pamięć termometru), zmienną przechowującą aktualny stan procedury obsługi układu Slave oraz zmienną odpowiedzialną za żądanie pomiaru temperatury. Jak wiemy, zmienne globalne to w dużym uproszczeniu zło (z przymrużeniem oka), gdyż utrudniają optymalizację kodu, lecz w przypadku styku na poziomie program główny/funkcje narzędziowe a funkcje obsługi przerwań systemowych są niezbędną koniecznością. Specyfikację zmiennych globalnych naszego urządzenia pokazano na listingu 1.
//Status Slave’a 1-wire
volatile uint8_t Status;
//ID układu Slave. 8. bajt to CRC8
volatile uint8_t ID[8] = {‘R’, ‘o’, ‘b’, ‘e’, ‘r’, ‘t’, ‘W’, 0x00};
//Scratchpad układu Slave. 1. i 2. bajt to LSB i MSB temperatury. 9. bajt to CRC8
volatile uint8_t Scratchpad[9] = {0xAA, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x0C, 0x10, 0x00};
//Żądanie pomiaru temperatury
volatile uint8_t measureTemp;
Dalej, na listingu 2, pokazano plik nagłówkowy programu obsługi aplikacji definiujący zarówno ustawienia sprzętowe, jak i wprowadzający niezbędne definicje stałych poprawiających czytelność kodu (jak i ułatwiających jego modyfikacje).
//Definicje portu 1-wire
#define ONE_WIRE_DDR DDRB
#define ONE_WIRE_PIN PINB
#define ONE_WIRE_NR PB2 //INT0
#define ONE_WIRE_CLEAR ONE_WIRE_DDR |= (1<<ONE_WIRE_NR)
#define ONE_WIRE_RELEASE ONE_WIRE_DDR &= ~(1<<ONE_WIRE_NR)
#define ONE_WIRE_READ ((ONE_WIRE_PIN & (1<<ONE_WIRE_NR))>>ONE_WIRE_NR)
//Definicje dla Timera0
#define STOP_TIMER TCCR0B = 0x00
//Preskaler = 1024, 7812Hz
#define START_TIMER TCCR0B = (1<<CS02)|(1<<CS00)
#define RESET_TIMER TCNT0 = 0
#define TIMEOUT_2MS (256-15)
#define TIMEOUT_5MS (256-39)
#define TIMEOUT_15MS (256-117)
//Definicje statusów układu Slave
#define STATUS_IDLE 0x00
#define STATUS_WAITING_FOR_CMD 0x01
#define STATUS_SEARCH_ROM_ACTIVE 0x02
#define STATUS_READ_ROM_ACTIVE 0x03
#define STATUS_MATCH_ROM_ACTIVE 0x04
#define STATUS_SLAVE_SELECTED 0x05
#define STATUS_READ_SCRATCHPAD_ACTIVE 0x06
//Definicje komend 1-wire
#define CMD_SEARCH_ROM 0xF0
#define CMD_READ_ROM 0x33
#define CMD_MATCH_ROM 0x55
#define CMD_SKIP_ROM 0xCC
#define CMD_READ_SCRATCHPAD 0xBE
#define CMD_CONVERT_T 0x44
//Definicje dla mechanizmu wyszukiwania numeru ID
#define SEARCH_SEND_BYTE 0x00
#define SEARCH_SEND_NEGATED_BYTE 0x01
#define SEARCH_COMPARE_BYTE 0x02
//Definicje czasów 1wire - us
#define INTR_HANDLING_TIME 6
#define RESET_PULSE_MIN 3 //Takty zegara Timer0
#define PRESENCE_PULSE 240
#define SLAVE_SAMPLE_TIME (25-INTR_HANDLING_TIME)
#define SLAVE_ZERO_TIME 20
W związku z tym, że obsługa protokołu odbywa się w całości w ramach procedury obsługi przerwania INT0 (oraz jak się okaże, przerwania Timera0) funkcja główna naszego programu obsługi ogranicza się wyłącznie do konfiguracji niezbędnych peryferiów mikrokontrolera i wykonywania pomiaru temperatury na żądanie użytkownika. Ciało tejże funkcji pokazano na listingu 3.
int main(void){
//Redukcja poboru mocy przez wyłączenie modułów
//(lub ich zegarów): TIMER1, USI
PRR = (1<<PRTIM1)|(1<<PRUSI);
//Wyłączenie komparatora analogowego
//dla zmniejszenia poboru mocy
ACSR = (1<<ACD);
//Konfiguracja przetwornika ADC
//Ref = 1.1V, wejście ADC2
ADMUX = (1<<REFS1)|(1<<MUX1);
//Konfiguracja i uruchomienie przerwania INT0
//wyzwalane zboczem opadającym - obsługa 1wire
MCUCR |= (1<<ISC01);
GIMSK = (1<<INT0);
//Uruchomienie przerwania od przepełnienia Timera0
//obsługa timeout-ów
TIMSK = (1<<TOIE0);
//Odczytujemy numer ID lub zostajemy przy domyślnym
readID();
sei();
while(1){
//Obsługa żądania pomiaru temperatury
if(measureTemp){
measureTemp = 0;
readADC();
}
}
}
Jak widać, i o czym nie wspomniano wcześniej, w programie obsługi używany jest także timer sprzętowy (w tym przypadku 8-bitowy Timer0) jak i przerwanie od jego przepełnienia (TIMER0_OVF_vect). Użycie tego peryferium konieczne jest w przypadku, gdyby układ Master z jakiś powodów wysłał niepełną sekwencję sygnałów sterujących (np. tylko sygnał Reset bez jakichkolwiek dalszych komend) co spowodowałoby zmianę stanu procedury obsługi magistrali 1-Wire na predefiniowaną wcześniej wartość STATUS_WAITING_FOR_CMD i oczekiwanie na rozkaz sterujący uniemożliwiając tym samym dalsze, poprawne funkcjonowanie algorytmu obsługi. Timer ten każdorazowo ustawiany jest w taki sposób aby po upłynięciu zadanego czasu (zależnego od oczekiwanych operacji po stronie układu Master) przywrócić spoczynkowy stan procedury obsługi (STATUS_IDLE) w przypadku błędów po stronie układu nadrzędnego.
Przejdźmy zatem do właściwego programu obsługi układu Slave magistrali 1-Wire. Jak wspomniano wcześniej, stanem spoczynkowym układu Slave jest oczekiwanie na sygnał Reset a po jego wykryciu, wystawienie sygnału Presence i oczekiwanie na rozkaz sterujący.
Tą część algorytmu programu obsługi pokazana na listingu 4 (o wykonaniu poszczególnych części programu obsługi decyduje wartość zmiennej Status). Po wykonaniu tych czynności, układ Slave oczekuje (przez czas 2 ms) na odebranie komendy sterującej i w zależności od jej rodzaju realizuje pozostałe funkcje typowe dla emulowanego układu typu DS1820.
//Gotowy na sygnał Reset - stan wyjściowy
case STATUS_IDLE:
//Sprawdzamy długość sygnału niskiego oczekując sygnału Reset
RESET_TIMER;
START_TIMER;
while(!ONE_WIRE_READ);
STOP_TIMER;
if(TCNT0 >= RESET_PULSE_MIN) {
//Wysyłamy sygnał Presence
delay_us(15);
ONE_WIRE_CLEAR;
_delay_us(PRESENCE_PULSE);
ONE_WIRE_RELEASE;
//Przygotowanie zmiennych i zmiana statusu
bitNr = Byte = 0;
Status = STATUS_WAITING_FOR_CMD;
//Uruchomienie Timera0 jako układu
//odmierzającego timeout
setTimeout(TIMEOUT_2MS);
}
break;
Nasze urządzenie obsługuje 6 rodzajów komend sterujących: CMD_SEARCH_ROM, CMD_READ_ROM, CMD_MATCH_ROM, CMD_SKIP_ROM, CMD_CONVERT_T oraz CMD_READ_SCRATCHPAD. Odbiór komendy sterującej realizuje część programu obsługi pokazana na listingu 5.
//Gotowy na przyjęcie rozkazu - po wyemitowaniu sygnału Presence
case STATUS_WAITING_FOR_CMD:
//Czekamy 30us aż znajdziemy się w środku
//przedziału czasu na emisję bitu
_delay_us(SLAVE_SAMPLE_TIME);
//Odczyt bitu
if(ONE_WIRE_READ) Byte |= (1<<bitNr);
//Jeśli mamy kompletny bajt rozkazu to sprawdzamy
// jaki to rozkaz i decydujemy o dalszym toku programu
if(++bitNr == 8) {
switch(Byte){
case CMD_SEARCH_ROM:
Status = STATUS_SEARCH_ROM_ACTIVE;
//Uruchomienie Timera0
//jako układu odmierzającego timeout
setTimeout(TIMEOUT_15MS);
break;
case CMD_READ_ROM:
Status = STATUS_READ_ROM_ACTIVE;
//Uruchomienie Timera0
//jako układu odmierzającego timeout
setTimeout(TIMEOUT_5MS);
break;
case CMD_MATCH_ROM:
Status = STATUS_MATCH_ROM_ACTIVE;
//Wyzerowanie bufora danych
for(uint8_t i = 0; i<8; i++) Buffer[i] = 0;
//Uruchomienie Timera0
//jako układu odmierzającego timeout
setTimeout(TIMEOUT_5MS);
break;
case CMD_SKIP_ROM:
Status = STATUS_SLAVE_SELECTED;
//Uruchomienie Timera0
//jako układu odmierzającego timeout
setTimeout(TIMEOUT_2MS);
break;
default:
//Nieobsługiwany rozkaz
//wracamy do stanu wyjściowego
Status = STATUS_IDLE;
STOP_TIMER;
break;
}
bitNr = byteNr = searchNr = Byte = 0;
}
break;
Następnie, w zależności od rodzaju odebranej komendy sterującej, realizowana jest odpowiednia część programu obsługi (o wykonaniu poszczególnych części programu obsługi jak zwykle decyduje wartość zmiennej Status). Rozkaz READ_ROM (czyli odczyt numeru seryjnego przez układ Master) realizuje część programu obsługi aplikacji pokazana na listingu 6.
//Jesteśmy w trybie STATUS_READ_ROM_ACTIVE,
//czyli odczytywania numeru ID przez układ Master
case STATUS_READ_ROM_ACTIVE:
//Czekamy na zwolnienie magistrali przez Mastera
//aby nie liczyć tego samego bitu jako kolejnego
while(!ONE_WIRE_READ);
//Wysyłamy kolejny bit adresu układu Slave.
//Jeśli bit=0 to przedłużamy stan niski
if(!(ID[byteNr] & (1<<bitNr))) {
ONE_WIRE_CLEAR;
_delay_us(SLAVE_ZERO_TIME);
ONE_WIRE_RELEASE;
}
//Jeśli wysłaliśmy kompletny bajt adresu
//to przechodzimy do kolejnego
if(++bitNr == 8) {
bitNr = 0;
//Sprawdzamy, czy wysłaliśmy wszystkie bajty adresu.
//Jeśli tak to przechodzimy do stanu spoczynkowego.
if(++byteNr == 8) {
Status = STATUS_IDLE;
STOP_TIMER;
}
}
break;
Rozkaz MATCH_ROM (czyli wysyłania przez układ Master numeru ID aby zaadresować naszego Slave-a) realizuje część programu obsługi aplikacji pokazana na listingu 7.
//Jesteśmy w trybie STATUS_MATCH_ROM_ACTIVE,
//czyli wysyłania przez układ Master numeru ID by zaadresować naszego Slave-a
case STATUS_MATCH_ROM_ACTIVE:
//Czekamy 30us aż znajdziemy się
//w środku przedziału czasu na emisję bitu
_delay_us(SLAVE_SAMPLE_TIME);
//Odczyt bitu
if(ONE_WIRE_READ) Buffer[byteNr] |= (1<<bitNr);
//Jeśli mamy kompletny bajt adresu
// to przechodzimy do kolejnego
if(++bitNr == 8)
bitNr = 0;
//Sprawdzamy, czy odebraliśmy
//kompletny numer ID.
if(++byteNr == 8){
//Odebraliśmy kompletny numer ID,
//więc porównujemy go z naszym i
//podejmujemy decyzję o dalszym toku programu
Byte = 0;
for(uint8_t i = 0; i<8; i++) if(Buffer[i] != ID[i]) Byte = 1;
if(Byte) {
Status = STATUS_IDLE;
STOP_TIMER;
} else {
Status = STATUS_SLAVE_SELECTED;
//Uruchomienie Timera0
//jako układu odmierzającego timeout
setTimeout(TIMEOUT_2MS);
//Przygotowanie zmiennych
bitNr = byteNr = Byte = 0;
}
}
}
break;
Jak widać, przesłanie niepoprawnego adresu układu Slave powoduje dezaktywację naszego urządzenia (stan domyślny STATUS_IDLE wymuszający oczekiwanie na sygnał RESET), zaś przesłanie adresu zgodnego powoduje adresację urządzenia (STATUS_SLAVE_SELECTED) i oczekiwanie na przesłanie bajta realizowanej funkcjonalności (rozkazy CMD_CONVERT_T i CMD_READ_SCRATCHPAD). Skądinąd takie samo zachowanie powoduje przesłanie rozkazu CMD_SKIP_ROM (pokazane na listingu 5), który wymusza pominięcie adresacji konkretnego urządzenia i ma sens wyłącznie wtedy, gdy na magistrali 1-Wire znajduje się tylko jedno urządzenie typu Slave.
Dalej, na listingu 8 pokazano fragment algorytmu programu obsługi odpowiedzialny za realizację funkcji CMD_SEARCH_ROM, czyli wyszukiwania przez układ Master numerów ID podłączonych do magistrali układów Slave.
//Jesteśmy w trybie STATUS_SEARCH_ROM_ACTIVE,
//czyli wyszukiwania przez układ Master numeru ID
case STATUS_SEARCH_ROM_ACTIVE:
//Dla każdego bitu przeprowadzamy 3 operacje
switch(searchNr) {
case SEARCH_SEND_BYTE:
//Wysyłamy bit adresu układu Slave.
//Jeśli bit=0 to przedłużamy stan niski
if(!(ID[byteNr] & (1<<bitNr))) {
ONE_WIRE_CLEAR;
_delay_us(SLAVE_ZERO_TIME);
ONE_WIRE_RELEASE;
}
break;
case SEARCH_SEND_NEGATED_BYTE:
//Wysyłamy zanegowany bit adresu układu Slave.
//Jeśli bit=1 to przedłużamy stan niski
if(ID[byteNr] & (1<<bitNr)) {
ONE_WIRE_CLEAR;
_delay_us(SLAVE_ZERO_TIME);
ONE_WIRE_RELEASE;
}
break;
case SEARCH_COMPARE_BYTE:
//Układ Master przesyła w odpowiedzi bit
//a Slave porównuje ze swoim bitem w tym miejscu
//Jeśli się zgadza to proces idzie dalej
//a jeśli nie to Slave się resetuje i czeka na sygnał Reset
_delay_us(SLAVE_SAMPLE_TIME);
//Odczyt bitu i porównanie z przesyłanym wcześniej
if(ONE_WIRE_READ != ((ID[byteNr] & (1<<bitNr))>>bitNr)) {
Status = STATUS_IDLE;
STOP_TIMER;
}
break;
}
//Sprawdzamy, czy wszystkie 3 operacje wykonano dla bieżącego bitu
if(++searchNr > SEARCH_COMPARE_BYTE) {
searchNr = SEARCH_SEND_BYTE;
//Jeśli przesłaliśmy kompletny bajt adresu
//to przechodzimy do kolejnego
if(++bitNr == 8) {
bitNr = 0;
//Sprawdzamy, czy przesłaliśmy
//kompletny numer ID.
if(++byteNr == 8) {
Status = STATUS_IDLE;
STOP_TIMER;
}
}
}
break;
Tak jak wspomniano wcześniej poprawne zaadresowanie układu Slave powoduje przejście urządzenia w tryb oczekiwania (STATUS_SLAVE_SELECTED) na przesłanie bajta realizowanej funkcjonalności (rozkazy CMD_CONVERT_T i CMD_READ_SCRATCHPAD). Za ten etap programu obsługi odpowiada fragment algorytmu programu obsługi pokazany na listingu 9.
//Jesteśmy w trybie STATUS_SLAVE_SELECTED,
//czyli nasz Slave został zaadresowany poleceniem MATCH_ROM lub
//wybrany poprzez polecenie SKIP_ROM. W takim wypadku oczekujemy
//na rozkaz dotyczący wykonywanej operacji
case STATUS_SLAVE_SELECTED:
//Czekamy 30us aż znajdziemy się w środku przedziału czasu na emisję bitu
_delay_us(SLAVE_SAMPLE_TIME);
//Odczyt bitu
if(ONE_WIRE_READ) Byte |= (1<<bitNr);
//Jeśli mamy kompletny rozkaz to przechodzimy
// do kolejnego etapu programu
if(++bitNr == 8) {
switch(Byte) {
case CMD_CONVERT_T:
//Żądanie pomiaru temperatury
measureTemp = 1;
Status = STATUS_IDLE;
STOP_TIMER;
break;
case CMD_READ_SCRATCHPAD:
Status = STATUS_READ_SCRATCHPAD_ACTIVE;
//Uruchomienie Timera0
//jako układu odmierzającego timeout
setTimeout(TIMEOUT_5MS);
//Przygotowanie zmiennych
bitNr = byteNr = 0;
break;
default:
//Nieobsługiwany rozkaz
//wracamy do stanu wyjściowego
Status = STATUS_IDLE;
STOP_TIMER;
break;
}
}
break;
Jak widać, odbiór rozkazu CMD_CONVERT_T powoduje ustawienie zmiennej globalnej measureTemp wymuszającej wykonanie pomiaru temperatury realizowane w pętli głównej aplikacji i powodujące wpisanie (atomowe) wyników pomiaru do tablicy Scratchpad, zaś odebranie rozkazu CMD_READ_SCRATCHPAD powoduje przejście programu obsługi do fragmentu odpowiedzialnego za wysłanie (na żądanie Mastera) przez układ Slave zawartości tablicy Scratchpad, przy czym liczba bajtów, jaka zostanie przesłana zależy wyłącznie od przebiegu transmisji inicjowanej przez układ nadrzędny (od 1 do 9 bajtów). Odczyt przez układ Master liczby bajtów mniejszej niż cała zawartość scratchpad-a (9 bajtów) powoduje wystąpienie timeout-u i powrót stanu urządzenia Slave do pozycji wyjściowej (STATUS_IDLE) i oczekiwanie na sygnał RESET.
Uważny Czytelnik zastanowi się z pewnością, dlaczego rozkazy CMD_CONVERT_T i CMD_READ_SCRATCHPAD nie są obsługiwane od razu po detekcji sygnału RESET i wysłaniu sygnału PRESENCE (listing 5). Otóż nie jest to możliwe, gdyż wykonanie tych rozkazów wymaga wcześniejszej adresacji układu Slave dokonywanej dzięki obsłudze rozkazów CMD_SKIP_ROM i CMD_MATCH_ROM. Na listingu 10 pokazano fragment algorytmu programu obsługi odpowiedzialny za realizację funkcji READ_SCRATCHPAD.
//Jesteśmy w trybie STATUS_READ_SCRATCHPAD_ACTIVE,
//czyli układ Master będzie czytał zawartość scratchpad-a.
//Maksymalnie 9 bajtów.
case STATUS_READ_SCRATCHPAD_ACTIVE:
//Czekamy na zwolnienie magistrali przez Mastera
//aby nie liczyć tego samego bitu jako kolejnego
while(!ONE_WIRE_READ);
//Wysyłamy kolejny bit scratchpad-a układu Slave.
//Jeśli bit=0 to przedłużamy stan niski
if(!(Scratchpad[byteNr] & (1<<bitNr))) {
ONE_WIRE_CLEAR;
_delay_us(SLAVE_ZERO_TIME);
ONE_WIRE_RELEASE;
}
//Jeśli wysłaliśmy kompletny bajt scratchpad-a
//to przechodzimy do kolejnego
if(++bitNr == 8) {
bitNr = 0;
//Sprawdzamy, czy wysłaliśmy wszystkie bajty scratchpad-a.
//Jeśli tak to przechodzimy do stanu spoczynkowego.
if(++byteNr == 9) {
Status = STATUS_IDLE;
STOP_TIMER;
}
}
break;
Na koniec tej tematyki wspomniana wcześniej funkcja obsługi przerwania od przepełnienia licznika Timer0 niezbędna z punktu widzenia odmierzania tzw. czasu timeout, której to ciało pokazano na listingu 11.
ISR(TIM0_OVF_vect) {
Status = STATUS_IDLE;
STOP_TIMER;
}
Kilka niezbędnych słów uwagi wymaga tematyka czasu latencji procedury obsługi przerwania INT0 realizującej obsługę interfejsu 1-Wire, które to zagadnienie jest szczególnie istotne w przypadku wysyłania danych przez układ Slave. Jak wiemy odczyt danych przez układ Master inicjowany jest poprzez wygenerowanie opadającego zbocza sygnału (ściągnięcie magistrali do logicznego „0”) przez czas z zakresu 1…5 μs. Po wystąpieniu takiego zbocza sygnału układ Slave wystawia na magistralę bieżący bit danych a układ Master dokonuje jego odczytu, przy czym czas od wystąpienia opadającego zbocza sygnału do operacji odczytu nie może przekroczyć 15 μs. Zwykle implementuje się, że układ Master dokonuje odczytu przesłanego bitu na końcu okna odczytu, czyli po ok. 15 μs od wystąpienia opadającego zbocza sygnału. Jest to dla nas o tyle istotne, że procedura obsługi przerwania INT0 musi zostać napisana w taki sposób, aby zapewnić wystawienie bitu na magistralę w nieprzekraczalnym czasie, o którym wspomniałem powyżej a najlepiej jak najszybciej po zwolnieniu magistrali przez układ Master (po ustąpieniu ściągnięcia magistrali).
Jak wiemy standardowy czas latencji od wystąpienie przerwania do skoku do wektora przerwania dla mikrokontrolerów AVR wynosi 4 takty zegara taktującego, a więc bardzo mało. W takim razie to nie stanowi potencjalnego problemu przy taktowaniu zegarem o częstotliwości 8 MHz. Jest jednak małe, acz istotne „ale”. Skok do wektora przerwania nie jest równoznaczny z natychmiastową reakcją programu obsługi przerwania na zainicjowany proces odczytu. Zanim program przejdzie do tego miejsca wykonywanych jest szereg innych instrukcji, których obecność wynika z pracy kompilatora, który dba o integralność rejestrów procesora używanych w procedurze obsługi przerwania tak, aby ich wartości po wyjściu z tejże procedury pozostały niezmienione (odkłada je na stos). Co więcej, na liczbę tych niezbędnych operacji nie mamy większego wpływu, gdyż piszemy w języku wysokiego poziomu, a co za tym idzie, ten aspekt programowania pozostawiamy kompilatorowi. Na szczęście możemy podejrzeć plik deasemblacji (*.lss) i przekonać się, jak wygląda wygenerowany kod maszynowy co jednocześnie pozwala ocenić czas wykonania poszczególnych partii programu. Innym sposobem jest wykorzystanie wbudowanego w środowisko programistyczne symulatora.
Właśnie użycie pierwszej z możliwości pozwoliło mi ocenić czas odpowiedzi naszego urządzenia na żądanie odczytu przesłane przez układ Master, który w przybliżeniu wynosi 50 taktów zegara, czyli w granicach 6,25 μs. Potwierdziłem to również w symulatorze środowiska Microchip Studio. Jest to wartość w zupełności akceptowalna i pozwalająca zmieścić się w 15 μs oknie odczytu urządzenia Master. W przypadku zapisu do układu Slave latencja nie jest aż tak krytycznym elementem, gdyż odczyt stanu magistrali dokonywany przez układ podrzędny zachodzi dopiero po upłynięciu czasu 30 μs od wystąpienia opadającego zbocza sygnału. Niemniej jednak nawet w tym wypadku, jak i w ogóle w przypadku implementacji funkcji ISR, ważny jest sumaryczny, maksymalny czas obsługi zdarzenia (czyli od wejścia do wyjścia z funkcji ISR), który nie może przekroczyć wartości 60 μs, czyli czasu trwania pojedynczego bitu (gdyż z takim interwałem będzie wywoływane przerwanie INT0).
Przeprowadzone testy praktyczne potwierdzone wcześniejszą symulacją w środowisku Microchip Studio wykazały, że maksymalny czas obsługi zdarzenia wynosi około 51 μs, a więc całkiem sporo. Wynika to poniekąd z czasu oczekiwania (30 μs) na odczyt stanu magistrali dokonywany przez układ podrzędny (w przypadku zapisu przez układ Master) i jest pokłosiem przyjętego rozwiązania programowego implementacji magistrali 1-Wire w wersji Slave. Czas ten można byłoby wydatnie skrócić poprzez zrezygnowanie z oczekiwania wspomnianych 30 μs, lecz wymagałoby to wykorzystania dodatkowego timera (oraz przerwania od jego przepełnienia) oraz gruntownej modyfikacji i komplikacji kodu obsługi wszystkich przerwań, co z pewnością odbiłoby się na wielkości kodu wynikowego aplikacji. Nie zdecydowałem się na ten krok, gdyż zachowujemy odpowiedni margines bezpieczeństwa a jedyne zmiany, jakie postanowiłem wprowadzić to skrócenie czasu oczekiwania na odczyt stanu magistrali dokonywany przez układ podrzędny do wartości 25 μs (opcjonalnie można ustawić 20 μs), przez co sumaryczny, maksymalny (dla najgorszego przypadku) czas obsługi przerwania INT0 wynosi 46 μs. Jest to wartość w pełni bezpieczna. Co prawda podczas obsługi transmisji 1-Wire pozostaje wyłącznie około 24% czasu procesora na wykonywanie innych zadań (np. w pętli głównej), ale nasze urządzenie w zasadzie żadnych innych zadań nie wykonuje, gdyż pomiar temperatury inicjowany jest tylko i wyłącznie rozkazem przesłanym magistralą przez co w czasie pomiaru nie odbywa się żaden „ruch” w ramach tego medium.
Poza tym pomiar dokonywany przy udziale przetwornika ADC nie koliduje w żaden sposób z implementacją magistrali 1-Wire. Niemniej jednak Czytelnicy, którzy chcieliby samodzielnie zastosować omówione rozwiązania w swoim oprogramowaniu muszą te kwestie szczegółowo przeanalizować nawet wtedy, gdy zdecydują się na zwiększenie częstotliwości taktowania mikrokontrolera, co samo w sobie minimalizuje ryzyko potencjalnych problemów będąc tak naprawdę rozwiązaniem najprostszym, lecz nie pozbawionym wad. Główną wadą takiej solucji jest wydatne zwiększenie zapotrzebowania na moc co niejednokrotnie nie jest pożądane.
Uff, tyle w kwestii implementacji magistrali 1-Wire w wersji Slave i emulacji termometru DS1820. Wiem, że było to dość długie opracowanie, ale moim zdaniem bardzo wartościowe z punktu widzenia poznania zasad działania tego medium transmisyjnego i zastosowania go do swoich potrzeb. Już na sam koniec 2 funkcje niezwiązane bezpośrednio z samą realizacją obsługi magistrali 1-Wire, ale niezbędne do emulacji układu DS1820. Pierwsza z nich to funkcja dokonująca pomiaru napięcia przetwornika TC1047A i konwertująca otrzymane dane na format temperatury zgodny ze specyfikacją układu DS1820 a następnie zapisująca je w pamięci Scratchpad. Ciało tej funkcji pokazano na listingu 12.
void readADC(void) {
int16_t Temperature;
uint8_t Sign, CRC8 =0;
//Start konwersji - Prescaler= 64 (125kHz)
ADCSRA = (1<<ADEN)|(1<<ADSC)|(1<<ADPS2)|(1<<ADPS1);
//Czekamy na jej zakończenie - 120us
while(ADCSRA & (1<<ADSC));
//Przeliczenie na zakres -80...250 (jednostka 0.5’C)
Temperature = -100 + ((ADC * (int32_t) 3418) / (int32_t) 10000);
if(Temperature < 0) Sign = 0xFF; else Sign = 0x00;
Temperature &= 0xFF;
//Obliczamy CRC8 z pierwszych 8 bajtów
//Aktualizacja CRC8
_crc_ibutton_update(CRC8, Temperature);
//Aktualizacja CRC8
_crc_ibutton_update(CRC8, Sign);
//Aktualizacja CRC8
for(uint8_t i = 2; i<8; i++)
CRC8 = _crc_ibutton_update(CRC8, Scratchpad[i]);
//Atomowa aktualizacja scratchpad-a
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
Scratchpad[0] = Temperature;
Scratchpad[1] = Sign;
Scratchpad[8] = CRC8;
}
}
Dla dociekliwych dodam, że pomiar napięcia przetwornika ADC, jego konwersja na temperaturę w standardzie układu DS1820, obliczenie CRC8 i aktualizacja scratchpad-a zajmuje ok. 270 μs, więc dokładnie tyle potrzeba aby po wysłaniu rozkazu CONVERT_T przystąpić do odczytu scratchpad-a, zapominając o magicznych 750 ms, jakie potrzebował układ DS1820 na konwersję temperatury. Druga i zarazem ostatnia funkcja to funkcja odpowiedzialna za odczyt adresu ID układu Slave z pamięci EEPROM mikrokontrolera (jeśli został tam zapisany) lub pozostanie przy adresie domyślnym utworzonym podczas definicji tablicy ID. Ciało tej funkcji pokazano na listingu 13.
void readID(void) {
uint8_t CRC8 = 0;
//Sprawdzamy czy w EEPROMie zapisano unikalny numer ID.
//Jeśli tak, to czytamy zamiast domyślnego.
if(eeprom_read_byte(&IDEE[0]) != 0xFF)
eeprom_read_block((uint8_t *) ID, IDEE, 8);
//Obliczamy CRC8 umieszczony w 8. bajcie
for(uint8_t i = 0; i<7; i++)
//Aktualizacja CRC8
CRC8 = _crc_ibutton_update(CRC8, ID[i]);
ID[7] = CRC8;
}
CKSEL3...0: 0010
SUT1...0: 10
CKDIV8: 1
CKOUT: 1
DWEN: 1
EESAVE: 0
Fakt istnienia indywidualnego (zamiast domyślnego) adresu urządzenia w pamięci EEPROM oceniany jest na podstawie wartości pierwszego bajta pamięci EEPROM. Jeśli jest on różny od wartości 0xFF to przyjmuje się, że użytkownik wpisał swój własny numer do pamięci EEPROM, w przeciwnym wypadku pozostawiany jest numer domyślny (inicjowany na wstępie programu obsługi aplikacji). Powyższa funkcja oblicza ponadto wartość ósmego bajta adresu, który to za każdym razem powinien być sumą CRC8. Ustalenie adresu urządzenia dokonywane jest jednorazowo na początku programu obsługi aplikacji.
Na koniec, w tabeli 1, pokażę pseudokod funkcji obsługujących nasze urządzenie DS18SW20 (uruchamiany po stronie układu Master). Jak widać, programowa realizacja układu DS1820 jak i interfejsu 1-Wire jest niezmiernie prosta i zarazem bardzo ciekawa i skłania do stosowania tego interesującego interfejsu komunikacyjnego we własnych zastosowaniach czego przykładem niech będą chociażby dwa z moich wcześniejszych projektów zrealizowane w tamtym czasie przy użyciu pakietu Bascom: c-button (kopiarka pastylek DS1990, EP 2/2009) oraz 1-Wire LED (4 segmentowy, 3-kolorowy wyświetlacz LED wyposażony w sprzęg 1-Wire, EP 04/2011).
Jako ciekawostkę, na rysunku 8 pokazano rzeczywiste przebiegi sygnałów sterujących magistrali 1-Wire dla przypadku rozkazu READ_ROM wysłanego do naszego urządzenia DS18SW20.
Robert Wołgajew, EP
- R1: 22 kΩ 1%
- R2: 13 kΩ 1%
- R3: 4.7 kΩ
- C1, C2: 100 nF ceramiczny X7R
- U1: ATtiny25/45/85 (SOIC-8)
- U2: TC1047A (SOT-23)
- CON: złącze GOLDPIN kątowe 3×1 pin