Kurs podstawowy
Obsługa przycisku część pierwsza

Praktycznie każde urządzenie zbudowane w oparciu o mikrokontroler posiada co najmniej jeden przycisk. Jest on elementem komunikacji mikrokontrolera z użytkownikiem. Za pomocą przycisku możemy wpływać na funkcjonowanie urządzenia lub wprowadzać dane. Funkcje przycisku ogranicza wyłącznie wyobraźnia konstruktora. W tym artykule zmodyfikujemy projekt migająca dioda rozbudowując układ o przycisk. Poznamy sposoby obsługi przycisku oraz problemy z tym związane.

 

 

Czas na kolejny krok w nauce programowania mikrokontrolerów AVR. Migająca dioda jest fajna, ale fajniej byłoby, gdyby dioda migała wtedy, kiedy my chcemy. Naszą wolę musimy jakoś przekazać mikrokontrolerowi. Tylko jak to zrobić? Najłatwiej polecenia przekazywać za pomocą przycisku. Z chwilę pokarzę w jaki sposób napisać program obsługujący wciśnięcie przycisku. Sprawa wydaje się być banalna. Ale po kolei.

 

Najpierw musimy określić co chcemy osiągnąć. Po włączeniu zasilania dioda ma być wyłączona. Wciśnięcie przycisku ma spowodować miganie diody. Ponowne wciśnięcie przycisku ma wyłączyć diodę. Cykl ma być powtarzalny do czasu wyłączenia zasilania.

Zmodyfikujmy schemat z projektu migająca dioda, Przycisk podłączymy do pinu PA1. Pin PA1 będzie zwierany przyciskiem do masy.

 

Rys. 1. Schemat układu z podłączonym przyciskiem.

 

Schemat został nieznacznie zmodyfikowany względem projektu migająca dioda. Do portu PA1 został podłączony mikroprzełącznik S1 typu TACT.  Możemy teraz zmodyfikować układ na płytce prototypowej.

 

Rys. 2. Widok zmodyfikowanego układu na płytce stykowej.

 

Warswtę sprzętową mamy gotową. Utworzymy nowy projekt w programie Atmel Studio 7.0. Ja tworzę projekt pod nazwą BLINKING LED WB, ale nie od razu będzie tam migająca dioda. W pierwszym etapie przycisk będzie tylko zapalał i gasił diodę. Więc do dzieła.

 

#include < avr/io.h > /*Dołączenie biblioteki z definicjami rejestrów i bitów I/O*/
#define F_CPU 16000000UL /*Definicja stałej określająca częstotliwość taktowania mikrokontrolera dla funkcji delay.h*/
#include < util/delay.h > /*Dołączenie biblioteki z funkcjami opóźnień*/
#define LED (1 << PA0) /*Definicja stałej określającej pin do którego podłączona jest dioda LED*/
#define BUTTON (1 << PA1) /*Definicja stałej określającej pin, do którego podłączony jest przycisk*/


int main(void)
{
    /*Ustawienie wszystkich pinów jako wyjścia. Wszystkie wyjścia mają stan niski*/
	DDRA = 0xFF;
	DDRB = 0xFF;
	DDRC = 0xFF;
	DDRD = 0xFF;
	
	DDRA &= ~BUTTON; /*Ustawienie pinu PA1 jako wejście. Zapisanie wartości 0 do bitu nr 1 rejestru DDRA)*/
	PORTA |= BUTTON; /*Załączenie rezystora podciągającego na pinie PA1. Zapisanie wartości 1 do bitu nr 1 rejestru PORTA*/
	_delay_ms(10); /*Zatrzymanie programu na 10 ms celem załączenia rezystora podciągającego i ustalenia się stanu wysokiego na wejściu.*/
	
    while (1) 
    {
		if(!(PINA & BUTTON)) /*Reakcja na wciśnięty przycisk. Reakcja na stan niski na pinie PA1*/
		{ 
			PORTA ^= LED; /*Zmiana stanu na pinie PA0*/
			_delay_ms(300); /*Zatrzymanie wykonywania pętli na 300 ms. Czas potrzebny na zwolnienie przycisku*/
		}
    }
}

 

#include < avr/io.h > - biblioteka dołączona automatycznie przez Atmel Studio. Znajdują się w niej definicje portów, rejestrów i pinów wejścia wyjścia. Ta biblioteka musi być dołączana zawsze.

#define F_CPU 16000000UL - definicja stałej F_CPU niezbędnej dla biblioteki delay.h. W definicji stałej F_CPU podajemy częstotliwość taktowania mikrokontrolera wyrażoną w Hertzach.

#include < util/delay > - dołączenie biblioteki delay.h, w której znajdują się funkcje realizujące opóźnienia.

#define LED (1 << PA0) - definicja stałej wskazującej na pin PA0. W funkcji głównej wystarczy posługiwać się wyrażeniem LED aby móc manipulować pinem PA0.

#define BUTTON (1 << PA1) - definicja stałej wskazującej na pin PA1. W funkcji głównej wystarczy posługiwać się wyrażeniem BUTTON aby móc manipulować pinem PA1.

int main(void) {...} - funkcja główna programu, w której umieszcza się definicje oraz deklaracje funkcji i zmiennych, a także instrukcje inicjalizujące, konfigurujące. W funkcji głównej znajduje się również pętla główna programu.

DDRA = 0xFF; DDRB = 0xFF; DDRC = 0xFF; DDRD = 0xFF; - zapisanie wartości 1 do wszystkich bitów każdego rejestru kierunkowego mikrokontrolera. W ten sposób zrealizowałem ustawienie wszystkich pinów jako wyjścia. Uniknąłem tym samym niepodłączonych pinów wejściowych w stanie wysokiej impedancji.

DDRA &= ~BUTTON  - Zapisanie do bitu nr 1 rejestru kierunkowego portu PA wartości 0. Pin PA1 otrzymuje funkcję wejścia.

PORTA |= BUTTON  - Zapisanie do bitu nr 1 rejestru PORT portu PA wartości 1. Rezystor podciągający na pinie PA1 zostanie podłączony

_delay_ms(10) - zatrzymanie wykonywania na czas 500 ms. Oczekiwanie na załączenie rezystora pull-up i ustalenie się stanu wysokiego na pinie PA1.

while(1) {...} - pętla główna programu. Pętla nieskończona, w której znajdują się instrukcje wykonywane przez mikrokontroler zgodnie z cyklami zegarowymi.

if(!(PINA & BUTTON))  {...} - Warunek, czy na .pinie PA1 jest stan niski.

PORTA ^= LED - instrukcja która zmienia stan pinu PA0 na przeciwny. W tej jednej linii zawarty jest cały program mikrokontrolera. Tę samą instrukcję można by rozpisać na kilka linii oddzielnie ustawiając naprzemiennie stan wysoki oraz stan niski na pinie.

_delay_ms(300) - zatrzymanie wykonywania pętli programu na czas 300 ms. Czas potrzebny na zwolnienie przycisku .Jeżeli przycisk będzie wciśnięty dłużej, pętla wykona się jeszcze raz i dioda zostanie zgaszona. Cykl będzie się powtarzał dopóki przycisk nie zostanie zwolniony.

 

Program jest gotowy do skompilowania i załadowania do mikrokontrolera. Sprawdźmy jego działanie. Układ reaguje prawidłowo. Po załączeniu zasilania dioda jest wygaszona. Naciśnięcie przycisku powoduje zapalenie diody. Kolejne naciśnięcie przycisku wyłącza ją. Działanie programu można zobaczyć na filmie.

 

 

Spróbujmy teraz troszkę popsuć. Zobaczymy, co stanie się po usunięciu wywołania funkcji _delay_ms(10); z linii 18. Kompilujemy program i ładujemy go do mikrokontrolera. Gdy program rozpocznie swoje działanie widać, że dioda została zapalona bez naciśnięcia przycisku, pomimo, że na pinie wyjściowym ustawiliśmy stan niski. Odpowiedź na pytanie, czemu tak się dzieje, czy jest jakiś błąd w programie jest banalna. Otóż załączenie tranzystora, który dołącza rezystor podciągający do Vcc oraz ustabilizowanie się napięcia na pinie PA1 zajmuje jakiś czas. Jest on dłuższy niż czas wykonywania kolejnych instrukcji i dochodzi do wykonania po raz pierwszy pętli programu. Na pinie PA1 panuje jeszcze stan niski. Warunek czy przycisk podłączony do pinu PA1 jest wciśnięty sprawdzany jest, przed ustabilizowaniem się stanu wysokiego na pinie PA1, na którym w dalszym ciągu jest stan niski. W związu z tym, że wciśnięcie przycisku powoduje podanie na pin PA1 stanu niskiego, stan fizyczny całego układu podczas pierwszego wykonania pętli programu odpowiada stanowi, jaki występuje podczas wciśnięcia przycisku. Program działa prawidłowo, dokładnie tak jak założyliśmy. Należy tylko przewidzieć wszystkie przypadki, które mogą się przytrafić. Zatrzymując działanie programu na krótki czas, przed pierwszym wykonaniem pętli programu dajemy czas na pełną inicjalizację układu, zwłaszcza stanu na wejściach mikrokontrolera. 10 ms, to czas na tyle krótki, że użytkownik zupełnie tego nie odczuje, a nasz układ stanie się odporny na wszelkie zwłoki czasowe. Przywróćmy zatem wywołanie funkcji opóźniającej i załadujmy program do mikrokontrolera.

Przejdźmy do testów działania przycisku. Gdy wciskamy go krótko, nasz układ działa prawidłowo, Spróbujmy przyciskać przycisk z różnym czasem. Nawet kilka sekund. Da się zauważyć, że dłuższe trzymanie wciśniętego rzycisku powduje cykliczne przygasanie diody. Jest to związane z tym, że w kolejnych przebiegach pętli programu warunek, w którym sprawdzane jest ciśnięcie przycisku jest prawdziwy i następuje przełączenie stanu wyjścia PA0 na przeciwny. To znowu nie jest błąd. Tak napisaliśmy program. Głównymi cechami każdego urządzenia powinny być prostota i niezawodność. Konieczne jest dodanie wszystkich zabezpieczeń w programie, które uodpornią nasz układ na działania niepożądane. Nawet te celowe. Aby tego dokonać trzeba zablokować przełączanie diody w kolejnych przebiegach funkcji. Dodamy definicję zmiennej lock, która zainicjowana będzie wartością 0.

 

...
#define BUTTON (1 << PA1) /*Definicja stałej określającej pin, do którego podłączony jest przycisk*/
uint8_t lock = 0; /*deklaracja zmiennej lock, wykorzystana zostanie do zablokowania klawisza*/

int main(void)
...

 

Zmodyfikujmy ciało pętli głównej w następujący sposób:

 

while (1) 
    {
		if(!lock && !(PINA & BUTTON)) /*Reakcja na wciśnięty przycisk. Sprawdzenie, czy zmienna lock ma wartość 0 i czy występuje stan niski na pinie PA1*/
		{ 
			lock = 1; /*Zapisanie do zmiennej lock wartości 1. Zablokowanie klawisza.*/
			PORTA ^= LED; /*Zmiana stanu na pinie PA0*/
			_delay_ms(20); /*Zatrzymanie wykonywania pętli na 20 ms po naciśnięciu przycisku. Czas potrzebny eliminację drgań styków*/
		} else if (lock && (PINA & BUTTON)) /*Sprawdzenie czy zmienna lock ma wartość 1 i czy na pinie PA1 jest stan wysoki. Sprawdzenie, czy przycisk został zwolniony.*/
		{
			_delay_ms(20);/*Zatrzymanie wykonywania pętli na 20 ms po zwolnieniu przycisku. Czas potrzebny eliminację drgań styków*/
			if(lock && (PINA & BUTTON)) /* Sprawdzenie, czy przycisk jest nadal zwolniony*/
			{
				lock = 0; /*Zapisanie do zmiennej lock wartości 0. Odblokowanie klawisza.*/
			}
		}
    }

 

Cóż takiego się zmieniło. W trakcie wykonywania programu sprawdzane jest nie tylko, czy przycisk został wciśnięty, ale czy w momencie wciśnięcia przycisku zmienna lock ma wartość 0. Następnie zmieniany jest stan na wyjściu PA0 na przeciwny. Wykonywanie pęti programu zatrzymane jest na 20 ms celem wyeliminowania wpływu drań styków. Zagadnienie drgań styków omówię bliżej w następnym artykule. Kolejnym krokiem jest sprawdzenie, czy nastąpiło zwolnienie przycisku i czy w tym momencie zmienna lock ma wartość 1. Jeśli warunek jest prawdziwy, zatrzymujemy wykonywanie pętli programu na 20 ms, aby wyeliminować wpływ drgania styków. Jeśli po tych 20 ms przycisk nadal jest zwolniony zapisujemy do zmiennej lock wartość 0. Gdy zmienna lock ma wartość 0 układ gotowy jest na obsługę wciśnięcia przycisku, w przeciwnym wypadku wciśnięty przycisk nie spowoduje żadnych reakcji. Warto sprawdzić działanie programu w naszym układzie. Wciskamy przycisk bardzo szybko, a także przytrzymując go  dłuższą chwilę. Można stwierdzić, że nie występują jakiekolwiek nieprawidłowości w jego działaniu.

 

Teraz możemy dokończyć nasz program, rozbudowując go funkcję migania diody. W tym celu musimy dodać nową zmienną, np. blink, w której przechowywać będziemy wartość odpowiadającą stanowi diody. Wartość 1 oznaczać będzie, że dioda miga, wartość 0, że dioda jest zgaszona. W pierwszym warunku sprawdzającym, czy przycisk został wciśnięty będziemy zamiast przełączać stan pinu PA0, zmieniać wartość zmiennej blink. Przed końcem pętli programu umieścimy instrukcję, która przełączać będzie stan na pinie PA0  co 500 ms w przypadku, gdy zmienna blink będzie miała wartość 1, a stan niski na tym pinie będzie ustawiany, gdy zmienna blink będzie wynosiła 0. Zmodyfikujmy więc kod programu.

 

#include < avr/io.h > /*Dołączenie biblioteki z definicjami rejestrów i bitów I/O*/
#define F_CPU 16000000UL /*Definicja stałej określająca częstotliwość taktowania mikrokontrolera dla funkcji delay.h*/
#include < util/delay.h > /*Dołączenie biblioteki z funkcjami opóźnień*/
#define LED (1 << PA0) /*Definicja stałej określającej pin do którego podłączona jest dioda LED*/
#define BUTTON (1 << PA1) /*Definicja stałej określającej pin, do którego podłączony jest przycisk*/
uint8_t lock = 0; /*deklaracja zmiennej lock, wykorzystana zostanie do zablokowania klawisza*/
uint8_t blink = 0; /*deklaracja zmiennej blink, oznaczającej stan diody*/

int main(void) {
    /*Ustawienie wszystkich pinów jako wyjścia. Wszystkie wyjścia mają stan niski*/
	DDRA = 0xFF;
	DDRB = 0xFF;
	DDRC = 0xFF;
	DDRD = 0xFF;
	
	DDRA &= ~BUTTON; /*Ustawienie pinu PA1 jako wejście. Zapisanie wartości 0 do bitu nr 1 rejestru DDRA)*/
	PORTA |= BUTTON; /*Załączenie rezystora podciągającego na pinie PA1. Zapisanie wartości 1 do bitu nr 1 rejestru PORTA*/
	_delay_ms(10); /*Zatrzymanie programu na 10 ms celem załączenia rezystora podciągającego i ustalenia się stanu wysokiego na wejściu.*/
	
    while(1) {
		if(!lock && !(PINA & BUTTON)) /*Reakcja na wciśnięty przycisk. Sprawdzenie, czy zmienna lock ma wartość 0 i czy występuje stan niski na pinie PA1*/
		{ 
			lock = 1; /*Zapisanie do zmiennej lock wartości 1. Zablokowanie klawisza.*/
			blink = (blink == 0)? 1 : 0; /*Zmiana wartości zmiennej blink między wartościami 1 i 0*/
			_delay_ms(20); /*Zatrzymanie wykonywania pętli na 20 ms po naciśnięciu przycisku. Czas potrzebny eliminację drgań styków*/
		} else if (lock && (PINA & BUTTON)) /*Sprawdzenie czy zmienna lock ma wartość 1 i czy na pinie PA1 jest stan wysoki. Sprawdzenie, czy przycisk został zwolniony.*/
		{
			_delay_ms(20);/*Zatrzymanie wykonywania pętli na 20 ms po zwolnieniu przycisku. Czas potrzebny eliminację drgań styków*/
			if(lock && (PINA & BUTTON)) /* Sprawdzenie, czy przycisk jest nadal zwolniony*/
			{
				lock = 0; /*Zapisanie do zmiennej lock wartości 0. Odblokowanie klawisza.*/
			}
		}
		if(blink) /*Sprawdzenie czy zmienna blink ma wartość 1*/
		{
			 PORTA ^= LED; /*Zmiana stanu na pinie PA0, gdy zmienna blink ma wartość 1*/ 
			 _delay_ms(500); /*Zatrzymanie pracy programu na czas 500 ms*/
		} else if(!blink && (PORTA & LED))
		{
			PORTA &= ~LED; /*Gdy zmienna blink ma wartość 0 i na pinie PA0 jest stan wysoki ustawienie na pinie PA0 stanu niskiego*/
		} 
    }
}

 

Przyjrzyjmy się zatem zmianom, które zastosowaliśmy. Zastąpiliśmy instrukcję przełączająca stan na wyjściu PA0 alternatywnym zapisem funkcji warunkowej if ...else.

 

blink = (blink == 0)? 1 : 0;

 

Mamy tutaj przypisanie do zmiennej blink wartości 1 gdy zmienna blink wynosi 0, a w przypadku gdy zmienna blink na wartość 1 przypisywana jest wartość 0.  Jest tożsame z zapisem

 

if(blink == 0) {
   blink = 1;
} else {
   blink = 0;
}

 

Jest to szybki zapis, wygodny i upraszczający kod. Sam często go stosuję. Jedynym warunkiem jest wykonywanie pojedynczych poleceń. Przy większej ilości kod zaczyna być zagmatwany. Kolejną zmianą jest dopisanie przed końcem pętli programu funkcji warunkowej uzależniającej stan diody, od wartości zmiennej blink.

 

if(blink) /*Sprawdzenie czy zmienna blink ma wartość 1*/
{
   PORTA ^= LED; /*Zmiana stanu na pinie PA0, gdy zmienna blink ma wartość 1*/ 
   delay_ms(500); /*Zatrzymanie pracy programu na czas 500 ms*/
} else if(!blink && (PORTA & LED))
{
   PORTA &= ~LED; /*Gdy zmienna blink ma wartość 0 i na pinie PA0 jest stan wysoki ustawienie na pinie PA0 stanu niskiego*/
} 

 

Przeanalizujmy działanie tego fragmentu kodu. Podczas przebiegu pętli programu sprawdzana jest wartość zmiennej blink. Jeśli jej wartość wynosi 1  następuje przełączenie stanu na pinie PA0 na przeciwny, następnie wykonywanie programu zostaje zatrzymane na 500 ms. Cykl powtarza się tak długo, jak zmienna blink ma wartość 1.  Niby dobrze. Ale może być lepiej. Po przełączeniu stanu wyjścia PA0 wykonanie programu zatrzymane jest na 500 ms. W tym czasie wciskanie klawisza nie powoduje zmiany stanu układu. Aby go zmienić konieczne jest przytrzymanie wciśniętego przycisku przez ponad 500 ms. W takim kształcie praca mikrokontrolera jest bardzo spowolniona. Pętla programu wykona się najwyżej dwa razy na sekundę. Taka sytuacja jest nie do zaakceptowania. Musimy skrócić czas wykonania pętli programu. W tym artykule uprę się na korzystanie z funkcji delay, choć widać doskonale, że nie jest ona optymalna. Dodamy kolejną zmienną np. counter, którą będziemy inkrementować z każdym przebiegiem pętli programu. Zmniejszymy czas zatrzymania programu do 2 ms, a zmienną coounter będziemy zerować, gdy jej wartość przekroczy 250. W sumie da nam to opóźnienie 500 ms. Przełączanie stanu na wyjściu PA0 będzie odbywało się gdy zmienna counter będzie wynosiła 0.

 

uint8_t counter = 0; /*deklaracja zmiennej counter, wykorzystanej do odliczania czasu*/
...

...
if(blink) /*Sprawdzenie czy zmienna blink ma wartość 1*/
{
   if (counter > 250) counter = 0; /* Zerowanie zmiennej counter, gdy jej wartość przekroczy 250 */
   if(counter == 0) PORTA ^= LED; /*Zmiana stanu na pinie PA0, gdy zmienna counter ma wartość 0 i zmienna blink = 1*/ 
   _delay_ms(2); /*Zatrzymanie pracy programu na czas 2 ms*/
   counter++; /* Zwiększenie zmiennej counter o 1 */
} else if (!blink && (PORTA & LED))
{
   PORTA &= ~LED; /*Gdy zmienna blink ma wartość 0 i na pinie PA0 jest stan wysoki, ustawienie na pinie PA0 stanu niskiego*/
}

...

 

Ładujemy program do mikrokontrolera i sprawdzimy jego działanie, które pokazane jest na filmie.

 

 

Widać, ze wszystko działa prawidłowo i nie występują żadne anomalie. Program może uznać za zakończony.

W następnym artykule omówię bardzo ważne zagadnienie dotyczące przycisków, a mianowicie drgania styków. Czym są i jak sobie z nimi radzić. Pokażę także sposoby eliminacji wpływu drgań styków, które nie wykorzystują funkcji delay i są bardziej optymalne dla programu i pracy urządzeń. Istnieje ogólne przekonanie, że funkcja delay jest zła i nie wolno jej używać. Ja mam troszkę liberalne do niej podejście i uważam, że w prostych aplikacjach, które nie mają skomplikowanych funkcji, czy też tam, gdzie prędkość działania  nie ma większego znaczenia, a liczba obsługiwanych zdarzeń jest znikoma, to czemu nie. Jeśli program mieści się w pamięci mikrokontrolera i nie ma z nim kłopotu ? Czemu nie. Przykładem może być opisana w tym artykule aplikacja. Prosta, bo prosta, ale jakąś funkcjonalność posiada. Choćby włącz/wyłącz, daje możliwość sterowania innym urządzeniem włączając go lub wyłączając. Proste, a może być przydatne.

Autor: Orici
Wyświetleń: 924|Komentarzy: 0|Ocena: 0|Głosów: 0