SlideShare a Scribd company logo
Mikrokontrolery AVR, język C, podstawy
                               programowania




Niniejsza darmowa publikacja zawiera jedynie fragment pełnej
wersji 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łącznie
w formie dostarczonej przez Wydawnictwo KRAM. Zabronione są jakiekolwiek zmiany w
zawartości publikacji bez pisemnej zgody Wydawnictwa KRAM - wydawcy niniejszej
publikacji. Zabrania się jej odsprzedaży.


Pełna wersja niniejszej publikacji jest do nabycia w
sklepie internetowym

                                http://witmir.pl
Styczeń 2011




  ATNEL
                Mikrokontrolery AVr
  WYDAWNICTWO

                       język   C
                podstAwy progrAMowAniA


                               Mirosław Kardaś




                                   Mojej Żonie – Kasi
Książka przeznaczona jest dla elektroników i hobbystów, którzy chcą szybko, w oparciu o interesujące
przykłady, poznać język C przeznaczony dla mikrokontrolerów AVR i nauczyć się pisać dla nich
programy. 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ępny
opisana została także architektura oraz możliwości samych mikrokontrolerów AVR wchodzących w
skład dwóch rodzin: ATmega i ATtiny. Prezentowany materiał podzielony jest na trzy części. Pierwsza
obejmuje zagadnienia związane z budową mikrokontrolerów, druga to wykład na temat podstaw samego
ję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 Atnel
Szczecin 2011


ISBN 978-83-931797-0-1




Wydawnictwo ATNEL
ul. Jasna 15/38
70-777 Szczecin

fax: 91 4635 683
http://www.atnel.pl
e-mail: biuro@atnel.pl

Wydanie I

Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz
wydawnictwo Atnel dołożyli wszelkich starań, by publikowane tu informacje były kompletne i rzetelne. Nie biorą jednak żadnej
odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor
oraz wydawnictwo Atnel nie ponoszą także żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji
zawartych w książce. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentów niniejszej
publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii całości lub fragmentów książki bądź dołączonej płyty
DVD 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.
Spis treści
                                                                                                                                                                                                        Strona | 3




Przedmowa                                    .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 7
1	 Wstęp .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 8

   2.1 Pierwszy „pusty” program w C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2	 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3	 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
Strona | 4
        3.6.13	 Bootloader	–	niesamowite	możliwości  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 56

   4.1 Zagadnienia ogólne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4	 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
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  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 173
5	 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
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  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 348
6	   	FuseBity	–	MkAvrCalculator  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 356
           6.16.1	 Fusebity,	Lockbity  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 356
           6.16.2	 MkAvrCalculator  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 360
7	   Bootloader .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 368

     8.1 Pilot na podczerwień . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
8	   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 .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 430

9	   Środowisko	ECLIPSE  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 455
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.
Strona | 8

1 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.
Strona | 58

4 Podstawy	języka	C
         Wreszcie dotarliśmy do miejsca, gdzie będzie można poznać więcej informacji na temat samego




4.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.
Strona | 59
4.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.
Strona | 60
4.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
Strona | 61
warunek, który może być dowolnym wyrażeniem, tylko wtedy zostanie wykonana instrukcja
występująca w dalszej części.
Druga postać stosowana jest do tzw. „rozgałęzień” programu. Oznacza, że jeśli będzie spełniony
warunek 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, co
moż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 nawiasami
klamrowymi {}

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óżna
wartość do zmiennej o nazwie wysokosc.
W drugiej postaci w zależności od spełnionego warunku lub nie, ustawilśmy pewne
wartości kilku różnym zmiennym, dlatego zastosowaliśmy nawiasy klamrowe ograniczające
odpowiednio 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żona
jest 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 kodu
programu 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 takich
sytuacjach do sporych problemów szczególnie, jeśli nie zastosujemy w odpowiedni sposób
wcięć w programie.
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.
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
       }
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;
          }
Strona | 65
W powyższym przykładzie sekcję init stanowi instrukcja i=0. Inicjalizujemy w ten sposób
zmienną i, która będzie odpowiedzialna za iterację (wielokrotnie powtarzalną czynność).
Sekcja wyrażenie_warunkowe to w naszym przypadku warunek i<10. Zatem pętla
będzie się wykonywała do momentu, dokąd warunek będzie prawdziwy. Jako że zmienna
i została zainicjalizowana wartością zero, można powiedzieć, że pierwszy przebieg pętli
zostanie 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 za
tym 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ększym
tempie. 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ętle
for z innych języków programowania, to śmiało stwierdzisz, że składnia tej pętli w języku
C 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ź wszystkie
części składowe pętli, pozostawiając jedynie znaki średników, które je oddzielają. Zatem
poniższy zapis:

for(;;)
{
                 // instrukcje;
}

Często spotkasz, jako pętlę nieskończoną. Opuszczenie sekcji wyrażenie_warunek
jest 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ępnie
zostaje 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, że
pę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 konstrukcji
ze 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
}
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;
         }
Strona | 67
Wyglą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ędzie
w 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 dobrze
może wystąpić wyrażenie matematyczne, którego wynik będzie przełącznikiem. Następnie
wewnątrz nawiasów klamrowych mamy sekcje o nazwie case lub default, dzięki którym
możemy zdecydować, jakie instrukcje chcemy wykonać w zależności od konkretnych wartości
naszego wyrażenia/przełącznika. Po słówku case zawsze podajemy wartość przełącznika, jaka
nas 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łącznika
case, może wystąpić instrukcja break. Tylko dlatego ująłem ją w powyższym schematycznym
w przykładzie w nawiasy kwadratowe, aby zakomunikować, że instrukcja break może w
tym miejscu występować, ale nie musi. Nie jest to obligatoryjne. Jednak, jeśli jej nie ma, to
zostaną wykonane kolejne instrukcje zawarte instrukcji następnych sekcji case, switch. Może to
spowodować, że całość nie zareaguje tylko na jeden przełącznik, a na kilka. Zatem jeśli zależy
nam na wykonaniu instrukcji dotyczących tylko jednego przełącznika, to najczęściej będziemy
blok rozpoczynający się od słówka case kończyli rozkazem break, który przerwie dalsze
wykonywanie instrukcji zawartych w switch, ponieważ uznajemy, iż inne są niepotrzebne
w 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 przyjmuje
wartość równą dwa. Rozpoczyna się teraz instrukcja switch sprawdzająca wartość zmiennej
x, pełniącej dla nas rolę przełącznika, od którego chcemy spowodować, aby z kolei zmienna
czas przyjęła pewną konkretną wartość. Zakładamy także, że jeśli zmienna x nie osiągnie
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.
Strona | 69
4.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:
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 ramach




4.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
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
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.
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*1018

W standardowym języku C występują jeszcze inne typy, jednak na razie nie będziemy
o nich wspominać, gdyż nie wszystkie dotyczą naszych mikrokontrolerów. Przedstawię
raczej zestawienie typów wbudowanych, z jakimi będziemy mieli do czynienia korzystając
z 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
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];
Strona | 75
Poprzez 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 razu
zainicjalizować je konkretnymi wartościami, to możemy, aczkolwiek nie musimy, podawać
w nawiasach kwadratowych ilości elementów. Kompilator obliczy sobie tę ilość na podstawie
podanych wartości, jakimi będziesz potrzebował zainicjalizować takie tablice. Poniżej
przykł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 1
do 5. Natomiast tab2 będzie posiadała 6 elementów dwubajtowych, zainicjalizowanych
kolejno 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 wtedy
kompilator 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ów
w nawiasach kwadratowych. Zobacz, w jak prosty sposób definiujemy tablice łańcuchów
przy 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 na
jej 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ą one
wyliczone na podstawie długości znaków tekstu plus jeden na znak null. Oznacza to, że
w tym konkretnym przypadku na tablicę o nazwie napis2, kompilator zarezerwuje 11
bajtów. (10 Znaków tekstu oraz jeden znak null). Zapytasz zapewne od razu, gdzie taka
tablica znaków zostanie zarezerwowana, w jakiej pamięci – RAM, FLASH, czy EEPROM?
Jeśli nie zostanie podany żaden dodatkowy specyfikator standardu AVR GCC, to zawsze
zostanie domyślnie zarezerwowane miejsce w pamięci RAM.
O ile czasem potrzeba nam buforów na znaki czy teksty, na których będziemy wykonywali
róż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ęci
EEPROM, żeby można było później programowo podmieniać ich zawartość wg własnego
uznania. Np. jakieś napisy na wyświetlaczu LCD itd.
Jak wspomniałem, aby dokonać rezerwacji w innej pamięci niż RAM, trzeba użyć specjalnych
specyfikatorów. Spowoduje to jednocześnie, że odczyt takich danych z pamięci FLASH i
EEPROM będzie wyglądał inaczej niż z pamięci RAM. A jeszcze inaczej będziemy dokonywali
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
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);
Strona | 78
         Teraz krótkie wyjaśnienie do powyższych linii kodu programu. W pierwszej i drugiej
         wykonaliśmy definicję wskaźnika o nazwie wsk oraz p, które pokazują nam na obiekt
         nieznanego typu. Napisałem obiekt, ponieważ równie dobrze taki wskaźnik będzie mógł
         później posłużyć do pokazywania na zmienną dowolnego typu podstawowego lub złożonego
         albo nawet na funkcję programu.
         W trzeciej linijce pierwszy specyfikator void ten przez nazwą funkcji mówi o tym, że
         zdefiniowana w ten sposób funkcja nie będzie zwracać żadnego wyniku. Natomiast specyfikator
         void pomiędzy nawiasami okrągłymi mówi, że do tej funkcji nie będą przekazywane żadne
         argumenty.


4.3.1.4 Specyfikator const
         Czasem zdarzać się będzie, że w programie zechcesz używać niektórych zmiennych, które
         będą przechowywały przez całe życie programu pewne stałe wartości. Powiem więcej,
         chciałbyś mieć możliwość, żeby przez przypadek żaden fragment rozbudowanego programu nie
         zniszczył przypadkiem tej stałej. Wtedy przyjdzie ci z pomocą specyfikator const. Załóżmy, że
         w jednej zmiennej dla całego programu chcesz przechowywać wartość jakiegoś współczynnika
         podziału. Niech posiada on stałą wartość równą np. 45. Inna zmienna będzie przechowywała
         do pewnych obliczeń liczbę PI. Możemy zatem używając specyfikatora const napisać:

         const uint8_t wspolczynnik = 45;
         const float PI = 3.14;

         Od tej pory możesz używać zmiennej współczynnik oraz PI ale tylko i wyłącznie w trybie
         do odczytu. Gdy tylko spróbujesz nawet przez pomyłkę zmienić zawartość jednej z tych
         zmiennych, od razu kompilator zareaguje błędem w trakcie przeprowadzania kompilacji.
         Zwrócę uwagę na dodatkową kwestię. Wprawdzie nie znasz jeszcze zagadnień związanych
         z preprocesorem. Jednak istnieje pewna dyrektywa tegoż preprocesora o nazwie #define.
         Dzięki niej moglibyśmy uzyskać bardzo podobny efekt, jeśli chodzi o możliwość zdefiniowana
         pewnych stałych wartości, o jakich mówiłem powyżej. Oznacza to, że używając zapisu z
         przykładu poniżej:

         #define wspolczynnik 45
         #define PI 3.14

         otrzymujemy pozornie identyczną sytuację. Od tej pory możemy się posługiwać identyfikatorami
         wspolczynnik oraz PI na podobnej zasadzie. Istnieją jednak podstawowe różnice,
         o których warto wiedzieć, gdyż może to się okazać bardzo przydatne w trakcie pisania różnych
         programów. Czasem warto będzie skorzystać ze specyfikatora const, a czasem wystarczy
         #define. Oceni się to samemu, kiedy posiądzie się odpowiednią wiedzę i praktykę w tym
         zakresie. Czym jednak z praktycznego punktu widzenia różni się dla nas deklaracja zmiennej
         za pomocą #define od definicji ze specyfikatorem const? W oparciu o informacje podane
         wcześniej na temat różnic pomiędzy deklaracją a definicją zmiennej już powinieneś dostrzec
         podstawową różnicę. Otóż definicja zmiennej/stałej ze specyfikatorem const od razu rezerwuje
         miejsce w pamięci na tę zmienną. Natomiast sama deklaracja za pomocą dyrektywy #define
         tego nie czyni. Dyrektywa #define powoduje z punktu widzenia kompilatora zadeklarowanie
         stałej dosłownej, zatem kompilacja odbywa się w ten sposób, że w każdym miejscu, gdzie
         kompilator napotka nazwę zadeklarowaną za pomocą dyrektywy #define po prostu podstawi
         w to miejsce konkretną stałą wartość, która widnieje w tej deklaracji.
Strona | 79
        Kolejna różnica w tym, że stałe definiowane przy użyciu const będą mogły uzyskiwać
        różne zakresy widoczności, w zależności od tego, w jakim miejscu zostaną zdefiniowane.
        Natomiast stałe zdeklarowane z użyciem #define będą zawsze widoczne dla kompilatora
        w całym programie. Jeżeli spotkasz się z sytuacjami, kiedy warto będzie ukrywać zasięg
        swoich stałych, wtedy sięgniesz po const.
        Kolejna różnica polega na tym, że stała zdefiniowana za pomocą const ma swoje
        odzwierciedlenie w pamięci mikrokontrolera, a co za tym idzie, można do niej odwołać
        się za pomocą wskaźnika. Tymczasem stała zadeklarowana poprzez #define, jako że nie
        rezyduje w pamięci, nigdy nie będzie dostępna poprzez wskaźnik. Czasem może to być
        bardzo potrzebne. Mam tylko nadzieję, że starasz się śledzić dokładnie, w jakich momentach
        używam słowa deklaracja, a w jakich definicja. Nie robię tego przypadkowo i zamiennie,
        ponieważ każde z tych określeń ma swoje istotne znaczenie. Teraz widzisz, jak ważne jest
        i ile rzeczy się wiąże z dobrym zrozumieniem tego zagadnienia.


4.3.1.5 Specyfikator volatile
        Z angielskiego słowo volatile oznacza „ulotny”. Tak też kompilator traktuje zmienne, które
        zostały zaopatrzone w trakcie definicji w przydomek volatile. Przykład definicji zmiennej
        z tym specyfikatorem/przydomkiem:

        volatile int a;

        Kiedy powinniśmy go stosować? Zawsze w takich sytuacjach, gdy chcemy, aby kompilator nie
        optymalizował dostępu do takiej zmiennej. Co się takiego wiąże z optymalizacją zmiennych
        bez przydomka volatile? Kompilator nie widząc tego przydomku zakłada, że taka zmienna
        nigdy nie będzie mogła się zmienić bez wiedzy kompilatora. Założenie to czyni już na etapie
        kompilacji i w wyniku tego często, jeśli np. w jakiejś funkcji ma wykonywać operacje na tejże
        zmiennej, pozwala sobie na optymalizację, mającą na celu przyśpieszenie działania programu.
        W mikrokontrolerach jest to szczególnie istotne. Optymalizacja taka polega najczęściej na tym,
        że bezpośrednio po wejściu do funkcji main() czy jakiejkolwiek innej, kompilator zapamiętuje
        sobie zawartość komórki tej pamięci w podręcznym i wolnym rejestrze mikrokontrolera.
        Zwykle ma spory zapas takich wolnych rejestrów. Dzięki temu w dalszej części funkcji już
        nigdy nie odwołuje się do zawartości tej komórki pamięci, tylko operuje na zawartości rejestru.
        Dopiero przy wyjściu z funkcji oczywiście przy wyjściu z innej funkcji niż main() dokona
        zapisu aktualizowanej w rejestrze zawartości bezpośrednio do tej komórki. A ponieważ pętla
        główna programu main() nigdy się nie kończy to można przypuszczać, że cały czas będzie
        program się odwoływał i pracował tylko w oparciu o ten rejestr, żeby szybciej wykonywać
        działania. Jest to z jednej strony bardzo pożyteczne i pożądane. Jednak czasami wprowadzasz
        do programu procedurę obsługi jakiegoś przerwania, która także będzie miała za zadanie
        wykonywać operacje na tej samej zmiennej. I jeśli nie będzie ona opatrzona przydomkiem
        volatile, to kompilator spowoduje, że w trakcie wejścia w przerwanie, zmienna trafi do innego
        rejestru, na nim dokonane zostaną stosowne aktualizacje a przy wyjściu z procedury obsługi
        przerwania, zawartość rejestru trafi znowu do komórki pamięci tej zmiennej. No i katastrofa!
        Bo przecież w pętli głównej main() program nigdy nie zajrzy, jak wspomniałem wyżej, do
        tej komórki w związku z umieszczeniem jej zawartości w „podręcznym” rejestrze. W takiej
        sytuacji zawsze działa na podręcznym rejestrze, do którego raz wczytał tę zmienną. Zatem
        pętla główna nigdy się nie dowie o tym, że w „coś” (np. procedura przerwania) podmieniło
        zawartość tej komórki i zacznie dochodzić do przedziwnych błędów w programie, których
        przyczyny trudno będzie się od razu domyślić.
Strona | 80
         Dlatego, jeśli wiesz, że niektóre zmienne będą zapisywane i odczytywane zarówno w funkcjach,
         ale i procedurach przerwań mikrokontrolera, czyli mogą się zmieniać w różnym czasie, to
         trzeba wymusić, aby kompilator nie optymalizował dostępu do takich zmiennych. Musi za
         każdym razem, gdy chce wykonać na nich operację, odwołać się bezpośrednio do zawartości
         komórek pamięci, gdzie te zmienne rezydują i to ich zawartość modyfikować wedle potrzeb,
         a nie jakiś podręczny rejestr pełniący rolę bufora. Brak takiej optymalizacji wpłynie wprawdzie
         na to, że dostęp do zmiennej będzie nieco wolniejszy niż do rejestru (co mogłoby być w wielu
         przypadkach sporą wadą), ale dlatego nie wszystkie zmienne definiujemy jako volatile. Tylko
         te, których jak było powiedziane na początku, zawartość może być ulotna, czyli zmieniać
         się bez wiedzy kompilatora, np. w procedurach obsługi przerwań.


4.3.1.6 Specyfikator register
         Pamiętasz? W poprzednim rozdziale wspominaliśmy, że kompilator tam, gdzie można, dokonuje
         optymalizacji dostępu do zmiennych. Jednym ze sposobów optymalizacji jest umieszczanie
         wartości zmiennej w rejestrze, a ponieważ w kodzie maszynowym dostęp do rejestru jest
         o wiele szybszy niż do pamięci RAM, to optymalizacja taka zawsze zdecydowanie przyśpiesza
         działanie programu, co bywa bardzo istotne w procedurach krytycznych czasowo. Zdarza się
         jednak czasami, że niekoniecznie każdą zmienną kompilator załaduje do rejestru wewnątrz
         funkcji. Czasem włączy inny rodzaj optymalizacji. My jednak, jako programiści, możemy
         próbować wymusić na kompilatorze, aby niejako „na siłę” zastosował ten wariant, i próbował
         umieścić w rejestrze. Mogą być czasem procedury/funkcje, gdzie z naszego punktu widzenia
         czas ich wykonywania jest bardzo krytyczny. Są to zwykle krótkie funkcje i jeśli szczególnie
         zależy nam na szybkości ich wykonywania, to możemy zaopatrzyć zmienną w przydomek
         register. Kompilator będzie się wtedy starał jak może, aby tylko wykonać dla nas tę operację.
         Przykład takiej definicji zmiennej:


         register uint8_t licznik;


         Z uwagi jednak na to, że wszystkie rejestry mikrokontrolerów AVR mają postać jednego
         bajtu. Tylko niektóre mogą być traktowane w połączeniu, jako rejestry indeksowe, nie ma
         większego sensu, aby dodawać przydomek register zmiennym, które posiadają rozmiar
         większy niż jeden bajt ostatecznie dwa bajty.


4.3.1.7 Instrukcja Typedef
         Wspomniałem już chyba wcześniej króciutko, że ogromną zaletą języka C jest możliwość
         definiowania nowych, nawet własnych typów danych. Można powiedzieć, że pozwala to
         płynnie rozszerzać możliwości podstawowego standardu języka. I to stanowi m.in. o olbrzymiej
         przewadze języka C nad innymi językami wyższego rzędu. Do tych celów przydatna będzie
         właśnie instrukcja o nazwie Typedef.
         Za jej pomocą można np. nadać nową nazwę istniejącemu już typowi. Dlatego często w cudzych
         programach napisanych dla mikrokontrolerów AVR spotka się takie definicje zmiennych:


         u08 a;
         u16 z;
         u32 g;
Strona | 81
Bardzo często programistom nie chce się wpisywać specyficznych typów dla AVR GCC
i mikrokontrolerów AVR jak: uint8_t, uint16_t czy też uint32_t. Dlatego też
często w tych programach znajdziesz użytą instrukcję typedef w celu utworzenia nowych
typów o nazwach jak wyżej: u08, u16 czy u32. Dzięki temu nie trzeba tyle pisać za
każdym razem przy definicji zmiennej. Wystarczy na początku programu napisać:

typedef uint8_t u08;
typedef uint16_t u16;
typedef uint32_t u32;

Oznacza to, że właśnie zdefiniowaliśmy na własne potrzeby trzy nowe typy o nazwach jak
w przykładzie, dzięki czemu jeśli w dalszej części kodu programu zdefiniujemy zmienne
korzystając z tych typów, zostaną one potraktowane dokładnie jak zmienne typu, od którego
pochodzi nasz nowo utworzony typ. Jednym słowem zmienna typu u08 będzie tak naprawdę
zmienną typu uint8_t i analogicznie następne. Nie tylko w taki celach przydatne bywa
definiowane nowych typów. Np. będziesz w programie bardzo często posługiwał się zmiennymi,
która będą miały np. za zadanie przechowywać wartość poziomu oleju. Definicje takich
zmiennych będą się pojawiały w wielu funkcjach i miejscach programu. Charakterystyczne
dla nich będzie to, że zawsze zawierają liczbę całkowitą ze znakiem z zakresu int. Ciężko
będzie jednak później analizować program, który posiada zdefiniowaną sporą ilość zmiennych,
nazwy są różne i jak na pierwszy rzut oka, szybko „wyłapać”, które z nich przechowują ten
poziom oleju? Odpowiedź jest prosta, wystarczy zdefiniować nowy typ:

typedef int P_olej;

Od tej pory będziesz mógł spokojnie w różnych częściach programu definiować różne zmienne
a patrząc na ich typ będziesz w mig wiedział, do jakiego celu je utworzyłeś.


P_olej poziom1;
P_olej poziom2;

P_olej wskaznik3;
P_olej miarka2;


Przyznaj, że to bardzo przydatna rzecz. Naturalnie tego polecenia można także używać
w związku z typami złożonymi, jak np. wskaźniki. Przykłady:

typedef int * wskaznik_do _int;
typedef char * napis;

później definicje

wskaźnik_do_int p1;               // czyli: int * p1
napis komunikat;                  // czyli: char * komunikat


Bardziej docenisz to zagadnienie z powyższego przykładu, gdy dowiesz się dużo więcej na
temat samych wskaźników oraz możliwości ich stosowania.
Zapewniam cię wyprzedając fakty, że wskaźniki to jedno z najlepszych narzędzi języka C,
Strona | 82
         choć trzeba przyznać, że także nieco skomplikowane i na początku ciężko je zrozumieć bez
         dobrego wytłumaczenia i przykładów. Nie martw się, postaram się, abyś jak najszybciej
         i jak najwięcej zrozumiał te zagadnienia w dalszych rozdziałach.


4.3.1.8 Typy wyliczeniowe enum
         To kolejne piękne narzędzie programistyczne. Praktycznie w innych popularnych językach
         dla mikrokontrolerów nieosiągalne. Jest tak bardzo przydatne, że niekorzystanie z niego w
         swoich programach mógłbym żartobliwie określić jako ciężki „grzech”.
         Typ wyliczeniowy to całkiem osobny typ, za pomocą którego możemy szybko utworzyć
         zestaw stałych całkowitych. Nagminnym przypadkiem jest, że w programach często trzeba
         przechowywać nie liczby, lecz pewien rodzaj informacji. Wprawdzie będziemy te informacje
         przechowywać w postaci liczb, chyba że mamy możliwość skorzystania z typu wyliczeniowego.
         Wtedy sama liczba nie będzie dla nas aż tak istotna. Pewnym istotnym dla nas wartościom
         będziemy mogli nadać nazwy i to nimi się posługiwać zamiast liczbami.
         Wyobraź sobie zmienną, która normalnie, gdy nie znasz tego mechanizmu, przechowuje
         w postaci liczb poziom menu, na jakim znajduje się aktualnie użytkownik. Musisz pamiętać w
         każdym miejscu programu, że wartość 0 oznacza poziom główny, ale już jakaś liczba wyrwana z
         kontekstu np. 23 oznacza trzeci poziom podmenu o nazwie „Ustawienia”. O ile pamiętanie o tym,
         że poziom zerowy to menu główne, nie nastręcza problemów, to już każda kolejna liczba, jeśli
         tych poziomów jest dużo, powoduje, że czasem się w tym gubimy. Bardzo często w takiej sytuacji
         bierzemy zwykłą kartkę papieru lub w kodzie programu tworzymy w jakimś miejscu specjalną
         tabelkę, gdzie opisujemy na własne potrzeby, co oznacza każda liczba, tzn. który poziom menu
         ona reprezentuje. Gorzej, gdy karteczka się zgubi albo, gdy program składa się z wielu plików
         i musimy przełączać się między nimi, aby odnaleźć te swoje zapiski w tabelce. Jeszcze
         gorzej, jeśli w trakcie programu potrzebujemy często tworzyć nowe poziomy pomiędzy już
         istniejącymi. Wtedy cała tabelka „bierze w łeb” i trzeba ją żmudnie przepisywać od nowa
         i dokonywać mnóstwo zmian w kodzie.
         Po co jednak tak się męczyć? Toż bardzo blisko jest od takiej tabelki, która przyporządkowuje
         każdą liczbę do konkretnego poziomu menu, do zastosowania typu wyliczeniowego enum!
         Wystarczy, że zastosujesz się do takiej składni:

         enum nazwa_typu {wartosc1, wartosc2,…………, wartoscN};

         Gdzie w miejsce nazwa typu w naszym konkretnym przypadku wprowadzimy nazwę np.
         menu_poz a w nawiasach klamrowych podasz tylko wartości opisowe dla poszczególnych
         poziomów menu z tradycyjnej tabelki, np. tak jak poniżej. Jednak w przykładzie z oczywistych
         względów wpiszę tylko kilka wartości. Przyjmijmy założenie, że zbudowaliśmy zegar oraz
         utworzyliśmy menu główne, z którego można przejść do kolejnych poziomów aby ustawić
         czas, ustawić datę czy też ustawić alarm (budzik).

         enum    menu_poz {mglowne, mczas, mdata,                malarm};

         enum menu_poz idx=mglowne;

         Proszę bardzo, właśnie w pierwszej linii powyższego przykładu zdefiniowaliśmy nowy typ
         wyliczeniowy o nazwie menu_poz, a następnie w kodzie programu możemy już zdefiniować
         konkretną zmienną o nazwie w tym przypadku idx. Dokonujemy jednocześnie jej inicjalizacji
         (UWAGA!), nie za pomocą stałej liczbowej, lecz za pomocą wartości, która istnieje w
Strona | 83
naszym typie wyliczeniowym. Zatem możemy już wyrzucić naszą karteczkę z rozpisaną
tabelką, bądź usunąć tabelkę z opisu w pliku. Więcej się nam ona nie przyda. Ale zapytasz
zapewne, co się stanie, jeśli teraz zechcemy dodać nową pozycję w naszym menu? Ależ nic
prostszego, załóżmy, że kolejna pozycja menu powinna umożliwiać ustawienia parametrów
zegara. Taki nasz „setup”.

enum    menu_poz {mglowne, msetup, mczas, mdata,                   malarm};

enum menu_poz idx=mglowne;

Zauważ, że specjalnie wstawiłem tę pozycję gdzieś w środek naszych wartości typów, bo
założenie jest takie, iż kolejną pozycją po menu głównym ma być właśnie setup. Jednak istnieje
w tym przypadku zupełna dowolność, można dodać na końcu bez żadnych konsekwencji,
tak jak to było, gdy prowadziliśmy swoje tabelki z opisami.

enum    menu_poz {mglowne, mczas, mdata,                malarm, msetup};

Mam nadzieję, że dostrzegasz teraz ogromne możliwości tego mechanizmu? Możesz się
jednak zastanawiać, co tak naprawdę będzie zawierać zmienna idx, jeśli przypiszemy do niej
dowolną wartość typu wyliczeniowego. Już wyjaśniam. Zasada jest prosta, domyślnie, jeżeli
sami nie wprowadzimy zmian, rozpoczyna się numerowanie wartości od zera. W związku
z tym pozycja mglowne=0, mczas=1, mdata=2, malarm=3, setup=4. Jednak programista ma
możliwość wpływu na tę numerację. Wystarczy dokonać takiego zapisu:

enum    menu_poz {mglowne, mczas=7, mdata,                malarm=43, msetup};

Spowoduje to, że teraz:
mglowne          = 0 (domyślnie gdyż nie przydzieliliśmy ręcznie żadnej wartości)
mczas            = 7 (widać wyżej dlaczego)
mdata            = 8 (ponieważ numeracja będzie dalej biegła od poprzedniego numeru)
malarm           = 43
setup            = 44


Pamiętaj, że zmienną naszego nowego typu możesz posługiwać się także, jak zwykłą liczbą.
Dozwolone są poniższe działania:

enum menu_poz idx;
uint8_t a,b=10;
idx = malarm;
a = b + idx;

W wyniku takiego działania zmienna a będzie miała wartość 53. Ponieważ do zmiennej idx
przypisaliśmy malarm, który zgodnie z przykładem wyżej posiada zdefiniowaną wartość =
43. Natomiast zmienną b zainicjalizowaliśmy liczbą 10.
Zatem wynikiem b + idx jest liczba 53. Okazuje się także, że kompilator AVR GCC zezwala na
przypisanie do zmiennej idx także innych wartości tzn. czysto liczbowych za pomocą stałych
Strona | 84
         lub zmiennych. Niektóre inne kompilatory nie zezwalają na taką operację generując błąd
         w trakcie kompilacji. Czy taki mechanizm będzie ci potrzebny ocenisz już sam. W każdym
         razie można zastosować poniższe działania:

         enum menu_poz idx;
         uint8_t a=10;
         idx = 122;
         idx = a + 87;

         Wykorzystanie tego zależy już tylko od twojej inwencji twórczej. Podam jednak jeszcze
         jeden przykład popularnego zastosowania dla typu enum. Później w części praktycznej,
         gdy zajmiemy się oprogramowaniem układu scalonego, RTC, który jest zegarem czasu
         rzeczywistego, okaże się, że można z niego odczytać, jaki mamy aktualnie numer dnia
         tygodnia. Jak się dowiesz, występuje tam taka zależność, że odczytana liczba 0 odpowiada
         poniedziałkowi, 1 to wtorek, 2 środa, 3 to czwartek, 4 piątek, 5 sobota oraz 6 niedziela. Gdy
         napiszesz program do obsługi takiego zegarka, zapewne będziesz chciał łatwo i szybko
         pokazywać nazwy dni tygodnia na własnym wyświetlaczu LCD. Jednak za każdym razem
         będziesz musiał pamiętać powyższe przypisania w różnych procedurach, gdzie będzie
         trzeba sprawdzać, jaki jest aktualnie dzień tygodnia. Aby sobie ułatwić życie, zastosujesz
         jednak typ wyliczeniowy enum.

         enum t_dzien {pon, wto, sro, czw, pia, sob, nie};

         później utworzysz zmienną bądź zmienne, w których będziesz przetrzymywał dni tygodnia,
         ale nie będziesz musiał się już posługiwać na pamięć cyframi kolejnych dni. Teraz będziesz
         używał już wygodnych i łatwych do zapamiętania nazw.

         enum t_dzien dzien = sro;

         następnie gdzieś dalej w programie sprawdzanie, jaki jest dzień w zmiennej:

         if (dzien == pia) instrukcja;            // wyłącz filtr w akwarium


         lub z instrukcją switch:

         switch(dzien)
         {
              case sro:
              instrukcja1;
              instrukcja2;
              break;

                case pia;
                instrukcja3;
                instrukcja4;
                break;

                case nie:
                instrukcja5;
         }
Strona | 85
        Jak widać w zależności od dnia tygodnia można wykonać różne czynności w programie,
        symbolizują to różne numery instrukcji w przykładzie.
        Mam nadzieję, że wyczerpująco przedstawiłem to ważne narzędzie, jakim jest typ wyliczeniowy.



4.3.2 stałe W języku c
        Brzmi „groźnie”, ale to na szczęście banalny, chociaż istotny temat w naszych rozważaniach
        i nauce języka C. Ze stałymi będziesz miał wciąż do czynienia. Nazywa się je stałymi
        dosłownymi. Wyróżniamy następujące rodzaje stałych:
         1.   Liczbowe całkowite
         2.   Liczbowe zmiennoprzecinkowe
         3.   Znakowe
         4.   Tekstowe
        Przypomnę, że już w wielu przykładach powyżej skorzystaliśmy ze stałych dosłownych.
        Były to jak do tej pory stałe liczbowe całkowite. Gdy pisaliśmy np. definicję zmiennej
        int a=122; to liczba 122, którą zainicjalizowaliśmy zmienną, jest właśnie pierwszym
        przykładem stałej dosłownej.


4.3.2.1 Stałe jako liczby całkowite
        Mogą to być liczby zapisywane w taki sposób, jaki nam najbardziej odpowiada. Możemy
        przy tym korzystać z postaci dziesiętnej liczb, z postaci szesnastkowej czy ósemkowej. Mogą
        to być liczby ze znakiem, czyli także ujemne, i bez znaku. Przykład:
         Dziesiętnie:

         22    8         71        -15    0         455      1024              itd.

         Szesnastkowo:

         0x10 0xf2       0x00      0x045 0x01       0xffff            itd.

        Nie muszę chyba przypominać, jak stosować zapis szesnastkowy, liczę na to, że już wiesz
        dokładnie, o co w tym chodzi. Zwrócę jedynie uwagę, że języku C, jeśli chcemy przedstawić
        liczbę w postaci szesnastkowej, to w odróżnieniu od postaci dziesiętnej musimy ją poprzedzić
        znakami 0x (zero oraz x). Dodam jeszcze, że można w zapisie szesnastkowym inaczej
        zwanym hexadecymalnym używać zarówno dużych jak i małych liter. Dla kompilatora
        będzie to zupełnie obojętne.
        Trzeba sobie jednak zdawać sprawę, do jakiego typu konkretnie kompilator zalicza domyślnie
        stałe będące liczbami całkowitymi. Domyślnie, jeśli napiszemy np. liczbę 200, zostanie ona
        zakwalifikowana jakby była typu int. Jeśli oczywiście chcemy podać, jako stałą liczbę,
        która wykracza poza zakres int (patrz Tabela.1), to wtedy zostanie uznana, oczywiście
        automatycznie, za kolejny większy typ, czyli np. long int.
        Mamy jednak możliwość aby świadomie dać znać, aby np. liczbę 200 traktował od razu
        jako typ long int. Wystarczy, że na końcu takiej liczby postawimy literkę L. Może to
        być mała lub duża litera, jednak ze względu na czytelność lepiej posługiwać się dużą. Wtedy
        zapis będzie wyglądał tak 200L, 0L, 33L itd.
Strona | 86
         Mamy także wpływ na to, czy kompilator ma przyjmować stałą, jako liczbę bez znaku,
         (jako unsigned). Wtedy musimy na końcu zastosować literkę u. Przykład: 223u, 10u,
         1234u itd.
         Można także łączyć literkę u z literką L, wtedy określamy, że chodzi nam o typ unsigned
         long int, przykłady: 200uL, 1000000uL, 855uL itd.


4.3.2.2 Stałe jako liczby zmiennoprzecinkowe
         Przykłady stałych – liczb zmiennoprzecinkowych:


          18.3           24.99             0.167              -44.82            itd.

         W naszym standardzie języka AVR GCC ze względu na opisane wyżej ograniczenia będą
         zawsze traktowane jako typ float z należną mu precyzją.
         Generalnie, musisz pamiętać, żeby jak najrzadziej korzystać w ogóle z typów
         zmiennoprzecinkowych. Wiąże się to z tym, że mikrokontrolery AVR nie posiadają rozkazów
         na poziomie kodu maszynowego, które mogłyby dokonywać obliczeń bezpośrednio na takich
         liczbach. Zatem wszelkie operacje wymagają zastosowania przez kompilator dosyć sporych
         objętościowo bibliotek programowych, które zajmują spore ilości pamięci programu, a także
         pamięci RAM mikrokontrolera. Dlatego często początkujący adepci języka AVR GCC są
         bardzo zdziwieni, że jeśli na pewnym etapie tworzenia programu zastosują chociaż jedną
         zmienną typu float, na której zechcą wykonać operacje matematyczne, to od razu po kompilacji
         okazuje się, że drastycznie wzrosło zużycie pamięci programu FLASH oraz często także
         pamięci RAM naszego mikrokontrolera. O tym, jak sobie z tym radzić i jak unikać typu
         float będzie później, szczególnie w trakcie ćwiczeń.


4.3.2.3 Stałe znakowe
         Tego typu stałe, jak sama nazwa wskazuje, służą do reprezentacji pojedynczych znaków
         alfanumerycznych. Zapisuje się je podając znak wewnątrz dwóch apostrofów. Przykłady:


          ‘a’    ‘B’     ‘Z’      ‘9’      ‘0’       ‘+’      ‘#’      itd.

         W pierwszych trzech przypadkach mamy do czynienia z literami, a w kolejnych dwóch ze
         znakami cyfr, jeszcze w kolejnych dwóch stałe reprezentujące znak plus oraz hash.
         Wykorzystujemy je wtedy, gdy chcemy zainicjalizować jakąś nowo zdefiniowaną zmienną, np.:

         char znak = ‘A’;
         char p = ‘a’;

         Oczywiście mikrokontroler nie potrafi przechowywać znaku A czy też znaku cyfry 6. Za to
         potrafi podstawić do tych zmiennych kody ASCII tych znaków. W tym przypadku zmienna
         znak będzie zawierała tak naprawdę liczbę 65 a zmienna p liczbę 97. Są to dokładne kody
         znaków z tabeli ASCII.
         Ale to nie wszystko, ponieważ nie wszystkie znaki ASCII jesteśmy w stanie wpisać w postaci
         znaku w jakimkolwiek edytorze. Weźmy na przykład znany ci dobrze znak ASCII o nazwie
         ENTER. Posiada on kod = 13. Ale są także inne niedrukowalne na ekranie znaki. Określa
Strona | 87
się je mianem znaków specjalnych i w języku C mamy do nich dostęp w prosty sposób.
Poniżej krótkie zestawienie niektórych znaków specjalnych.

b     -   Backspace
f     -   Form feed
n     -   New line
r     -   carriage Return
t     -   Tabulator
v     -   Vertical tabulator
a     -   Alarm

Posłużyłem się nazwami angielskimi, ponieważ i tak najczęściej takimi się posługujemy.
Bardzo często będziemy używać znaków r czyli nasz znak ENTER (kod 13) oraz n,
czyli znak nowej linii (kod 10). Żeby przypisać do zmiennej taki znak posłużymy się także
apostrofami w których zamkniemy taki znak specjalny:

char znak = ‘r’;

Od tej pory zmienna znak będzie przechowywała kod, czyli liczbę 13. Ponieważ jednak do
zapisu znaków specjalnych musimy używać ukośnika  oraz apostrofów, to pojawi się pewien
kłopot, jeśli będziemy chcieli podstawić do zmiennej czy dokonać porównania w warunku,
znaku apostrofa, ukośnika, ale też jeszcze kilku innych znaków. Dlatego podam jeszcze
kolejną krótką listę znaków specjalnych, tzn. jak należy je zapisywać w kodzie.

     -   ukośnik
’     -   apostrof
”     -   cudzysłów
?     -   znak zapytania
0     -   null, czyli znak o kodzie zero

Oczywiście takie znaki także musimy otoczyć apostrofami, więc czasem wyjdą dziwne
konstrukcje np. w przypadku znaku apostrofa, będziemy musieli napisać tak: char
znak=’’’. Jednak to jest jedyny prawidłowy zapis z użyciem znaku specjalnego.
Mamy do dyspozycji jednak jeszcze jeden sposób przedstawiania konkretnego znaku ASCII
wewnątrz apostrofów. Możemy go podać bezpośrednio jako liczbę, ale tylko w zapisie
hexadecymalnym lub ósemkowym. Najczęściej będziemy się posługiwać jeśli już zapisem
hexadecymalnym. Pozwala on nam na reprezentację każdego znaku ASCII bez wyjątku.
Aby móc zaprezentować znak w postaci hexadecymalnej, musimy użyć prefixu/zapisu: x,
który będzie poprzedzał wartość szesnastkową. Przykład:

char = ‘x41’;

 co jest równoznaczne

char = ‘A’;

 ponieważ liczba 0x41 to dziesiętnie 65, natomiast 65 jest kodem ASCII dużej literky A.
Strona | 88
4.3.2.4 Stałe tekstowe, stringi
         Bardzo często w programach będziesz zmuszony posługiwać się stałymi w postaci różnego
         rodzaju tekstów. Temat ten związany jest zagadnieniem zwanym „C-string”. Jest to stała
         tekstowa w postaci ciągu znaków ujętych w cudzysłowy. Przykłady:

         „jakiś tekst”
         „Napis na wyświetlacz LCD”
         „Pomiar napięcia”

         W skrócie mówimy na takie ciągi znaków po prostu „stringi”. Wewnątrz takiego ciągu
         znaków możemy bez problemu wstawić znaki specjalne opisane powyżej, np.:

         „Pierwsza linia tekstu rn Druga linia tekstu”

         Jak widzisz wstawiłem dwa znaki po sobie jeden to znak ENTER r a drugi to znak nowej
         linii n. Dzięki temu, gdybyśmy taki ciąg znaków przesłali np. do terminala, spowodowałby
         to, że pojawiłyby się na jego ekranie dwie linie tekstu zamiast jednej. Tekst „Pierwsza linia
         tekstu” wyświetlony zostałby w pierwszej linii, następnie znaki specjalne spowodowałyby
         przejście do początku nowej linii oraz wyświetlenia w niej kolejnej części tekstu „ Druga
         linia tekstu”. Jak widzisz na początku pozostałaby spacja, którą wstawiłem specjalnie w
         tekście stringa aby wyraźnie uwidocznić znaki specjalne. Nie trzeba oczywiście stosować
         takich spacji. Stringi możemy zapisywać na wiele różnych sposobów. Jeden już znamy,
         można wstawiać do środka znaki specjalne. Jeśli na przykład chcemy zapisać bardzo długi
         ciąg znaków, który nie mieści nam się w jednej linii w oknie edytora, możemy go rozbić na
         części w poniższy sposób, stawiając średnik na samym końcu:
         char tab[] = „Przykład linii, w której”
                            „występuje bardzo długi tekst”
                     „i nie mieści się w jednej linii”;
         Wprawdzie jeszcze nie wiesz co oznacza zapis tab[], ale ważne, abyś pamiętał, że średnik
         postawiony dopiero na końcu trzeciej linii spowoduje, iż kompilator połączy wszystkie
         występujące w każdej linii łańcuchy w jeden długi. Teraz najważniejsza rzecz: jakiego typu
         są stałe typu C-string? Rozpatrzmy to na kolejnym króciutkim przykładzie:

         „Procesor”

         Będzie typem const char[9]. Zapewne zdziwi ciebie bardzo skąd wzięła się tutaj liczba 9,
         skoro nasz łańcuch ma tylko 8 znaków? Już wyjaśniam. Rzeczywiście tekst w cudzysłowach
         posiada tylko 8 znaków, jednak po nich zgodnie ze standardem C-string, musi wystąpić znak
         null (fizycznie liczba zero). Zatem łącznie taki string musi zawierać 9 znaków. Oczywiście,
         jeśli wpiszemy inny tekst, to zawsze w nawiasie kwadratowym pojawi się liczba znaków
         tekstu plus jeden. Mam nadzieję, że pamiętasz, iż specyfikator const mówi o tym, iż zmienna
         taka nie może być później w kodzie w żaden sposób modyfikowana. Jeśli spróbujesz tego
         dokonać, to kompilator wyświetli błąd i uniemożliwi przeprowadzenie kompilacji. Oznacza
         to tak naprawdę, że kompilator musi gdzieś umieścić w pamięci takie stałe. Zarezerwować
         na nie miejsce. Mamy oczywiście możliwość zdecydowania, w jakiej pamięci stałe te mają
         być umieszczone. Jednak odpowiednie specyfikatory wskazujące na pamięć FLASH, pamięć
         RAM lub EEPROM poznamy później. Ważne jest, że raz zarezerwowany obszar na dowolną
         stałą nie może być w późniejszym terminie zmieniany przez program. Stąd specyfikator const.
Mikrokontrolery AVR, język C, podstawy
                               programowania




Niniejsza darmowa publikacja zawiera jedynie fragment pełnej
wersji 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łącznie
w formie dostarczonej przez Wydawnictwo KRAM. Zabronione są jakiekolwiek zmiany w
zawartości publikacji bez pisemnej zgody Wydawnictwa KRAM - wydawcy niniejszej
publikacji. Zabrania się jej odsprzedaży.


Pełna wersja niniejszej publikacji jest do nabycia w
sklepie internetowym

                                http://witmir.pl

More Related Content

PDF
Instrukcia
PDF
Windows Vista PL. Zabawa z multimediami
PDF
Po prostu Windows Vista PL. Wydanie II
PDF
Diagnostyka sprzętu komputerowego
PDF
Anatomia PC. Kompendium. Wydanie IV
PDF
Fotografia cyfrowa. Poradnik bez kantów
PDF
Kompresja dźwięku i obrazu wideo Real World
PDF
Anatomia PC. Kompendium
Instrukcia
Windows Vista PL. Zabawa z multimediami
Po prostu Windows Vista PL. Wydanie II
Diagnostyka sprzętu komputerowego
Anatomia PC. Kompendium. Wydanie IV
Fotografia cyfrowa. Poradnik bez kantów
Kompresja dźwięku i obrazu wideo Real World
Anatomia PC. Kompendium

What's hot (18)

PDF
JavaScript. Pierwsze starcie
PDF
ABC sam składam komputer
PDF
Po prostu Mac OS X 10.5 Leopard PL
PDF
Anatomia PC. Kompendium. Wydanie II
PDF
Asembler dla procesorów Intel. Vademecum profesjonalisty
PDF
Anatomia PC. Kompendium. Wydanie III
PDF
Po prostu Java 2
PDF
Instrukcja obslugi pompy objetosciowej Medima P2
PDF
Mikroprocesory jednoukładowe PIC
PDF
Sharp 16
PDF
Od zera-do-ecedeela-cz-2
PDF
Co potrafi Twój iPhone? Podręcznik użytkownika. Wydanie II
PDF
Cubase SX. Szybki start
PDF
Sharp 11
PDF
Po prostu Flash MX 2004
PDF
Fotografia cyfrowa. Edycja zdjęć. Wydanie IV
PDF
Microsoft Visual C++ 2008. Tworzenie aplikacji dla Windows
PDF
Po prostu Flash MX
JavaScript. Pierwsze starcie
ABC sam składam komputer
Po prostu Mac OS X 10.5 Leopard PL
Anatomia PC. Kompendium. Wydanie II
Asembler dla procesorów Intel. Vademecum profesjonalisty
Anatomia PC. Kompendium. Wydanie III
Po prostu Java 2
Instrukcja obslugi pompy objetosciowej Medima P2
Mikroprocesory jednoukładowe PIC
Sharp 16
Od zera-do-ecedeela-cz-2
Co potrafi Twój iPhone? Podręcznik użytkownika. Wydanie II
Cubase SX. Szybki start
Sharp 11
Po prostu Flash MX 2004
Fotografia cyfrowa. Edycja zdjęć. Wydanie IV
Microsoft Visual C++ 2008. Tworzenie aplikacji dla Windows
Po prostu Flash MX
Ad

Viewers also liked (10)

PDF
Mikrokontrolery avr język c podstawy programowania
PPTX
Inżtech elearning
PDF
Język c. pasja programowania mikrokontrolerów 8 bitowych
PDF
PDF
Podstawowe fakty ngo 2012_klonjawor_raport
DOC
Projekt Bazy Danych
ODT
Resocjalizacja dorosłych, nieletnich w Polsce i zagranicą
PPT
Przetworniki inteligentne mier
PDF
Teoria i metodologia informatologii 16_17
PPT
Co to jest metoda?
Mikrokontrolery avr język c podstawy programowania
Inżtech elearning
Język c. pasja programowania mikrokontrolerów 8 bitowych
Podstawowe fakty ngo 2012_klonjawor_raport
Projekt Bazy Danych
Resocjalizacja dorosłych, nieletnich w Polsce i zagranicą
Przetworniki inteligentne mier
Teoria i metodologia informatologii 16_17
Co to jest metoda?
Ad

Similar to Mikrokontrolery avr język c podstawy programowania (20)

PDF
Anatomia PC. Wydanie IX
PDF
Praktyczny kurs asemblera
PDF
Czytanie kodu. Punkt widzenia twórców oprogramowania open source
PDF
Technik.teleinformatyk 312[02] z1.03_u
PDF
41. montowanie i uruchamianie komputera
PDF
Programowanie w języku C. Szybki start
PDF
Złóż własny komputer
PDF
Technik.teleinformatyk 312[02] z1.01_u
PDF
Asembler. Sztuka programowania
PDF
Pompa strzykawkowa Alaris PK, instrukcja obsługi
PDF
RS 232C - praktyczne programowanie. Od Pascala i C++ do Delphi i Buildera. Wy...
PDF
Win32ASM. Asembler w Windows
PDF
Profesjonalne programowanie. Część 1. Zrozumieć komputer
PDF
Język C. Wskaźniki. Vademecum profesjonalisty
PDF
Technik.elektryk 311[08] z4.04_u
PDF
Struktura organizacyjna i architektura systemów komputerowych
PDF
21. Pisanie i uruchamianie programów w asemblerze
PDF
Analiza dokumentacji technicznej podzespołów komputerowych
PDF
Architektura mikrokontrolera pisana słowem.
PPT
Isyp07
Anatomia PC. Wydanie IX
Praktyczny kurs asemblera
Czytanie kodu. Punkt widzenia twórców oprogramowania open source
Technik.teleinformatyk 312[02] z1.03_u
41. montowanie i uruchamianie komputera
Programowanie w języku C. Szybki start
Złóż własny komputer
Technik.teleinformatyk 312[02] z1.01_u
Asembler. Sztuka programowania
Pompa strzykawkowa Alaris PK, instrukcja obsługi
RS 232C - praktyczne programowanie. Od Pascala i C++ do Delphi i Buildera. Wy...
Win32ASM. Asembler w Windows
Profesjonalne programowanie. Część 1. Zrozumieć komputer
Język C. Wskaźniki. Vademecum profesjonalisty
Technik.elektryk 311[08] z4.04_u
Struktura organizacyjna i architektura systemów komputerowych
21. Pisanie i uruchamianie programów w asemblerze
Analiza dokumentacji technicznej podzespołów komputerowych
Architektura mikrokontrolera pisana słowem.
Isyp07

More from WKL49 (20)

PDF
Z archiwum WKL 75 lat
PDF
Z archiwum WKL 75 lat
PDF
Wkl archiwum
PDF
Z archiwum wkl w 4
PPTX
Pr
PPTX
Wanda rutkiewicz
PPTX
Wanda rutkiewicz prezentacja
PPTX
Wanda rutkiewicz maja latkowska 3 b
PPTX
Mount everest
PDF
Piekarstwo receptury, normy, porady i przepisy prawne
PDF
Vademecum piekarza do nauki zawodu
PDF
Modelowanie 3 d w programie autocad zbigniew krzysiak wnit
PDF
Ogniwa sloneczne. budowa, technologia i zastosowanie wkl
PDF
100% gramatyki języka francuskiego. tablice gramatyczne
PDF
Słownik prawniczy. angielsko polski, polsko-angielski. english for professionals
PDF
Rozmówki wzory listów i pism francuskich - poradnik oraz słownik
PDF
Rozmówki pomoc domowa w wielkiej brytanii
PDF
Rozmówki pomoc gastronomiczna w wielkiej brytanii
PDF
Słownik techniczno budowlany. angielsko-polski, polsko-angielski. english for...
PDF
Rozmówki pomoc domowa we francji
Z archiwum WKL 75 lat
Z archiwum WKL 75 lat
Wkl archiwum
Z archiwum wkl w 4
Pr
Wanda rutkiewicz
Wanda rutkiewicz prezentacja
Wanda rutkiewicz maja latkowska 3 b
Mount everest
Piekarstwo receptury, normy, porady i przepisy prawne
Vademecum piekarza do nauki zawodu
Modelowanie 3 d w programie autocad zbigniew krzysiak wnit
Ogniwa sloneczne. budowa, technologia i zastosowanie wkl
100% gramatyki języka francuskiego. tablice gramatyczne
Słownik prawniczy. angielsko polski, polsko-angielski. english for professionals
Rozmówki wzory listów i pism francuskich - poradnik oraz słownik
Rozmówki pomoc domowa w wielkiej brytanii
Rozmówki pomoc gastronomiczna w wielkiej brytanii
Słownik techniczno budowlany. angielsko-polski, polsko-angielski. english for...
Rozmówki pomoc domowa we francji

Mikrokontrolery avr język c podstawy programowania

  • 1. Mikrokontrolery AVR, język C, podstawy programowania Niniejsza darmowa publikacja zawiera jedynie fragment pełnej wersji 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łącznie w formie dostarczonej przez Wydawnictwo KRAM. Zabronione są jakiekolwiek zmiany w zawartości publikacji bez pisemnej zgody Wydawnictwa KRAM - wydawcy niniejszej publikacji. Zabrania się jej odsprzedaży. Pełna wersja niniejszej publikacji jest do nabycia w sklepie internetowym http://witmir.pl
  • 2. Styczeń 2011 ATNEL Mikrokontrolery AVr WYDAWNICTWO język C podstAwy progrAMowAniA Mirosław Kardaś Mojej Żonie – Kasi
  • 3. Książka przeznaczona jest dla elektroników i hobbystów, którzy chcą szybko, w oparciu o interesujące przykłady, poznać język C przeznaczony dla mikrokontrolerów AVR i nauczyć się pisać dla nich programy. 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ępny opisana została także architektura oraz możliwości samych mikrokontrolerów AVR wchodzących w skład dwóch rodzin: ATmega i ATtiny. Prezentowany materiał podzielony jest na trzy części. Pierwsza obejmuje zagadnienia związane z budową mikrokontrolerów, druga to wykład na temat podstaw samego ję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 Atnel Szczecin 2011 ISBN 978-83-931797-0-1 Wydawnictwo ATNEL ul. Jasna 15/38 70-777 Szczecin fax: 91 4635 683 http://www.atnel.pl e-mail: biuro@atnel.pl Wydanie I Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz wydawnictwo Atnel dołożyli wszelkich starań, by publikowane tu informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz wydawnictwo Atnel nie ponoszą także żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentów niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii całości lub fragmentów książki bądź dołączonej płyty DVD 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. Spis treści Strona | 3 Przedmowa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1 Wstęp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.1 Pierwszy „pusty” program w C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 3 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. Strona | 4 3.6.13 Bootloader – niesamowite możliwości . . . . . . . . . . . . . . . . . . . . . . . . 56 4.1 Zagadnienia ogólne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 4 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. 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 5 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. 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348 6 FuseBity – MkAvrCalculator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 6.16.1 Fusebity, Lockbity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 6.16.2 MkAvrCalculator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360 7 Bootloader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368 8.1 Pilot na podczerwień . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371 8 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 . . . . . . . . . . . . . . . . . . . . . . . . . . 430 9 Środowisko ECLIPSE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455
  • 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. Strona | 8 1 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. Strona | 58 4 Podstawy języka C Wreszcie dotarliśmy do miejsca, gdzie będzie można poznać więcej informacji na temat samego 4.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. Strona | 59 4.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. Strona | 60 4.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. Strona | 61 warunek, który może być dowolnym wyrażeniem, tylko wtedy zostanie wykonana instrukcja występująca w dalszej części. Druga postać stosowana jest do tzw. „rozgałęzień” programu. Oznacza, że jeśli będzie spełniony warunek 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, co moż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 nawiasami klamrowymi {} 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óżna wartość do zmiennej o nazwie wysokosc. W drugiej postaci w zależności od spełnionego warunku lub nie, ustawilśmy pewne wartości kilku różnym zmiennym, dlatego zastosowaliśmy nawiasy klamrowe ograniczające odpowiednio 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żona jest 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 kodu programu 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 takich sytuacjach do sporych problemów szczególnie, jeśli nie zastosujemy w odpowiedni sposób wcięć w programie.
  • 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. 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. 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. Strona | 65 W powyższym przykładzie sekcję init stanowi instrukcja i=0. Inicjalizujemy w ten sposób zmienną i, która będzie odpowiedzialna za iterację (wielokrotnie powtarzalną czynność). Sekcja wyrażenie_warunkowe to w naszym przypadku warunek i<10. Zatem pętla będzie się wykonywała do momentu, dokąd warunek będzie prawdziwy. Jako że zmienna i została zainicjalizowana wartością zero, można powiedzieć, że pierwszy przebieg pętli zostanie 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 za tym 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ększym tempie. 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ętle for z innych języków programowania, to śmiało stwierdzisz, że składnia tej pętli w języku C 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ź wszystkie części składowe pętli, pozostawiając jedynie znaki średników, które je oddzielają. Zatem poniższy zapis: for(;;) { // instrukcje; } Często spotkasz, jako pętlę nieskończoną. Opuszczenie sekcji wyrażenie_warunek jest 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ępnie zostaje 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, że pę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 konstrukcji ze 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. 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. Strona | 67 Wyglą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ędzie w 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 dobrze może wystąpić wyrażenie matematyczne, którego wynik będzie przełącznikiem. Następnie wewnątrz nawiasów klamrowych mamy sekcje o nazwie case lub default, dzięki którym możemy zdecydować, jakie instrukcje chcemy wykonać w zależności od konkretnych wartości naszego wyrażenia/przełącznika. Po słówku case zawsze podajemy wartość przełącznika, jaka nas 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łącznika case, może wystąpić instrukcja break. Tylko dlatego ująłem ją w powyższym schematycznym w przykładzie w nawiasy kwadratowe, aby zakomunikować, że instrukcja break może w tym miejscu występować, ale nie musi. Nie jest to obligatoryjne. Jednak, jeśli jej nie ma, to zostaną wykonane kolejne instrukcje zawarte instrukcji następnych sekcji case, switch. Może to spowodować, że całość nie zareaguje tylko na jeden przełącznik, a na kilka. Zatem jeśli zależy nam na wykonaniu instrukcji dotyczących tylko jednego przełącznika, to najczęściej będziemy blok rozpoczynający się od słówka case kończyli rozkazem break, który przerwie dalsze wykonywanie instrukcji zawartych w switch, ponieważ uznajemy, iż inne są niepotrzebne w 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 przyjmuje wartość równą dwa. Rozpoczyna się teraz instrukcja switch sprawdzająca wartość zmiennej x, pełniącej dla nas rolę przełącznika, od którego chcemy spowodować, aby z kolei zmienna czas przyjęła pewną konkretną wartość. Zakładamy także, że jeśli zmienna x nie osiągnie
  • 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. Strona | 69 4.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. 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 ramach 4.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. 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. 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. 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*1018 W standardowym języku C występują jeszcze inne typy, jednak na razie nie będziemy o nich wspominać, gdyż nie wszystkie dotyczą naszych mikrokontrolerów. Przedstawię raczej zestawienie typów wbudowanych, z jakimi będziemy mieli do czynienia korzystając z 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. 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. Strona | 75 Poprzez 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 razu zainicjalizować je konkretnymi wartościami, to możemy, aczkolwiek nie musimy, podawać w nawiasach kwadratowych ilości elementów. Kompilator obliczy sobie tę ilość na podstawie podanych wartości, jakimi będziesz potrzebował zainicjalizować takie tablice. Poniżej przykł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 1 do 5. Natomiast tab2 będzie posiadała 6 elementów dwubajtowych, zainicjalizowanych kolejno 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 wtedy kompilator 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ów w nawiasach kwadratowych. Zobacz, w jak prosty sposób definiujemy tablice łańcuchów przy 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 na jej 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ą one wyliczone na podstawie długości znaków tekstu plus jeden na znak null. Oznacza to, że w tym konkretnym przypadku na tablicę o nazwie napis2, kompilator zarezerwuje 11 bajtów. (10 Znaków tekstu oraz jeden znak null). Zapytasz zapewne od razu, gdzie taka tablica znaków zostanie zarezerwowana, w jakiej pamięci – RAM, FLASH, czy EEPROM? Jeśli nie zostanie podany żaden dodatkowy specyfikator standardu AVR GCC, to zawsze zostanie domyślnie zarezerwowane miejsce w pamięci RAM. O ile czasem potrzeba nam buforów na znaki czy teksty, na których będziemy wykonywali róż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ęci EEPROM, żeby można było później programowo podmieniać ich zawartość wg własnego uznania. Np. jakieś napisy na wyświetlaczu LCD itd. Jak wspomniałem, aby dokonać rezerwacji w innej pamięci niż RAM, trzeba użyć specjalnych specyfikatorów. Spowoduje to jednocześnie, że odczyt takich danych z pamięci FLASH i EEPROM będzie wyglądał inaczej niż z pamięci RAM. A jeszcze inaczej będziemy dokonywali
  • 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. 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);
  • 30. Strona | 78 Teraz krótkie wyjaśnienie do powyższych linii kodu programu. W pierwszej i drugiej wykonaliśmy definicję wskaźnika o nazwie wsk oraz p, które pokazują nam na obiekt nieznanego typu. Napisałem obiekt, ponieważ równie dobrze taki wskaźnik będzie mógł później posłużyć do pokazywania na zmienną dowolnego typu podstawowego lub złożonego albo nawet na funkcję programu. W trzeciej linijce pierwszy specyfikator void ten przez nazwą funkcji mówi o tym, że zdefiniowana w ten sposób funkcja nie będzie zwracać żadnego wyniku. Natomiast specyfikator void pomiędzy nawiasami okrągłymi mówi, że do tej funkcji nie będą przekazywane żadne argumenty. 4.3.1.4 Specyfikator const Czasem zdarzać się będzie, że w programie zechcesz używać niektórych zmiennych, które będą przechowywały przez całe życie programu pewne stałe wartości. Powiem więcej, chciałbyś mieć możliwość, żeby przez przypadek żaden fragment rozbudowanego programu nie zniszczył przypadkiem tej stałej. Wtedy przyjdzie ci z pomocą specyfikator const. Załóżmy, że w jednej zmiennej dla całego programu chcesz przechowywać wartość jakiegoś współczynnika podziału. Niech posiada on stałą wartość równą np. 45. Inna zmienna będzie przechowywała do pewnych obliczeń liczbę PI. Możemy zatem używając specyfikatora const napisać: const uint8_t wspolczynnik = 45; const float PI = 3.14; Od tej pory możesz używać zmiennej współczynnik oraz PI ale tylko i wyłącznie w trybie do odczytu. Gdy tylko spróbujesz nawet przez pomyłkę zmienić zawartość jednej z tych zmiennych, od razu kompilator zareaguje błędem w trakcie przeprowadzania kompilacji. Zwrócę uwagę na dodatkową kwestię. Wprawdzie nie znasz jeszcze zagadnień związanych z preprocesorem. Jednak istnieje pewna dyrektywa tegoż preprocesora o nazwie #define. Dzięki niej moglibyśmy uzyskać bardzo podobny efekt, jeśli chodzi o możliwość zdefiniowana pewnych stałych wartości, o jakich mówiłem powyżej. Oznacza to, że używając zapisu z przykładu poniżej: #define wspolczynnik 45 #define PI 3.14 otrzymujemy pozornie identyczną sytuację. Od tej pory możemy się posługiwać identyfikatorami wspolczynnik oraz PI na podobnej zasadzie. Istnieją jednak podstawowe różnice, o których warto wiedzieć, gdyż może to się okazać bardzo przydatne w trakcie pisania różnych programów. Czasem warto będzie skorzystać ze specyfikatora const, a czasem wystarczy #define. Oceni się to samemu, kiedy posiądzie się odpowiednią wiedzę i praktykę w tym zakresie. Czym jednak z praktycznego punktu widzenia różni się dla nas deklaracja zmiennej za pomocą #define od definicji ze specyfikatorem const? W oparciu o informacje podane wcześniej na temat różnic pomiędzy deklaracją a definicją zmiennej już powinieneś dostrzec podstawową różnicę. Otóż definicja zmiennej/stałej ze specyfikatorem const od razu rezerwuje miejsce w pamięci na tę zmienną. Natomiast sama deklaracja za pomocą dyrektywy #define tego nie czyni. Dyrektywa #define powoduje z punktu widzenia kompilatora zadeklarowanie stałej dosłownej, zatem kompilacja odbywa się w ten sposób, że w każdym miejscu, gdzie kompilator napotka nazwę zadeklarowaną za pomocą dyrektywy #define po prostu podstawi w to miejsce konkretną stałą wartość, która widnieje w tej deklaracji.
  • 31. Strona | 79 Kolejna różnica w tym, że stałe definiowane przy użyciu const będą mogły uzyskiwać różne zakresy widoczności, w zależności od tego, w jakim miejscu zostaną zdefiniowane. Natomiast stałe zdeklarowane z użyciem #define będą zawsze widoczne dla kompilatora w całym programie. Jeżeli spotkasz się z sytuacjami, kiedy warto będzie ukrywać zasięg swoich stałych, wtedy sięgniesz po const. Kolejna różnica polega na tym, że stała zdefiniowana za pomocą const ma swoje odzwierciedlenie w pamięci mikrokontrolera, a co za tym idzie, można do niej odwołać się za pomocą wskaźnika. Tymczasem stała zadeklarowana poprzez #define, jako że nie rezyduje w pamięci, nigdy nie będzie dostępna poprzez wskaźnik. Czasem może to być bardzo potrzebne. Mam tylko nadzieję, że starasz się śledzić dokładnie, w jakich momentach używam słowa deklaracja, a w jakich definicja. Nie robię tego przypadkowo i zamiennie, ponieważ każde z tych określeń ma swoje istotne znaczenie. Teraz widzisz, jak ważne jest i ile rzeczy się wiąże z dobrym zrozumieniem tego zagadnienia. 4.3.1.5 Specyfikator volatile Z angielskiego słowo volatile oznacza „ulotny”. Tak też kompilator traktuje zmienne, które zostały zaopatrzone w trakcie definicji w przydomek volatile. Przykład definicji zmiennej z tym specyfikatorem/przydomkiem: volatile int a; Kiedy powinniśmy go stosować? Zawsze w takich sytuacjach, gdy chcemy, aby kompilator nie optymalizował dostępu do takiej zmiennej. Co się takiego wiąże z optymalizacją zmiennych bez przydomka volatile? Kompilator nie widząc tego przydomku zakłada, że taka zmienna nigdy nie będzie mogła się zmienić bez wiedzy kompilatora. Założenie to czyni już na etapie kompilacji i w wyniku tego często, jeśli np. w jakiejś funkcji ma wykonywać operacje na tejże zmiennej, pozwala sobie na optymalizację, mającą na celu przyśpieszenie działania programu. W mikrokontrolerach jest to szczególnie istotne. Optymalizacja taka polega najczęściej na tym, że bezpośrednio po wejściu do funkcji main() czy jakiejkolwiek innej, kompilator zapamiętuje sobie zawartość komórki tej pamięci w podręcznym i wolnym rejestrze mikrokontrolera. Zwykle ma spory zapas takich wolnych rejestrów. Dzięki temu w dalszej części funkcji już nigdy nie odwołuje się do zawartości tej komórki pamięci, tylko operuje na zawartości rejestru. Dopiero przy wyjściu z funkcji oczywiście przy wyjściu z innej funkcji niż main() dokona zapisu aktualizowanej w rejestrze zawartości bezpośrednio do tej komórki. A ponieważ pętla główna programu main() nigdy się nie kończy to można przypuszczać, że cały czas będzie program się odwoływał i pracował tylko w oparciu o ten rejestr, żeby szybciej wykonywać działania. Jest to z jednej strony bardzo pożyteczne i pożądane. Jednak czasami wprowadzasz do programu procedurę obsługi jakiegoś przerwania, która także będzie miała za zadanie wykonywać operacje na tej samej zmiennej. I jeśli nie będzie ona opatrzona przydomkiem volatile, to kompilator spowoduje, że w trakcie wejścia w przerwanie, zmienna trafi do innego rejestru, na nim dokonane zostaną stosowne aktualizacje a przy wyjściu z procedury obsługi przerwania, zawartość rejestru trafi znowu do komórki pamięci tej zmiennej. No i katastrofa! Bo przecież w pętli głównej main() program nigdy nie zajrzy, jak wspomniałem wyżej, do tej komórki w związku z umieszczeniem jej zawartości w „podręcznym” rejestrze. W takiej sytuacji zawsze działa na podręcznym rejestrze, do którego raz wczytał tę zmienną. Zatem pętla główna nigdy się nie dowie o tym, że w „coś” (np. procedura przerwania) podmieniło zawartość tej komórki i zacznie dochodzić do przedziwnych błędów w programie, których przyczyny trudno będzie się od razu domyślić.
  • 32. Strona | 80 Dlatego, jeśli wiesz, że niektóre zmienne będą zapisywane i odczytywane zarówno w funkcjach, ale i procedurach przerwań mikrokontrolera, czyli mogą się zmieniać w różnym czasie, to trzeba wymusić, aby kompilator nie optymalizował dostępu do takich zmiennych. Musi za każdym razem, gdy chce wykonać na nich operację, odwołać się bezpośrednio do zawartości komórek pamięci, gdzie te zmienne rezydują i to ich zawartość modyfikować wedle potrzeb, a nie jakiś podręczny rejestr pełniący rolę bufora. Brak takiej optymalizacji wpłynie wprawdzie na to, że dostęp do zmiennej będzie nieco wolniejszy niż do rejestru (co mogłoby być w wielu przypadkach sporą wadą), ale dlatego nie wszystkie zmienne definiujemy jako volatile. Tylko te, których jak było powiedziane na początku, zawartość może być ulotna, czyli zmieniać się bez wiedzy kompilatora, np. w procedurach obsługi przerwań. 4.3.1.6 Specyfikator register Pamiętasz? W poprzednim rozdziale wspominaliśmy, że kompilator tam, gdzie można, dokonuje optymalizacji dostępu do zmiennych. Jednym ze sposobów optymalizacji jest umieszczanie wartości zmiennej w rejestrze, a ponieważ w kodzie maszynowym dostęp do rejestru jest o wiele szybszy niż do pamięci RAM, to optymalizacja taka zawsze zdecydowanie przyśpiesza działanie programu, co bywa bardzo istotne w procedurach krytycznych czasowo. Zdarza się jednak czasami, że niekoniecznie każdą zmienną kompilator załaduje do rejestru wewnątrz funkcji. Czasem włączy inny rodzaj optymalizacji. My jednak, jako programiści, możemy próbować wymusić na kompilatorze, aby niejako „na siłę” zastosował ten wariant, i próbował umieścić w rejestrze. Mogą być czasem procedury/funkcje, gdzie z naszego punktu widzenia czas ich wykonywania jest bardzo krytyczny. Są to zwykle krótkie funkcje i jeśli szczególnie zależy nam na szybkości ich wykonywania, to możemy zaopatrzyć zmienną w przydomek register. Kompilator będzie się wtedy starał jak może, aby tylko wykonać dla nas tę operację. Przykład takiej definicji zmiennej: register uint8_t licznik; Z uwagi jednak na to, że wszystkie rejestry mikrokontrolerów AVR mają postać jednego bajtu. Tylko niektóre mogą być traktowane w połączeniu, jako rejestry indeksowe, nie ma większego sensu, aby dodawać przydomek register zmiennym, które posiadają rozmiar większy niż jeden bajt ostatecznie dwa bajty. 4.3.1.7 Instrukcja Typedef Wspomniałem już chyba wcześniej króciutko, że ogromną zaletą języka C jest możliwość definiowania nowych, nawet własnych typów danych. Można powiedzieć, że pozwala to płynnie rozszerzać możliwości podstawowego standardu języka. I to stanowi m.in. o olbrzymiej przewadze języka C nad innymi językami wyższego rzędu. Do tych celów przydatna będzie właśnie instrukcja o nazwie Typedef. Za jej pomocą można np. nadać nową nazwę istniejącemu już typowi. Dlatego często w cudzych programach napisanych dla mikrokontrolerów AVR spotka się takie definicje zmiennych: u08 a; u16 z; u32 g;
  • 33. Strona | 81 Bardzo często programistom nie chce się wpisywać specyficznych typów dla AVR GCC i mikrokontrolerów AVR jak: uint8_t, uint16_t czy też uint32_t. Dlatego też często w tych programach znajdziesz użytą instrukcję typedef w celu utworzenia nowych typów o nazwach jak wyżej: u08, u16 czy u32. Dzięki temu nie trzeba tyle pisać za każdym razem przy definicji zmiennej. Wystarczy na początku programu napisać: typedef uint8_t u08; typedef uint16_t u16; typedef uint32_t u32; Oznacza to, że właśnie zdefiniowaliśmy na własne potrzeby trzy nowe typy o nazwach jak w przykładzie, dzięki czemu jeśli w dalszej części kodu programu zdefiniujemy zmienne korzystając z tych typów, zostaną one potraktowane dokładnie jak zmienne typu, od którego pochodzi nasz nowo utworzony typ. Jednym słowem zmienna typu u08 będzie tak naprawdę zmienną typu uint8_t i analogicznie następne. Nie tylko w taki celach przydatne bywa definiowane nowych typów. Np. będziesz w programie bardzo często posługiwał się zmiennymi, która będą miały np. za zadanie przechowywać wartość poziomu oleju. Definicje takich zmiennych będą się pojawiały w wielu funkcjach i miejscach programu. Charakterystyczne dla nich będzie to, że zawsze zawierają liczbę całkowitą ze znakiem z zakresu int. Ciężko będzie jednak później analizować program, który posiada zdefiniowaną sporą ilość zmiennych, nazwy są różne i jak na pierwszy rzut oka, szybko „wyłapać”, które z nich przechowują ten poziom oleju? Odpowiedź jest prosta, wystarczy zdefiniować nowy typ: typedef int P_olej; Od tej pory będziesz mógł spokojnie w różnych częściach programu definiować różne zmienne a patrząc na ich typ będziesz w mig wiedział, do jakiego celu je utworzyłeś. P_olej poziom1; P_olej poziom2; P_olej wskaznik3; P_olej miarka2; Przyznaj, że to bardzo przydatna rzecz. Naturalnie tego polecenia można także używać w związku z typami złożonymi, jak np. wskaźniki. Przykłady: typedef int * wskaznik_do _int; typedef char * napis; później definicje wskaźnik_do_int p1; // czyli: int * p1 napis komunikat; // czyli: char * komunikat Bardziej docenisz to zagadnienie z powyższego przykładu, gdy dowiesz się dużo więcej na temat samych wskaźników oraz możliwości ich stosowania. Zapewniam cię wyprzedając fakty, że wskaźniki to jedno z najlepszych narzędzi języka C,
  • 34. Strona | 82 choć trzeba przyznać, że także nieco skomplikowane i na początku ciężko je zrozumieć bez dobrego wytłumaczenia i przykładów. Nie martw się, postaram się, abyś jak najszybciej i jak najwięcej zrozumiał te zagadnienia w dalszych rozdziałach. 4.3.1.8 Typy wyliczeniowe enum To kolejne piękne narzędzie programistyczne. Praktycznie w innych popularnych językach dla mikrokontrolerów nieosiągalne. Jest tak bardzo przydatne, że niekorzystanie z niego w swoich programach mógłbym żartobliwie określić jako ciężki „grzech”. Typ wyliczeniowy to całkiem osobny typ, za pomocą którego możemy szybko utworzyć zestaw stałych całkowitych. Nagminnym przypadkiem jest, że w programach często trzeba przechowywać nie liczby, lecz pewien rodzaj informacji. Wprawdzie będziemy te informacje przechowywać w postaci liczb, chyba że mamy możliwość skorzystania z typu wyliczeniowego. Wtedy sama liczba nie będzie dla nas aż tak istotna. Pewnym istotnym dla nas wartościom będziemy mogli nadać nazwy i to nimi się posługiwać zamiast liczbami. Wyobraź sobie zmienną, która normalnie, gdy nie znasz tego mechanizmu, przechowuje w postaci liczb poziom menu, na jakim znajduje się aktualnie użytkownik. Musisz pamiętać w każdym miejscu programu, że wartość 0 oznacza poziom główny, ale już jakaś liczba wyrwana z kontekstu np. 23 oznacza trzeci poziom podmenu o nazwie „Ustawienia”. O ile pamiętanie o tym, że poziom zerowy to menu główne, nie nastręcza problemów, to już każda kolejna liczba, jeśli tych poziomów jest dużo, powoduje, że czasem się w tym gubimy. Bardzo często w takiej sytuacji bierzemy zwykłą kartkę papieru lub w kodzie programu tworzymy w jakimś miejscu specjalną tabelkę, gdzie opisujemy na własne potrzeby, co oznacza każda liczba, tzn. który poziom menu ona reprezentuje. Gorzej, gdy karteczka się zgubi albo, gdy program składa się z wielu plików i musimy przełączać się między nimi, aby odnaleźć te swoje zapiski w tabelce. Jeszcze gorzej, jeśli w trakcie programu potrzebujemy często tworzyć nowe poziomy pomiędzy już istniejącymi. Wtedy cała tabelka „bierze w łeb” i trzeba ją żmudnie przepisywać od nowa i dokonywać mnóstwo zmian w kodzie. Po co jednak tak się męczyć? Toż bardzo blisko jest od takiej tabelki, która przyporządkowuje każdą liczbę do konkretnego poziomu menu, do zastosowania typu wyliczeniowego enum! Wystarczy, że zastosujesz się do takiej składni: enum nazwa_typu {wartosc1, wartosc2,…………, wartoscN}; Gdzie w miejsce nazwa typu w naszym konkretnym przypadku wprowadzimy nazwę np. menu_poz a w nawiasach klamrowych podasz tylko wartości opisowe dla poszczególnych poziomów menu z tradycyjnej tabelki, np. tak jak poniżej. Jednak w przykładzie z oczywistych względów wpiszę tylko kilka wartości. Przyjmijmy założenie, że zbudowaliśmy zegar oraz utworzyliśmy menu główne, z którego można przejść do kolejnych poziomów aby ustawić czas, ustawić datę czy też ustawić alarm (budzik). enum menu_poz {mglowne, mczas, mdata, malarm}; enum menu_poz idx=mglowne; Proszę bardzo, właśnie w pierwszej linii powyższego przykładu zdefiniowaliśmy nowy typ wyliczeniowy o nazwie menu_poz, a następnie w kodzie programu możemy już zdefiniować konkretną zmienną o nazwie w tym przypadku idx. Dokonujemy jednocześnie jej inicjalizacji (UWAGA!), nie za pomocą stałej liczbowej, lecz za pomocą wartości, która istnieje w
  • 35. Strona | 83 naszym typie wyliczeniowym. Zatem możemy już wyrzucić naszą karteczkę z rozpisaną tabelką, bądź usunąć tabelkę z opisu w pliku. Więcej się nam ona nie przyda. Ale zapytasz zapewne, co się stanie, jeśli teraz zechcemy dodać nową pozycję w naszym menu? Ależ nic prostszego, załóżmy, że kolejna pozycja menu powinna umożliwiać ustawienia parametrów zegara. Taki nasz „setup”. enum menu_poz {mglowne, msetup, mczas, mdata, malarm}; enum menu_poz idx=mglowne; Zauważ, że specjalnie wstawiłem tę pozycję gdzieś w środek naszych wartości typów, bo założenie jest takie, iż kolejną pozycją po menu głównym ma być właśnie setup. Jednak istnieje w tym przypadku zupełna dowolność, można dodać na końcu bez żadnych konsekwencji, tak jak to było, gdy prowadziliśmy swoje tabelki z opisami. enum menu_poz {mglowne, mczas, mdata, malarm, msetup}; Mam nadzieję, że dostrzegasz teraz ogromne możliwości tego mechanizmu? Możesz się jednak zastanawiać, co tak naprawdę będzie zawierać zmienna idx, jeśli przypiszemy do niej dowolną wartość typu wyliczeniowego. Już wyjaśniam. Zasada jest prosta, domyślnie, jeżeli sami nie wprowadzimy zmian, rozpoczyna się numerowanie wartości od zera. W związku z tym pozycja mglowne=0, mczas=1, mdata=2, malarm=3, setup=4. Jednak programista ma możliwość wpływu na tę numerację. Wystarczy dokonać takiego zapisu: enum menu_poz {mglowne, mczas=7, mdata, malarm=43, msetup}; Spowoduje to, że teraz: mglowne = 0 (domyślnie gdyż nie przydzieliliśmy ręcznie żadnej wartości) mczas = 7 (widać wyżej dlaczego) mdata = 8 (ponieważ numeracja będzie dalej biegła od poprzedniego numeru) malarm = 43 setup = 44 Pamiętaj, że zmienną naszego nowego typu możesz posługiwać się także, jak zwykłą liczbą. Dozwolone są poniższe działania: enum menu_poz idx; uint8_t a,b=10; idx = malarm; a = b + idx; W wyniku takiego działania zmienna a będzie miała wartość 53. Ponieważ do zmiennej idx przypisaliśmy malarm, który zgodnie z przykładem wyżej posiada zdefiniowaną wartość = 43. Natomiast zmienną b zainicjalizowaliśmy liczbą 10. Zatem wynikiem b + idx jest liczba 53. Okazuje się także, że kompilator AVR GCC zezwala na przypisanie do zmiennej idx także innych wartości tzn. czysto liczbowych za pomocą stałych
  • 36. Strona | 84 lub zmiennych. Niektóre inne kompilatory nie zezwalają na taką operację generując błąd w trakcie kompilacji. Czy taki mechanizm będzie ci potrzebny ocenisz już sam. W każdym razie można zastosować poniższe działania: enum menu_poz idx; uint8_t a=10; idx = 122; idx = a + 87; Wykorzystanie tego zależy już tylko od twojej inwencji twórczej. Podam jednak jeszcze jeden przykład popularnego zastosowania dla typu enum. Później w części praktycznej, gdy zajmiemy się oprogramowaniem układu scalonego, RTC, który jest zegarem czasu rzeczywistego, okaże się, że można z niego odczytać, jaki mamy aktualnie numer dnia tygodnia. Jak się dowiesz, występuje tam taka zależność, że odczytana liczba 0 odpowiada poniedziałkowi, 1 to wtorek, 2 środa, 3 to czwartek, 4 piątek, 5 sobota oraz 6 niedziela. Gdy napiszesz program do obsługi takiego zegarka, zapewne będziesz chciał łatwo i szybko pokazywać nazwy dni tygodnia na własnym wyświetlaczu LCD. Jednak za każdym razem będziesz musiał pamiętać powyższe przypisania w różnych procedurach, gdzie będzie trzeba sprawdzać, jaki jest aktualnie dzień tygodnia. Aby sobie ułatwić życie, zastosujesz jednak typ wyliczeniowy enum. enum t_dzien {pon, wto, sro, czw, pia, sob, nie}; później utworzysz zmienną bądź zmienne, w których będziesz przetrzymywał dni tygodnia, ale nie będziesz musiał się już posługiwać na pamięć cyframi kolejnych dni. Teraz będziesz używał już wygodnych i łatwych do zapamiętania nazw. enum t_dzien dzien = sro; następnie gdzieś dalej w programie sprawdzanie, jaki jest dzień w zmiennej: if (dzien == pia) instrukcja; // wyłącz filtr w akwarium lub z instrukcją switch: switch(dzien) { case sro: instrukcja1; instrukcja2; break; case pia; instrukcja3; instrukcja4; break; case nie: instrukcja5; }
  • 37. Strona | 85 Jak widać w zależności od dnia tygodnia można wykonać różne czynności w programie, symbolizują to różne numery instrukcji w przykładzie. Mam nadzieję, że wyczerpująco przedstawiłem to ważne narzędzie, jakim jest typ wyliczeniowy. 4.3.2 stałe W języku c Brzmi „groźnie”, ale to na szczęście banalny, chociaż istotny temat w naszych rozważaniach i nauce języka C. Ze stałymi będziesz miał wciąż do czynienia. Nazywa się je stałymi dosłownymi. Wyróżniamy następujące rodzaje stałych: 1. Liczbowe całkowite 2. Liczbowe zmiennoprzecinkowe 3. Znakowe 4. Tekstowe Przypomnę, że już w wielu przykładach powyżej skorzystaliśmy ze stałych dosłownych. Były to jak do tej pory stałe liczbowe całkowite. Gdy pisaliśmy np. definicję zmiennej int a=122; to liczba 122, którą zainicjalizowaliśmy zmienną, jest właśnie pierwszym przykładem stałej dosłownej. 4.3.2.1 Stałe jako liczby całkowite Mogą to być liczby zapisywane w taki sposób, jaki nam najbardziej odpowiada. Możemy przy tym korzystać z postaci dziesiętnej liczb, z postaci szesnastkowej czy ósemkowej. Mogą to być liczby ze znakiem, czyli także ujemne, i bez znaku. Przykład: Dziesiętnie: 22 8 71 -15 0 455 1024 itd. Szesnastkowo: 0x10 0xf2 0x00 0x045 0x01 0xffff itd. Nie muszę chyba przypominać, jak stosować zapis szesnastkowy, liczę na to, że już wiesz dokładnie, o co w tym chodzi. Zwrócę jedynie uwagę, że języku C, jeśli chcemy przedstawić liczbę w postaci szesnastkowej, to w odróżnieniu od postaci dziesiętnej musimy ją poprzedzić znakami 0x (zero oraz x). Dodam jeszcze, że można w zapisie szesnastkowym inaczej zwanym hexadecymalnym używać zarówno dużych jak i małych liter. Dla kompilatora będzie to zupełnie obojętne. Trzeba sobie jednak zdawać sprawę, do jakiego typu konkretnie kompilator zalicza domyślnie stałe będące liczbami całkowitymi. Domyślnie, jeśli napiszemy np. liczbę 200, zostanie ona zakwalifikowana jakby była typu int. Jeśli oczywiście chcemy podać, jako stałą liczbę, która wykracza poza zakres int (patrz Tabela.1), to wtedy zostanie uznana, oczywiście automatycznie, za kolejny większy typ, czyli np. long int. Mamy jednak możliwość aby świadomie dać znać, aby np. liczbę 200 traktował od razu jako typ long int. Wystarczy, że na końcu takiej liczby postawimy literkę L. Może to być mała lub duża litera, jednak ze względu na czytelność lepiej posługiwać się dużą. Wtedy zapis będzie wyglądał tak 200L, 0L, 33L itd.
  • 38. Strona | 86 Mamy także wpływ na to, czy kompilator ma przyjmować stałą, jako liczbę bez znaku, (jako unsigned). Wtedy musimy na końcu zastosować literkę u. Przykład: 223u, 10u, 1234u itd. Można także łączyć literkę u z literką L, wtedy określamy, że chodzi nam o typ unsigned long int, przykłady: 200uL, 1000000uL, 855uL itd. 4.3.2.2 Stałe jako liczby zmiennoprzecinkowe Przykłady stałych – liczb zmiennoprzecinkowych: 18.3 24.99 0.167 -44.82 itd. W naszym standardzie języka AVR GCC ze względu na opisane wyżej ograniczenia będą zawsze traktowane jako typ float z należną mu precyzją. Generalnie, musisz pamiętać, żeby jak najrzadziej korzystać w ogóle z typów zmiennoprzecinkowych. Wiąże się to z tym, że mikrokontrolery AVR nie posiadają rozkazów na poziomie kodu maszynowego, które mogłyby dokonywać obliczeń bezpośrednio na takich liczbach. Zatem wszelkie operacje wymagają zastosowania przez kompilator dosyć sporych objętościowo bibliotek programowych, które zajmują spore ilości pamięci programu, a także pamięci RAM mikrokontrolera. Dlatego często początkujący adepci języka AVR GCC są bardzo zdziwieni, że jeśli na pewnym etapie tworzenia programu zastosują chociaż jedną zmienną typu float, na której zechcą wykonać operacje matematyczne, to od razu po kompilacji okazuje się, że drastycznie wzrosło zużycie pamięci programu FLASH oraz często także pamięci RAM naszego mikrokontrolera. O tym, jak sobie z tym radzić i jak unikać typu float będzie później, szczególnie w trakcie ćwiczeń. 4.3.2.3 Stałe znakowe Tego typu stałe, jak sama nazwa wskazuje, służą do reprezentacji pojedynczych znaków alfanumerycznych. Zapisuje się je podając znak wewnątrz dwóch apostrofów. Przykłady: ‘a’ ‘B’ ‘Z’ ‘9’ ‘0’ ‘+’ ‘#’ itd. W pierwszych trzech przypadkach mamy do czynienia z literami, a w kolejnych dwóch ze znakami cyfr, jeszcze w kolejnych dwóch stałe reprezentujące znak plus oraz hash. Wykorzystujemy je wtedy, gdy chcemy zainicjalizować jakąś nowo zdefiniowaną zmienną, np.: char znak = ‘A’; char p = ‘a’; Oczywiście mikrokontroler nie potrafi przechowywać znaku A czy też znaku cyfry 6. Za to potrafi podstawić do tych zmiennych kody ASCII tych znaków. W tym przypadku zmienna znak będzie zawierała tak naprawdę liczbę 65 a zmienna p liczbę 97. Są to dokładne kody znaków z tabeli ASCII. Ale to nie wszystko, ponieważ nie wszystkie znaki ASCII jesteśmy w stanie wpisać w postaci znaku w jakimkolwiek edytorze. Weźmy na przykład znany ci dobrze znak ASCII o nazwie ENTER. Posiada on kod = 13. Ale są także inne niedrukowalne na ekranie znaki. Określa
  • 39. Strona | 87 się je mianem znaków specjalnych i w języku C mamy do nich dostęp w prosty sposób. Poniżej krótkie zestawienie niektórych znaków specjalnych. b - Backspace f - Form feed n - New line r - carriage Return t - Tabulator v - Vertical tabulator a - Alarm Posłużyłem się nazwami angielskimi, ponieważ i tak najczęściej takimi się posługujemy. Bardzo często będziemy używać znaków r czyli nasz znak ENTER (kod 13) oraz n, czyli znak nowej linii (kod 10). Żeby przypisać do zmiennej taki znak posłużymy się także apostrofami w których zamkniemy taki znak specjalny: char znak = ‘r’; Od tej pory zmienna znak będzie przechowywała kod, czyli liczbę 13. Ponieważ jednak do zapisu znaków specjalnych musimy używać ukośnika oraz apostrofów, to pojawi się pewien kłopot, jeśli będziemy chcieli podstawić do zmiennej czy dokonać porównania w warunku, znaku apostrofa, ukośnika, ale też jeszcze kilku innych znaków. Dlatego podam jeszcze kolejną krótką listę znaków specjalnych, tzn. jak należy je zapisywać w kodzie. - ukośnik ’ - apostrof ” - cudzysłów ? - znak zapytania 0 - null, czyli znak o kodzie zero Oczywiście takie znaki także musimy otoczyć apostrofami, więc czasem wyjdą dziwne konstrukcje np. w przypadku znaku apostrofa, będziemy musieli napisać tak: char znak=’’’. Jednak to jest jedyny prawidłowy zapis z użyciem znaku specjalnego. Mamy do dyspozycji jednak jeszcze jeden sposób przedstawiania konkretnego znaku ASCII wewnątrz apostrofów. Możemy go podać bezpośrednio jako liczbę, ale tylko w zapisie hexadecymalnym lub ósemkowym. Najczęściej będziemy się posługiwać jeśli już zapisem hexadecymalnym. Pozwala on nam na reprezentację każdego znaku ASCII bez wyjątku. Aby móc zaprezentować znak w postaci hexadecymalnej, musimy użyć prefixu/zapisu: x, który będzie poprzedzał wartość szesnastkową. Przykład: char = ‘x41’; co jest równoznaczne char = ‘A’; ponieważ liczba 0x41 to dziesiętnie 65, natomiast 65 jest kodem ASCII dużej literky A.
  • 40. Strona | 88 4.3.2.4 Stałe tekstowe, stringi Bardzo często w programach będziesz zmuszony posługiwać się stałymi w postaci różnego rodzaju tekstów. Temat ten związany jest zagadnieniem zwanym „C-string”. Jest to stała tekstowa w postaci ciągu znaków ujętych w cudzysłowy. Przykłady: „jakiś tekst” „Napis na wyświetlacz LCD” „Pomiar napięcia” W skrócie mówimy na takie ciągi znaków po prostu „stringi”. Wewnątrz takiego ciągu znaków możemy bez problemu wstawić znaki specjalne opisane powyżej, np.: „Pierwsza linia tekstu rn Druga linia tekstu” Jak widzisz wstawiłem dwa znaki po sobie jeden to znak ENTER r a drugi to znak nowej linii n. Dzięki temu, gdybyśmy taki ciąg znaków przesłali np. do terminala, spowodowałby to, że pojawiłyby się na jego ekranie dwie linie tekstu zamiast jednej. Tekst „Pierwsza linia tekstu” wyświetlony zostałby w pierwszej linii, następnie znaki specjalne spowodowałyby przejście do początku nowej linii oraz wyświetlenia w niej kolejnej części tekstu „ Druga linia tekstu”. Jak widzisz na początku pozostałaby spacja, którą wstawiłem specjalnie w tekście stringa aby wyraźnie uwidocznić znaki specjalne. Nie trzeba oczywiście stosować takich spacji. Stringi możemy zapisywać na wiele różnych sposobów. Jeden już znamy, można wstawiać do środka znaki specjalne. Jeśli na przykład chcemy zapisać bardzo długi ciąg znaków, który nie mieści nam się w jednej linii w oknie edytora, możemy go rozbić na części w poniższy sposób, stawiając średnik na samym końcu: char tab[] = „Przykład linii, w której” „występuje bardzo długi tekst” „i nie mieści się w jednej linii”; Wprawdzie jeszcze nie wiesz co oznacza zapis tab[], ale ważne, abyś pamiętał, że średnik postawiony dopiero na końcu trzeciej linii spowoduje, iż kompilator połączy wszystkie występujące w każdej linii łańcuchy w jeden długi. Teraz najważniejsza rzecz: jakiego typu są stałe typu C-string? Rozpatrzmy to na kolejnym króciutkim przykładzie: „Procesor” Będzie typem const char[9]. Zapewne zdziwi ciebie bardzo skąd wzięła się tutaj liczba 9, skoro nasz łańcuch ma tylko 8 znaków? Już wyjaśniam. Rzeczywiście tekst w cudzysłowach posiada tylko 8 znaków, jednak po nich zgodnie ze standardem C-string, musi wystąpić znak null (fizycznie liczba zero). Zatem łącznie taki string musi zawierać 9 znaków. Oczywiście, jeśli wpiszemy inny tekst, to zawsze w nawiasie kwadratowym pojawi się liczba znaków tekstu plus jeden. Mam nadzieję, że pamiętasz, iż specyfikator const mówi o tym, iż zmienna taka nie może być później w kodzie w żaden sposób modyfikowana. Jeśli spróbujesz tego dokonać, to kompilator wyświetli błąd i uniemożliwi przeprowadzenie kompilacji. Oznacza to tak naprawdę, że kompilator musi gdzieś umieścić w pamięci takie stałe. Zarezerwować na nie miejsce. Mamy oczywiście możliwość zdecydowania, w jakiej pamięci stałe te mają być umieszczone. Jednak odpowiednie specyfikatory wskazujące na pamięć FLASH, pamięć RAM lub EEPROM poznamy później. Ważne jest, że raz zarezerwowany obszar na dowolną stałą nie może być w późniejszym terminie zmieniany przez program. Stąd specyfikator const.
  • 41. Mikrokontrolery AVR, język C, podstawy programowania Niniejsza darmowa publikacja zawiera jedynie fragment pełnej wersji 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łącznie w formie dostarczonej przez Wydawnictwo KRAM. Zabronione są jakiekolwiek zmiany w zawartości publikacji bez pisemnej zgody Wydawnictwa KRAM - wydawcy niniejszej publikacji. Zabrania się jej odsprzedaży. Pełna wersja niniejszej publikacji jest do nabycia w sklepie internetowym http://witmir.pl