Koło elektroniczno-informatyczne

Port B

 

Parametry mikrokontrolerów AVR

 
   
Firma Atmel produkuje całą gamę różnych mikrokontrolerów. Programuje się je bardzo podobnie za pomocą tego samego programatora oraz środowiska IDE. Różnią się one parametrami oraz możliwościami. Poniżej przedstawiamy kilka wybranych mikrokontrolerów rodziny AVR.

ATTINY13

obrazek obrazek

Jest to jeden z najmniejszych mikrokontrolerów, który nadaje się do prostych zastosowań. Układ posiada 8 wyprowadzeń, z których 6 pełni funkcję portów wejścia/wyjścia. Zawiera wydajny, 8-bitowy procesor RISC, który może wykonywać do 20 milionów operacji na sekundę przy taktowaniu 20MHz. Oprócz wersji podstawowej produkowane są układy o powiększonych możliwościach.

Model ATTINY13 ATTINY25 ATTINY45 ATTINY85    
Pamięć Flash programu 1 2 4 8 kB 10.000 cykli zapisu
Pamięć RAM danych 64 128 256 512 B  
Pamięć EEPROM 64 128 256 512 B 100.000 cykli zapisu
Zasilanie 2,7...5,5 V  
Częstotliwość zegara 0...20 MHz  

 

ATTINY24

obrazek obrazek

Mikrokontroler ATTINY24 posiada większe możliwości od opisanego wyżej ATTINY13. Dostępnych jest więcej linii portów (12 zamiast 6), co pozwala w prosty sposób sterować większą liczbą urządzeń lub urządzeniami, które wymagają kilku linii sterujących. Posiada również więcej pamięci programu i danych.

Model ATTINY24 ATTINY44 ATTINY84    
Pamięć Flash programu 2 4 8 kB 10.000 cykli zapisu
Pamięć RAM danych 128 256 512 B  
Pamięć EEPROM 128 256 512 B 100.000 cykli zapisu
Zasilanie 2,7...5,5 V  
Częstotliwość zegara 0...20 MHz  
 

ATTINY2313

obrazek obrazek

Mikrokontroler ATTINY2313 jest najchętniej stosowanym mikrokontrolerem przez hobbystów. Powodem jest niska cena (około 4...6zł za sztukę) oraz względnie duża liczba wyprowadzeń, co umożliwia mu sterowanie nawet skomplikowanymi urządzeniami. Również powiększona pamięć Flash dla programu pozwala pomieścić bardziej złożone programy.

Model ATTINY2313 ATTINY4313    
Pamięć Flash programu 2 4 kB 10.000 cykli zapisu
Pamięć RAM danych 128 256 B  
Pamięć EEPROM 128 256 B 100.000 cykli zapisu
Zasilanie 2,7...5,5 V  
Częstotliwość zegara 0...20 MHz  

 

ATMEGA8

obrazek obrazek

Mikrokontroler ATMEGA8 posiada bardzo duże możliwości i jest stosowany tam, gdzie trzeba sterować skomplikowanymi urządzeniami lub program musi być bardzo złożony. Występuje również w kilku wersjach o różnych możliwościach. Do programowania ATMEGA8 potrzebny jest profesjonalny programator, ponieważ programowanie zwykłym programatorem USBasp zajmuje mnóstwo czasu (chyba że masz dużo cierpliwości).

Model ATMEGA8 ATMEGA16 ATMEGA32 ATMEGA64 ATMEGA128    
Pamięć Flash programu 8 16 32 64 128 kB 10.000 cykli zapisu
Pamięć RAM danych 1 1 2 4 4 kB  
Pamięć EEPROM 512 512 1024 2048 4096 B 100.000 cykli zapisu
Zasilanie 2,7...5,5 V  
Częstotliwość zegara 0...16 MHz  

 

 

 

Port B

 
   
Porty pełnią w mikrokontrolerach funkcję bram, poprzez które mikroprocesor komunikuje się ze światem zewnętrznym. Różne typy mikrokontrolerów posiadają różną liczbę linii portów wejścia/wyjścia:
obrazek

 

obrazek obrazek
obrazek

Porty oznaczane są kolejnymi literami alfabetu. Mamy zatem PORTA, PORTB, PORTC i PORTD. Na zewnątrz obudowy wyprowadzone są linie portów, które oznaczamy PA0...PA7, PB0...PB7, itd. Mikrokontroler ATTINY13, którym początkowo będziemy się zajmować, posiada jedynie PORTB, z którego dostępne jest sześć linii PB0...PB5. Linia PB5 posiada specjalną funkcję resetowania mikrokontrolera, zatem nie można jej używać jako linii wejścia (ponieważ stan niski na tej linii wymuszałby resetowanie – można to wyłączyć przez ustawienie tzw. fusebitów, czyli bitów bezpiecznikowych, lecz po tej operacji mikrokontrolera nie da się już zaprogramować w zwykły sposób, gdyż linia RESET traci swoją funkcję i staje się zwykłą linią portu we/wy). Najlepiej zostawić tę linię w spokoju (w aplikacji łączymy ją z plusem zasilania przez opornik 4,7kΩ...10kΩ). Dodatkowo linie portów pełnią funkcje linii przesyłu danych w czasie programowania mikrokontrolera (pełnią też inne funkcje, co omówimy w dalszej części kursu). Linie te posiadają dodatkowe nazwy. Poniższe nazwy dotyczą interfejsu ISP (ang. In-System Programming), czyli programowania mikrokontrolera bezpośrednio w docelowym układzie elektronicznym:
RESET włącza tryb programowania ISP, gdy przyjmuje stan niski, co symbolizuje kreska nad literami.
SCK (ang. Serial ClocK) dostarcza taktów zegara, który synchronizuje transmisję pomiędzy programatorem a mikrokontrolerem
MISO (ang. Master In – Slave Out) przesył danych z mikrokontrolera
MOSI (ang. Master Out – Slave In) pobieranie danych do mikrokontrolera

Mikrokontrolery stosują system programowania zwany ISP (ang. In System Programming). Polega on na tym, że mikrokontroler może być programowany bez wyjmowania go z układu aplikacyjnego. Wystarczy dostarczyć do odpowiednich końcówek opisane powyżej sygnały z programatora. Na czas programowania programator przejmuje kontrolę nad mikrokontrolerem. Po zaprogramowaniu programator zwalnia linie programujące. Mikrokontroler rozpoczyna wtedy wykonywanie programu. ISP jest znaczącym ułatwieniem. W pierwszych mikrokontrolerach układ należało umieścić w podstawce programatora, zaprogramować, po czym wstawić do obwodu aplikacyjnego. W trakcie uruchamiania bardziej skomplikowanej aplikacji czynność taką wykonywało się dziesiątki razy, co często mogło doprowadzić do mechanicznego uszkodzenia mikrokontrolera. Przy ISP mikrokontroler pozostaje cały czas w swoim układzie aplikacyjnym i rozpoczyna pracę bezpośrednio po zakończeniu procesu programowania.

Teraz zajmiemy się portem B w ATTINY13 (informacje tutaj podane dotyczą również pozostałych portów w innych mikrokontrolerach). Każda z linii PB0...PB5 może pracować jako wejście lub wyjście danych. Jeśli linia pracuje jako wejście, to stan tej linii jest określany przez urządzenie zewnętrzne. Jeśli linia pracuje jako wyjście, to mikroprocesor określa jej stan. Zatem pierwszym zadaniem programu będzie odpowiednie skonfigurowanie linii portu jako wejście lub wyjście danych. Do tego celu służy rejestr kierunku DDRB (ang. Data Direction Register B). Rejestr posiada osiem bitów, lecz tylko 6 z nich jest aktywnych, ponieważ PORTB posiada w ATTiny13 tylko 6 aktywnych bitów.

bit 7 6 5 4 3 2 1 0
nazwa DDB5 DDB4 DDB3 DDB2 DDB1 DDB0
R/W R R R/W R/W R/W R/W R/W R/W
stan 0 0 0 0 0 0 0 0

Rejestr kierunku danych portu B: DDRB

Każdy z bitów DDB0...DDB5 steruje kierunkiem odpowiadającej mu linii PB0...PB5. Z powyższego rysunku wynika, że wszystkie linie przyjmują początkowy stan 0 (czyli stan po resecie mikrokontrolera lub po włączeniu zasilania). Stan 0 bitu DDBx oznacza, że odpowiednia linia PBx pracuje jako wejście, czyli czyta dane z zewnątrz. Bity DDBx posiadają status R/W (ang. Read/Write). Oznacza to, że mikroprocesor może wpisywać do nich informację (W) lub odczytywać informację przechowywaną przez rejestr (R). Stan bitów 7 i 6 mikroprocesor może jedynie odczytywać, nie może ich zmienić (i tak nie mają tu przydzielonej funkcji).

Załóżmy, że chcemy ustawić linie PB0 i PB1 jako wyjścia danych, a pozostałe linie jako wejścia. Musimy zatem wpisać do rejestru DDRB liczbę binarną 00000011:

bit 7 6 5 4 3 2 1 0
nazwa DDB5 DDB4 DDB3 DDB2 DDB1 DDB0
stan 0 0 0 0 0 0 1 1

Liczba 00000011 w rejestrze DDRB ustawia PB0 i PB1 jako wyjścia, a pozostałe linie PB2...PB5 jako wejścia

 

Kolejnym rejestrem dostępnym dla mikroprocesora jest rejestr wyjścia danych PORTB.  Rejestr ten również zawiera 6 bitów aktywnych:

bit 7 6 5 4 3 2 1 0
nazwa PORTB5 PORTB4 PORTB3 PORTB2 PORTB1 PORTB0
R/W R R R/W R/W R/W R/W R/W R/W
stan 0 0 0 0 0 0 0 0

Rejestr wyjścia danych portu B: PORTB

Bity rejestru PORTB współpracują z bitami rejestru DDRB oraz z wyjściowymi liniami PBx mikrokontrolera w sposób następujący:

DDBx PORTBx PBx Opis
1 1 1 Stan bitu PORTBx pojawia się na linii PBx.
1 0 0
0 0 X Linia PBx przechodzi w tryb wejścia, Znajduje się wtedy w stanie wysokiej impedancji
i jej stan logiczny określa urządzenie zewnętrzne.
0 1 1 Linia PBx zostaje podłączona do wewnętrznego opornika podciągającego,
który podwyższa na niej napięcie do wartości Vcc. W tym trybie linię PBx można
zwierać (np. przyciskiem) do masy, sprowadzając stan wyjścia do poziomu 0.

Zwróć uwagę na ostatnią opcję. Pozwala ona wykorzystywać linię PBx do odczytu stanu przycisków bez stosowania zewnętrznych oporników, co upraszcza aplikację:

obrazek

DDBx = 0, PORTBx = 1

Ostatnim rejestrem jest rejestr wejścia danych PINB. Umożliwia on odczyt stanu linii PBx.

bit 7 6 5 4 3 2 1 0
nazwa PINB5 PINB4 PINB3 PINB2 PINB1 PINB0
R/W R R R/W R/W R/W R/W R/W R/W
stan 0 0 N/A N/A N/A N/A N/A N/A

Rejestr wejścia danych portu B: PINB

Bez względu na stan bitu DDBx odczyt bitu PINBx daje zawsze stan logiczny linii PBx. Natomiast zapis bitu o wartości 1 do PINBx powoduje, iż bit PORTBx zmienia wartość na przeciwną (jeśli linia PBx pracuje jako wyjście, tzn. DDBx = 1, to stan tego wyjścia zmienia się odpowiednio z 0 na 1 lub z 1 na 0). Ta funkcja jest dostępna tylko w mikrokontrolerach z rodziny Tiny. W rodzinie Mega należy stosować inne operacje, które opisujemy w dalszej części kursu.

 

 

Ćwiczenia z portem B

 
   
Po części teoretycznej przyszedł czas na nieco ćwiczeń praktycznych, które utrwalą nam zdobytą wiedzę o portach. Zbuduj na płytce stykowej następujący układ (do odpowiednich końcówek układu ATTINY13 podłącz sygnały z programatora, nie będziemy o tym już więcej przypominali):
obrazek        obrazek

 

Spis elementów
Element Ilość Opis
Programator AVR 1  
płytka stykowa + kable 1  
ATTiny13 1 Mikrokontroler AVR
opornik 4,7kΩ/0,125W 1 –(                )–
opornik 470Ω/0,125W 1 –(                )–
czerwona dioda LED 1 do sygnalizacji stanu 1

 

obrazek obrazek

Otwórz środowisko Eclipse i stwórz nowy projekt AVR Cross Target Application dla ATTiny13 (jak to zrobić, opisujemy w poprzednim rozdziale, nie zapomnij wybrać odpowiedniego programatora dla swojego projektu). W projekcie utwórz nowy plik źródłowy. Eclipse umieści w tym pliku jedynie komentarz nagłówkowy:

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

W naszym programie chcemy uzyskać dostęp do rejestrów DDRB, PORTB i PINB. W tym celu należy dołączyć do programu tzw. plik nagłówkowy (ang. header file). W plikach nagłówkowych umieszcza się definicje różnych obiektów, których nie definiuje sam język C. Do programu dopisz:

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>

Teraz dopisujemy szablon funkcji main, który omówiliśmy na początku tego rozdziału:

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>

int main(void)
{
    while(1)
    {
	
    }
}

W układzie elektronicznym do linii PB0 jest podłączona dioda LED, której świeceniem będzie sterował mikrokontroler. Linia ta musi pracować jako wyjście danych. Należy zatem ustawić na 1 bit DDB0 w rejestrze kierunku DDRB.

bit 7 6 5 4 3 2 1 0
nazwa DDB5 DDB4 DDB3 DDB2 DDB1 DDB0
stan 0 0 0 0 0 0 0 1

Rejestr kierunku danych portu B: DDRB

Przed pętlą while dopisujemy polecenie:

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>

int main(void)
{
    DDRB  = 1; // Określamy PB0 jako wyjście
    while(1)
    {

    }
}

Do rejestrów zapisujemy dane przy pomocy instrukcji:

 

nazwa_rejestru = wartość;

 

Po każdej instrukcji w języku C umieszczamy średnik. To ważne, jeśli opuścisz chociaż jeden średnik, program nie zostanie skompilowany i kompilacja zakończy się błędem.

W programie będą nam potrzebne opóźnienia. Opóźnienie tworzy funkcja _delay_ms(liczba milisekund). Funkcja stanie się dostępna po dołączeniu pliku nagłówkowego util/delay.h:

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB  = 1; // Określamy PB0 jako wyjście
    while(1)
    {

    }
}

Nasz układ elektroniczny z mikrokontrolerem jest bardzo prosty, co nie znaczy, że nie będziemy mogli dla niego stworzyć kilku pouczających programów. Zaczynamy od prostego migacza. Dioda LED ma się zapalać na 1 sekundę, po czym gasnąć również na jedną sekundę. Aby zapalić diodę, mikrokontroler ustawia linię PB0 w stan wysoki 1. Aby ją zgasić, ustawia linię PB0 w stan niski 0. Pomiędzy tymi działaniami wprowadzamy opóźnienie 1 sekundy, czyli 1000 milisekund:

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB  = 1; // Określamy PB0 jako wyjście
    while(1)
    {
        PORTB = 1;       // zapalamy diodę LED
        _delay_ms(1000); // czekamy 1 sekundę
        PORTB = 0;       // gasimy diodę LED
        _delay_ms(1000); // czekamy 1 sekundę
                         // i od nowa w pętli
    }
}

Zapisz swój program na dysku (Ctrl+S), a następnie skompiluj go, klikając ikonę młotka na pasku narzędziowym Eclipse. Jeśli nie popełniłeś błędu, to w oknie konsoli na spodzie ekranu otrzymasz raport kompilacji. Zużycie pamięci mikrokontrolera jest dla tego programu następujące:

 

AVR Memory Usage
----------------
Device: attiny13

Program:      82 bytes (8.0% Full)
(.text + .data + .bootloader)

Data:          0 bytes (0.0% Full)
(.data + .bss + .noinit)

 

Przesyłamy teraz wynik kompilacji do mikrokontrolera. Uruchamiamy AVRDUDE (Ctrl+Alt+U). Znów, jeśli nie popełniłeś błędu w połączeniu programatora z mikrokontrolerem, w oknie konsoli otrzymasz raport:

 

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e9007
avrdude: NOTE: FLASH memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: current erase-rewrite cycle count is 4 (if being tracked)
avrdude: erasing chip
avrdude: warning: cannot set sck period. please check for usbasp firmware update.
avrdude: reading input file "002.hex"
avrdude: input file 002.hex auto detected as Intel Hex
avrdude: writing flash (82 bytes):

Writing | ################################################## | 100% 0.08s

avrdude: 82 bytes of flash written
avrdude: verifying flash memory against 002.hex:
avrdude: load data flash data from input file 002.hex:
avrdude: input file 002.hex auto detected as Intel Hex
avrdude: input file 002.hex contains 82 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.05s

avrdude: verifying ...
avrdude: 82 bytes of flash verified

avrdude done.  Thank you.

avrdude finished
 

Po przesłaniu programu mikrokontroler podejmuje jego wykonywanie i dioda LED zaczyna migać. Program zajął 82 bajty pamięci FLASH. Zachodzi pytanie, czy da się go skrócić? Mikrokontrolery z serii Tiny posiadają specjalną funkcję: jeśli zapis zostanie dokonany do rejestru PINB, to spowoduje on odwrócenie stanu bitów na przeciwne. Rejestr PINB normalnie służy do odczytu stanu linii portu B, zatem zapis do niego nie ma normalnego sensu. Zapamiętaj to sobie. Funkcja ta pozwoli nam nieco skrócić program:

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB  = 1; // Określamy PB0 jako wyjście
    while(1)
    {
        PINB = 1;        // zapalamy lub gasimy diodę LED
        _delay_ms(1000); // czekamy 1 sekundę
                         // i od nowa w pętli
    }
}

Po wprowadzeniu zmian w tekście programu zapisz go na dysku, bo inaczej będziesz wciąż kompilował poprzedni program. Skompiluj program i sprawdź końcówkę raportu kompilacji:

 

Device: attiny13

Program:      66 bytes (6.4% Full)
(.text + .data + .bootloader)

Data:          0 bytes (0.0% Full)
(.data + .bss + .noinit)

 

Program zajmuje teraz 66 bajtów, a nie 82. A robi dokładnie to samo, co poprzedni. Wniosek z tego jest taki, że należy dokładnie poznać funkcje swojego kontrolera, gdyż pozwoli to pisać krótsze programy. Ale my na początku nie musimy tak aptekarsko oszczędzać, wróćmy zatem do pierwszej wersji programu, lecz zmodyfikujmy opóźnienia:

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB  = 1; // Określamy PB0 jako wyjście
    while(1)
    {
        PORTB = 1;       // zapalamy diodę LED
        _delay_ms(50);   // czekamy 1/20 sekundy
        PORTB = 0;       // gasimy diodę LED
        _delay_ms(950);  // czekamy resztę sekundy
                         // i od nowa w pętli
    }
}

Teraz dioda błyska co około sekundę (wewnętrzne odmierzanie czasu przez mikrokontroler nie jest zbyt precyzyjne, zegarów w każdym razie na tym nie buduj).

W kolejnym programie stworzymy kilka nowych funkcji. Funkcje te będą realizowały proste zadania: mrugnięcie szybkie (kropka), mrugnięcie dłuższe (kreska) oraz mruganie dla liter S i O w alfabecie Morse'a. Następnie wykorzystamy te funkcje do stworzenia sygnalizatora SOS. W programie wykorzystaliśmy informacje o interwałach czasowych w kodzie Morse'a, które bez problemu znajdziesz w sieci. Podstawą miary czasu jest czas trwania kropki. Tutaj przyjęliśmy 1/10 sekundy. Kreska trwa trzykrotnie dłużej od kropki. Odstępy czasowe pomiędzy kropkami i kreskami w znaku wynoszą tyle samo, co czas trwania kropki. Odstępy pomiędzy kolejnymi literami trwają tyle co kreska. Odstępy pomiędzy kolejnymi słowami trwają 7 okresów kropki.

/*
 * main.c
 *
 *  Created on: 22 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>
#include <util/delay.h>

// Funkcja powoduje szybkie mrugnięcie diody
void kropka(void)
{
    PORTB = 1;      // zapalamy LED
    _delay_ms(100); // czekamy 1/10 sekundy
    PORTB = 0;      // gasimy diodę LED
    _delay_ms(100); // czekamy 1/10 sekundy
}

// Funkcja powoduje trzy razy dłuższe mrugnięcie diody
void kreska(void)
{
    PORTB = 1;      // zapalamy LED
    _delay_ms(300); // czekamy 3/10 sekundy
    PORTB = 0;      // gasimy diodę LED
    _delay_ms(100); // czekamy 1/10 sekundy
}

// Funkcja dla znaku S w alfabecie Morse'a
void S(void)
{
    kropka();       // wywołujemy funkcję
    kropka();       // wywołujemy funkcję
    kropka();       // wywołujemy funkcję
    _delay_ms(200); // odstęp pomiędzy literami
}

// Funkcja dla znaku O w alfabecie Morse'a
void O(void)
{
    kreska();       // wywołujemy funkcję
    kreska();       // wywołujemy funkcję
    kreska();       // wywołujemy funkcję
    _delay_ms(200); // odstęp pomiędzy literami
}

// Główna funkcja programu
int main(void)
{
    DDRB  = 1; // Określamy PB0 jako wyjście
    while(1)
    {
        S();            // wywołujemy funkcję
        O();            // wywołujemy funkcję
        S();            // wywołujemy funkcję
        _delay_ms(500); // odstęp pomiędzy wyrazami
    	                // i od nowa w pętli
    }
}

Tutaj właśnie objawia się potęga układów z mikrokontrolerem. Dzięki jego uniwersalności zmiana programu powoduje, że ten sam układ zaczyna wykonywać inne zadania.

Nowe funkcje posiadają następującą budowę:

void nazwa(void)
{
   treść;
}

Słówko void przed nazwą funkcji informuje kompilator, że dana funkcja nic nie zwraca. Słówko void w nawiasach za nazwą funkcji informuje z kolei, że funkcja nie potrzebuje żadnych parametrów do swojego działania.

Ważna jest kolejność definicji. Funkcja musi być zdefiniowana przed użyciem w programie. Najpierw utworzyliśmy funkcje kropka() i kreska(). Następnie utworzyliśmy funkcje S() i O(), które w swoim kodzie wywołują odpowiednią liczbę razy poprzednio zdefiniowane funkcje kropka() i kreska(). Wywołanie funkcji powoduje wykonanie zawartego w niej kodu i powrót do miejsca, z którego funkcja została wywołana. Aby wywołać funkcję, w programie umieszczamy jej nazwę wraz z nawiasami, które tutaj są akurat puste, ponieważ funkcje nie potrzebują parametrów – tymi zagadnieniami zajmiemy się w dalszej części kursu.

Teraz poćwiczymy odczyt danych. Zbuduj na płytce stykowej poniższy układ:

obrazek        obrazek

 

Spis elementów
Element Ilość Opis
Programator AVR 1  
płytka stykowa + kable 1  
ATTiny13 1 Mikrokontroler AVR
opornik 4,7kΩ/0,125W 1 –(                )–
opornik 470Ω/0,125W 1 –(                )–
czerwona dioda LED 1 do sygnalizacji stanu 1
przycisk 1  

 

obrazek obrazek

W układzie pojawił się przycisk W, który jest podłączony do linii PB0. Dioda LED podłączona jest to linii PB1. Linia PB0 będzie pracowała jako wejście danych. Wewnętrznie podepniemy ją do opornika podciągającego. Linia PB1 będzie pracowała jako wyjście danych. Konfiguracja rejestrów sterujących portem B jest następująca:

bit 7 6 5 4 3 2 1 0
nazwa DDB5 DDB4 DDB3 DDB2 DDB1 DDB0
stan 0 0 0 0 0 0 1 0

Rejestr kierunku danych portu B: DDRB
bit 7 6 5 4 3 2 1 0
nazwa PORTB5 PORTB4 PORTB3 PORTB2 PORTB1 PORTB0
stan 0 0 0 0 0 0 0 1

Rejestr danych portu B: PORTB

W rejestrze kierunku DDRB ustawiamy na 1 bit DDB1, aby linia PB1 pracowała jako wyjście. W rejestrze PORTB ustawiamy na 1 bit PORTB0, aby do linii PB0 (pracującej jako wejście, ponieważ DDB0 = 0) został podłączony opornik podciągający.

Uruchom środowisko Eclipse i utwórz sobie nowy projekt dla ATTINY13 (możesz też skasować poprzedni program i tworzyć nowy w starym projekcie, to zostawiam już twojej decyzji). W edytorze wpisz:

/*
 * main.c
 *
 *  Created on: 23 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>

int main(void)
{
    DDRB  = 0b000010; // PB1 jako wyjście na diodę LED, pozostałe jako wejścia
    PORTB = 0b000001; // do PB0 opornik podciągający
    while(1)
    {

    }
}

Przed pętlą while umieszczone są dwie instrukcje, które przygotowują port B do pracy w naszym układzie. Do rejestrów zapisujemy liczby w postaci binarnej, ponieważ odzwierciedla ona najlepiej stan poszczególnych bitów. W języku C stała binarna rozpoczyna się od dwóch znaków 0b, za którymi umieszczamy kolejne bity liczby od najstarszego do najmłodszego. Zamiast liczb binarnych moglibyśmy użyć odpowiadających im liczb dziesiętnych, lecz wtedy czytelność operacji spadłaby znacznie. Porównaj:

0b011100 i 28

obie stałe mają dokładnie taką samą wartość liczbową, lecz w stałej binarnej 0b011100 od razu widzimy stan poszczególnych bitów, czego nie daje nam stała dziesiętna 28. Później pokażemy jeszcze bardziej czytelny sposób ustawiania bitów w rejestrach. Jeśli nie wiesz nic o liczbach binarnych, to przeczytaj dokładnie poniższe artykuły z naszego serwisu:

 

 

Znajomość systemu dwójkowego jest absolutnie niezbędna przy programowaniu mikrokontrolerów, ponieważ będziesz pracował z bitami.

Napiszemy program, który systematycznie odczytuje stan przycisku i, jeśli przycisk będzie wciśnięty, to zapali diodę LED. Jeśli przycisk nie będzie wciśnięty, dioda LED pozostanie zgaszona. W takim programie napotykamy na pewną trudność: jak zbadać stan określonego bitu rejestru? Otóż do tego celu wykorzystujemy operację logiczną & (AND, koniunkcja, iloczyn logiczny). W języku C operacja & działa na poszczególnych bitach argumentów. Można ją opisać poniższą tabelką:

a b a & b
0 0 0
0 1 0
1 0 0
1 1 1

Bit wyniku operacji & jest równy 1, jeśli oba bity a i b miały stan wysoki 1. W przeciwnym razie bit wyniku przyjmuje stan 0. Gdy chcemy sprawdzić stan określonego bitu, tworzymy tzw. maskę bitową, w której wszystkie bity są wyzerowane za wyjątkiem bitu na testowanej pozycji, który ma wartość 1. Jeśli teraz wykonamy operację & wartości bitowej z tą maską, to wszystkie bity wyniku, dla których bity maski mają wartość 0, zostaną wyzerowane. Natomiast na pozycji, gdzie w masce jest bit o stanie 1 otrzymamy 0, jeśli badany bit ma wartość 0 lub 1 w przypadku przeciwnym.

Na przykład chcemy badać stan bitu nr 3. Tworzymy maskę z ustawionym bitem nr 3:

maska = 0b00001000

Dla badanej wartości 0b11010101 otrzymamy:

0b 11010101
& 0b 00001000
0b 00000000

Otrzymaliśmy wynik równy zero, czyli badany bit posiadał stan 0.

Dla badanej wartości 0b11011101 otrzymamy:

0b 11011101
& 0b 00001000
0b 00001000

Otrzymaliśmy wynik różny od zera, czyli badany bit miał stan 1.

Zatem operacja rejestr & maska pozwoli nam wydzielić odpowiedni bit rejestru. Co dalej? Teraz w zależności od stanu tego bitu powinniśmy wykonać różne zadania. To wymaga podjęcia decyzji. Do podejmowania decyzji w języku C służy instrukcja warunkowa if. Posiada składnię:

 

if(wyrażenie) operacja1; else operacja2;
 
if(wyrażenie)
    operacja1;
else
    operacja2;
 
if(wyrażenie)
{
  dowolny ciąg operacji1;
}
else
{
  dowolny ciąg operacji2;
}

 

Instrukcja if działa w sposób następujący:

Najpierw zostaje wyliczone wyrażenie, które umieszczamy w nawiasach za if. Jeśli wyrażenie ma wartość różną od zera, to zostaje wykonana operacja 1 lub ciąg operacji1 w klamerkach. Po wykonaniu tej operacji instrukcja if nie wykonuje już operacji2. Mikrokontroler przechodzi do wykonania pierwszej instrukcji za if. Jeśli wyrażenie ma wartość 0, to operacja 1 zostaje pominięta i mikrokontroler wykonuje tylko operację2.

Dzięki tej instrukcji mikrokontroler może postępować w sposób "inteligentny". Zależnie od zastanych warunków wykonuje odpowiednie operacje. Wracając do naszego programu, umieść w pętli while następujące instrukcje:

/*
 * main.c
 *
 *  Created on: 23 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>

int main(void)
{
    DDRB  = 0b000010; // PB1 jako wyjście na diodę LED, pozostałe jako wejścia
    PORTB = 0b000001; // do PB0 opornik podciągający
    while(1)
    {
        if(PINB & 0b000001)   // badamy stan przycisku W na linii PB0
            PORTB = 0b000001; // PB0=1; przycisk niewciśnięty, dioda gaszona
        else
            PORTB = 0b000011; // PB0=0; przycisk wciśnięty, dioda zapalana
                              // i od początku w pętli
     }
}

Zapisz program na dysku, skompiluj i prześlij wynik do mikrokontrolera. Jeśli nie popełniłeś błędu, to po naciśnięciu przycisku W dioda LED powinna się zaświecić. Zwolnienie przycisku gasi diodę.

Zwróć uwagę, że przy zapisie do PORTB zachowujemy cały czas stan ostatniego bitu, który kontroluje dołączaniem opornika podciągającego. Sprawdź, co się stanie, jeśli bit ten nie będzie ustawiany na 1 (zmień odpowiednio swój program). W takim przypadku linia PB0 będzie "wisiała w powietrzu" i zacznie się zachowywać jak antena, zbierając wszelkie zakłócenia z okolicy. Mikrokontroler ATTiny13 zbudowany jest z tranzystorów polowych, a te, jak pamiętasz, posiadają bardzo dużą oporność wejściową. Układ przestanie działać prawidłowo, chyba że podłączysz do nóżki PB0 opornik zewnętrzny 1...10k.

 

Następny program mruga cyklicznie diodą LED. Jeśli jednak zostanie wciśnięty przycisk W, to mruganie znacznie przyspiesza. W tym programie instrukcja warunkowa będzie decydowała o opóźnieniu pomiędzy zmianami stanu portu PB1, który steruje świeceniem diody LED.

/*
 * main.c
 *
 *  Created on: 23 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB  = 0b000010; // PB1 jako wyjście na diodę LED, pozostałe jako wejścia
    PORTB = 0b000001; // do PB0 opornik podciągający
    while(1)
    {
        PINB = 0b000010;    // zmieniamy stan linii PB1 na przeciwny
        if(PINB & 0b000001) // testujemy stan linii PB0
            _delay_ms(500); // PB0=1; przycisk niewciśnięty, długie opóźnienie
        else
            _delay_ms(100); // PB0=0; przycisk wciśnięty, krótkie opóźnienie
                            // i od początku w pętli
    }
}

Podnosimy poprzeczkę. Następny program ma zapalać diodę LED po naciśnięciu przycisku i gasić ją po ponownym naciśnięciu przycisku. Brzmi prosto, lecz wcale takie proste nie jest. Musimy rozwiązać kilka problemów. Po pierwsze, jak wykrywać kolejne naciśnięcia przycisku? Następnie należy pamiętać, że styki przycisków mechanicznych wykonują mikrodrgania, co powoduje powstanie szybkiego ciągu impulsów. Mikrokontroler jest dosyć szybki i może potraktować takie drgania jako kilkakrotne naciśnięcie przycisku.

Algorytm będzie następujący:

 

K1: Czytamy port B
K2: Jeśli linia PB0 = 1, to wracamy do kroku K1
K3: Zmieniamy stan linii PB1 na przeciwny
K4: Odczekujemy 10 ms aż styki przycisku przestaną drgać
K5: Czytamy port B
K6: Jeśli linia PB0 = 0, to wracamy do kroku K5
K7: Odczekujemy 10 ms aż styki przycisku przestaną drgać
K8: Koniec

 

Takie operacje musimy umieścić w pętli while. Przyjrzyj się temu dobrze i upewnij się, że rozumiesz wszystko. W tym algorytmie mamy dwie pętle w krokach K1, K2 oraz w krokach K5, K6. Pierwsza pętla czeka aż przycisk zostanie wciśnięty, czyli aż linia PB0 przyjmie stan 0. Po wykryciu naciśnięcia przycisku wykonujemy operację związaną z przyciskiem (tutaj będzie to zapalenie bądź zgaszenie diody LED). Następnie musimy odczekać pewien czas, aż styki przycisku przestaną wykonywać drgania. Teraz następuje druga pętla, która czeka na zwolnienie przycisku, czyli aż linia PB0 wróci do stanu 1. Gdy tak się stanie, pętla zostaje przerwana i znów odczekujemy krótki czas na wygaśnięcie drgań przycisków. Teraz możemy kontynuować pętlę główną i czekać na kolejne naciśnięcie przycisku. Program jest następujący:

/*
 * main.c
 *
 *  Created on: 23 sie 2015
 *      Author: Geo
 */

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB  = 0b000010; // PB1 jako wyjście na diodę LED, pozostałe jako wejścia
    PORTB = 0b000001; // do PB0 opornik podciągający
    while(1)
    {
        while(PINB & 0b000001);        // czekamy na PB0 = 0
        PINB = 0b000010;               // zapalamy lub gasimy diodę LED
        _delay_ms(10);                 // czekamy na wygaśnięcie drgań styków przycisku
        while((PINB & 0b000001) == 0); // czekamy na PB0 = 1
        _delay_ms(10);                 // czekamy na wygaśnięcie drgań styków przycisku
                                       // i od początku w pętli
    }
}

Do oczekiwania na naciśnięcie lub zwolnienie przycisku wykorzystujemy tutaj pętle while. A dlaczego nie instrukcję if? Początkującym często mylą się te instrukcje. Różnica jest zasadnicza: pętla while testuje swój warunek, jeśli jest spełniony, wykonuje instrukcję, po czym wraca na początek i znów testuje warunek. Działania te są powtarzane w kółko, aż warunek osiągnie wartość 0. Wtedy pętla zostaje przerwana i mikrokontroler wykonuje kolejną operację w programie. Instrukcja warunkowa if też testuje swój warunek i jeśli ma wartość różną od zera to wykonuje jedną z dwóch operacji. Jednakże instrukcja if nigdy nie wraca na swój początek. Działa jednorazowo. A przecież mikrokontroler musi cierpliwie czekać, aż użytkownik naciśnie przycisk. Należy cyklicznie sprawdzać stan linii PB0, aby wykryć moment tego naciśnięcia. Tutaj właśnie używamy instrukcji pętli warunkowej:

 

while(PINB & 
					0b000001);

 

Pętla ta oblicza wyrażenie w nawiasach. Jeśli jest prawdziwe (czyli PB0=1), to wraca na początek i znów wylicza wyrażenie. Dlaczego? Ponieważ po nawiasie zamykającym wyrażenie nie umieściliśmy żadnej instrukcji do wykonania wewnątrz pętli. To nie błąd. Tak właśnie ma być. Celem tej pętli jest cykliczne sprawdzanie, czy linia PB0 ma wartość 1, co oznacza, że przycisk W nie jest wciśnięty. Wykonanie programu "zatrzymuje się" w tym miejscu do czasu, aż PB0 przyjmie stan 0. Wtedy wyrażenie w nawiasach przestanie być prawdziwe i pętla zostanie przerwana.

Po wyjściu z pętli mikrokontroler zmienia na przeciwny stan linii PB0, po czym odczekuje 10 ms na wygaśnięcie drgań styków. To ważne. Jeśli nie zaczekasz, to dalsza część programu może zawieść.

Druga pętla:

 

while((PINB & 0b000001) == 0);

 

Wykonuje zadanie odwrotne do pętli pierwszej. Czeka na zwolnienie przycisku, czyli aż linia PB0 wróci do stanu 1. Wyjaśnienia wymaga zawartość nawiasów instrukcji while. Umieściliśmy tam wyrażenie porównawcze. Wyrażenie to jest prawdziwe, gdy wynik PINB & 0b000001 jest równy zero. W języku C jest więcej takich wyrażeń i możesz je stosować w instrukcjach while oraz if:

 
a == b   prawdziwe, gdy a jest równe b
a != b   prawdziwe, gdy a jest różne od b
a > b   prawdziwe, gdy a jest większe od b
a < b   prawdziwe, gdy a jest mniejsze od b
a >= b   prawdziwe, gdy a jest większe lub równe b
a <= b   prawdziwe, gdy a jest mniejsze lub równe b

 


   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