Mikrokontrolery AVR, język C, podstawy                               programowaniaNiniejsza darmowa publikacja zawiera jed...
Styczeń 2011  ATNEL                Mikrokontrolery AVr  WYDAWNICTWO                       język   C                podstAw...
Książka przeznaczona jest dla elektroników i hobbystów, którzy chcą szybko, w oparciu o interesująceprzykłady, poznać języ...
Spis treści                                                                                                               ...
Strona | 4        3.6.13	 Bootloader	–	niesamowite	możliwości  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . ...
Strona | 5        4.4.3	     Pozostałe	operatory	przypisania .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  ....
Strona | 6     5.5   Wyświetlacz LCD (hd44780) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ....
Przedmowa                                                                                    Strona | 7   Stale rosnące za...
Strona | 81 Wstęp         Odkąd poznałem język C, byłem oczarowany jego możliwościami, prostotą i logiką         programow...
Strona | 584 Podstawy	języka	C         Wreszcie dotarliśmy do miejsca, gdzie będzie można poznać więcej informacji na tema...
Strona | 594.1.2 Definicja a DeKlaracja       Zapamiętaj różnice pomiędzy deklaracją a definicją, żebyśmy później dobrze s...
Strona | 604.1.3 Wyrażenia logiczne (Warunki)           W języku C występuje wiele instrukcji sterujących programem (pozna...
Strona | 61warunek, który może być dowolnym wyrażeniem, tylko wtedy zostanie wykonana instrukcjawystępująca w dalszej częś...
Strona | 62         if(warunek_1)         if(warunek_2) instrukcja1;         else         {         instrukcja2;         }...
Strona | 63       Dodam jeszcze, że instrukcje warunkowe if mogą sprawdzać warunki złożone, tzn. składające       się z wi...
Strona | 64          Zgodnie z tym co mówiliśmy o prawdzie i fałszu w języku C, wartość większa od zera będzie          za...
Strona | 65W powyższym przykładzie sekcję init stanowi instrukcja i=0. Inicjalizujemy w ten sposóbzmienną i, która będzie ...
Strona | 66         W tym przypadku dostrzeżesz, iż zmienna i służy do iteracji, natomiast niejako dodatkowo         można...
Strona | 67Wygląda to może troszkę skomplikowanie na pierwszy rzut oka, ale to tylko złudzenie,zapewniam cię. Już wyjaśnia...
Strona | 68         żadnej z założonych wartości, to zmienna czas domyślnie zostanie wyzerowana. Po wejściu         w inst...
Strona | 694.2.8 nawiasy Klamrowe       Kilka praktycznych porad jak ich używać aby uniknąć pomyłek, o które szczególnie ł...
Strona | 70         goto etykieta         …..         …..         …..         etykieta: instrukcje;         Etykieta to do...
Strona | 71          typach danych/zmiennych ma operować i załączać już konkretne procedury matematyczne?          Musimy ...
Strona | 72       •	     Typy do przechowywania kodów znaków alfanumerycznych            char            W istocie typ cha...
Strona | 73          Nazwa                Typ                                Zakres                     Bajty           ch...
Strona | 74         językach, występuje konieczność definiowania zmiennych na początku bloku kodu programu         czy blo...
Strona | 75Poprzez dodanie nawiasów kwadratowych utworzyliśmy typ złożony, jakim są tablice,przechowujące wiele elementów ...
Strona | 76         modyfikacji ich zawartości, czyli zapisu do pamięci EEPROM. Wybiegając jednak troszeczkę w         prz...
Strona | 77       plikach. Jednak zasięg globalny w tym momencie nie odnosi się do całego projektu, czyli       wszystkich...
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Mikrokontrolery avr język c podstawy programowania
Upcoming SlideShare
Loading in …5
×

Mikrokontrolery avr język c podstawy programowania

6,551 views
6,447 views

Published on

Published in: Education
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
6,551
On SlideShare
0
From Embeds
0
Number of Embeds
1
Actions
Shares
0
Downloads
14
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Mikrokontrolery avr język c podstawy programowania

  1. 1. Mikrokontrolery AVR, język C, podstawy programowaniaNiniejsza darmowa publikacja zawiera jedynie fragment pełnejwersji całej publikacji.Aby przeczytać ten tytuł w pełnej wersji kliknij tutaj.Niniejsza publikacja może być kopiowana, oraz dowolnie rozprowadzana tylko i wyłączniew formie dostarczonej przez Wydawnictwo ATNEL. Zabronione są jakiekolwiek zmiany wzawartości publikacji bez pisemnej zgody Wydawnictwa ATNEL - wydawcy niniejszejpublikacji. Zabrania się jej odsprzedaży.Pełna wersja niniejszej publikacji jest do nabycia wsklepie internetowym http://witmir.pl
  2. 2. Styczeń 2011 ATNEL Mikrokontrolery AVr WYDAWNICTWO język C podstAwy progrAMowAniA Mirosław Kardaś Mojej Żonie – Kasi
  3. 3. Książka przeznaczona jest dla elektroników i hobbystów, którzy chcą szybko, w oparciu o interesująceprzykłady, poznać język C przeznaczony dla mikrokontrolerów AVR i nauczyć się pisać dla nichprogramy. Jest to język wysokiego poziomu o nieograniczonych możliwościach, pozwala równieżłatwo i wygodnie dokonywać połączeń z językiem maszynowym asembler. W sposób przystępnyopisana została także architektura oraz możliwości samych mikrokontrolerów AVR wchodzących wskład dwóch rodzin: ATmega i ATtiny. Prezentowany materiał podzielony jest na trzy części. Pierwszaobejmuje zagadnienia związane z budową mikrokontrolerów, druga to wykład na temat podstaw samegojęzyka, a trzecia zawiera szereg ćwiczeń wraz z kodami źródłowymi, komentarzami i bogatymi opisami.Opracowanie graficzne: Mirosław KardaśRedakcja: Małgorzata Koczańska© Copyright by Wydawnictwo AtnelSzczecin 2011ISBN 978-83-931797-0-1Wydawnictwo ATNELul. Jasna 15/3870-777 Szczecinfax: 91 4635 683http://www.atnel.ple-mail: biuro@atnel.plWydanie IWszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor orazwydawnictwo Atnel dołożyli wszelkich starań, by publikowane tu informacje były kompletne i rzetelne. Nie biorą jednak żadnejodpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autororaz wydawnictwo Atnel nie ponoszą także żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacjizawartych w książce. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentów niniejszejpublikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii całości lub fragmentów książki bądź dołączonej płytyDVD metodą kserograficzną, fotograficzną, a także kopiowanie książki lub płyty DVD na nośnikach filmowych, magnetycznych,elektronicznych lub na nieutoryzowanych stronach internetowych powoduje naruszenie praw autorskich niniejszej publikacji.
  4. 4. Spis treści Strona | 3Przedmowa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 Wstęp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.1 Pierwszy „pusty” program w C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Zaczynamy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.2 Od programu do procesora . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2.1 Kompilacja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2.2 Środowisko . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.2.3 Programator sprzętowy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.2.4 Programowanie procesora . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 2.2.5 Uruchamiamy AVR Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.2.6 Platforma sprzętowa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 3.1 Informacje ogólne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 Procesory AVR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3.2 Programowanie ISP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 3.3 Sposoby taktowania procesorów . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 3.3.1 Wewnętrzny oscylator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.3.2 Zewnętrzny rezonator kwarcowy . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 3.3.3 Zewnętrzny oscylator RC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 3.4 Zagadnienia związane z zasilaniem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.3.4 Zewnętrzny generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 3.5 Układ resetu mikrokontrolera AVR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 3.6 Wewnętrzne moduły procesorów AVR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 3.6.1 Pamięć FLASH, RAM, EEPROM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 3.6.2 Przerwania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.6.3 Timery sprzętowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.6.3.1 Podstawowe tryby pracy Timerów . . . . . . . . . . . . . . . . . . . 42 3.6.3.1.1 Tryb zwykłego LICZNIKA . . . . . . . . . . . . . . . . . . . . 42 3.6.3.1.2 Tryb CTC – jeden z najważniejszych . . . . . . . . . . 44 3.6.3.1.3 Tryb PWM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 3.6.4 Przetwornik ADC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 3.6.5 Moduł komparatora analogowego . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 3.6.6 Moduł UART/USART, (czyli RS232) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 3.6.7 Moduł SPI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 3.6.8 Moduł TWI, (czyli I2C) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 3.6.9 Watchdog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.6.10 Tryby oszczędzania energii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.6.11 FUSE BITS (ustawienia konfiguracji AVR) . . . . . . . . . . . . . . . . . . . . . . 54 3.6.12 LOCK BITS (zabezpieczenia AVR) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
  5. 5. Strona | 4 3.6.13 Bootloader – niesamowite możliwości . . . . . . . . . . . . . . . . . . . . . . . . 56 4.1 Zagadnienia ogólne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 584 Podstawy języka C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 4.1.1 Komentarze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 4.1.2 Definicja a deklaracja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 4.2 Najważniejsze instrukcje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 4.1.3 Wyrażenia logiczne (warunki) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 4.2.1 Instrukcja warunkowa If , else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 4.2.2 Pętla while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.2.3 Pętla do..while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 4.2.4 Pętla for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 4.2.5 Instrukcja break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 4.2.6 Instrukcja switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 4.2.7 Instrukcja continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 4.2.8 Nawiasy klamrowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 4.3 Typy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 4.2.9 Instrukcja goto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 4.3.1 Systematyka typów języka C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 4.3.1.1 Typy złożone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 4.3.1.2 Zakres widoczności zmiennych . . . . . . . . . . . . . . . . . . . . . . 76 4.3.1.3 Typ void . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 4.3.1.4 Specyfikator const . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 4.3.1.5 Specyfikator volatile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 4.3.1.6 Specyfikator register . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 4.3.1.7 Instrukcja Typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 4.3.1.8 Typy wyliczeniowe enum . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 4.3.2 Stałe w języku C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.3.2.1 Stałe jako liczby całkowite . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.3.2.2 Stałe jako liczby zmiennoprzecinkowe . . . . . . . . . . . . . . . 86 4.3.2.3 Stałe znakowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 4.4 Operatory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 4.3.2.4 Stałe tekstowe, stringi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 4.4.1 Arytmetyczne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 4.4.1.1 Modulo, czyli % . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 4.4.1.2 Inkrementacja i dekrementacja ++ -- . . . . . . . . . . . . . . . . 91 4.4.1.3 Operator przypisania = . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 4.4.2 Operatory Logiczne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 4.4.2.1 Operatory relacji . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 4.4.2.2 Suma || oraz iloczyn && logiczny . . . . . . . . . . . . . . . . . . . . . 94 4.4.2.3 Negacja – wykrzyknik ! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 4.4.2.4 Operatory bitowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
  6. 6. Strona | 5 4.4.3 Pozostałe operatory przypisania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 4.4.4 Operator pobierania adresu & . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 4.4.5 Wyrażenie warunkowe ? : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 4.4.6 Operator sizeof() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 4.4.7 Priorytety operatorów . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 4.5 Funkcje *** . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 4.4.8 Operatory rzutowania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 4.5.1 Wynik działania funkcji – jak to działa? . . . . . . . . . . . . . . . . . . . . . . 110 4.5.2 Stos – ujarzmianie “potwora” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 4.5.3 Przekazywanie argumentów przez wartość . . . . . . . . . . . . . . . . . . . 114 4.5.4 Funkcje typu inline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 4.5.5 Zakresy widoczności nazw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 4.5.5.1 Zakres globalny . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 4.5.5.2 Zakres lokalny i zmienne automatyczne . . . . . . . . . . . . . 123 4.5.5.3 Zmienne i funkcje statyczne . . . . . . . . . . . . . . . . . . . . . . . . . 124 4.6 Preprocesor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 4.5.6 Funkcje w różnych plikach projektu . . . . . . . . . . . . . . . . . . . . . . . . . . 126 4.6.1 Dyrektywa #define . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 4.6.2 Makrodefinicje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 4.6.3 Dyrektywa #undef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 4.6.4 Operator ## - sklejanie nazw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 4.6.5 Operator zamiany na string # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 4.6.6 Dyrektywy kompilacji warunkowej . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 4.6.7 Dyrektywy #ifdef oraz #ifndef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 4.6.8 Dyrektywy #error i pozostałe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 4.7 Tablice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 4.6.9 Dyrektywa #include . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 4.7.1 Tablice wielowymiarowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 4.7.2 Tablica jako argument funkcji . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 4.8 Wskaźniki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 4.7.3 Tablice znakowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 4.9 Struktury, unie, pola bitowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 4.9.1 Struktury . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 4.9.2 Unie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 4.9.3 Połączenie struktury z unią . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 4.9.4 Pola bitowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 5.1 Przygotowanie procesora do pracy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1735 Warsztaty – zajęcia praktyczne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 5.2 Migocząca dioda LED . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 5.3 Obsługa klawiszy typu micro-switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 5.4 Multipleksowanie LED - przerwania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
  7. 7. Strona | 6 5.5 Wyświetlacz LCD (hd44780) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 5.6 Sterowanie PWM (kolorowa dioda RGB) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223 5.7 Pomiar napięcia za pomocą ADC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 5.7.1 Klawiatura analogowa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246 5.8 Komunikacja RS232 / RS485 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 5.7.2 Różnicowy pomiar napięcia - amperomierz . . . . . . . . . . . . . . . . . . . . . 246 5.8.1 Inicjalizacja, kalibracja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 5.9 Odczyt-zapis magistrali I2C (RTC, EEPROM) . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 5.8.2 UART, przerwania, bufor cykliczny . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266 5.9.1 RTC – sprzętowa obsługa I2C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278 5.9.2 Programowa implementacja I2C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285 5.10 Moduł SPI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 5.9.3 EEPROM – I2C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289 5.10.1 Sprzętowa obsługa SPI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 5.11 Magistrala 1Wire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 5.10.2 Programowa obsługa SPI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297 5.12 Odbiór kodów RC5 w podczerwieni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307 5.13 Sterowanie silnikami DC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316 5.14 Silnik krokowy unipolarny . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320 5.15 Silnik krokowy bipolarny . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326 5.16 Odczyt/zapis kart pamięci SD (FAT) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330 5.16.1 FatFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333 5.16.2 PetitFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3486 FuseBity – MkAvrCalculator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 6.16.1 Fusebity, Lockbity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 6.16.2 MkAvrCalculator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3607 Bootloader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368 8.1 Pilot na podczerwień . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3718 Projekty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371 8.2 Moduł Bluetooth (BTM-112/222) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379 8.3 Ściemniacz – płynna regulacja mocy 230V . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384 8.4 Wstęp do systemów czasu rzeczywistego . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395 8.5 Obsługa stosu AVR - TCP/IP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 8.5.1 Karta sieciowa ethernet – ENC28J60 . . . . . . . . . . . . . . . . . . . . . . . . . . 419 8.5.2 Serwer http . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422 8.6 Programator USBASP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454 8.5.3 Sterownik urządzeń – protokół UDP . . . . . . . . . . . . . . . . . . . . . . . . . . 4309 Środowisko ECLIPSE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455
  8. 8. Przedmowa Strona | 7 Stale rosnące zainteresowanie językiem C, dla mikrokontrolerów serii AVR firmy ATMEL, powoduje duże zapotrzebowanie na wszelkiego rodzaju kursy, poradniki, e-booki czy też książki. Z tymi ostatnimi jest niestety bardzo słabo. a to dlatego, że po prostu nie istniała dotąd żadna pozycja, która dotyczyłaby właśnie języka C oraz rodziny AVR. Postanowiłem napisać tę książkę, by pomóc wszystkim, chcącym poznać od podstaw tajniki tego uniwersalnego języka programowania. Ma ona za zadanie,w możliwie najprostszy sposób wprowadzić do świata C także te osoby, które do tej pory nie miały żadnego kontaktu z programowaniem i stoją na rozdrożu, próbując zdecydować się, jakiego języka zacząć się uczyć, aby efektywnie i szybko programować mikrokontrolery. Dlaczego C? W zamierzchłych czasach, gdy powstawały pierwsze mikroprocesory, rozwój oprogramowania był ściśle związany ze specyficznym językiem maszynowym każdego mikroprocesora. Powodowało to konieczność pisania programów dla ściśle określonych urządzeń. Języki najniższego poziomu, asemblery bazują na ‘mnemonikach’, które zastępują prawdziwy język ‘numeryczny’ zrozumiały dla procesora. Jednak, aby nie trzeba było pisać programów w postaci ciągu cyfr w systemie szesnastkowym typu: 0x3A, 0x1B, 0x41, 0x05, co miałoby spowodować załadowanie np. liczby 22 do określonej komórki pamięci RAM, można posługiwać się mnemonikami takich rozkazów. Dzięki czemu powyższy ciąg cyfr zastąpić można w asemblerze poleceniem o wieleprzyjaźniejszym dla oka, np.: MOV BUFOR, 22. Kompilator asemblera przetłumaczy sam taką mnemonik na ciąg cyfr zrozumiały dla konkretnego mikrokontrolera, które zostały przedstawione wyżej. Reasumując, asembler jest najniższą formą kodu maszynowego, dającego się zrozumieć przez człowieka. Pisanie programów w czystym asemblerze jest jak najbardziej możliwe i jeśli ktoś ma wieloletnie doświadczenie, pozwala to na osiąganie znacznej wydajności programu napisanego w ten sposób. Jak wspomniałem, aby efektywnie i dobrze pisać programy w języku najniższego poziomu, trzeba poświęcić wiele lat na naukę, a pomimo to nadal pisanie większych programów staje się bardzo uciążliwe, długotrwałe oraz wymaga czasu na przetestowanie i sprawdzenie wszelkiego rodzaju błędów. Na dodatek program napisany w specyficznym kodzie maszynowym jednego procesora będzie bardzo trudny do przeniesienia na inny typ. Czasem będzie to w ogóle niemożliwe i spowoduje konieczność napisania programu od początku. W związku z powyższym ogromny wkład pracy w napisanie programu zostaje niejednokrotnie zniweczony, gdy przychodzi zmiana założeń i konieczność zastosowania w urządzeniu innego typu mikroprocesora, a czasu na modyfikację i sprawdzenie działania jest niewiele. W takim momencie bardzo pomocny okazuje się język C. Jest to język ogólnego przeznaczenia, który może pracować na każdym mikrokontrolerze, dla którego stworzony jest kompilator C. W dzisiejszych czasach praktycznie nie ma procesorów, których nie można byłoby programować w C, za to zdarzają się już takie przypadki, gdzie producent wręcz nie dostarcza asemblera do swoich produktów, w zamian dając tylko kompilator C. Dzięki C można: szybko i łatwo poruszać się między różnymi rodzinami mikrokontrolerów, o wiele szybciej, efektywniej i wydajniej pisać i testować programy, a także tworzyć kod, który jest o wiele łatwiejszy do nauki, zrozumienia i zapamiętania.
  9. 9. Strona | 81 Wstęp Odkąd poznałem język C, byłem oczarowany jego możliwościami, prostotą i logiką programowania. Obecnie zauważam specyficzne podejście wielu osób, które po pierwszych próbach samodzielnej nauki C szybko się zniechęcają z powodu rzekomej dużej trudności i zawiłości zasad tego języka. Tymczasem prawdziwym powodem jest nierzadko brak literatury opisującej zasady języka C w oparciu o praktyczne przykłady, dzięki którym można z marszu rozwiązywać dużą część swoich początkowo przyziemnych problemów. Rzadko, kiedy książka na temat języka C, a szczególnie w aspekcie programowania mikrokontrolerów AVR, jest pisana dla osób, które nie mają jeszcze żadnego doświadczenia z programowaniem w ogóle. Sporo doświadczeń do napisania tej książki zebrałem podczas prowadzenia kursów języka AVR GCC dla procesorów AVR. Zatem jednym z celów, do których dążę w tej książce, jest próba przybliżenia i zainteresowania tym niezwykle przyjemnym i łatwym językiem osób, które właśnie stoją na rozdrożu i muszą podjąć ciężki wybór. W którą stronę pójść, aby w efektywny i łatwy sposób programować całą rodzinę mikrokontrolerów AVR Język C często traktowany jest jako narzędzie dla specjalistów a nie amatorów, hobbystów itp. Postaram się, więc przełamać te mity i udowodnić, że każdy po przeczytaniu tej książki będzie potrafił napisać samodzielnie przynajmniej proste programy ze zrozumieniem podstawowych zasad tego języka. Ponieważ jednak języka C ciężko uczyć się od strony praktycznej w oderwaniu od sprzętu, czyli w naszym konkretnym przypadku od mikrokontrolerów serii, AVR, dlatego konieczne będzie także przybliżenie zasad działania procesorów tej rodziny. Większość przykładów będzie odwoływała się do mikrokontrolerów serii ATmega, ale postaram się pokazać, że dzięki temu, iż korzystać będziemy z C to zaprogramowanie mikrokontrolerów z serii ATtiny nie będzie się praktycznie niczym różniło. Jedyne różnice, jakie wystąpią w tym przypadku, to pewne ograniczenia wynikające z możliwości sprzętowych. Dzięki powyższym założeniom książka ta skierowana jest do bardzo szerokiego grona czytelników, którzy usilnie poszukują wszelkich informacji na te tematy. Będę starał się używać prostego, czasem potocznego języka, aby przybliżyć bardziej skomplikowane zagadnienia. Na pierwszy rzut oka struktura książki może wydać się nieco chaotyczna, gdyż nie opisuję w niej po kolei całych zagadnień w oderwaniu od siebie. Nie znajdzie się tu pierwszej części, w której będzie w kilku kolejnych rozdziałach opisana rodzina mikrokontrolerów AVR. Nie znajdzie się kolejnej, gdzie będzie opisany czysty język C i nie znajdzie następnych rozdziałów osobno traktujących o środowiskach programistycznych, o programatorach czy o sposobach wgrywania programów fizycznie do mikrokontrolera. Przyjąłem założenie, iż książka będzie napisana w postaci kursu, jaki zwykle serwuję uczestnikom na zajęciach, gdzie wykłady z teorii przeplatane są z praktyką, czyli tzw. „warsztatami”, na których pod okiem instruktora każdy może uczyć się, pisać czy testować własne programy. Pozwoliło mi to na płynne przechodzenie z tematu na temat tak, aby w jak najprostszy sposób wprowadzić czytelnika do świata mikrokontrolerów AVR oraz ich programowania. Może więc nie w osobnych działach, ale w pewnej logicznej kolejności będę starał się podawać informacje tak, aby jak najszybciej można było je przyswajać. W sposób, który sprawdził się w praktyce. Potraktuj tę książkę jak dobrego przewodnika w trakcie przeprawy przez dżunglę, jaką mogą się wydawać zakresy szczegółowej wiedzy z wielu dziedzin elektroniki cyfrowej i programowania.
  10. 10. Strona | 584 Podstawy języka C Wreszcie dotarliśmy do miejsca, gdzie będzie można poznać więcej informacji na temat samego4.1 Zagadnienia ogólne języka C. Podobnie jak w przypadku omawiania podstawowych zagadnień dotyczących całej rodziny mikrokontrolerów, teraz będę musiał omówić składnię języka. W języku C stosujemy tzw. „wolny format” jeśli chodzi o pisanie kodu. Nie obowiązują tu reguły jak w innych językach, gdzie trzeba się ograniczać do pisania rozkazów w jednej linii. Nie ma tu żadnych przymusów. Wszystko, co chcemy zapisać, może się znaleźć w każdym miejscu linii, a nawet można to samo rozpisać na kilka linijek. Związane jest to z tym, że koniec instrukcji, jaką wydajemy, jest określony przez średnik, który stawiamy na końcu, a nie przez to, że kończy się linia programu. Wewnątrz instrukcji może znajdować się dowolna ilość tzw. białych znaków, do których zaliczamy spacje czy tabulatory. Są one ignorowane przez kompilator. Z tego względu nie ma różnicy w tym, jak zapiszemy poniższą linię - możemy to zrobić tak: int main(void) {return 0;} lub tak: int main(void) { // od tego miejsca zaczyna się start programu. /* komentarze */ return 0 ; // koniec programu }4.1.1 Komentarze Białe znaki są ignorowane przez kompilator, służą one jedynie programiście. Słyszałeś zapewne przy okazji pisania kodów programu o tzw. „wcięciach”. Dobrze napisany kod jest wtedy, gdy ma stosowane wcięcia. Bez nich kod staje się mało czytelny i bardzo ciężko wrócić do jego analizy po dłuższym czasie. Zauważyłeś powyżej w jednym z przykładów dwie charakterystyczne linie, w których widać tzw. komentarze. To opisy, które można wstawić do kodu w celu zwiększenia czytelności programowanych zagadnień. Jeśli w dowolnym miejscu linii kompilator napotka dwa znaki // następujące po sobie, to ignoruje wszystkie kolejne aż do końca tej linii. Inna forma do oznaczania całego bloku linii, w których chcemy umieścić opisy, może być zawarta pomiędzy dwoma znacznikami, gdzie jeden rozpoczyna blok /* natomiast drugi */ kończy taki blok. Zapamiętaj, że komentarze w języku C są bardzo istotnym elementem. Program napisany bez żadnych komentarzy czy krótkich chociaż objaśnień, nie jest napisany w dobrym stylu programistycznym. Stosuj komentarze zawsze, gdy przygotowujesz skomplikowane procedury, funkcje czy obliczenia tak, aby stanowiło to ułatwienie dla ciebie, gdy po dłuższym czasie wrócisz do analizy kodu. Komentarze także są istotne dla innych osób, które będą miały możliwość zapoznania się z kodem źródłowym twojego programu.
  11. 11. Strona | 594.1.2 Definicja a DeKlaracja Zapamiętaj różnice pomiędzy deklaracją a definicją, żebyśmy później dobrze się rozumieli. Brak zrozumienia tego zagadnienia na samym początku prowadzi do wielu nieporozumień, bywa także powodem rzekomych trudności w nauce języka C. 1. Deklaracja – określa pewne własności identyfikatora (zmiennej czy funkcji), jednak nie rezerwuje pamięci. 2. Definicja – zajmuje pamięć dla nowego obiektu i jednocześnie go deklaruje. Wynika z powyższego, że definicja jest równocześnie deklaracją, ale nigdy na odwrót. Przykłady Deklaracji: extern int a1; extern uint8_t tab[]; int max(int a, int b); 1. Informuje kompilator, że identyfikator a1 oznacza zmienną typu int. Jednocześnie słówko extern oznacza, że zmienna ta jest tworzona poza aktualnym plikiem źródłowym. 2. Informuje kompilator, że identyfikator tab jest tablicą elementów jedno-bajtowych bez znaku. 3. Informuje kompilator, że identyfikator max jest funkcją zwracającą wartość typu int, oraz przyjmującą dwa argumenty typu int. Przykłady Definicji: int b1; int c2 = 5; uint16_t tab[20]; int max(int a, int b) { return (a>b) ? a : b; } 1. Tworzy zmienną b1, zajmuje dla niej pamięć (w języku AVR GCC) będą to dwa bajty, oraz informuje kompilator, że identyfikator b1 oznacza zmienną typu int. 2. Tworzy zmienną c2, zajmuje dla niej pamięć, zostaje ona zainicjalizowana wartością 5, oraz informuje kompilator, że c2 oznacza zmienną typu int. 3. Tworzy tablicę tab, zajmuje dla niej pamięć 40 bajtów oraz informuje kompilator, że identyfikator tab jest tablicą dwubajtowych elementów bez znaku. 4. Tworzy funkcję max, zajmuje dla niej pamięć lecz tym razem w obszarze pamięci programu FLASH, umieszcza w niej program funkcji, oraz informuje kompilator, że funkcja max jest funkcją zwracającą wartość typu int a także o tym, że przyjmuje ona dwa argumenty także o typie int. Nazwy zmiennych i funkcji można tworzyć dowolnie, ale z pewnymi ograniczeniami: nie mogą one być nazwami słów kluczowych używanych przez kompilator oraz nie mogą zaczynać się od cyfry. Nazwy mogą być pisane zarówno wielkimi jak i małymi literami, jednak trzeba o tym pamiętać, ponieważ jeśli zdefiniujemy zmienną o nazwie Rozmiar (zaczyna się dużą literą), to później w kodzie kompilator nie rozpozna tej nazwy, jeśli napiszesz ją tak: rozmiar.
  12. 12. Strona | 604.1.3 Wyrażenia logiczne (Warunki) W języku C występuje wiele instrukcji sterujących programem (poznasz je w kolejnym rozdziale), które podejmują decyzje o wykonaniu lub niewykonaniu pewnych zadań w zależności od spełnienia lub niespełnienia jakiegoś warunku. Dokładniej mówiąc w zależności od tego, czy jakieś wyrażenie jest prawdziwe, czy fałszywe. Najpierw jednak musisz się dowiedzieć, co to jest prawda i fałsz w języku C. Poniżej kilka przykładów wyrażeń logicznych: 1. ( x < 50 ) 2. ( x == a ) 3. ( x != a ) Nie znając wartości zmiennych x lub a nie jesteśmy w stanie ocenić czy te wyrażenia są prawdziwe czy fałszywe. Jeżeli jednak powiem, tobie teraz, że x=7 natomiast a=10, to jesteś w stanie szybko stwierdzić, że: 1. Wyrażenie jest prawdziwe ponieważ 7 jest mniejsze od 50 2. Wyrażenie jest fałszywe ponieważ 7 nie równa się 10 3. Wyrażenie jest prawdziwe ponieważ 7 jest różne od 10 Zaraz, zaraz ale skąd będzie o tym wiedział mikrokontroler. Okazuje się, że to nie będzie dla niego żadnym problemem. Jeśli mikrokontroler napotka np. taki warunek ( x < 50 ), to najpierw podobnie jak my dokona obliczenia i na tej podstawie sprawdzi, czy jest on prawdziwy, czy fałszywy. Zmienna x przecież musiała być gdzieś wcześniej zdefiniowana w programie, stąd będzie znana jej wartość w momencie, gdy dojdzie do sprawdzania warunku. ZAPAMIĘTAJ! Wartość zero – jest zawsze rozumiana, jako stan: fałsz Wartość inna niż zero – jest zawsze rozumiana, jako stan: prawda Dzięki temu w wyniku operacji a=(5<10) kompilator przydzieli zmiennej a wartość jeden, natomiast w wyniku operacji a=(25<10) zmienna a przyjmie wartość zero. Dzięki powyższemu, zamiast w instrukcji sterującej wpisywać warunek sprawdzający czy np. wartość x jest większa od zera w tradycyjny sposób: (x>0), można zapisać to samo w prostszy (x). Ponieważ zgodnie z powyższymi definicjami prawdy i fałszu w języku C, warunek (x) będzie spełniony (prawdziwy) tylko wtedy, gdy x będzie większe od zera.4.2 Najważniejsze instrukcje Przy założeniu oczywiście, że korzystamy z typu liczby całkowitej bez znaku. W związku z tym warunek zapisany z kolei w ten sposób (1) będzie zawsze prawdziwy (spełniony). Zaczniemy od kluczowych instrukcji, bez których nie można byłoby napisać żadnego programu.4.2.1 instrukcja WarunkoWa if , else W języku C instrukcja if (co oznacza po polsku „jeśli”) może występować w dwóch postaciach: if(warunek) instrukcja if(warunek) instrukcja1 else instrukcja2 Są to podstawowe instrukcje języka C. Pierwsza postać oznacza, że jeśli będzie spełniony
  13. 13. Strona | 61warunek, który może być dowolnym wyrażeniem, tylko wtedy zostanie wykonana instrukcjawystępująca w dalszej części.Druga postać stosowana jest do tzw. „rozgałęzień” programu. Oznacza, że jeśli będzie spełnionywarunek to zostanie wykonana instrukcja1, a jeśli warunek nie będzie spełniony,to zostanie wykonana instrukcja2.Symbolicznie oznaczona instrukcja może stanowić zarówno jedną instrukcję programu, comożna zapisać w kodzie tak:if(x<50) wysokosc=0;else wysokosc=1;ale może także oznaczać kilka instrukcji, tyle że wtedy musimy je zebrać pomiędzy nawiasamiklamrowymi {}if(x<50){ wysokosc=0; y=0;}else{ wysokosc=100; y=20; z=33;}W pierwszej prostszej postaci w zależności od warunku (x<50) była przydzielana różnawartość do zmiennej o nazwie wysokosc.W drugiej postaci w zależności od spełnionego warunku lub nie, ustawilśmy pewnewartości kilku różnym zmiennym, dlatego zastosowaliśmy nawiasy klamrowe ograniczająceodpowiednio pierwszą i drugą (po else) sekcję warunku.Należy wspomnieć także, iż instrukcje if mogą być zagnieżdżone. Spójrzmy na kod poniżej.Widać na nim dwie instrukcje warunkowe zagnieżdżone, a dokładniej mówiąc, zagnieżdżonajest instrukcja if(warunek_2). Została ona tutaj specjalnie wyróżniona szarym kolorem ramki.Kolejnym wyróżnikiem, jaki występuje w kodzie programu, są „wcięcia” tabulatorów. Widzimy,że cały zagnieżdżony warunek jest przesunięty w prawo. Bez takich wcięć analiza koduprogramu byłaby prawie niemożliwa, a przynajmniej bardzo, ale to bardzo utrudniona.if(warunek_1){ if(warunek_2) { //instrukcje }}else{ // instrukcje}Wiemy jednak, że nawiasy klamrowe nie zawsze muszą występować, może dojść w takichsytuacjach do sporych problemów szczególnie, jeśli nie zastosujemy w odpowiedni sposóbwcięć w programie.
  14. 14. Strona | 62 if(warunek_1) if(warunek_2) instrukcja1; else { instrukcja2; } Jak przeanalizować taki kod? Do którego warunku odnosi się instrukcja else? Dla kompilatora jest to jasne jak słońce, ponieważ występuje zasada, że jeśli brak nawiasów klamrowych przed instrukcją else, to odnosi się ona zawsze do najbliższej poprzedzającej ją instrukcji if. Zobaczmy jednak, jak należy to zapisać tak, abyśmy także my mogli to spokojnie i bez błędów analizować. Znowu ważne są wcięcia. if(warunek_1) if(warunek_2) instrukcja1; else { instrukcja2; } Myślę, że teraz także dla ciebie na początku drogi w C będzie to bardzo przejrzysty zapis. Nie martw się, jeśli do tej pory miałeś problemy ze zrozumieniem różnego rodzajów kodów programów napisanych w C przez inne osoby. Nie znałeś jeszcze zasad, jakie rządzą składnią, a na dodatek mogłeś natknąć się na programy pisane bez wcięć przez niedoświadczone osoby lub takie, które już coś potrafią, ale uważają, że wcięcia nie są im potrzebne. Jednak takie podejście, uwierz mi, zawsze prędzej czy później skończy się źle. Bywają pewne formy, gdzie musi nastąpić wybór wielowariantowy za pomocą wielu instrukcji if … else. W takich sytuacjach można pominąć tabulatory (wcięcia), o ile będzie to np. taki prosty blok: if(warunek_1) instrukcja1; else if(warunek_2) instrukcja2; else if(warunek_3) instrukcja3; else if(warunek_4) instrukcja4; kolejne_instrukcje; Taki blok analizujemy następująco: jeśli spełniony jest warunek_1, wykonaj instrukcję1, zakończ działanie bloku i przejdź do kolejnych instrukcji programu. Jeśli jednak warunek_1 nie jest spełniony, to sprawdź warunek_2, jeśli jest spełniony, to zakończ działanie bloku i przejdź do kolejnych instrukcji programu. Jeśli warunek_2 nie jest spełniony, to sprawdź warunek_3 i tak dalej. Tego typu bloki konstrukcji wielopoziomowego wyboru od razu mogą skojarzyć się z pomysłem zastosowania tego mechanizmu do oprogramowania wielopoziomowego MENU dla użytkownika. Rzeczywiście, przy prostej budowie menu można z tego korzystać. Jednak niedługo poznamy specjalną instrukcję, która jeszcze wygodniej pozwala nam organizować wielopoziomowe wybory w kodzie programu.
  15. 15. Strona | 63 Dodam jeszcze, że instrukcje warunkowe if mogą sprawdzać warunki złożone, tzn. składające się z wielu warunków bądź obliczeń. Przyjrzymy się temu bliżej ,gdy będziemy omawiać operatory. Wtedy lepiej zrozumiesz zapis typu: if ( (x>50 && x<100) || (x==5) ) instrukcja1; Na razie powiem tylko, że instrukcja1 zostanie tylko wtedy wykonana, jeśli zmienna x zawiera się w przedziale od 51 do 99 lub jest równa 5. Znaki && oraz || to właśnie operatory.4.2.2 Pętla While Konstrukcja while (po polsku „dopóki”) służy do realizacji jednej z podstawowych pętli programowych. Występuje ona w formie: while(warunek) instrukcja(-e); Oznacza to, że dopóki warunek będzie spełniony (prawda), dopóty będzie wykonywana instrukcja. Zgodnie ze składnią języka, o której pisaliśmy wyżej, pojedynczą instrukcję można zastąpić dowolnym blokiem wielu instrukcji tyle, że trzeba je umieścić wewnątrz nawiasów klamrowych {}. Można więc w ramach jednej pętli zapisać wiele instrukcji w ten sposób: x=0; while(x<10) { // instrukcja1 // instrukcja2 // instrukcja3 // …………. // instrukcjaN x=x+1; } // kolejne instrukcje programu Zawartość pętli będzie wykonana dziesięciokrotnie. Zauważ, że przed rozpoczęciem pętli przypisaliśmy zmiennej x wartość zero. Zatem warunek (x<10) jest spełniony i zostaną wykonane kolejno instrukcje wewnątrz nawiasów klamrowych. Ostatnia instrukcja spowoduje zwiększenie wartości x o jeden, po czym znowu nastąpi sprawdzenie warunku. Jako że x równy będzie 1, to i tym razem warunek zostanie spełniony. Blok instrukcji będzie dotąd wykonywany, dopóki zmienna x w wyniku zwiększania zawartości o jeden nie osiągnie w końcu wartości równej dziesięć. W takiej sytuacji warunek (x<10) nie będzie już prawdziwy/ spełniony i pętla nie wykona instrukcji zawartych w nawiasach klamrowych. Rozpocznie się wykonywanie kolejnych instrukcji programu. Bardzo często stosuje się w programach tzw. pętlę nieskończoną. Chodzi o to, aby wykonywać pewien blok instrukcji bez końca. Można wtedy posłużyć się konstrukcją: while(1) { // instrukcje }
  16. 16. Strona | 64 Zgodnie z tym co mówiliśmy o prawdzie i fałszu w języku C, wartość większa od zera będzie zawsze oznaczać prawdę. Zatem warunek (1) będzie zawsze spełniony, ponieważ liczba 1 jest większa od zera i symbolizuje w tym warunku „prawdę”. Zauważ, proszę, istotę działania tej pętli. Otóż, zawsze przed jej pierwszym wykonaniem sprawdzany jest warunek. Gdyby nie był on spełniony (prawdziwy), to nigdy nie doszłoby do wykonania instrukcji w jej wnętrzu.4.2.3 Pętla do..While Konstrukcja do … while … oznacza z angielskiego Rób … Dopóki … i pozwala na realizację innego rodzaju pętli programowej. Jej forma to: do instrukcja1 while(warunek); Po analizie oznacza to, rób (wykonuj) instrukcję1, dopóki będzie spełniony warunek. Jak zwykle też pojedynczą instrukcję możemy zastąpić blokiem wielu instrukcji umieszczonych wewnątrz nawiasów klamrowych. do { Instrukcja1; Instrukcja2; Instrukcja3; } while(warunek); Zauważ, że w odróżnieniu od omawianej wyżej zwykłej pętli while, tutaj mamy do czynienia z sytuacją, w której najpierw wykonywana jest instrukcja1 lub blok instrukcji, a dopiero na końcu sprawdzany warunek. Zatem w pierwszym przebiegu tej pętli zostaną zawsze wykonane instrukcje w jej wnętrzu.4.2.4 Pętla for Ten typ pętli programowej wykonywany jest zdecydowanie najczęściej w różnych programach. Posiada ona postać: for( init ; wyrażenie_warunkowe ; krok) treść_pętli; init oznacza instrukcję bądź grupę instrukcji, które służą do inicjalizacji pracy pętli. W praktyce najczęściej będziesz stosował tu pojedynczą instrukcję. wyrażenie_warunkowe tak jak to zwykle bywało w instrukcjach warunkowych, będzie obliczane przed każdym wykonaniem pojedynczego obiegu pętli. Jeśli wyrażenie/warunek będzie spełniony, to przebieg pętli zostanie wykonany, jeśli przestanie być prawdziwy, to przebieg nie zostanie wykonany. krok to instrukcja wpływająca na licznik wykonywania pętli. Jest ona realizowana za każdym razem na zakończenie pojedynczego obiegu pętli tuż przed ponownym sprawdzeniem wyrażenia warunkowego na początku pętli. W praktyce będzie to wyglądało tak: for(i=0;i<10;i=i+1) { instrukcja1; }
  17. 17. Strona | 65W powyższym przykładzie sekcję init stanowi instrukcja i=0. Inicjalizujemy w ten sposóbzmienną i, która będzie odpowiedzialna za iterację (wielokrotnie powtarzalną czynność).Sekcja wyrażenie_warunkowe to w naszym przypadku warunek i<10. Zatem pętlabędzie się wykonywała do momentu, dokąd warunek będzie prawdziwy. Jako że zmiennai została zainicjalizowana wartością zero, można powiedzieć, że pierwszy przebieg pętlizostanie na pewno wykonany, gdyż warunek taki będzie prawdziwy.Dzięki sekcji krok, która u nas ma postać i=i+1 wiemy, że za każdym przebiegiem pętli,pod koniec wykonywania każdego jej obiegu zmienna i będzie zwiększana o jeden. Co zatym idzie, można śmiało wywnioskować, że pętla taka wykona się 10 razy.Ile razy wykonana zostałaby pętla for zapisana w poniższy sposób?for(i=0;i<10;i=i+2) instrukcja1;Tylko pięć razy, ponieważ wartość zmiennej i w sekcji krok, jest zmieniana w większymtempie. Tym razem i=i+2. Zatem wyrażenie_warunek będzie spełnione tylko wtedy,gdy wartości zmiennej i będą wynosiły kolejno: 0, 2, 4, 6, 8.Mam nadzieję, że ten krótki opis dał tobie dużo do myślenia i jeśli przypadkiem znasz pętlefor z innych języków programowania, to śmiało stwierdzisz, że składnia tej pętli w językuC jest zdecydowanie najlepsza. Daje ogrom możliwości i nie wprowadza wielu ograniczeń.Dodatkową ciekawostką jest to, że w języku C można śmiało pomijać niektóre bądź wszystkieczęści składowe pętli, pozostawiając jedynie znaki średników, które je oddzielają. Zatemponiższy zapis:for(;;){ // instrukcje;}Często spotkasz, jako pętlę nieskończoną. Opuszczenie sekcji wyrażenie_warunekjest zawsze równoznaczne w tym przypadku z tym, jakby warunek był zawsze spełniony.Można także skorzystać z zapisu:for(i=5;x>20;) instrukcja;W takim przypadku mamy do czynienia z inicjalizacją zmiennej i w pętli, następniezostaje sprawdzany warunek (x>20), który wcale nie musi być związany ze zmiennątypu iteracyjnego, czyli i. Natomiast pominęliśmy w ogóle sekcję krok. Oznacza to, żepętla będzie pracować w zależności od tego, co wewnątrz niej będzie się działo z wartościązmiennej x.Jak wspominałem wcześniej, sekcja inicjalizacji bądź sekcja krok mogą składać sięz kilku instrukcji oddzielonych od siebie przecinkiem. Nie nadużywaj jednak takich konstrukcjize względu na możliwość znacznego zmniejszenia czytelności kodu programu. Przykład:for(i=0,k=10;i<10;i=i_1,k=k-1){ // instrukcje pętli}
  18. 18. Strona | 66 W tym przypadku dostrzeżesz, iż zmienna i służy do iteracji, natomiast niejako dodatkowo można wykorzystać sekcje pętli do cyklicznych działań z innymi zmiennymi, które mogą być przydatne wewnątrz pętli. Każda pętla może także zostać przerwana w dowolnym momencie za pomocą specjalnej instrukcji, o której powiem za chwilę.4.2.5 instrukcja break Instrukcja break z angielskiego oznacza w tym przypadku „przerwać”. Może zostać ona użyta wewnątrz dowolnej pętli programowej lub wewnątrz instrukcji switch. Powoduje ona natychmiastowe i bezwarunkowe przerwanie działania pętli bądź instrukcji switch oraz jej opuszczenie. W związku z czym program rozpoczyna wykonywanie kolejnych instrukcji programu, jakie znajdują się po wystąpieniu pętli lub instrukcji switch. Oznacza to, że można przerwać działanie każdej formy tzw. pętli nieskończonej. Wystarczy w jej wnętrzu wstawić polecenie break. Oczywiście takie polecenie najczęściej w tego typu przypadkach zostaje użyte w zależności od zaistnienia pewnej sytuacji, czyli jednym słowem w zależności od spełnienia jakiegoś warunku/wyrażenia, np. while(1) { // instrukcje if(warunek) break; // instrukcje } Poznaliśmy już wcześniej taką konstrukcję pętli nieskończonej z użyciem pętli while, jednak równie dobrze moglibyśmy zastosować konstrukcję for(;;) zamiast while(1). Tak czy inaczej wewnątrz za każdym obiegiem sprawdzany jest jakiś warunek, i jeśli zostanie on spełniony, wykonywanie obiegu pętli zostanie natychmiast przerwane. Nie wykona się w jej wnętrzu już żadna następna instrukcja.4.2.6 instrukcja sWitch Switch z angielskiego oznacza „przełącznik”. Tak też zachowuje się ta instrukcja. Służy ona do podejmowania wielowariantowych decyzji. To właśnie za jej pomocą można zastąpić blok wielowariantowego wyboru, o jakim mówiłem w rozdziale poświęconym instrukcjom if… else. Oto jak wygląda postać takiej instrukcji. Jest to pewna konstrukcja, spójrz poniżej: switch(wyrażenie) { case wartosc1: instrukcje; [break;] case wartosc2: instrukcje; [break;] case wartosc3: instrukcje; [break;] default: instrukcje; }
  19. 19. Strona | 67Wygląda to może troszkę skomplikowanie na pierwszy rzut oka, ale to tylko złudzenie,zapewniam cię. Już wyjaśniam, co to wszystko po kolei oznacza. To potężne narzędziew języku C.Instrukcja rozpoczyna się od sprawdzenia naszego przełącznika, którym jest wyrażenie.Oznacza to, że w nawiasach okrągłych może wystąpić sama zmienna np. x, ale równie dobrzemoże wystąpić wyrażenie matematyczne, którego wynik będzie przełącznikiem. Następniewewnątrz nawiasów klamrowych mamy sekcje o nazwie case lub default, dzięki którymmożemy zdecydować, jakie instrukcje chcemy wykonać w zależności od konkretnych wartościnaszego wyrażenia/przełącznika. Po słówku case zawsze podajemy wartość przełącznika, jakanas interesuje, co oznacza, że jeśli przełącznik będzie miał w momencie wejścia w instrukcjęswitch taką wartość, to instrukcje występujące w kolejnych liniach po słówku case zostanąwykonane, jeśli inną wartość, to pominięte i zostanie rozpatrzona kolejna pozycja case.Po każdym pakiecie instrukcji następujących po sprawdzeniu określonej wartości przełącznikacase, może wystąpić instrukcja break. Tylko dlatego ująłem ją w powyższym schematycznymw przykładzie w nawiasy kwadratowe, aby zakomunikować, że instrukcja break może wtym miejscu występować, ale nie musi. Nie jest to obligatoryjne. Jednak, jeśli jej nie ma, tozostaną wykonane kolejne instrukcje zawarte instrukcji następnych sekcji case, switch. Może tospowodować, że całość nie zareaguje tylko na jeden przełącznik, a na kilka. Zatem jeśli zależynam na wykonaniu instrukcji dotyczących tylko jednego przełącznika, to najczęściej będziemyblok rozpoczynający się od słówka case kończyli rozkazem break, który przerwie dalszewykonywanie instrukcji zawartych w switch, ponieważ uznajemy, iż inne są niepotrzebnew tym momencie. W praktyce może to wyglądać tak:x=2;switch(x){ case 0: czas=10; break; case 1: czas=23; break; case 2: czas=38; break; case 3: czas=42; break; default: czas=0;}Króciutko przeanalizujemy, co stanie się w wyniku działania powyższego kodu programu.Na początku bądź „ręcznie”, bądź w wyniku wykonania jakiejś funkcji, zmienna x przyjmujewartość równą dwa. Rozpoczyna się teraz instrukcja switch sprawdzająca wartość zmiennejx, pełniącej dla nas rolę przełącznika, od którego chcemy spowodować, aby z kolei zmiennaczas przyjęła pewną konkretną wartość. Zakładamy także, że jeśli zmienna x nie osiągnie
  20. 20. Strona | 68 żadnej z założonych wartości, to zmienna czas domyślnie zostanie wyzerowana. Po wejściu w instrukcję switch za pomocą pierwszego słówka case, sprawdzamy, czy nasz przełącznik, jakim jest wartość zmiennej x, nie posiada wartości zero. Jeśli nie, to zignorowane zostaną kolejne linijki programu aż do napotkania kolejnego momentu, w którym pojawi się słówko case. Oznaczać to będzie, że po raz kolejny sprawdzamy, czy nasz przełącznik nie posiada wartości równej jeden. Jeśli nie, to program przeskakuje do kolejnego słówka case, które tym razem sprawdza, czy x równa się dwa? Zgadza się, jak widać przed instrukcją switch, zmienna x jest równa dwa. W takim razie, rozpoczną się wykonywać kolejne instrukcje, które znajdują się po tym właśnie sprawdzeniu słówkiem case. W naszym przypadku jest to tylko jedna instrukcja, ale można równie dobrze w kolejnych liniach napisać ich więcej. Tutaj można, ale nie trzeba, koniecznie stosować do bloku instrukcji, nawiasów klamrowych. Zauważ jednak, że na zakończenie tych instrukcji zostaje wykonana instrukcja break. Powoduje ona zakończenie działania całości. O to nam chodziło. Aby w zależności od konkretnej wartości zmiennej x odpowiednio ustawić zmienną czas. Dodajmy na koniec, że gdyby wartość zmiennej x była różna od 0, 1, 2, 3 (bo takie wartości zostają sprawdzane za pomocą słówek case), to zrealizowana zostałaby sekcja instrukcji na końcu po słówku default. W naszym przypadku zmienna czas zostałaby wyzerowana. Jeśli taka sekcja case lub default występuje na samym końcu, to zbędne jest już użycie instrukcji break.4.2.7 instrukcja continue Instrukcja ta bywa przydatna wewnątrz każdej z omawianych pętli programowych. Może czasem wystąpić sytuacja, gdy pętla zawiera długi blok instrukcji programu występujących jedna po drugiej, że w zależności od jakiegoś czynnika chcemy pominąć wykonywanie części bloku tychże instrukcji. Jej postać przedstawia się następująco: for(;;) { instrukcja1; instrukcja2; instrukcja3; if(warunek) continue; instrukcja4; instrukcja5; } Oczywiście rodzaj pętli może być dowolny, równie dobrze w tym przykładzie moglibyśmy zastosować while() czy też do…while(). Jak to działa? Otóż zakładając, że jeśli warunek nie jest spełniony, to dokładnie w każdym obiegu pętli wykonywane są wszystkie instrukcje od 1 do 5. Jeśli jednak w konkretnym czy też w wielu przebiegach warunek zacznie być spełniony/prawdziwy, to instrukcje od 4 do 5 są całkowicie pomijane. Można powiedzieć, że instrukcja continue powoduje przejście na sam koniec pętli. Efekt będzie taki, jakby całość została wykonana, następuje zakończenie obiegu, po czym zostaje sterowanie przekazane znowu na początek pętli, gdzie sprawdzane są jej warunki pracy.
  21. 21. Strona | 694.2.8 nawiasy Klamrowe Kilka praktycznych porad jak ich używać aby uniknąć pomyłek, o które szczególnie łatwo, jeśli stosować będziemy wiele zagnieżdżonych instrukcji if, a w nich jeszcze rozbudowanych pętli, które na dodatek także mogą zawierać kolejne instrukcje if. Poniżej przedstawię często spotykane trzy sposoby używania nawiasów klamrowych w programach. while(1) { instrukcje; I sposób } while(1) { instrukcje; II sposób } while(1) { Instrukcje; III sposób } Każdy sposób jest generalnie prawidłowy, gdyż zawiera odpowiednie wcięcia. Jednak warto zdecydować się na jeden z nich taki, który tobie będzie sprawiał najmniej problemów. Dla mnie najlepszym sposobem, jakiego najczęściej korzystam, gdy piszę własne kody, jest ten trzeci. Powiem więcej, żeby uniknąć pomyłek związanych z pisaniem długiego kodu programu i zagnieżdżonych instrukcji, po których stosuję klamry, zawsze podchodzę do tego właśnie tak. Po napisaniu instrukcji warunkowej czy pętli wciskam klawisz ENTER, po czym równo pod rozpoczynającą się instrukcją stawiam otwarty nawias klamrowy, ponownie klikam klawisz ENTER (nawet dwukrotnie) i wstawiam zamknięty nawias klamrowy równiutko pod tym otwartym powyżej. Dopiero wtedy przenoszę kursor pomiędzy oba nawiasy i zaczynam wpisywać kod programu pomiędzy nimi. Dzięki temu rzadko mylę się, jeśli chodzi o stosowanie tych nawiasów. Dodam, że niektóre zaawansowane środowiska jak np. ECLIPSE, opisane przeze mnie wyżej czynności wykonują za mnie automatycznie! Oznacza to, że gdy po napisaniu instrukcji warunkowej lub pętli wcisnę raz klawisz ENTER, to automatycznie pod spodem umieszczone zostają od razu dwa nawiasy klamrowe a kursor umiejscawia się wraz z poprzedzającym go tabulatorem/wcięciem w linii pomiędzy nimi, dzięki czemu bez uciążliwych wyżej opisanych czynności przystępuję do pisania kodu. Inne środowiska i edytory oferują jeszcze inne narzędzia/gadżety wspomagającą pracę programisty w tym zakresie. Dlatego pisanie programu w zwykłym lub lekko zaawansowanym programie typu notatnik, który oferuje tylko kolorowanie składni, bywa w dzisiejszych czasach bardzo uciążliwe.4.2.9 instrukcja goto Pozostawiłem tę instrukcję na koniec. Najchętniej w ogóle bym jej nie omawiał, ponieważ jej istnienie powoduje, że początkujący często nabierają złych nawyków programowania, gdy się przyzwyczają zbytnio do tej instrukcji. Niemniej jednak jest kilka drobnych sytuacji, gdzie może się ona przydać. Wtedy nie jest wstydem jej używanie. W pozostałych przypadkach jej nadmierne stosowanie wręcz świadczy tylko źle o programiście. Cóż to za „wstydliwa” instrukcja? Jej składnia to:
  22. 22. Strona | 70 goto etykieta ….. ….. ….. etykieta: instrukcje; Etykieta to dowolna aczkolwiek niezarezerwowana nazwa, po której musi wystąpić znak dwukropka. Działanie jest banalnie proste, sprowadza się do tego, że jeśli program napotka tę instrukcję, to wykonuje skok do miejsca, które wskazywane jest przez etykietę. Ważne, że etykieta musi znajdować się w odpowiednim zakresie widoczności. O zakresach widoczności, więcej powiem w następnych rozdziałach. Wspominałem jednak, że bywają sytuacje, gdzie możemy prawie bez żadnego wstydu z niej skorzystać. Co wcale nie oznacza, że bez niej sytuacja jest bez wyjścia. Wszystko zależy od inwencji twórczej programisty jak zwykle. Zatem wyobraź sobie, na razie czysto teoretycznie, że masz wielokrotnie zagnieżdżone pętle wraz z zagnieżdżonymi warunkami if() lub funkcjami switch() wewnątrz nich. Przychodzi jednak taki moment, że bezwarunkowo musisz opuścić te wszystkie pętle bez konieczności wielokrotnego używania rozkazu break, który już znasz. Wtedy można sięgnąć po instrukcję goto, za pomocą której jednym prostym sposobem, przenosisz sterowanie programu całkowicie na koniec takiego długiego zagnieżdżonego bloku instrukcji. Jednak zawsze tylko w ramach4.3 Typy widoczności. Napomknę tylko, że np. nie można wykonać skoku goto pomiędzy różnymi funkcjami. To właśnie stanowi ograniczenie zakresu jej widoczności. Przechodzimy do omówienia jednego z najbardziej istotnych zagadnień języka C, którego zrozumienie posiada fundamentalne znaczenie dla dalszej nauki. Traktując to zbyt pobieżnie wyrządziłbym ci krzywdę. Postaraj się uważnie przeczytać i dobrze zrozumieć oraz zapamiętać podane tutaj informacje. Bez nich „ani rusz” w dalszej naszej drodze. Każda nazwa, jaka występuje w języku C (poza nazwami etykiet np. przy instrukcjach goto), zanim zostanie użyta w programie, musi koniecznie zostać zdeklarowana. Tak naprawdę wspominaliśmy już o deklaracjach i różnicach pomiędzy definicjami w rozdziale „Deklaracja a Definicja”. Przyjrzyjmy się temu nieco bliżej. Załóżmy, że kompilator napotka na swojej drodze zapis typu: a = b + c; Występuje tu operacja dodawania oraz podstawienie wyniku tej operacji do zmiennej a. Kompilator musi, zatem uruchomić wewnętrzne procedury, które będą mogły dokonać stosownych obliczeń. Jednak dla różnych działań matematycznych i nie tylko matematycznych, mogą występować różne procedury. Poza tym nawet, jeśli w tym przypadku będzie to działanie matematyczne (dodawanie), to kompilator musi wiedzieć, jakiego typu są te zmienne. Inaczej będzie bowiem wykonywał dodawanie liczb całkowitych bez znaku, inaczej dodawanie liczb całkowitych ze znakiem, inaczej dodawanie liczb zmiennoprzecinkowych lub mieszanych, jeszcze inaczej liczb o różnych możliwych zakresach wielkości. Jeśli liczby a oraz b będą się mieściły np. w zakresie od 0 do 255, to będzie oznaczać, że ich wartości można zapisać tylko w jednym bajcie. Jednak już wynik takiej operacji, jak się domyślasz, może być większy niż 255, więc będzie musiał zostać zapisany w zmiennej składającej się z dwóch bajtów. Jednak skąd nasz „biedny” kompilator może wiedzieć na podstawie tylko zapisu w formie pokazanej powyżej, jakich operacji ma użyć, skoro nie powiedzieliśmy mu wprost, na jakich
  23. 23. Strona | 71 typach danych/zmiennych ma operować i załączać już konkretne procedury matematyczne? Musimy wcześniej zadeklarować, a jeśli obliczenia mają być wykonane podczas działania programu w oparciu o pamięć RAM mikrokontrolera, to musimy zmienne zdefiniować. Pamiętając, że definicja zmiennej jest równoważna z jej deklaracją. Dobrze spójrzmy, w jakiej postaci można podać te wszystkie informacje kompilatorowi. int a; uint8_t b=188, c=220; a = b+c; Proszę bardzo, po dokonaniu takiego zapisu, kompilator nie „piśnie” już słówka o błędach podczas przeprowadzania kompilacji tak napisanego kodu programu. Wyjaśnijmy sobie, jakich operacji dokonujemy w kolejnych liniach. Umiejętność takiej analizy to podstawa. Aby pisać program, który będzie zrozumiały dla kompilatora, musisz się nauczyć myśleć jak kompilator, w pewnym zakresie przynajmniej. A zatem: 1. Następuje deklaracja zmiennej a, która mówi, że zmienna a będzie oznaczała liczbę całkowitą ze znakiem w rozmiarze wynoszącym dwa bajty. Jednak, ponieważ jest to przede wszystkim konkretna definicja, to zostaje zarezerwowane miejsce w pamięci RAM mikrokontrolera o wielkości dwóch bajtów. To w tym miejscu kompilator będzie przetrzymywał podczas „życia” całego programu zawartość zmiennej a. Definicja ta nie powoduje jednak ustawienia wstępnej wartości tej zmiennej. Zatem może ona być przypadkowa albo może być automatycznie inicjalizowana wartością zero. (Niedługo dowiesz się, kiedy przypadkowa, a kiedy automatyczna ). 2. Następuje na podobnej zasadzie jak wyżej definicja oraz deklaracja zmiennych o nazwie b oraz c. Jednocześnie zostaje dla nich zarezerwowana pamięć. Po jednym bajcie na każdą, co związane jest z typem uint8_t specyficznym dla kompilatora AVR GCC. Jednocześnie obydwie zmienne zostają zainicjalizowane wartościami 188 oraz 200. 3. Ta linijka programu to już nie deklaracja ani nie definicja. To jest już konkretna instrukcja programu. W wyniku jej działania kompilator podłączy procedury, które wykonają najpierw dodawanie liczb całkowitych bez znaku o rozmiarze jednego bajta, a następnie procedurę, która wynik tego dodawania umieści w zmiennej a. To nic, że zmienna a posiada inny typ. Ważne, że typ int potrafi pomieścić liczbę większą od 255. Jak widzisz, to programista, czyli ty – musi dbać o to, jakimi typami danych/zmiennych się posługuje!. Pamiętaj o tym na zawsze. Reasumując, jeszcze raz przypomnę bardzo istotną różnicę pomiędzy deklaracją a definicją za pomocą nieco innych słów. Musi to do ciebie dotrzeć w pełni. Deklaracja – tylko informuje kompilator o tym, jakiego typu może być zmienna. Definicja – nie tylko informuje kompilator, ale rezerwuje pamięć mikrokontrolera.4.3.1 systematyka tyPóW języka c W standardowej definicji języka C istnieją, tzw. podstawowe typy wbudowane. Nie będę tu omawiał wszystkich dokładnie i w szczegółach, ponieważ nas będą bardziej interesowały, specyficzne typy wbudowane w naszą wersję kompilatora AVR GCC. • Typy do przechowywania i pracy z liczbami całkowitymi short int int long int
  24. 24. Strona | 72 • Typy do przechowywania kodów znaków alfanumerycznych char W istocie typ char nie służy tylko do przechowywania kodów znaków alfanumerycznych, może on przechowywać liczby całkowite podobnie jak unsigned short int. Jednak na początku postaraj się kojarzyć go z kodami znaków alfanumerycznych. (To moja propozycja nie tylko na potrzeby tej publikacji, ale także dla ułatwienia wejścia w świat C). Wszystkie powyższe typy mogą występować w dwóch wariantach, liczby ze znakiem i bez znaku. Co oznacza, że do poszczególnych typów można dodawać znaczniki: signed oraz unsigned, np.: signed int – liczba całkowita ze znakiem unsigned int – liczba całkowita bez znaku podobnie z typem alfanumerycznym: signed char – liczba całkowita reprezentująca ze znakiem unsigned char – liczba całkowita reprezentująca znak alfanumeryczny bez znaku W przypadku typu char wyszło nam w opisie troszkę takie „masło maślane”, ale już wyjaśniam, o co chodzi. Wszędzie, gdzie mówimy o znaku czyli signed oraz unsigned mamy na myśli typy, które mogą przechowywać tylko liczby dodatnie i ujemne – te oznaczone signed, natomiast te oznaczone unsigned mogą przechowywać tylko liczby dodatnie. • Typy do przechowywania i pracy z liczbami zmiennoprzecinkowymi float double Pozwalają one pracować na liczbach rzeczywistych o różnej precyzji. Z tym, że ze względu na ograniczenia możliwości małych mikrokontrolerów, do jakich zalicza się rodzina AVR, pozostał wprawdzie typ double, aby była zgodność ze standardem, jednakże jego precyzja jest dokładnie taka sama jak typu float. • Typy do przechowywania i pracy z wartościami logicznymi bool Zmienne tego typu mogą przechowywać tylko dwie wartości, prawdę lub fałsz. W praktyce można takim zmiennym przypisywać wartości oznaczone, jako false lub true. Co z kolei i tak na końcu sprowadza się do tego, że zmienna tego typu i tak będzie tak posiadała wartość zero albo jeden. W związku z czym niezbyt często używa się tych typów. Tym bardziej, że wiąże się to z koniecznością załączania do programu oddzielnej biblioteki zwanej stdbool.h.
  25. 25. Strona | 73 Nazwa Typ Zakres Bajty char całkowity -128…127 1 unsigned char całkowity 0…255 1 short int całkowity -32768…32767 2 unsigned short int całkowity 0…65535 2 long int całkowity -2^31…2^31-1 4 unsigned long int całkowity 0…2^32-1 4 long long int całkowity -2^63…2^63-1 8 long long unsigned int całkowity 0…2^64-1 8 int całkowity = short int 2 unsigned int całkowity = unsigned short int 2 float rzeczywisty 6 znaków precyzji 4 double rzeczywisty 10 znaków precyzji 1 8 bool logiczny logiczny 2 Dla oznaczenia braku danych void pusty 0 1 Język o którym mówimy, AVR GCC nie obsługuje formatu liczb zmiennoprzecinkowych podwójnej precyzji. Jednak ze względu na zgodność ze standardem można deklarować zmienne typu double tyle, że kompilator potraktuje je jakby były to zmienne typu float. Poniżej charakterystyczne typy tylko dla AVR GCC: Wielkość Typ Zakres bity bajty uint8_t 8 1 0 to 255 int8_t 8 1 -128 to 127 uint16_t 16 2 0 to 65535 int16_t 16 2 -32768 do 32767 uint32_t 32 4 0 do 4294967295 int32_t 32 4 -2147483648 do 2147483647 uint64_t 64 8 0 do 1.8*1019 int64_t 64 8 -9.2*1018 do 9.2*1018W standardowym języku C występują jeszcze inne typy, jednak na razie nie będziemyo nich wspominać, gdyż nie wszystkie dotyczą naszych mikrokontrolerów. Przedstawięraczej zestawienie typów wbudowanych, z jakimi będziemy mieli do czynienia korzystającz naszej wersji kompilatora AVR GCC.Bardzo istotną i pozytywną cechą języka C jest to, że mamy możliwość definiowana zmiennych„w locie”. Cóż to oznacza? Najpierw odwołam się do innych języków, być może miałeśmożliwość poznania wcześniej niektórych. Okazuje się, bowiem, że najczęściej w innych
  26. 26. Strona | 74 językach, występuje konieczność definiowania zmiennych na początku bloku kodu programu czy bloku funkcji itp. Na szczęście w języku C nie ma takich ograniczeń, co oznacza, że możemy definiować zmienne w dowolnym miejscu kodu programu! Poniżej przykład: uint8_t a=5,b=6; uint16_t c; c=a+b; int z; z=c+a+b; Jak widać zmienną o nazwie „z” typu int zdefiniowaliśmy niejako „po drodze”. Przeciwnicy języka C twierdzą, że to wprowadza bałagan w kodzie i trudności z jego analizowaniem. Moim zdaniem mylą się. (Tak pół żartem, pół serio) Po prostu zazdroszczą, że nie mają takich możliwości. Wybierając standard kompilacji na ten o nazwie „ISO C99 + GNU Extensions (-std=gnu99)”, otrzymujemy także bardzo ciekawą możliwość definiowania np. zmiennej iteracyjnej w pętli for podczas jej inicjalizacji. Przykład: for(uint8_t i=0;i<10;I=I+1) instrukcja; Jak widzisz definicja zmiennej i mieści się w znanej ci już sekcji inicjalizacyjnej pętli typu for.4.3.1.1 Typy złożone Typy złożone to w uproszczeniu takie typy, których nazwa składa się z nazwy jednego z typów podstawowych, o jakich mowa była wyżej oraz jednego z czterech operatorów. O samych operatorach będziemy mówić później, jednak poniżej przedstawię listę tych, dzięki którym można tworzyć typy złożone. [] - pozwala utworzyć tablicę obiektów danego typu * - (gwiazdka) pozwala utworzyć wskaźnik () - pozwala utworzyć funkcję zwracającą wartość danego typu Wyobraź sobie, że potrzebujesz zdefiniować 50 zmiennych jednobajtowych typu uint8_t, które zostaną zainicjalizowane określonymi wartościami początkowymi. Musiałbyś napisać albo 50 linii kodu, albo co najmniej kilkanaście, gdzie w każdej zdefiniować po kilka takich zmiennych. W przypadku pomyłki szybka zmiana kodu byłaby męczarnią. Czy nie lepiej byłoby, gdybyś miał możliwość ułożenia jeden po drugim w formie tablic takich elementów typu uint8_t? Na pewno tak! Ale równie dobrze można zdeklarować tablicę elementów dowolnego typu. Trzeba tylko zgodnie z tym, co pisałem wyżej, do nazwy typu prostego dodać dwa nawiasy kwadratowe, a pomiędzy nimi określić ilość elementów, aby kompilator wiedział, ile pamięci musi zarezerwować. W przypadku 50 elementów typu uint8_t będzie to 50 bajtów, a jak się domyślasz, w przypadku 50 elementów uint16_t bądź int będzie to 100 bajtów. Zapiszemy to tak: uint8_t tablica1[50]; int tablica2[50];
  27. 27. Strona | 75Poprzez dodanie nawiasów kwadratowych utworzyliśmy typ złożony, jakim są tablice,przechowujące wiele elementów jednego typu. W przypadku powyższych definicji musiałeśpodać koniecznie liczbę elementów, jednak gdy chcemy (a mamy taką możliwość) od razuzainicjalizować je konkretnymi wartościami, to możemy, aczkolwiek nie musimy, podawaćw nawiasach kwadratowych ilości elementów. Kompilator obliczy sobie tę ilość na podstawiepodanych wartości, jakimi będziesz potrzebował zainicjalizować takie tablice. Poniżejprzykłady:uint8_t tab1[] = {1,2,3,4,5};int tab2[] = {433,1200,20,0,30,288};Widać z powyższego, że zmienna tablicowa o nazwie tab1 będzie posiadała pięćelementów jednobajtowych, które zostaną zainicjalizowane po kolei wartościami od 1do 5. Natomiast tab2 będzie posiadała 6 elementów dwubajtowych, zainicjalizowanychkolejno liczbami podanymi w nawiasach klamrowych. Proste, prawda? Wiesz już, jak tworzyći inicjalizować tablice w języku C. Dla jasności mógłbyś także dokonać zapisu:uint8_t tab1[5] = {1,2,3,4,5};Jednak gdybyś się pomylił i w inicjalizacji wpisałbyś nie 5 a więcej elementów, to wtedykompilator ostrzegłby cię o tej sytuacji wyraźnie.uint8_t tab1[5] = {1,2,3,4,5,6,7,8};Taka sytuacja jak powyżej spowodowałaby błąd w trakcie kompilacji, dlatego najczęściej,jeśli inicjalizujemy tablicę w momencie definiowania, pomijamy także ilość elementóww nawiasach kwadratowych. Zobacz, w jak prosty sposób definiujemy tablice łańcuchówprzy użyciu stałych tekstowych, o których pisałem wyżej:char bufor[100];char napis2[] = „Nowy tekst”;Pierwsza tablica znaków o nazwie bufor została zdefiniowana i zarezerwowane zostało najej potrzeby 100 bajtów przez kompilator. Jednak nie dokonaliśmy inicjalizacji. Ponieważchcemy zainicjalizować drugą tablicę, to nie wpisujemy ilości bajtów, gdyż zostaną onewyliczone na podstawie długości znaków tekstu plus jeden na znak null. Oznacza to, żew tym konkretnym przypadku na tablicę o nazwie napis2, kompilator zarezerwuje 11bajtów. (10 Znaków tekstu oraz jeden znak null). Zapytasz zapewne od razu, gdzie takatablica znaków zostanie zarezerwowana, w jakiej pamięci – RAM, FLASH, czy EEPROM?Jeśli nie zostanie podany żaden dodatkowy specyfikator standardu AVR GCC, to zawszezostanie domyślnie zarezerwowane miejsce w pamięci RAM.O ile czasem potrzeba nam buforów na znaki czy teksty, na których będziemy wykonywaliróżne operacje w pamięci RAM, co oczywiste. To jednak często potrzebować będziemy,aby zdefiniować pewne łańcuchy znaków, teksty na stałe w pamięci FLASH albo w pamięciEEPROM, żeby można było później programowo podmieniać ich zawartość wg własnegouznania. Np. jakieś napisy na wyświetlaczu LCD itd.Jak wspomniałem, aby dokonać rezerwacji w innej pamięci niż RAM, trzeba użyć specjalnychspecyfikatorów. Spowoduje to jednocześnie, że odczyt takich danych z pamięci FLASH iEEPROM będzie wyglądał inaczej niż z pamięci RAM. A jeszcze inaczej będziemy dokonywali
  28. 28. Strona | 76 modyfikacji ich zawartości, czyli zapisu do pamięci EEPROM. Wybiegając jednak troszeczkę w przyszłość, pokażę ci, jak prosto można umieścić napis w tych rodzajach pamięci nieulotnych. char tab1[] EEMEM = „Napis w pamięci EEPROM”; char tab2[] PROGMEM = „Tekst w pamięci FLASH”; Wystarczyło posłużyć się pisanymi dużą literą specyfikatorami EEMEM lub PROGMEM. Prawda, że proste? Wprawdzie będzie to się wiązało jeszcze z podłączeniem odpowiednich bibliotek za pomocą plików nagłówkowych jak: eeprom.h dla specyfikatora EEMEM i operacji na pamięci EEPROM oraz pgmspace.h dla specyfikatora PROGMEM i operacji odnośnie odczytu z pamięci FLASH. W tej chwili tylko to sygnalizuję, za jakiś czas powrócimy w szczegółach do tych tematów, ponieważ będą nam bardzo potrzebne. Na temat typów złożonych, jak wskaźniki i funkcje, porozmawiamy dokładniej w dalszych częściach książki.4.3.1.2 Zakres widoczności zmiennych Jest to bardzo istotne zagadnienie. Jak zwykle brak wiedzy na temat choćby jego podstaw powoduje wiele problemów nie tylko ze zrozumieniem programów w języku C ale także z ich prawidłowym pisaniem. Jak to jest? Do tej pory sporo mówiliśmy o definiowaniu zmiennych różnych typów. Nigdy jednak nie wspominaliśmy, w jaki sposób są one widoczne, w jakich częściach programu się znajdują. Wiesz już na pewno, że program w języku C zawsze składa się przynajmniej z jednej funkcji – tej o nazwie main. Ale w rzeczywistości na cały kod programu składają się dziesiątki, a czasem setki i tysiące różnorodnych funkcji. Zacznę, więc od przykładu, jeśli zdefiniujemy zmienne w taki sposób: int k,w; // definicja zmiennych globalnych int max(uint8_t a, uint8_t b); // deklaracja funkcji max() int main(void) // początek programu – main() { uint8_t z=5, s=20; // definicja zmiennych lokalnych uint8_t m=13; // definicja zmiennej lokalnej k=max(z,s); // wywołanie funkcji max, wynik do k } int max(uint8_t a, uint8_t b) // definicja funkcji max() { int z=10; // definicja zmiennej lokalnej // obliczenia i zwrot wyniku return (a>b) ? (a*z)+w : (b*z)+w; } To w wyniku jego analizy możemy określić po kolei, co się dzieje w następujący sposób. (Pewne informacje zawarłem już, jak widzisz, w przydatnych komentarzach). Zmienne k oraz w posiadać będą zakres globalny w pliku, w którym znajduje się ta część kodu programu. Może jeszcze nie wiesz, ale kod programu może znajdować się w wielu
  29. 29. Strona | 77 plikach. Jednak zasięg globalny w tym momencie nie odnosi się do całego projektu, czyli wszystkich plików programu, a tylko i wyłącznie do tego pliku (o ile nie zmienimy tego stanu rzeczy za pomocą specjalnych specyfikatorów, o czym później). Zakres globalny w ramach pliku oznacza, że zmienna taka jest widoczna i nadaje się do użycia (odczyt lub zapis, o ile nie jest typu const), we wszystkich funkcjach programu! Jak widzisz, używamy zmiennej globalnej o nazwie k w funkcji main(), natomiast zmiennej w także wewnątrz funkcji max(). Nie ma z tym najmniejszego problemu, kompilator nie zgłasza żadnych zastrzeżeń. Wewnątrz funkcji main()definiujemy kilka zmiennych, które nazywam już w komentarzach zmiennymi lokalnymi. Oznacza to, że zakres ich widoczności znacznie się ograniczył. Podobnie wewnątrz funkcji max() zdefiniowana jest zmienna lokalna o nazwie z. Wchodząc w szczegóły wyjaśniam, że np. zmienne lokalne zdefiniowane wewnątrz funkcji main() będą dostępne tylko i wyłącznie dla dowolnych instrukcji programu także tylko wewnątrz funkcji main(), nigdzie poza nią. Zatem gdybyśmy próbowali się w jakikolwiek sposób odwołać do którejś z nich w innej funkcji np. max() – to kompilator zgłosiłby błąd i przerwał kompilację. Podobnie ze zmienną lokalną o nazwie z zdefiniowaną wewnątrz funkcji max(), nie jesteśmy w stanie z niej skorzystać poza tą funkcją, czyli w funkcji main() albo dowolnej innej, jeśli by taka jeszcze występowała. Próba użycia także skończyłaby się błędem w trakcie kompilacji. Aby dokończyć analizę tego programu, dodajmy, że widać także powyżej głównej funkcji programu deklarację funkcji max(). Dzięki temu kompilator analizując kod od góry, linia po linii, gdy natrafi na odwołanie się w kodzie (wewnątrz funkcji main) do funkcji max(), będzie wiedział, że taka istnieje. Zadeklarowaliśmy ją w tym celu wcześniej. Natomiast poniżej funkcji main()widzimy już definicję funkcji max(), czyli cały kod programu, jaki ona zawiera. Reasumując: Zmienne globalne to te, które zdefiniujemy na początku kodu programu, przed ciałem funkcji main(), będą zawsze miały globalny zakres widoczności. Każda funkcja programu będzie miała do nich dostęp. Zmienne lokalne to te, które zdefiniowane zostaną wewnątrz każdej z funkcji,w tym także funkcji main(). Będą one widoczne tylko dla instrukcji kodu programu zawartych wewnątrz funkcji, w których są zdefiniowane. Występują jeszcze inne typy zmiennych, jak np. takie ze specyfikatorem static, ale o tym później.4.3.1.3 Typ void Ten typ, void, wiąże się ściśle z typami złożonymi, o których pisałem wyżej. Praktycznie samodzielnie, w pojedynkę nigdy nie występuje. Natomiast w połączeniu z typami złożonymi może mieć nieco różne znaczenia, choć zwykle mówi o tym, że mamy do czynienia z czymś nieznanym. Jest to tak naprawdę jeden z fundamentalnych typów języka C. Poniżej kilka przykładów, chociaż ich szczegółowym wyjaśnianiem także zajmiemy się później: void *wsk; void *p; void fun(void);

×