Technika cyfrowa, robotyka – mikrokontrolery AVR – odczyt z portów

Na poprzednich zajęciach poznaliśmy podstawowe funkcje portów mikrokontrolera ATTINY13 pełniących rolę wyjścia. Dzisiaj zajmiemy się wejściem danych. Najpierw jednak omówimy podstawowe operacje na bitach w języku C.

Bity rejestrów

Dane zapisujemy do portów w postaci bitów. Załóżmy, że chcemy w jednym z portów mikrokontrolera umieścić następujące bity:

 

 0   0   1   1   0   1   0   1 

 

Jak to zrobić? Istnieje kilka możliwości:

Liczbę możemy zapisać jako stałą binarną. W tym celu poprzedzamy ją zerem i literką b:

 

0b00110101

 

Jest to sposób najprostszy, jednakże dla ludzi mało czytelny. Człowiek po prostu źle reaguje na mało zróżnicowane dane. Dla ludzi naturalnym systemem zapisu liczb jest system dziesiętny. Liczbę binarną zawsze da się przedstawić jako liczbę dziesiętną. Poszczególne bity w zapisie liczby posiadają tzw. wagi, które zaznaczyliśmy poniżej kolorem czerwonym.

 
128 64 32 16 8 4 2 1
 0   0   1   1   0   1   0   1 

 

Aby otrzymać wartość tej liczby binarnej, wystarczy zsumować wagi pozycji, na których bit ma wartość 1. Otrzymamy:

 

32 + 16 + 4 + 1 = 53

 

W wybranym porcie mikrokontrolera umieszczamy zatem wartość 53, która odpowiada wartości binarnej 00110101. Ten sposób, chociaż skuteczny, jest rzadko wykorzystywany, ponieważ wymaga przeliczeń.

Druga metoda polega na wyrażeniu naszej liczby binarnej za pomocą liczby ósemkowej. Język C obsługuje stałe ósemkowe – aby stała została potraktowana jako ósemkowa, musi rozpoczynać się od cyfry 0. Zaletą systemu ósemkowego jest to, że bardzo prosto przelicza się liczby pomiędzy nim a systemem dwójkowym. Należy nauczyć się na pamięć tabelki:

 
Cyfra
ósemkowa
Bity
0
1
2
3
4
5
6
7
000
001
010
011
100
101
110
111

 

Jedna cyfra ósemkowa odpowiada zawsze grupie trzech bitów. Aby zatem przekształcić naszą liczbę dwójkową na ósemkową, postępujemy tak:

Bity dzielimy na grupy po 3 od strony prawej do lewej, po czym każdą z tych grup zastępujemy cyfrą ósemkową (ostatnia grupa będzie zawierała tylko dwa bity, przyjmiemy zatem, że trzeci bit ma wartość 0):

 
00110101 000 110 101 065
    0 6 5    

 

W porcie umieszczamy liczbę ósemkową 065, która odpowiada liczbie dwójkowej 00110101. Ten sposób jest lepszy, ponieważ po nauczeniu się na pamięć tabelki praktycznie nic nie potrzebujemy liczyć, a konwersji dokonujemy w locie.

System ósemkowy nie psuje dokładnie do rozmiarów danych binarnych. Gdy dzieliliśmy bity na grupy 3 bitowe, to porcja 8 bitów została podzielona niezbyt zgrabnie na 2b 3b 3b. Z tego powodu programiści chętniej korzystają z systemu szesnastkowego przy konwersjach liczb dwójkowych. System szesnastkowy posiada dodatkowe sześć cyfr do reprezentacji wartości od 10 do 15 (cyfry systemu dziesiętnego kończą się na 9!). Należy nauczyć się tabelki konwersji:

 
Cyfra
szesnastkowa
Bity
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
1010
1011
1100
1101
1110
1111

 

W systemie szesnastkowym każda cyfra odpowiada czterem bitom. Dzięki temu bajt, czyli osiem bitów, daje się zapisać przy pomocy jedynie dwóch cyfr szesnastkowych. Zasada konwersji jest podobna do konwersji dwójkowo ósemkowej. Liczbę binarną dzielimy na grupy po 4 bity od strony prawej do lewej, a następnie każdą z tych grup zastępujemy cyfrą szesnastkową wg tabelki.

 
00110101 0011 0101 0x35
    3 5    

 

Stałe szesnastkowe w języku C zapisujemy z zerem i znakiem x na początku. Do portu wprowadzamy zatem stałą 0x35, która odpowiada liczbie dwójkowej 00110101.

Istnieje jeszcze prostszy sposób, który nie wymaga od programisty żadnych przeliczeń. Każdy bit w zapisie liczby dwójkowej posiada swój numer:

 
7 6 5 4 3 2 1 0
 0   0   1   1   0   1   0   1 

 

W naszej liczbie na 1 są ustawione bity o numerach 5, 4, 2 i 1. Aby przekształcić numer bitu na jego wartość w liczbie binarnej (czyli na wagę pozycji, na której ten bit się znajduje), należy skorzystać z operacji przesunięcia bitowego w lewo. Operacja ta ma postać:

 

wartość << liczba_przesunięć

 

Jeśli wartość jest równa 1, a liczba przesunięć jest równa numerowi bitu, to wynikiem operacji jest zawsze waga pozycji bitu o podanym numerze. Jak to działa? Przeanalizuj poniższe rachunki:

 

w = 00000001  (to jest wartość 1 zapisana dwójkowo jako liczba ośmiobitowa)

w << 3 = 00000001 << 3 = 0001000 = 8

 

Zatem:

 

1 << 0 = 1 (waga bitu na pozycji 0)
1 << 1 = 2 (waga bitu na pozycji 1)
1 << 2 = 4 (waga bitu na pozycji 2)
...

Otrzymane w wyniku przesunięć wartości bitów składamy w całość za pomocą bitowej operacji | (operacja logiczna LUB, daje wynik 1, jeśli jeden z argumentów ma wartość 1) lub dodawania:

 

00110101 = (1 << 5) | (1 << 4) | (1 << 2) | (1 << 0)

lub

00110101 = (1 << 5) + (1 << 4) + (1 << 2) + (1 << 0)

 

Bity w rejestrach mikrokontrolera posiadają zdefiniowane nazwy odpowiednie do rejestru. W kontrolerze ATTINY13 są zdefiniowane następujące nazwy portów i ich bitów (oczywiście zamiast tych nazw możesz również używać opisanych powyżej numerów bitów):

 

Funkcja Rejestr Nazwy bitów rejestru
Rejestr kierunku danych DDRB DDB5 DDB4 DDB3 DDB2 DDB1 DDB0
Rejestr wyjścia danych PORTB PB5 PB4 PB3 PB2 PB1 PB0
Rejestr wejścia danych PINB PINB5 PINB4 PINB3 PINB2 PINB1 PINB0

 

Oczywiście, nie ma obowiązku nazywania bitów rejestru DDRB nazwami DDBi, możesz równie dobrze użyć dla nich nazw PBi, ponieważ są to faktycznie numery bitów. Jednakże raczej stosuj tę konwencję, aby nie tworzyć zamieszania w programie.

 

Załóżmy teraz, że chcemy w naszym ATTINY13 ustawić jako wyjścia linie portu B PB4, PB3, PB2 i PB1, a linię PB0 jako wejście. Do rejestru kierunku danych będziemy musieli wprowadzić następujące bity:

 

DDRB DDB5 DDB4 DDB3 DDB2 DDB1 DDB0
  0 1 1 1 1 0

 

Zatem posłużymy się instrukcją języka C:

 

DDRB = (1 << DDB4) | (1 << DDB3) | (1 << DDB2) | (1 << DDB1);

 

To samo uzyskamy za pomocą poniższych instrukcji, które są sobie równoważne;

 

DDRB = (1 << 4) | (1 << 3) | (1 << 2) | (1 << 1);
DDRB = 0b11110;
DDRB = 0x1E;
DDRB = 036;
DDRB = 30;

 

Wybieraj ten sposób, który w danym momencie będzie najbardziej odpowiedni.

 

Podstawowe operacje na bitach

Mikrokontrolery wykonują intensywne operacje na bitach danych, które odczytują z portów lub do nich zapisują. Bity te dalej sterują różnymi urządzeniami współpracującymi z mikrokontrolerem. Na przykład, na poprzednich zajęciach wykorzystaliśmy najmłodszy bit portu B do sterowania diodą LED.

 

Ustawianie bitu

Często zdarza się sytuacja, że w porcie musimy ustawić na 1 wybrane bity. Operację taką wykonuje się za pomocą operacji bitowej alternatywy rejestru z maską. Przez maskę będziemy rozumieli daną binarną, w której są ustawione na 1 bity do zmiany. Załóżmy, że w rejestrze PORTB chcemy ustawić na 1 bit PB3. Maską będzie wyrażenie (1 << PB3). Wykonujemy zatem operację:

 

PORTB |= (1 << PB3);

 

Operator |= jest równoważny operacji:

 

PORTB = PORTB | (1 << PB3);

 

Ktoś spyta, dlaczego nie tak:

 

PORTB = (1 << PB3);

 

Różnica jest taka, że poprzednie operacje ustawiają na 1 bit PB3, lecz nie zmieniają stanu pozostałych bitów portu B. Może być to przecież bardzo istotne, jeśli pozostałe linie sterują innymi urządzeniami, nieprawdaż? Przyjrzyj się poniższym rachunkom:

 

PORTB      = 00010010
(1 << PB3) = 00001000

             00001000
           | 00010010
             00011010

 

W podobny sposób możemy ustawić na 1 kilka bitów. Wystarczy stworzyć maskę, gdzie jest więcej bitów o wartości 1. Np. ustawmy na 1 w rejestrze DDRB bity DDB2, DDB1 i DDB0 (czyli linie portu B PB2, PB1 i PB0 będą pracować jako wyjścia danych):

 

DDRB |= (1 << DDB2) | (1 << DDB1) | (1 << DDB0);

 

Jeśli żałujesz stukania w klawisze, to ten sam efekt uzyskasz instrukcjami:

 

DDRB |= 7;
DDRB |=0b111;

 

Osobiście jednak uważam, że ten pierwszy sposób jest bardziej czytelny, bo od razu widać, co ustawiamy.

 

Zerowanie bitu

Tym razem pragniemy wyzerować określony bit (bity) rejestru. Operację taką wykonujemy za pomocą bitowej koniunkcji z negacją maski i z odpowiednim rejestrem. Negację bitową uzyskamy za pomocą operatora ~. Na przykład:

 

~00011011 = 11100100

 

Zerowanie bitu wykonujemy tak:

 

PORTB &= ~(1 << PB3);

 

Jak to działa? Przyjrzyj się poniższym rachunkom:

 

PORTB      = 00011111
(1 << PB3) = 00001000
~00001000  = 11110111

             11110111
           & 00011111
             00010111

 

Zmiana bitu na przeciwny

Do zamiany bitu stanu bitu na przeciwny wykorzystuje się operację bitową sumy symetrycznej. Operacja ta jest dwuargumentowa:

 

a b a ^ b
0 0 0
0 1 1
1 0 1
1 1 0

 

Z tabelki tej wynika, że wynikiem operacji ^ jest 1 tylko wtedy, gdy oba bity biorące w niej udział mają różne stany. Tworzymy zatem maskę z bitem ustawionym na 1 i wykonujemy bitową operację ^ tej maski z wybranym portem. Odpowiedni bit portu zostanie zmieniony na przeciwny:

 

PORTB ^= (1 << PB3);

 

Przyjrzyj się rachunkom poniżej:

 

PORTB      = 00011111
(1 << PB3) = 00001000

             00001000
           ^ 00011111
             00010111

 

PORTB      = 00010111
(1 << PB3) = 00001000

             00001000
           ^ 00010111
             00011111

 

Odczyt z linii portu wejścia

Mikrokontroler ATTINY13 posiada 6 linii portu B, z których 5 może służyć jako wejście danych. Linia PB5 pełni funkcję RESET, zatem podanie na nią stanu niskiego powoduje zresetowanie mikrokontrolera. Z tego powodu raczej nie nadaje się ona do wykorzystania jako linia danych.

 

obrazek

 

Pozostają nam zatem linie PB0...PB4 (5 linii wejścia). Dotychczas sterowaliśmy układy cyfrowe za pomocą przycisku, który zwierał do masy wejście podpięte przez opornik do plusa zasilania:

 

obrazek

 

Użyty tu opornik nosi nazwę opornika podciągającego (ang. pull-up resistor), ponieważ ma on za zadanie "podciągać" napięcie na wejściu układu w górę do wartości Vcc, gdy przycisk jest rozwarty. Mikrokontrolery posiadają już wewnątrz swojej struktury odpowiednie oporniki podciągające wejścia do Vcc. Nie musimy ich zatem stosować jako osobnych elementów w budowanym układzie. Należy jedynie pamiętać, aby te oporniki przyłączyć do odpowiednich linii portu B (normalnie są one odłączone). Załóżmy, że chcemy do linii PB0 podłączyć przycisk zwierający ją do masy. Linia ta ma pracować jako wejście. Chcemy, aby przy rozwartym przycisku na linii PB0 panował wysoki poziom logiczny. Zatem linia PB0 musi wewnętrznie być połączona z opornikiem podciągającym. Robimy to tak:

 

DDRB  &= ~(1 << DDB0);   // Ustawiamy linię PB0 jako wejście danych (zerujemy bit 0)
PORTB |= (1 << PB0);     // Do linii PB0 podłączamy opornik podciągający
...


Tutaj wykorzystujemy dodatkową funkcję PORTB. Otóż, jeśli linia PBi pracuje jako wejście, to ustawienie na 1 bitu PBi w PORTB powoduje podłączenie do wejścia PBi opornika podciągającego. Opornik ten ustawi poziom logiczny 1 na tej linii.

 

Jeśli linię portu sterujesz za pomocą przycisku, to powinieneś wiedzieć, że przyciski są urządzeniami mechaniczno-elektrycznymi, które wewnątrz posiadają sprężyste styki. Gdy naciskasz przycisk, to styki te się zwierają. Jednak często występują przy tym krótkotrwałe drgania. W efekcie sygnał na wejściu mikrokontrolera wygląda tak:

 

obrazek

 

Mikrokontroler działa szybko. Prawidłowy odczyt stanu wejścia sterowanego przyciskiem często wymaga stosowania w programie krótkich opóźnień. Problem ten postaramy się rozwiązać w projektach, które zrealizujemy na tych zajęciach. Prześledź je uważnie.

 

Ćwiczenie nr 1

W ćwiczeniu tym zbudujemy prosty układ złożony z mikrokontrolera ATTINY13, dwóch diod LED czerwonej i żółtej oraz przycisku. Gdy przycisk będzie rozwarty, mikrokontroler będzie mrugał wolno diodą czerwoną. Gdy przycisk będzie zwarty, mikrokontroler przestanie mrugać diodą czerwoną, a zacznie mrugać szybko diodą żółtą. Zwolnienie przycisku spowoduje powrót do mrugania diodą czerwoną.

Zmontuj poniższy układ na płytce stykowej.

 

obrazek

  obrazek   
Sygnały programujące
na mikrokontrolerze

 

obrazek
Sygnały programujące
na wtyczce
programatora

 

Ponieważ zawsze świeci się tylko jedna z diod, mogą być podłączone do masy jednym opornikiem. Tutaj zastosujemy małe diody (o średnicy 3mm, koszt za sztukę około 20...30 gr) o prądzie 5...10mA, aby zbytnio nie obciążały wyjść mikrokontrolera.

Spis elementów:

 

Element Ilość Opis
programator ISP 1 do zasilania i programowania mikrokontrolera
płytka stykowa + kable 1 do montażu układu
ATTINY13 1 mikrokontroler (ATTINY2313, ATMEGA8)
opornik 270Ω/0,125W 1 –(                )–  do ograniczania napięcia i prądu diod LED
kondensator 100nF 1 do eliminacji zakłóceń (nie musisz go koniecznie stosować, lecz jest to zalecane)
czerwona dioda LED 1 do sygnalizacji stanu wysokiego na wyjściu PB1
żółta dioda LED 1 do sygnalizacji stanu wysokiego na wyjściu PB2
przycisk 1 do sterowania wejścia PB0

 

Nieużywane linie PB3, PB4 i PB5 ustawimy programowo jako wejścia i podepniemy do nich wewnętrzne oporniki podciągające. W naszej aplikacji poszczególne linie będą pracowały jako:
 
Linia Tryb Funkcja linii portu B
PB0 wejście odczyt stanu przycisku, podpięta do opornika podciągającego.
PB1 wyjście steruje diodą czerwoną
PB2 wyjście steruje diodą żółtą
PB3 wejście nieużywana, podpięta do opornika podciągającego.
PB4 wejście nieużywana, podpięta do opornika podciągającego.
PB5 wejście nieużywana, podpięta do opornika podciągającego. Jest to linia RESET. W środowisku bardzo zakłóconym (przemysł, bliskość silników lub pól magnetycznych) zaleca się podpięcie jej dodatkowo zewnętrznym opornikiem 10kΩ do Vcc. W układach prototypowych nie musimy tego stosować, lecz w produkcie końcowym jest to wręcz konieczne. W przeciwnym razie mikrokontroler może się przypadkowo resetować.

 

obrazek

 

Program dla mikrokontrolera ATTINY13:

 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
 * ATTINY13_0005.c
 *
 * Created: 2014-05-28 21:01:42
 *  Author: Jerzy Wałaszek
 */ 

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB =  (1<<DDB2)|(1<<DDB1);                 // Linie PB1 i PB2 jako wyjścia, pozostałe jako wejścia
    PORTB = (1<<PB5)|(1<<PB4)|(1<<PB3)|(1<<PB0); // Wszystkie wejścia podpięte do oporników podciągających
    
    while(1)
    {
        if(PINB & (1 << PINB0))   // Czytamy stan wejścia PB0
        {                         // Stan wysoki, przycisk rozwarty
            PORTB &= ~(1<<PB2);   // Gasimy diodę żółtą
            PINB  = (1<<PB1);     // Mrugamy diodą czerwoną
            _delay_ms(300);
        }
        else                      // Stan niski, przycisk zwarty
        {
            PORTB &= ~(1<<PB1);   // Gasimy diodę czerwoną
            PINB  = (1<<PB2);     // Mrugamy diodą żółtą
            _delay_ms(100);
        }
    }
}

 

Nie zmieniając układu, zaprogramuj mikrokontroler poniższym programem. Teraz każde naciśnięcie przycisku zmienia aktywną diodę. Zwróć uwagę na kod w wierszach 21...28.

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
 * ATTINY13_0006.c
 *
 * Created: 2014-05-28 21:01:42
 *  Author: Jerzy Wałaszek
 */ 

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    short int ry = 1;  // red/yellow
    
    DDRB =  (1<<DDB2)|(1<<DDB1);                 // Linie PB1 i PB2 jako wyjścia, pozostałe jako wejścia
    PORTB = (1<<PB5)|(1<<PB4)|(1<<PB3)|(1<<PB0); // Wszystkie wejścia podpięte do oporników podciągających
    
    while(1)
    {
        if((PINB & (1<<PINB0)) == 0)        // Sprawdzamy, czy został naciśnięty przycisk
        {
           PORTB &= ~((1<<PB2)|(1<<PB1));   // Gasimy obie diody
           _delay_ms(25);                   // Czekamy na zanik drgań styków
           while((PINB & (1<<PINB0)) == 0); // Czekamy na zwolnienie przycisku
           _delay_ms(25);                   // Czekamy na zanik drgań styków
           ry ^= 1;                         // Zmieniamy aktywną diodę
        }
                
        if(ry)  // ry = 1 - aktywna dioda czerwona, ry = 0 - aktywna żółta 
        {
            PORTB &= ~(1<<PB2);   // Gasimy diodę żółtą
            PINB  = (1<<PB1);     // Mrugamy diodą czerwoną
            _delay_ms(100);
        }
        else
        {
            PORTB &= ~(1<<PB1);   // Gasimy diodę czerwoną
            PINB  = (1<<PB2);     // Mrugamy diodą żółtą
            _delay_ms(100);
        }
    }
}

 

Ćwiczenie nr 2

Kolejny projekt symuluje włączanie jakiegoś urządzenia z opóźnieniem, w trakcie którego mikrokontroler informuje użytkownika o czasie, jaki pozostał do uruchomienia. Uruchamianie włączamy i wyłączamy naciśnięciem przycisku. W projekcie użyjemy mikrokontrolera ATTINY13, 4 diod LED (3 czerwone D1 D2 D3 i 1 zielona DU) oraz przycisku. Diody czerwone będą wskaźnikiem upływu czasu, dioda zielona będzie symbolizowała nasze urządzenie. Standardowo wszystkie diody są zgaszone. Gdy użytkownik naciśnie i zwolni przycisk, zacznie się odliczanie czasu. Najpierw zapalą się wszystkie trzy diody czerwone i będą mrugały jednocześnie. Po upływie każdych 5 sekund jedna z diod będzie gasła (od D3 do D1). W trakcie tego procesu użytkownik będzie mógł wstrzymać odliczanie ponownym naciśnięciem i zwolnieniem przycisku. Gdy upłynie 15 sekund i wszystkie diody czerwone zgasną, zaświeci się na stałe dioda zielona Du. Będzie się świecić dotąd aż użytkownik ponownie naciśnie i zwolni przycisk. Wtedy układ wraca do stanu początkowego, gdy wszystkie 4 diody LED są zgaszone.

Zmontuj poniższy układ na płytce stykowej.

 

obrazek

  obrazek   
Sygnały programujące
na mikrokontrolerze

 

obrazek
Sygnały programujące
na wtyczce
programatora

 

Spis elementów:

 

Element Ilość Opis
programator ISP 1 do zasilania i programowania mikrokontrolera
płytka stykowa + kable 1 do montażu układu
ATTINY13 1 mikrokontroler (ATTINY2313, ATMEGA8)
opornik 270Ω/0,125W 3 –(                )–  do ograniczania napięcia i prądu czerwonych diod LED
opornik 220Ω/0,125W 1 –(                )–  do ograniczania napięcia i prądu zielonej diody LED
kondensator 100nF 1 do eliminacji zakłóceń (nie musisz go koniecznie stosować, lecz jest to zalecane)
czerwona dioda LED 3 do sygnalizacji stanu wysokiego na wyjściach PB1, PB2 i PB3
zielona dioda LED 1 do sygnalizacji stanu wysokiego na wyjściu PB4
przycisk 1 do sterowania wejścia PB0

 

obrazek

 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/*
 * ATTINY13_0007.c
 *
 * Created: 2014-05-29 08:24:01
 *  Author: Jerzy Wałaszek
 */ 

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>

// Wybieramy lepsze nazwy na linie portu B

#define SW PB0
#define D1 PB1
#define D2 PB2
#define D3 PB3
#define DU PB4

// Funkcja sprawdza, czy jest naciśnięty przycisk podłączony do linii PB0
// Jeśli nie, to wraca natychmiast z wynikiem 0.
// Jeśli tak, to czeka na zwolnienie przycisku i wraca z wynikiem 1.
//-----------------------------------------------------------------------
char KeyPressed(void)
{
    if(PINB & (1<<SW)) return 0;   // Testujemy stan przycisku
    
    // Przycisk wciśnięty

    _delay_ms(25);                 // Czekamy na zanik wibracji styków
    while(!(PINB & (1<<SW)));      // Czekamy na zwolnienie przycisku
    _delay_ms(25);                 // Czekamy na zanik wibracji styków
    
    return 1;                      // Informujemy o naciśniętym przycisku
}

// **********************
// *** PROGRAM GŁÓWNY ***
// **********************
int main(void)
{
    char state = 0;                 // stan układu, 0 = wszystko wyłączone
    char cnt;                       // Licznik
    
    DDRB = (1<<DU)|(1<<D3)|(1<<D2)|(1<<D1); // Definiujemy linie wyjściowe portu B
    PORTB = (1<<PB5)|(1<<SW);       // Do PB5 (RESET) i do PB0 podłączamy oporniki podciągające
                                    // Gasimy diody D1,D2,D3 i DU
    while(1)
    {
        while(KeyPressed() == 0);   // Czekamy na wciśnięcie przycisku
        
        state = 4;                  // stan 4 = mrugają diody D1, D2 i D3, DU zgaszona
        cnt   = 0;    
        while(state)                // Wykonujemy cykl roboczy
        {
            if(KeyPressed()) state = 0; // Jeśli naciśnięto przycisk, zerujemy stan układu
            
            switch(state)
            {
                case 0 : PORTB = (1<<PB5)|(1<<SW); // Gasimy wszystkie diody LED
                         break;
                case 1 : PORTB = (1<<PB5)|(1<<DU)|(1<<SW); // Zaświecamy DU
                         break;
                case 2 : PORTB = (1<<PB5)|(1<<D1)|(1<<SW); // Zaświecamy D1
                         break;
                case 3 : PORTB = (1<<PB5)|(1<<D2)|(1<<D1)|(1<<SW); // Zaświecamy D1 i D2
                         break;
                case 4 : PORTB = (1<<PB5)|(1<<D3)|(1<<D2)|(1<<D1)|(1<<SW); // Zaświecamy D1, D2 i D3
                         break;
            }
            
            if(state > 1)
            {
                if(cnt & 1) _delay_ms(400); // W zależności od stanu licznika raz jest opóźnienie
                else                        // długie, a raz krótkie, co daje efekt mrugnięcia
                {
                    PORTB = (1<<PB5)|(1<<SW); // Gasimy diody D1, D2 i D3
                    _delay_ms(100);
                }
            
                cnt++;                  // Modyfikujemy licznik
            
                if(cnt == 10)           // Sprawdzamy wartość końcową licznika
                {
                    cnt = 0;            // Zerujemy licznik i zmniejszamy stan
                    state--;
                }
            }
        }        
    }
}

 

Ćwiczenie nr 3

Ten projekt jest symulacją sygnalizatora na skrzyżowaniu ulicznym. Sygnalizator posiada dwa zestawy świateł:
  1. Dla samochodów (czerwone, żółte i zielone)
  2. Dla pieszych (czerwone i zielone)

Sygnalizator jest sterowany przyciskiem. Standardowo świeci się światło zielone w sekcji samochodów oraz światło czerwone w sekcji pieszych. Gdy zostanie naciśnięty i zwolniony przycisk, następuje cykl zmiany świateł (wybrane czasy nie są naturalne):

 

Czas Samochody Piesi
2s (               ) (          )
2s (               ) (          )
2s (               ) (          )
1s (               ) (          )
10s (               ) (          )
1s (               ) (          )
2s (               ) (          )
2s (               ) (          )
koniec (               ) (          )

 

Zmontuj na płytce stykowej następujący układ (diody dla sekcji pieszych i samochodów umieść osobno). Diody sekcji pieszych są podłączone do linii PB1. Gdy na tej linii mamy stan niski, świeci się dioda zielona. Gdy jest stan wysoki, świeci się dioda czerwona. Wykorzystujemy jedną linię do dwóch diod LED! Jest to możliwe, ponieważ w sekcji pieszych zawsze świeci się tylko jedna z nich. Dzięki temu nasz układ wykorzystuje tylko 4 linie portu B na sterowanie 5 diod LED.

 

obrazek

  obrazek   
Sygnały programujące
na mikrokontrolerze

 

obrazek
Sygnały programujące
na wtyczce
programatora

DP – diody sygnalizacji dla pieszych

DS – diody sygnalizacji dla samochodów

 

Spis elementów:

 

Element Ilość Opis
programator ISP 1 do zasilania i programowania mikrokontrolera
płytka stykowa + kable 1 do montażu układu
ATTINY13 1 mikrokontroler (ATTINY2313, ATMEGA8)
opornik 270Ω/0,125W 2 –(                )–  do ograniczania napięcia i prądu czerwonych diod LED
opornik 120Ω/0,125W 3 –(                )–  do ograniczania napięcia i prądu zielonych i żółtych diod LED
kondensator 100nF 1 do eliminacji zakłóceń (nie musisz go koniecznie stosować, lecz jest to zalecane)
czerwona dioda LED 2 sygnał zakazu przejścia/wjazdu
żółta dioda LED 1 sygnał uwaga
zielona dioda LED 2 sygnał droga wolna
przycisk 1 do uruchamiania cyklu

 

obrazek

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/*
 * ATTINY13_0008.c
 *
 * Created: 2014-06-02 17:55:00
 *  Author: Jerzy Wałaszek
 */ 

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>

// Wybieramy lepsze nazwy na linie portu B

#define SW PB0
#define DP1 PB1
#define DS1 PB2
#define DS2 PB3
#define DS3 PB4

// Funkcja sprawdza, czy jest naciśnięty przycisk podłączony do linii PB0
// Jeśli nie, to wraca natychmiast z wynikiem 0.
// Jeśli tak, to czeka na zwolnienie przycisku i wraca z wynikiem 1.
//-----------------------------------------------------------------------
char KeyPressed(void)
{
    if(PINB & (1<<SW)) return 0;   // Testujemy stan przycisku
    
    // Przycisk wciśnięty
    _delay_ms(25);                 // Czekamy na zanik wibracji styków
    while(!(PINB & (1<<SW)));      // Czekamy na zwolnienie przycisku
    _delay_ms(25);                 // Czekamy na zanik wibracji styków
    
    return 1;                      // Informujemy o naciśniętym przycisku
}

// **********************
// *** PROGRAM GŁÓWNY ***
// **********************
int main(void)
{
    
    // Tablica definiuje czasy dla każdego ze stanów układu
    char TTime[] = {2,2,2,1,10,1,2,2,0};
        
    // Tablica definiuje światła dla każdego ze stanów
    char TState[] = {(1<<DS1),
                     (1<<DS1)|(1<<DS2),
                     (1<<DS2),
                     (1<<DS3),
                     (1<<DS3)|(1<<DP1),
                     (1<<DS3),
                     (1<<DS3)|(1<<DS2),
                     (1<<DS2),
                     (1<<DS1)};
                    
    unsigned char state,i;
    // Ustawiamy linie wyjścia portu B
    DDRB = (1<<DP1)|(1<<DS1)|(1<<DS2)|(1<<DS3);
    
    // Do linii PB5 i SW podłączamy oporniki podciągające, zapalamy diodę DS1
    PORTB = (1<<PB5)|(1<<SW)|(1<<DS1);

    while(1)
    {
       while(KeyPressed()==0);  // Czekamy na klawisz
       
       for(state = 0; state < 9; state++)
       {
           PORTB  = (1<<PB5)|(1<<SW); // Gasimy wszystkie diody
 
           PORTB |= TState[state];   // i zapalamy właściwe

           for(i  = 0; i < TTime[state]; i++) _delay_ms(1000);
       }
    }
}

 

Ćwiczenie nr 4

Kolejny projekt jest o tyle ciekawy, że pokazuje sposób podłączenia 8 diod do 5 linii portów ATTINY13. Na diodach tych zrealizujemy różne efekty świetlne. Schemat znajduje się poniżej:

 

obrazek

  obrazek   
Sygnały programujące
na mikrokontrolerze

 

obrazek
Sygnały programujące
na wtyczce
programatora

 
Jak to działa? Zawsze ustawimy linię PB4 jako wyjście. Do linii tej jest podłączony zbiorczy opornik, który ogranicza prąd diod. Z pozostałych linii PB0...PB3 zawsze tylko jedna będzie pracowała jako wyjście. Pozostałe będą ustawiane jako wejście. Załóżmy, że linie PB0 i PB4 są ustawione jako wyjścia i na obu jest stan 0. W takim przypadku nie świeci ani dioda D0, ani D1, ponieważ występuje na nich napięcie bliskie 0V. Jeśli teraz na wyjściu PB0 ustawimy stan wysoki (4,2V), to zaświeci dioda D0. D1 będzie zgaszona, ponieważ jest ustawiona w kierunku nieprzewodzenia (zaporowo). Wyobraźmy sobie, że teraz linia PB4 zmienia swój stan na wysoki. Dioda D0 gaśnie. Dioda D1 jest również zgaszona, ponieważ na obu diodach znów występuje napięcie bliskie 0V. Jeśli przy wysokim stanie linii PB4 ustawimy stan niski na PB0, to zaświeci się dioda D1, a D0 pozostanie zgaszona, ponieważ teraz ona jest skierowana zaporowo. To samo dotyczy linii PB1, PB2 i PB3 i podłączonych do nich diod.

 

obrazek obrazek obrazek obrazek

 

Dzięki takiemu połączeniu diod możemy sterować świeceniem każdej diody w parze. Zawsze uaktywniamy tylko jedną parę diod (aby nie przeciążać zbytnio linii PB4), ustawiając linię PB0, PB1, PB2 lub PB3 jako wyjście. Gdy linia portu nie jest wyjściem i nie ma podłączonego wewnętrznie opornika podciągającego, nie steruje diodami i są one zgaszone.

 

Spis elementów:

 

Element Ilość Opis
programator ISP 1 do zasilania i programowania mikrokontrolera
płytka stykowa + kable 1 do montażu układu
ATTINY13 1 mikrokontroler (ATTINY2313, ATMEGA8)
opornik 270Ω/0,125W 1 –(                )–  do ograniczania napięcia i prądu diod LED
kondensator 100nF 1 do eliminacji zakłóceń (nie musisz go koniecznie stosować, lecz jest to zalecane)
czerwona dioda LED 8 do linijki świetlnej

 

obrazek

 

Program tworzy ruchomy punkt świetlny przebiegający kolejno od D0 do D7.

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
 * ATTINY13_0009.c
 *
 * Created: 2014-06-06 20:41:00
 *  Author: Jerzy Wałaszek
 */ 

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    unsigned char mask = 1;
    
    DDRB = 0;         // Inicjujemy wszystkie linie jako wejścia
    PORTB = (1<<PB5); // Podłączamy opornik podciągający do RESET
    while(1)
    {
        DDRB  = (1<<PB4) | mask;
        PORTB = (1<<PB5) | mask; // Zaświecamy diodę w parze
        _delay_ms(50);
        PINB  = (1<<PB4) | mask; // Zaświecamy drugą diodę w parze odwracając stany PB4 i PBx
        _delay_ms(50);
        mask <<= 1;   // Przesuwamy bit maski
        if(mask & (1<<PB4)) mask = 1;
    }
}

 

Program tworzy ruchomy punkt świetlny, który porusza się tam i z powrotem.

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
 * ATTINY13_0010.c
 *
 * Created: 2014-07-06 07:28:00
 *  Author: Jerzy Wałaszek
 */ 

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    unsigned char mask = 1;
    unsigned char dir = 1;
    
    DDRB = 0;         // Inicjujemy wszystkie linie jako wejścia
    PORTB = (1<<PB5); // Podłączamy opornik podciągający do RESET
    while(1)
    {
        DDRB  = (1<<PB4) | mask;
        if(dir) PORTB = (1<<PB5) | mask; // Zaświecamy diodę w parze
        else    PORTB = (1<<PB5) | (1<<PB4);
        _delay_ms(50);
        PINB  = (1<<PB4) | mask; // Zaświecamy drugą diodę w parze odwracając stany PB4 i PBx
        _delay_ms(50);
        if(dir)  mask <<= 1;     // Przesuwamy bit maski
        else     mask >>= 1;
        if(mask & (1<<PB3)) dir = 0;
        if(mask & (1<<PB0)) dir = 1;
    }
}

 

A ten program jest przykładem, że ATTINY13 może sterować świeceniem tych diod w dowolny sposób. Program tworzy różne efekty świetlne. Postaraj się odkryć sposób jego działania (opiera się on na bezwładności oka ludzkiego).

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
 * ATTINY13_0011.c
 *
 * Created: 2014-06-07 10:06:00
 *  Author: Jerzy Wałaszek
 */ 

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    // Dane dla PORTB
    unsigned char T[] = {0b100000,  // Zgaszone
                         0b100001,  // Świeci D0
                         0b110000,  // Świeci D1
                         0b100010,  // Świeci D2
                         0b110000,  // Świeci D3
                         0b100100,  // Świeci D4
                         0b110000,  // Świeci D5
                         0b101000,  // Świeci D6
                         0b110000}; // Świeci D7
                         
    // Dane dla DDRB
    unsigned char D[] = {0b000000,  // Zgaszone wszystkie LED
                         0b010001,  // Aktywne D0/D1
                         0b010001,  // Aktywne D0/D1
                         0b010010,  // Aktywne D2/D3
                         0b010010,  // Aktywne D2/D3
                         0b010100,  // Aktywne D4/D5
                         0b010100,  // Aktywne D4/D5
                         0b011000,  // Aktywne D6/D7
                         0b011000}; // Aktywne D6/D7

    unsigned char i,j,k,m = 0,c = 0;
     
    while(1)
    {
        for(i = 0; i < 9; i++)
        {
            for(j = 0; j < 10; j++)
                for(k = 0; k < 9; k++)
                {
                    switch(c)
                    {
                        case 0: m = (k <= i)            ? k : 0; break;
                        case 1: m = (k <= 8 - i)        ? k : 0; break;
                        case 2: m = (k >= 8 - i)        ? k : 0; break;
                        case 3: m = (k >= i)            ? k : 0; break;
                        case 4: m = (i && (k == i))     ? k : 0; break;
                        case 5: m = (i && (k == 8 - i)) ? k : 0; break;
                        case 6: ;
                        case 7: m = (i && ((k == i) | (k == 8 - i))) ? k : 0; break;
                    }
                    DDRB  = D[m];
                    PORTB = T[m];
                    _delay_ms(1);
                }
        }
        c = (c + 1) & 0x7;
    }
}

 


   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2024 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe