Język C


Tematy pokrewne   Podrozdziały
(w budowie)
  Wprowadzenie do języka C
Port B
Ćwiczenia z portem B
Podsumowanie

 

 

Wprowadzenie do języka C

 
   
W ramach kursu przeprowadzimy naukę programowania w języku C. Jest to absolutnie niezbędne, ponieważ budowa aplikacji wykorzystujących mikrokontroler jest dwuetapowa. W pierwszym etapie projektuje się elektronikę, a następnie tworzy się odpowiedni program dla mikrokontrolera, który tę elektronikę obsługuje.

Mikroprocesor zawarty wewnątrz mikrokontrolera AVR rozumie tylko binarne kody instrukcji. Program umieszczony w pamięci Flash mikrokontrolera wygląda mniej więcej tak:

1001101111110101
1101000110001001
0001010110000101
0000011110011011
...

Każda instrukcja dla mikroprocesora AVR zajmuje w pamięci co najmniej 16 bitów. Przyznasz, że nie jest on specjalnie czytelny. Program zawierający bezpośrednio instrukcje dla mikroprocesora nazywamy programem maszynowym (ang. machine code programm), a język tych instrukcji nazywa się językiem maszynowym (ang. machine code). Jest to język niskiego poziomu (ang. LLL – Low Level Language), który odnosi się bezpośrednio do sprzętu komputera. Poszczególne instrukcje określają operacje, które ma wykonać mikroprocesor. Programowanie na tym poziomie nie jest już dzisiaj często stosowane. Jeśli ktoś bardzo chce programować bezpośrednio mikroprocesor, to stosuje tzw. język assemblera (ang. assembler language), w którym każda instrukcja zapisywana jest w postaci bardziej zrozumiałej dla ludzi, np. tak:

 

ADD R16, R17   ; Dodaj zawartość rejestru R16 do rejestru R17
DEC R17        ; Zmniejsz 0 1 zawartość rejestru R17
MOV R18, R16   ; Skopiuj zawartość R16 do R18
...

 

Program jest wciąż mało czytelny. Aby zwiększyć czytelność, wielu programistów w asemblerze opatruje każdą instrukcję komentarzem. Niewiele to pomaga, ponieważ zrozumienie działania programu wymaga wczytywania się w komentarze i bardzo szybko można zgubić watek przewodni (coś o tym wiem, ponieważ sam przez długi czas programowałem różne komputery właśnie w języku asemblera). Również uruchamianie takiego programu i wyszukiwanie w nim błędów (uwierz mi, w każdym większym programie asemblerowym prawie zawsze są jakieś błędy) jest bardzo żmudne i czasochłonne. Wynika z tego, że języki niskiego poziomu operują bezpośrednio na sprzęcie. Programista musi dokładnie znać strukturę komputera oraz listę dostępnych rozkazów. Programy w językach asemblerowych są krótkie i bardzo szybkie, ponieważ nie umieszcza się w nich zbędnych instrukcji. To jest niewątpliwie ich zaletą. Jednakże program taki jest mało czytelny i łatwo popełnić w nim pomyłkę, którą trudno później zlokalizować (nieraz zdarzało mi się tracić całe tygodnie na wyłuskanie błędu w kodzie maszynowym).

Aby ułatwić życie programistom i przyspieszyć znacząco proces tworzenia programu, opracowano języki wysokiego poziomu (ang. HLL – High Level Language), które nie operują bezpośrednio na sprzęcie. Oznacza to, że programista nie musi znać szczegółów budowy komputera oraz listy rozkazów jego mikroprocesora, aby stworzyć działający program (akurat w przypadku mikrokontrolerów AVR nie jest to tak do końca prawdą, ale o tym za chwilę). Co więcej, języki HLL umożliwiają tworzenie programów przenośnych, które będą działały również na innych komputerach, wyposażonych w zupełnie inne mikroprocesory. Jak to jest możliwe? Po prostu w języku HLL nie używamy instrukcji dla mikroprocesora. Każdy program źródłowy HLL musi zostać skompilowany, tzn. przeanalizowany i zamieniony w ciąg instrukcji binarnych dla docelowego mikroprocesora. Operację tę wykonuje kompilator.

Programista tworzy tekst programu w edytorze tekstowym. Następnie zostaje uruchomiony program kompilatora, który ten tekst przetłumaczy na kod maszynowy dla określonego komputera. Kod maszynowy jest bezpośrednio rozumiany przez mikroprocesor komputera. Wystarczy zatem umieścić ten kod w pamięci programu i nakazać jego wykonywanie mikroprocesorowi. Tak w skrócie wygląda programowanie w języku HLL. Proste, nieprawdaż? Diabeł tkwi w szczegółach.

Przejdźmy teraz do języka C dla mikrokontrolerów AVR. Musisz poznać i zrozumieć strukturę typowego programu w języku C. Gdy mikrokontroler zostaje uruchomiony, zawsze rozpoczyna wykonywanie programu od kodu, który umieszczamy w funkcji main() o następującej budowie:

 

int main(void)
{
  ...
}

 

Funkcja w języku C jest fragmentem programu, który możesz wielokrotnie wykorzystywać. To coś w rodzaju przepisu w książce kucharskiej: otwierasz na właściwej stronie i czytasz jak zrobić gulasz lub placek. W komputerze kod zawarty w funkcji wykonuje oczywiście mikroprocesor.

Słówko int, widoczne przed nazwą main, oznacza typ wyniku. Chodzi tutaj o to, iż funkcje w języku C stosowane są do wykonywania obliczeń i zwracania ich wyniku (w przykładzie z książką kucharską wynikiem jest to, co dany przepis/funkcja opisuje, np. placek ze śliwkami). Typ określa, co będzie tym wynikiem. Typ int to liczba całkowita (1, 5, -6, 1000, itp.).

Kolejnym słówkiem jest main, co po angielsku znaczy główny. Jest to zatem główna funkcja w programie. Od niej rozpocznie się wykonanie twojego kodu. Słowo main oznacza tutaj nazwę funkcji.

Za nazwą main pojawiają się nawiasy ze słówkiem void. W nawiasach umieszczamy definicję parametrów, czyli informacji, które chcemy przekazać do funkcji. Słowo void oznacza, że funkcja main nie oczekuje żadnych parametrów (odpowiednikiem w książce kucharskiej będzie spis niezbędnych składników, które są potrzebne, aby powstał np. tort czekoladowy lub pierogi z kapustą).

Kolejnym elementem są klamerki. Obejmują one kod funkcji, czyli fragment programu, który zostanie wykonany wewnątrz funkcji main (to nasz właściwy przepis kucharski, który krok po kroku opisuje czynności niezbędne do upichcenia naszej ulubionej potrawy, która stanie się wynikiem "funkcji kulinarnej"). Dobrze, wystarczy analogi z żarciem (przez żołądek do głowy). Wróćmy do czystej elektroniki.

Zwykle chcemy, aby kontroler działał w tzw. pętli, czyli cyklicznie wykonywał określone polecenia (w przeciwnym razie jego działanie byłoby żałośnie krótkie, nawet gdybyś wykorzystał całą pamięć programu, kod zaraz dobiegłby do końca i co byś z tego miał? No, chyba że ten mikrokontroler obsługiwałby odpalenie szybkodziałającej bomby zaraz po włączeniu zasilania). Dlatego kolejnym elementem programu będzie instrukcja pętli warunkowej:

 

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

 

Pętlę (ang. loop) tworzysz za pomocą instrukcji while (są też inne instrukcje pętli, omówimy je dokładnie później). Chociaż wygląda ona podobnie jak main, to nie jest nową funkcją. Po pierwsze, nie ma typu. A po drugie while zawarte jest wewnątrz klamerek funkcji main – w języku C nie wolno zagnieżdżać funkcji, czyli tworzyć nowych funkcji wewnątrz innych funkcji.

Za słowem kluczowym while pojawiają się nawiasy. W nawiasach tych umieszczamy wyrażenie, które mikrokontroler oblicza na początku każdego obiegu pętli. Jeśli wartość tego wyrażenia jest różna od zera, to zostanie wykonany kod zawarty w klamerkach instrukcji while. Jeśli natomiast obliczona wartość będzie równa zero, to pętla while zostanie przerwana i kontroler wykona pierwszą instrukcję za klamerką zamykającą blok pętli.

W naszym przypadku w nawiasie znajduje się liczba 1, która zawsze jest różna od 0. Dzięki temu ta konkretna pętla while nigdy się nie przerwie i będzie działać w nieskończoność (a przynajmniej do czasu aż zmienimy ten program na inny lub wyłączymy zasilanie). Pętla cyklicznie wykonuje to, co zawarte jest wewnątrz jej klamerek.

 

Swoje programy warto opatrywać komentarzami. Komentarz jest tekstem pomocniczym, za pomocą którego programista opisuje działanie lub przeznaczenie fragmentów swojego kodu. Komentarze nie są umieszczane w programie wynikowym, pozostają jedynie w tekście źródłowym. Zatem nie oszczędzaj na komentarzach i stosuj je często, lecz z głową.

Język C w wersji AVR rozpoznaje dwa rodzaje komentarzy:

 

Komentarz blokowy. Rozpoczyna się sekwencją /*, a kończy */. Może obejmować wiele wierszy:

 

/*
  Utworzono: 30 gru 2014
  Autor: Jerzy
  I LO w Tarnowie
*/

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

 

Komentarz wierszowy. Rozpoczyna się sekwencją // i obowiązuje do końca bieżącego wiersza.

 

/*
  Utworzono: 30 gru 2014
  Autor: Jerzy
  I LO w Tarnowie
*/

int main(void) // Główna funkcja programu
{
    while(1)   // Pętla nieskończona
    {
        ...    // Tutaj umieszczamy kod dla mikrokontrolera
    }
}

 

 

 

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

 

 

 

Podsumowanie

 
   
Budowa programu w języku C:
/*
 * komentarz blokowy
 */

#include <plik nagłówkowy>

int main(void) // funkcja główna
{
   treść programu
}

 

Zapis do rejestru:

rejestr = wartość;

 

Pętla warunkowa:

while(warunek);  // sprawdza tylko warunek

while(warunek) instrukcja; // wykonuje w pętli jedną instrukcję

while(warunek)
{
   instrukcje;  // dowolna liczba instrukcji w pętli
}

 

Instrukcja warunkowa:

if(warunek) instrukcja1; else instrukcja2;

if(warunek)
    instrukcja1:
else
    instrukcja2;

if(warunek)
{
    instrukcje1; // dowolna liczba instrukcji
}
else
{
    instrukcje2; // dowolna liczba instrukcji
}

Rejestry:

DDRB – rejestr kierunku portu B
PORTB – rejestr wyjściowy portu B
PINB – rejestr wejściowy portu 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