Table of Contents

Wymagania:

  1. Jak poprzednio + przyciski typu microswitch

Wejście

Poprzednio opisałem sposób na “wysyłanie informacji” z uC jednak za pomocą portów We/Wy możemy również informację odczytywać. Przeanalizujmy poniższy przykład:

Hardware

schematic_buttons1.jpgWartość R3 i R4 to 10 [kΩ]

Software

/*
 * main.c
 *
 *  Created on: 5-03-2011
 *      Author: Maciek
 *      Przyciski 1
 */
 
#include <avr/io.h>
#include <util/delay.h>
 
//Knfiguracja sprzętowa: do portu B podpięte są 2 diody do pinów 0 i 1
//Do pinów 2 i 3 podpięte są 2 przyciski podciągnięte (pull-up) do Vcc przez rezystory 10 [kOhm]
 
#define LED_DIR DDRB
#define LED_P   PORTB
#define LEDA    0
#define LEDB    1
 
#define SW_DIR DDRB
//Rejestr zapisu (pull-up)
#define SW_W   PORTB
//Rejestr odczytu
#define SW_R   PINB
#define SWA    2
#define SWB    3
 
//Uwaga:
//Konfiguracja sprzętowa powoduje że stanem aktywnym dla przycisku jest 0!
//Podciągnięcie do Vcc, przycisk zwiera z Gnd
 
int main()
{
	//Inicjalizacja portów
 
	//2 pierwsze piny jako wyjcia
	LED_DIR |= ( (1<<LEDA) | (1<<LEDB) );
	//na początku diody się nie mają swiecic
	LED_P &= ~( (1<<LEDA) | (1<<LEDB) );
 
//SEKCJA A
	//Przyciski jako wejscia
	SW_DIR &= ~( (1<<SWA) | (1<<SWB) );
	//Brak podciagniecia
	SW_W &= ~( (1<<SWA) | (1<<SWB) );
//KONIEC A
 
	//Pętla glowna
	while(1)
	{
		//Leda zapalamy wtedy gdy przycisk jest naciniety
		if ( (SW_R & (1<<SWA)) != 0 )
			LED_P &= ~(1<<LEDA);
		else
			LED_P |= (1<<LEDA);
 
		//Przycisk SWB tak samo, ta wersja jest "uproszczona"
		if (SW_R&(1<<SWB))
			LED_P&=~(1<<LEDB);
		else
			LED_P|=(1<<LEDB);
	}
}

Modyfikacja

Powyższą konfigurację możemy uprościć. Rezystory podciągające przyciski do Vcc możemy zastąpić wewnętrznym podciągnięciem, tym samym które umożliwia ustalenie stanu wysokiego na pinach. Z powyższego schematu usuwamy rezystory R3 oraz R4. Zgodnie z notą katalogową1) wartość wewnętrzego rezystora podciągającego zawiera się w zakresie 30÷80 [kΩ]. Zmodyfikujmy program:

//SEKCJA A
	//Przyciski jako wejscia
	SW_DIR&=~((1<<SWA)|(1<<SWB));
	//Tym razem podciągamy
	SW_W|=(1<<SWA)|(1<<SWB);
//KONIEC A

Działanie układu nie zmieni się, a pozbyliśmy się 2 elementów.

Dzień bez całki dniem straconym

Niestety pomimo że poruszane do tej pory zagadnienia są bardzo proste (bo logika 0 1 jest prosta a konkretniej: najprostsza możliwa). W tym momencie należy zwrócić uwagę na trochę bardziej zawiłe wiadomości których nie można pominąć. Przeanalizujmy działanie poniższego programu (Taka sama konfiguracja sprzętowa jak powyżej):

/*
 * main.c
 *
 *  Created on: 5-03-2011
 *      Author: Maciek
 *      Przyciski 3
 */
 
#include <avr/io.h>
#include <util/delay.h>
 
#define LED_DIR DDRB
#define LED_P   PORTB
#define LEDA    0
#define LEDB    1
 
#define SW_DIR DDRB
#define SW_W   PORTB
#define SW_R   PINB
#define SWA    2
#define SWB    3
 
int main()
{
	//Problem jest lepiej widoczny gdy zapala się tylko 1 dioda
	//Więc pin LEDB nie zostaje ustawiony jako wyjście
	LED_DIR |= ( (1<<LEDA) );
	LED_P &= ~( (1<<LEDA) | (1<<LEDB) );
	SW_DIR&=~((1<<SWA)|(1<<SWB));
	SW_W|=(1<<SWA)|(1<<SWB);
 
	char stan_poprzedni = 1;
	char nowy_stan;
	//Pętla glowna
	while(1)
	{
		nowy_stan = ((SW_R&(1<<SWA))?1:0);
 
		if (stan_poprzedni != nowy_stan)
		{
			//nastąpiła zmiana
			if(!nowy_stan)
			{
				if((LED_P&3)==3) // 3 binarnie to 0000 0011
					LED_P&=~((1<<LEDA)|(1<<LEDB));
				else
					LED_P++;
			}
			stan_poprzedni = nowy_stan;
		}
	}
}

Powyższy program powinien przełączać diodę (gasić gdy zapalona i vice versa), jednak jak się okazuje nie zawsze się tak dzieje. Szczególnie gdy przycisk jest naciskany gwałtownie. Dioda zamiast zmienić swój stan, pozostaje w tym samym lub krótko błyska.

Napięcia a stany logiczne

Przed przejściem do setna problemu przyjrzyjmy się poniższemu wykresowi (pochodzi ze strony http://www.interfacebus.com/voltage_threshold.html):

Wykres przedstawia różne rodziny układów cyfrowych i poziomy napięć przy jakich następują zmiany.
W rozdziale 25.2 DC Characteristics noty katalogowej układu ATmega8A opisane są następujące poziomy napięć (przy napięciu zasilania 5 [V]):

Symbol Wartość min [V] Wartość max [V]
VIL -0.5 0.2 VCC
VIH 0.6 VCC VCC + 0.5
VOL - 0.9
VOH 4.2 -

Uwaga: W nocie znajduje się również informacja o poziomach przy zasilaniu z 3 [V]. Podana tabela jest uproszczeniem dla większości pinów, jednak producent podaje inne wielkości dla niektórych specjalnych pinów (np. reset ma inne wartości, reset pracujący jako port I/O jeszcze inne itd.)
Wnioski:

Drgania styków

Problem jaki występuje tutaj to drgania styków przełącznika. Zamiast jednego naciśnięcia uC odczytuje kilka! Problem jest stosunkowo dobrze opisany np. na tej stronie: Debouncing. Zwróćmy uwagę na oscylogram (Pochodzi z wyżej wymienionej strony www):
debounce_switcha.jpg
Górny wykres przedstawia napięcie a wykres dolny interpretację logiczną tego napięcia. Dobrze widać że zamiast skoku jednostkowego otrzymaliśmy oscylacje. Sytuacja taka jest bardzo niepożądana (przede wszystkim frustrująca dla użytkownika który nie może uzyskać dokładnego cyfrowego nastawu!). Zwróćmy uwagę na fakt że rozbieżność w parametrach przełączników jest dosyć duża, jednak większość przełączników zmieściła się w zakresie czasu oscylacji 10 [ms] (patrz strona Debouncing). Warto też zwrócić uwagę na fakt że microswitche mają dosyć dobre parametry jeżeli chodzi o czas oscylacji, im większy przełącznik tym te parametry stają się coraz gorsze. Istnieją 2 proste sposoby poradzenia sobie z tą sytuacją:

Sprzętowy

Pomiędzy nóżki swicha wstawiamy kondensator tworząc układ całkujący (układ inercyjny RC pierwszego rzędu).

Skoro z oscylogramu wynika że drgania trwają 8 [ms] załóżmy (zgodnie z zasadą: lepiej raz a dobrze) że mogą trwać 20 [ms] (jest to czas zarazem wystarczający jak i nieobciążający użytkownika koniecznością zbyt długiego przytrzymywania przycisku). Wartość pojemności dobieramy zgodnie z wzorem na stałą czasową obwodu RC (τ=RC → C=τ/R). Podczas prób wybrałem (bez jakichkolwiek obliczeń) kondensator 100 [nF] (z zestawu), całkowicie spełnił swoją rolę. Zauważmy że stała czasowa dla takiego układu (załóżmy że R podciągający ma wartość 40 [kΩ]) to tylko 4 [ms] (tak jak napisałem wyżej microswitche mają dobre parametry i taka stała czasowa wystarcza w tym prostym przypadku), jeżeli faktycznie drgania są długie (i przede wszystkim mamy dostęp do różnych kondensatorów) musimy zastosować odpowiednio większy kondensator (dla 20 [ms] przy 40 [kΩ] rezystorze będzie to około 1 [μF]).

Programowy

Rozwiązanie programowe jest również proste i ma tą zaletę że nie wymaga dodawania żadnych elementów do układu (mniej elementów → mniejsza cena, wielkość itd.). Algorytm jest bardzo prosty: wykryłem zmianę → czekam pewien czas → jeżeli zmiana dalej się utrzymuje przyjmuje że nie jest ona przypadkowa. Wystarczy zmodyfikować główną pętle w podany sposób:

	while(1)
	{
		nowy_stan = ((SW_R&(1<<SWA))?1:0);
 
		if (stan_poprzedni != nowy_stan)
		{
			_delay_ms(10);
			if (nowy_stan != ((SW_R&(1<<SWA))?1:0)) continue;
			//nastąpiła zmiana
			if(!nowy_stan)
			{
				if((LED_P&3)==3) // 3 binarnie to 0000 0011
					LED_P&=~((1<<LEDA)|(1<<LEDB));
				else
					LED_P++;
			}
			stan_poprzedni = nowy_stan;
		}
	}

Inne

W większości przypadków przedstawione rozwiązania wystarczają, jednak kiedy potrzebna jest duża niezawodność stosuje się inne rozwiązania. Przede wszystkim trzeba określić jak długo trwają drgania na przełączniku (pomiary oscyloskopem). Istnieje bardzo dużo sposobów, np. zastosowanie:

Przerwania

Przerwanie jest to specjalny podprogram który zostaje uruchomiony przez pewien sygnał. W wyniku pojawienia się takiego sygnału aktualnie wykonywany program zostaje wstrzymany i układ zaczyna wykonywać odpowiedni podprogram. Przerwania są na sztywno ustawione w pamięci układu: na samym początku pamięci programu znajduje się tablica zawierająca adresy wszystkich przerwań (nawet tych które nie istnieją, wtedy prowadzą one do początku programu (reset) ale taka sytuacja występuje tylko w przypadku błędu, przerwania których nie zdefiniowaliśmy nie powinny być wywoływane). W środowisku WinAVR obsługa przerwań jest bardzo prosta (kompilator sam dba o to żeby np. przerwanie nie zakłóciło wykonywania głównego programu, a tablica wektorów przerwań jest tworzona automatycznie). Aby obsługiwać przerwania należy skorzystać z nagłówka <avr\interrupt.h>. Definiowanie przerwania jest bardzo proste: korzystamy z makra ISR (Interrupt Service Routine):

#include <avr/interrupt.h>
 
ISR(NAZWA_przerwania)
{
    // kod
}

Po resecie wszystkie przerywania są zablokowane, do włączania i wyłączania wszystkich przerwań (globalnie) stosujemy makra:

cli(); //(clear interrupts) Wyłącza przerwania
sei(); //(set interrupts) Włącza przerwania

W układzie ATmega8A jest dostępnych 19 przerwań2)

Przerwania zewnętrzne

Są wyzwalane przez piny INT0 INT1 (porty PD2 PD3). Oba piny ustawione jako źródło przerwań mogą reagować na 1 z 4 sposobów (Nota tabela 13-1) w zależności od ustawień w rejestrze MCUCR (MicroController Unit Control Register):

MCUCR - MCU Control Register
Bit 7 6 5 4 3 2 1 0
Nazwa bitu /SE/ /SM2/ /SM1/ /SM0/ ISC11 ISC10 ISC01 ISC00
Znaczenie A B A B
W tym przypadku nie istotne INT1 INT0
A B Opis
0 0 Przerwanie jest generowane przez logiczne 0
0 1 Jakakolwiek zmiana stanu logicznego generuje przerwanie
1 0 Przerwanie jest generowane przez opadające zbocze
1 1 Przerwanie jest generowane przez rosnące zbocze
Ex: MCUCR |= (1<<ISC11)|(1<<ISC10)|(1<<ISC01)|(1<<ISC00); //Oba przerwania sa wyzwalane zboczem rosnacym

Aby przerwanie działało nie tylko wystarczy odblokować przerwanie globalnie (sei();) musimy też zdefiniować które konkretnie przerwania są aktywne, w tym przypadku będzie do tego służył rejestr GICR (General Interrupt Control Register) i jego bity: INT0 oraz INT1;

GICR - General Interrupt Control
Bit 7 6 5 4 3 2 1 0
Nazwa bitu INT1 INT0 -xx- -xx- -xx- -xx- /IVSEL/ /IVCE/
GICR |= (1<<INT1)|(1<<INT0); //Przerwania INT0 i INT1 aktywne

Przykład 1

Hardware

Software

/*
 * main.c
 *
 *  Created on: 20-03-2011
 *      Author: Maciek
 *      Przerwania 1
 */
 
#include <avr/interrupt.h>
#include <util/delay.h>
#include <avr/io.h>
 
//Knfiguracja sprzętowa: do portu B podpięte są 2 diody do pinów 0 i 1 katody do masy
//Do pinów INT0 i INT1 portu D są podpięte 2 przyciski
 
#define LED_DIR DDRB
#define LED_P   PORTB
#define LEDA    0
#define LEDB    1
 
#define SW_DIR DDRD
#define SW_W   PORTD
#define SW_R   PIND
 
//Zmienna globalna
//Zauważmy że tym razem zużycie pamięci danych nie jest 0 lecz wynosi 1 bajt!
//slowo kluczowe volatile oznacza ulotny. Kompilator musi sie liczyc z tym ze ta zmienna
//bedzie sie zmieniac w trakcie pracy programu!
volatile char LED=2;
 
//0
ISR (SIG_INTERRUPT0)
{
	//Zapalamy diode B kiedy przerywanie jest wykonywane
	LED_P |= (1<<LEDB);
	LED = 1;
}
 
//1
ISR (SIG_INTERRUPT1)
{
	//Zapalamy diode B kiedy przerywanie jest wykonywane
	LED_P |= (1<<LEDB);
	LED = 2;
}
 
int main()
{
	//Port przycisków jako wejcie
	SW_DIR = 0;
	//Podciagamy zeby nie stosowac dodatkowych rezystorow
	SW_W = 0xFF;
 
	//Port LED, jako wyjscie i na poczatku ustalamy ze diody sie nie swieca
	LED_DIR|=(1<<LEDA)|(1<<LEDB);
	LED_P&=~((1<<LEDA)|(1<<LEDB));
 
	//Ustawiamy przerwania
	//UWAGA: Kiedys rejestr nazywał się GIMSK, nazwe zmieniono!
	GICR |= (1<<INT0)|(1<<INT1);
 
	//Aktywujemy przerwania
	sei();
 
	//Pętla główna
	while(1)
	{
		//Gasimy diode B
		LED_P &= ~(1<<LEDB);
		if(LED==1) LED_P |= (1<<LEDA); else LED_P &= ~(1<<LEDA);
	}
}

Wnioski

Co robi nasz program? Jeden przycisk zapala diodę drugi ją gasi. Co dzieje się z drugą diodą? Świeci się cały czas kiedy trzymamy przycisk! Co to oznacza? W pętli głównej gasimy diodę B, więc jeżeli pali ona się cały czas oznacza to że przerwanie jest wyzwalane cały czas. Dzieje się tak ponieważ nie ustawiliśmy rejestru MCUCR, zgodnie z tabelką przerwanie jest generowane przez stan logiczny niski, czyli cały czas podczas przyciskania przycisku. Zmodyfikujmy program w następujący sposób:

	//Ustawiamy przerwania
	//UWAGA: Kiedys rejestr nazywał się GIMSK, nazwe zmieniono!
	//INT0 wyzwalany rosnącym zboczem
	//INT1 zboczem opadającym
	MCUCR |= (1<<ISC11)|(1<<ISC01)|(1<<ISC00);
	GICR |= (1<<INT0)|(1<<INT1);

Co się zmieniło? Dioda zapala się w momencie kiedy puszczamy przycisk zapalający (zbocze rosnące) i gaśnie dokładnie w momencie naciśnięcia drugiego przycisku (zbocze opadające). Wszystko zgodnie z oczekiwaniami.

Przykład 2

Hardware tak jak w poprzednim przykładzie. Tym razem zadaniem jest sprawdzenie i zrozumienie działania poniższego programu.

 /*
 * main.c
 *
 *  Created on: 20-03-2011
 *      Author: Maciek
 *      Przerwania 02
 */
 
#include <avr/interrupt.h>
#include <avr/io.h>
 
//Knfiguracja sprzętowa: do portu B podpięte są 2 diody do pinów 0 i 1 katody do masy
//Do pinów INT0 i INT1 portu D są podpięte 2 przyciski
 
#define LED_DIR DDRB
#define LED_P   PORTB
#define LEDA    0
#define LEDB    1
 
#define SW_DIR DDRD
#define SW_W   PORTD
#define SW_R   PIND
 
volatile char period=0;
 
//Przerwania musza byc jak najkrotsze!
ISR (SIG_INTERRUPT0)
{
	//Zwiekszaj tylko do 255
	if(period<250)period+=5;
}
 
//Jak już jest warto wykozystac do czegos
ISR (SIG_INTERRUPT1)
{
	period = 0;
}
 
int main()
{
	uint8_t i=0;
	uint8_t delay=0;
 
	//Port przycisków jako wejcie
	SW_DIR = 0;
	//Podciagamy zeby nie stosowac dodatkowych rezystorow
	SW_W = 0xFF;
 
	//Port LED, jako wyjscie i na poczatku ustalamy ze diody sie nie swieca
	LED_DIR|=(1<<LEDA)|(1<<LEDB);
	LED_P&=~((1<<LEDA)|(1<<LEDB));
 
	//Ustawiamy przerwania
	//UWAGA: Kiedys rejestr nazywał się GIMSK, nazwe zmieniono!
	//Wyzwalanie przy kazdej zmianie stanu logicznego
	MCUCR |= (1<<ISC10)|(1<<ISC00);
	GICR  |= (1<<INT0)|(1<<INT1);
 
	//Aktywujemy przerwania
	sei();
 
	//Pętla główna
	while(1)
	{
		//Warto preinkrementowac ++i a nie postinkrementowac i++ !!
		for(i=0;i<255;++i)
		{
			if(period>i) LED_P |= (1<<LEDA);
			else LED_P &= ~(1<<LEDA);
		}
 
		//Warunek opuzniajacy, zwiekszenie liczby powoduje dluzsze gaszenie diody
		//zmniejszenie krótsze
		if(++delay==10)
		{
			delay=0;
			if(period>0)--period;
		}
 
	}
}

Lista wszystkich przerwań układu ATMega8A

Niższa liczba porządkowa oznacza wyższy priorytet.

Lp. Nazwa Źródło/Opis
1 (RESET) Pin, Brownout, Watchdog, Power-on Reset
2 (INT0) SIG_INTERRUPT0 Zewnętrzny pin
3 (INT1) SIG_INTERRUPT1 Zewnętrzny pin
4 SIG_OUTPUT_COMPARE2 Timer/Counter2: wartość licznika osiągnęła zadaną wartość
5 SIG_OVERFLOW2 Timer/Counter2: przepełnienie licznika
6 SIG_INPUT_CAPTURE1 Timer/Counter1: przechwycenie
7 SIG_OUTPUT_COMPARE1A Timer/Counter1: wartość licznika osiągnęła zadaną wartość A
8 SIG_OUTPUT_COMPARE1B Timer/Counter1: wartość licznika osiągnęła zadaną wartość B
9 SIG_OVERFLOW1 Timer/Counter1: przepełnienie licznika
10 SIG_OVERFLOW0 Timer/Counter0: przepełnienie licznika
11 SIG_SPI SPI,STC Transmisja szeregowa zakończona
12 SIG_UART_RECV USART Odbiór zakończony
13 SIG_UART_DATA USART Rejestr danych pusty
14 SIG_UART_TRANS USART Nadawanie zakończone
15 SIG_ADC ACD konwersja dobiegła końca
16 SIG_EEPROM_READY Pamięć EEPROM gotowa
17 SIG_COMPARATOR Komparator analogowy
18 SIG_2WIRE_SERIAL Interfejs TWI(I2C)
19 SIG_SPM_READY Pamięć FLASH gotowa (bootloader)

Zadania

Uwagi

1)
Sekcja 25.2 DC Characteristics: wielkość Rpu
2)
Nota katalogowa dział 11 Interrupts