37
Programowanie
Elektronika dla Wszystkich
Podczas rozmowy o dalszych losach kursu,
który w³anie czytasz, przypomniano mi, ¿e
na p³ytce z kursu BASCOM istnieje jeszcze
element, którego do tej pory unika³em. Poja-
wi³a siê propozycja pe³nego wyczerpania
mo¿liwoci p³ytki przed publikacj¹ nowego
uk³adu. Mylê, ¿e idea powtórzenia wszyst-
kich æwiczeñ z kursu BASCOM-a mog³aby
nie wszystkim przypaæ do gustu. Jednak
zabawa istniej¹cym na p³ytce przetwornikiem
analogowo-cyfrowym i cyfrowo-analogowym
bêdzie bardzo po¿ytecznym zajêciem. Zaj-
miemy siê wiêc dzisiaj magistral¹ I
2
C na
przyk³adzie obs³ugi uk³adu PCF8591. Ponad-
to chcê, abymy dzi dokonali prze³omowej
zmiany w sposobie pisania programu - o tym
ju¿ w poni¿szym ródtytule.
Dzielenie kodu programu
Do tej pory ca³y kod naszego programu
umieszczalimy w jednym pliku. Podejcie
takie jest uzasadnione w przypadku pisania
niewielkich aplikacji. W praktyce jednak im
program bardziej skomplikowany, tym bar-
dziej op³acalne staje siê jego podzielenie na
pewne funkcjonalne bloki. Znacznie pomaga
to w utrzymaniu porz¹dku. Móg³bym uraczyæ
Ciê niepotrzebn¹ dawk¹ teorii, zamiast tego
zaczniemy jednak pisaæ nowy podzielony
program. Umo¿liwi to samodzielne przekona-
nie siê, o co w tym wszystkim chodzi i dla-
czego jest takie genialne.
Na rysunku 27 pokaza³em, w jaki sposób
bêdzie przebiegaæ kompilacja naszego dzi-
siejszego programu. Podobne, tylko bardziej
ogólne, obrazki znajdziesz tak¿e w innych
kursach jêzyka C. Przyjrzyj siê teraz wspom-
nianemu rysunkowi. Umówmy siê teraz, ¿e
ka¿dy element, który poddawany bêdzie
oddzielnemu procesowi kompilacji, nazwiemy
modu³em. Na nasze dzisiejsze potrzeby stwo-
rzymy modu³y o nazwach: lcd, i2c oraz delay.
Jak ³atwo siê domyliæ, zostan¹w nich
umieszczone elementy niezbêdne do obs³ugi
wywietlacza, magistrali I
2
C oraz podprogra-
my opónieñ. Dodatkowo pojawiaj¹ siê dwa
pliki nag³ówkowe, które nie s¹ elementem
¿adnego modu³u. W pliku harddef.h zostan¹
umieszczone wszystkie informacje dotycz¹ce
konfiguracji sprzêtowej czêci naszego urz¹-
dzenia - bêd¹ to g³ównie przypisania symbo-
licznych oznaczeñ odpowiednim wyprowa-
dzeniom. Nietrudno siê domyliæ, co znajdzie
siê w pliku makra.h. Wprowadzimy tutaj
przede wszystkim poznane w poprzedniej
czêci makra upraszcza-
j¹ce dostêp do portów.
Pliki hardder.h oraz
makra.h
do³¹czamy
wszêdzie tam, gdzie
informacje o wyprowa-
dzeniach oraz odpo-
wiednie makra s¹ nam
potrzebne. Dziêki takie-
mu podejciu wszystkie
niezbêdne makra napi-
szemy tylko raz - nie
bêdziemy musieli kop-
iowaæ ich wszêdzie,
gdzie bêd¹ one koniecz-
ne jeli postanowimy w
jaki sposób je ulep-
szyæ, zmiany pojawi¹ siê natychmiast we
wszystkich modu³ach. Definicja czêci sprzê-
towej w oddzielnym pliku, poza powy¿szymi
zaletami, ma jeszcze jedn¹ - jest czytelna.
Jeli zmienimy co w po³¹czeniach na p³ytce,
zawsze wiemy, gdzie szukaæ linijek wymaga-
j¹cych modyfikacji.
Zauwa¿, ¿e nag³ówków harddef.h oraz
makra.h nie do³¹czamy do modu³u delay.
Modu³ ten nie potrzebuje dostêpu do portów,
nie korzysta wiêc z zawartych tam informacji.
W zwi¹zku z tym do³¹czanie wymienionych
plików jest zbêdne.
Wspomniane modu³y nie bêd¹ w stanie
dzia³aæ same z siebie. Bêd¹ one tylko zbiorem
odpowiednich funkcji które trzeba jeszcze
wywo³aæ w odpowiedniej kolejnoci oraz
z odpowiednimi parametrami. Tym zajmie siê
modu³ nazwany main. Zauwa¿, ¿e nie ma on
przypisanego pliku nag³ówkowego. Jest to
zwi¹zane z tym, ¿e nie udostêpniamy ¿adnych
funkcji z modu³u main na zewn¹trz. To st¹d
bêdziemy wywo³ywaæ wszystkie zewnêtrzne
funkcje, nie na odwrót.
Kompilacja programu w C przebiega w
trzech fazach. Na samym pocz¹tku nastêpuje
³¹czenie ze sob¹ odpowiednich plików
nag³ówkowych z plikiem ród³owymi. Makra
oraz sta³e symboliczne s¹ zamieniane na zde-
finiowane formy (rozwijane). Usuwane s¹ tak
zwane bia³e znaki (spacje, tabulatory, znaki
koñca linii). Dzia³ania te wykonuje preproce-
sor. Nastêpnie tak obrobiony plik jest kompi-
lowany. Jeli dzia³anie to przebieg³o bezb³êd-
nie, jego wynikiem jest przynajmniej plik
z rozszerzeniem *.o. Kompilator umieszcza
w nim relokowalny kod maszynowy. Jest to
ju¿ praktycznie kod programu, jednak zapisa-
ny w taki sposób, ¿e funkcje nie maj¹ przypo-
rz¹dkowanych miejsc w pamiêci. W pliku
zawarte s¹ dodatkowo informacje o tym, jakie
funkcje on zawiera - umo¿liwia to ich wywo-
³anie za pomoc¹ nazwy z poziomu innych
modu³ów tego samego projektu.
Jeli wybrana zosta³a odpowiednia opcja,
kompilator utworzy dodatkowo plik zawiera-
j¹cy listing modu³u w kodzie asemblera. Jeli
zachowa³e kody programów z poprzednich
czêci, mo¿esz podejrzeæ teraz odpowiednie
pliki o rozszerzeniach *.lst.
Poprzednie dwie fazy odbywaj¹ siê
oddzielnie dla ka¿dego pliku ród³owego pro-
jektu. Ostatnim krokiem, dziel¹cym kod pro-
gramu od jego postaci umo¿liwiaj¹cej zapro-
gramowanie mikrokontrolera, jest linkowa-
nie. W procesie tym podejmowana jest decyz-
ja, w jakim dok³adnie miejscu umieszczone
maj¹ byæ zmienne, gdzie umieszczone maj¹
byæ funkcje, uzupe³niane s¹ wymagaj¹ce tego
instrukcje skoków. Efektem pracy linkera s¹
obrazy pamiêci programu oraz danych EEP-
ROM. Dodatkowo, jeli odpowiednie opcje
zosta³y wybrane, pojawi¹ siê pliki *.lss
zawieraj¹ce listing ca³ego programu, *.map
z opisem, pod jakimi adresami w pamiêci
zosta³y umieszczone poszczególne elementy,
P
P
r
r
o
o
g
g
r
r
a
a
m
m
o
o
w
w
a
a
n
n
i
i
e
e
p
p
r
r
o
o
c
c
e
e
s
s
o
o
r
r
ó
ó
w
w
w
w
j
j
ê
ê
z
z
y
y
k
k
u
u
C
C
czêæ 6
Tylko w jednym pliku ród³owym mo¿e
znaleæ siê tylko jedna funkcja o nazwie
main. To od niej zacznie siê wykonywanie
programu. Dla kompilatora nie ma znacze-
nia, w którym module zostanie ona umiesz-
czona. Dla w³asnej wygody oraz czytelno-
ci kodu umieszczamy j¹ w module g³ów-
nym programu.
Rysunek 27 - przebieg kompilacji dzisiejszego programu
oraz *.elf i *.sym - zawieraj¹ce informacje
u³atwiaj¹ce symulacje programu.
Jeli chcesz, zobacz, jak wygl¹daj¹ po-
szczególne pliki - otwórz je w notatniku albo
w naszym Programmers Notepadzie.
Zobacz, jaka jest ró¿nica miêdzy plikami
*.lst a *.lss.
Zanim zaczniesz
Zanim zaczniesz pisaæ podawane dalej kody,
koniecznie utwórz nowy katalog. W moim
przypadku nazywa³ siê on PCF8591. Mo¿esz
skopiowaæ do niego wzorzec pliku makefile
i wykonaæ w nim standardowe zmiany - jed-
nak aby ca³oæ dzia³a³a prawid³owo, koniecz-
na bêdzie jego dalsza edycja - napiszê o tym
po przedstawieniu kodów programu. Stwórz
w Programmers Notepadzie nowy projekt
i zapisz go w utworzonym katalogu. Pisane
pliki dodawaj od razu do projektu.
Pierwszy modu³ - delay
Tworzenie modu³ów proponujê rozpocz¹æ od
elementu najprostszego - funkcji opónieñ.
Konieczne jest utworzenie pliku nag³ówko-
wego oraz pliku kodu ród³owego. Aby lepiej
zrozumieæ dalej przedstawiane listingi, zaj-
rzyj do ramki Co i jak umieciæ w nag³ów-
ku.
Oba pliki, jakie musisz teraz napisaæ,
pokazuj¹ listingi 35 i 36. Zauwa¿, ¿e nie ma
tutaj nic nowego. Dok³adnie takie funkcje
opónieni by³y ju¿ przez nas napisane. Mo¿e
zainteresuje Ciê nowe makro delay500ns. Za-
pis taki powoduje wykonanie skoku do na-
stêpnej instrukcji. Co nam to daje? Opónie-
nie dwóch cykli, przy zajêciu tylko jednego
s³owa programu. Prosta sztuczka poprawiaj¹-
ca optymalnoæ kodu.
G³ówn¹ ró¿nic¹ miêdzy tym, co robilimy
do tej pory, jest podzielenie pliku. Zauwa¿, ¿e
definicja funkcji zosta³a
umieszczona w pliku kodu
ród³owego.
Do pliku ród³owego do-
³¹czam nag³ówek tego samego
modu³u. Regu³¹ jest, aby robiæ
to zawsze, chocia¿ czasami
nic nam to nie daje. W tym przypadku jednak
potrzebujemy zawartych w delay.h makr.
Makra
oraz definicja sprzêtu
Pierwszy modu³ móg³ byæ napisany bez wg³ê-
biania siê w to, co siedzi w plikach makra.h
oraz harddef.h, poniewa¿, jak to zosta³o ju¿
wspomniane - modu³ ten z nich nie korzysta.
Jednak przed pójciem dalej powinnimy
utworzyæ wspomniane pliki. Jedyna nowoæ na
listingu 37 to sposób zapisu. Z tego powodu
nie bêdê siê wg³êbia³ w jego opis. Nie przejmuj
siê pojawiaj¹c¹ siê na listingu 38 tajemnicz¹
sta³¹ I2C_SPEED. Wszystko stanie siê jasne
przy okazji omawiania modu³u i2c.
Ze wzglêdu na wygodê, kody przedsta-
wiam w ca³oci. Chcê jednak, aby wiedzia³,
¿e o ile plik z makrami zosta³ stworzony od
rêki, to jednak plik z definicj¹ sprzêtu by³
rozwijany, w ramach potrzeb, podczas pisania
kolejnych modu³ów. Oba pliki nale¿y trakto-
waæ w³anie w taki sposób - dodawaæ do nich
nowe wpisy w razie potrzeby.
Wywietlacz LCD
Poniewa¿ ostatnio napisalimy ju¿ procedury
obs³ugi wywietlacza LCD, wykorzystajmy
je teraz w celu stworzenia interfejsu u¿yt-
kownika.
Poza plikiem g³ównym, dobrym zwycza-
jem jest zaznaczanie w nazwie funkcji
z jakiego pliku ona pochodzi. Znakomicie
upraszcza to orientacjê w programie, zw³asz-
cza jeli przysiadamy do niego po d³u¿szym
czasie. Nie ma jakiej sztywnej regu³y, jak
takiego oznaczenia dokonywaæ - kompilator
zupe³nie siê tym nie interesuje. Polecam Ci
!&
Programowanie
Elektronika dla Wszystkich
ABC... C
Co i jak umieciæ w nag³ówku
Pliki nag³ówkowe mo¿emy, dla w³asnych potrzeb,
podzieliæ na dwa rodzaje: te, które maj¹ przyporz¹dko-
wany plik kodu ród³owego, oraz takie, które istniej¹
samodzielnie. Jest to podzia³ tworzony tylko przez nas
- kompilator nie interesuje siê tym, czy plik nag³ówko-
wy posiada jaki plik ród³owy o identycznej nazwie.
W pliku na-
g³ówka umieszcza-
my:
Definicje sta³ych.
Jednak jeli s¹ one
przeznaczone tylko
dla danego modu³u
i reszta programu
nie potrzebuje ich
znaæ, sta³e takie
mo¿na umieszczaæ
bezporednio
w
pliku ród³owym.
Makra, z identycznym zastrze¿eniem jak wy¿ej.
Funkcje statyczne, traktowane jako makra i z takim
samym, jak dotycz¹ce ich, zastrze¿eniem. Teraz mo¿e
nie jest to jasne, ale wyjani siê gdy tylko dowiesz siê,
czym s¹ funkcje statyczne.
Dodatkowo, tylko w przypadku plików skojarzonych
z plikami ród³owymi:
Deklaracje funkcji, które chcemy udostêpniæ do
wywo³ywania z zewn¹trz. Deklaracja taka teoretycz-
nie mo¿e pojawiæ siê w dowolnym pliku nag³ówko-
wym. Mo¿na nawet powtarzaæ j¹ bezporednio w pli-
ku ród³owym, tam, gdzie jest ona potrzebna. Jednak
ze wzglêdu na czytelnoæ kodu nie robi siê tego. Dek-
laracje takie umieszczaj zawsze w pliku nag³ówko-
wym o nazwie identycznej z nazw¹ pliku ród³owego,
w którym znajduje siê definicjê funkcji.
Szkielet pliku nag³ówkowego
Na listingu w tej ramce przedstawiam, w jaki sposób
prawid³owo utworzyæ plik nag³ówkowy. Bardzo
wa¿ne s¹ pojawiaj¹ce siê tutaj instrukcje preprocesora.
S¹ to instrukcje kompilacji warunkowej. W takim
zastosowaniu jak przedstawia listing zapewniaj¹ one,
¿e zawartoæ pliku zostanie przetworzona tylko raz.
Jest to cenna w³aciwoæ w du¿ych projektach, gdzie
czasami w samym pliku nag³ówkowym musimy do³¹-
czyæ plik, który sam do³¹cza plik, w którym w³anie
jestemy. W takim przypadku zosta³aby zg³oszona
masa b³êdów oznaczaj¹cych, ¿e sta³e zosta³y ju¿ zde-
finiowane, a funkcje zdeklarowane. (...) W praktyce
powi¹zania takie mog¹ byæ du¿o dalsze i trudniejsze
do wykrycia - wykorzystanie kompilacji warunkowej
rozwi¹zuje problem w sposób uniwersalny i eleganc-
ki.
Oczywicie aby ca³oæ dzia³a³a prawid³owo, ka¿dy
plik musi pos³ugiwaæ siê innym identyfikatorem.
Zwyk³o siê wykorzystywaæ w nim nazwê pliku oraz
s³owo INCLUDED (ang. do³¹czony). Na przyk³ad
dla pliku delay.h bêdzie to: DELAY_H_INCLUDED.
Poleceniu #ifndef równowa¿ny jest zapis: #if
!defined.
ABC... C
Funkcja itoa
Funkcja itoa nie nale¿y do standardu jêzyka C. Jest
jednak na tyle popularna, ¿e wiele kompilatorów j¹
udostêpnia. W AVR-GCC wymaga ona do³¹czenia
nag³ówka <stdlib.h>. itoa zamienia podan¹ liczbê
(pierwszy argument) na napis, który wpisuje do bufo-
ra (drugi argument). Liczba jest przekszta³cana z baz¹
podan¹ jako trzeci argument (na listingu 39 jest to
baza dziesiêtna). Jeli baza przekszta³canej liczby jest
równa 10, a przekszta³cana liczba ujemna - przed
wynikiem zostanie umieszczony znak -. Maksymal-
na baza, na jak¹ zezwala nasz kompilator, to 36 (jed-
nak nie jest to ujête w ¿aden standard). Jeli baza jest
wiêksza ni¿ 10, po cyfrze 9 wywietlana jest litera
a.
Powy¿sze informacje pozwalaj¹ nam obliczyæ
niezbêdn¹ wielkoæ bufora. Dla szesnastobitowych
liczb ze znakiem, wynikiem o maksymalnej d³ugoci
jest: -32768. Daje to 6 znaków plus znak zerowy.
Funkcja zwraca zawsze swój drugi argument
(wskanik do ³añcucha). Umo¿liwia to jej wstawie-
nie bezporednio w miejsce odpowiedniej zmiennej
(tak jak zosta³o to zrobione na listingu 39).
Listing 35 - plik delay.h
#ifndef DELAY_H_INCLUDED
#define DELAY_H_INCLUDED
#define delay250ns() {asm volatile(nop::);}
#define delay500ns()\
{asm volatile( \
rjmp exit%=\n\t\
exit%=:\n\t::);
#define delayus8(t)\
{asm volatile( \
delayus8_loop%=: \n\t\
nop \n\t\
dec %[ticks] \n\t\
brne delayus8_loop%= \n\t\
: :[ticks]r(t) );}
// DEC - 1 cykl, BRNE 2 cykle, + 1xnop. Zegar 4MHz
// Deklaracja funkcji o najd³u¿szym opónieniu
void
delay100us8(uint8_t t);
#endif
//DELAY_H_INCLUDED
Listing 36 - plik delay.c
#include <avr\io.h>
#include delay.h
void
delay100us8(uint8_t t)
{
while
(t>
0
)
{
delayus8(
100
);
t;
}
}
////////////////////////////////
// skrótowy opis pliku
//
// Autor, kompilator
////////////////////////////////
#ifndef IDENTYFIKATOR
#define IDENTYFIKATOR
// makra, sta³e, deklaracje...
#endif
//IDENTYFIKATOR
dodawanie przed nazw¹ funkcji nazwy pliku
oraz znaku podkrelenia. Mog¹ byæ to zarów-
no du¿e, jak i ma³e litery. Dla porz¹dku staraj
siê jednak przyj¹æ jedn¹ wersje w obrêbie
ca³ego programu. Stwórz plik lcd.c i skopiuj do
niego napisane przy okazji zesz³ego kursu
funkcje obs³ugi wywietlacza. Przyjêty sposób
oznaczañ wymaga zmiany nazw wszystkich
znajduj¹cych siê tutaj funkcji wywietlacza.
Pomocna w tym przypadku bêdzie komenda
Edit->Replace Programmers Notepada. Nie
pozwalaj jednak na w pe³ni automatyczn¹
zamianê, poniewa¿ w ten sposób pozmieniasz
tak¿e nazwy wszystkich sta³ych. Nie ma tego
du¿o, dlatego te¿ sprawdzenie ka¿dej propono-
wanej zmiany nie zajmie du¿o czasu.
Przyjrzyj siê listingowi 39. Ramkami
zaznaczy³em miejsca gdzie dodane zosta³y
nowe elementy. Wprowadzone zosta³y obie
funkcje pisz¹ce tekst, które utworzylimy
ostatnio. Dodatkowo utworzono funkcjê
wypisuj¹c¹ podan¹ liczbê. Jej dzia³anie opie-
ra siê na funkcji itoa, któr¹ opisuje odpowied-
nia ramka.
Poza tymi drobnymi funkcjami z pliku
zosta³y usuniête definicje komend, opónieñ
oraz makr pomocniczych. Zamiast tego wpro-
wadzone zosta³y odpowiednie nag³ówki.
Definicje komend oraz wszystkich funkcji,
które chcemy udostêpniæ na zewn¹trz, znajdu-
j¹ siê na listingu 40. Wpisz jego zawartoæ do
pliku lcd.h.
Interfejs I
2
C - sposób
przesy³ania danych
Podczas pisania tego fragmentu programu
podpiera³em siê dokumentacj¹ avr300 dostêp-
n¹ kiedy na stronie firmy ATMEL. Zawarto
w niej informacje o realizacji programowego
interfejsu I
2
C. Mimo ¿e dokumentacja ta znik-
nê³a ze strony producenta AVR-ów, nadal
mo¿na znaleæ j¹ za pomoc¹ wiêkszoci
wyszukiwarek internetowych. Pomocna oka-
za³a siê tak¿e dokumentacja przetwornika
PCF8591. Wszystkie te materia³y s¹ udostêp-
nione na Elportalu.
I
2
C jest dwuliniowym interfejsem synch-
ronicznym. Wykorzystywane linie to linia
danych (SDA) oraz linia zegara (SCL). Odpo-
wiednie wyjcia uk³adów powinny byæ
wyprowadzeniami typu otwarty kolektor,
podci¹ganie linii do stanu 1 (bêd¹cego sta-
nem spoczynkowym) odbywa siê za pomoc¹
zewnêtrznych rezystorów (rzêdu 4,7kΩ).
W sieci tworzonej za pomoc¹ interfejsu I
2
C
istniej¹ dwa typy urz¹dzeñ: urz¹dzenia MAS-
TER (nadrzêdne) i SLAVE (podrzêdne). To
MASTER inicjuje transmisje, wybiera uk³ad
oraz przesy³a do niego dane. SLAVE mo¿e
wysterowaæ liniê danych, tylko jeli otrzyma
odpowiednie polecenie. Tylko MASTER
mo¿e generowaæ sygna³ zegarowy.
!'
Programowanie
Elektronika dla Wszystkich
Listing 39 - plik lcd.c
Listing 40 - plik lcd.h
#ifndef LCD_H_INCLUDED
#define LCD_H_INCLUDED
// Komendy steruj¹ce
#define LCDC_CLS
0x01
#define LCDC_HOME
0x02
#define LCDC_MODE
0x04
#define LCDC_MODER
0x02
#define LCDC_MODEL
0
#define LCDC_MODEMOVE
0x01
#define LCDC_ON
0x08
#define LCDC_ONDISPLAY
0x04
#define LCDC_ONCURSOR
0x02
#define LCDC_ONBLINK
0x01
#define LCDC_SHIFT
0x10
#define LCDC_SHIFTDISP
0x08
#define LCDC_SHIFTR
0x04
#define LCDC_SHIFTL
0
#define LCDC_FUNC
0x20
#define LCDC_FUNC8b
0x10
#define LCDC_FUNC4b
0
#define LCDC_FUNC2L
0x08
#define LCDC_FUNC1L
0
#define LCDC_FUNC5x10
0x04
#define LCDC_FUNC5x7
0
#define LCDC_CGA
0x40
#define LCDC_DDA
0x80
// Deklaracje funkcji
void
lcd_command(uint8_t command);
void
lcd_data(uint8_t data);
void
lcd_cls(
void
);
void
lcd_home(
void
);
void
lcd_init(
void
);
void
lcd_str_P(prog_char* str);
void
lcd_str(
char
* str);
void
lcd_dec(
int
val);
#endif
//LCD_H_INCLUDED
Listing 38 - plik harddef.h
#ifndef HARDDEF_H_INCLUDED
#define HARDDEF_H_INCLUDED
// Interfejs I2C
#define I2C_SDAPORT D
#define I2C_SDA 6
#define I2C_SCLPORT D
#define I2C_SCL 5
#define I2C_SPEED 100000
// Definicje wyprowadzeñ wywietlacza
#define LCD_RS 2
#define LCD_RSPORT B
#define LCD_E 1
#define LCD_EPORT B
#define LCD_DPORT B
#define LCD_D4 4
//D5,D6,D7 kolejno
#endif
//HARDDEF_H_INCLUDED
Listing 37 - plik makra.h
#ifndef MAKRA_H_INCLUDED
#define MAKRA_H_INCLUDED
// Makra upraszczaj¹ce dostêp do portów
// *** Port
#define PORT(x) XPORT(x)
#define XPORT(x) (PORT##x)
// *** Pin
#define PIN(x) XPIN(x)
#define XPIN(x) (PIN##x)
// *** DDR
#define DDR(x) XDDR(x)
#define XDDR(x) (DDR##x)
// NOPek
#define NOP() {asm volatile(nop::);}
#endif
//MAKRA_H_INCLUDED
Pe³ny standard definiuje mo¿liwoæ wyst¹-
pienia w systemie kilku uk³adów typu MAS-
TER. Stworzono specjalny protokó³ umo¿li-
wiaj¹cy bezkonfliktow¹ ich wspó³pracê.
W rzeczywistych uk³adach najczêciej jednak
chcemy mieæ dostêp tylko do wybranych
uk³adów wykonawczych. Umo¿liwia to
znaczne uproszczenie algorytmu: Po pierw-
sze, znika koniecznoæ badania, czy linia jest
wolna albo czy nasza transmisja nie zosta³a
przerwana; Po drugie, mo¿emy zrezygnowaæ
z linii SCL w trybie otwartego kolektora -
tylko nasz procesor mo¿e generowaæ sygna³
zegarowy. Upraszczaj¹c nasze rozwi¹zanie,
odejdziemy jeszcze troszkê od standardu. Zre-
zygnujemy z zewnêtrznego rezystora podci¹-
gaj¹cego na korzyæ wewnêtrznego podci¹ga-
nia portów mikrokontrolera. Praktyka wyka-
zuje, ¿e takie rozwi¹zanie nie zak³óca dzia³a-
nia do³¹czonego uk³adu.
Podczas przesy³ania danych linia SDA nie
mo¿e siê zmieniaæ, gdy na linii SCL znajduje
siê stan wysoki (rysunek 28). Zmiana linii
danych podczas wysokiego stanu na wypro-
wadzeniu zegarowym ma specjalne znaczenie
i umo¿liwia generowanie tak zwanych warun-
ków START i STOP - pokazuje to rysunek
29. Stanem spoczynkowym obu linii jest stan
wysoki. Tylko miêdzy warunkami STOP
i START mo¿liwe jest utrzymanie innego ich
stanu.
Poniewa¿ na tej samej magistrali mo¿e
wspó³pracowaæ do 128 uk³adów, zaraz po roz-
poczêciu transmisji konieczne jest wys³anie
adresu uk³adu, do którego chcemy uzyskaæ
dostêp, oraz informacji, czy chcemy dokonaæ
zapisu, czy odczytu. Zaadresowany element
zg³asza swoj¹ gotowoæ poprzez wystawienie
impulsu ACK (ACKnowledge - potwierdze-
nie). Polega to na wystawieniu 0 na jeden takt
sygna³u zegarowego. Podczas transmisji to
zawsze odbiorca potwierdza odbiór danych
(tak wiêc przy odczycie jest to MASTER,
przy zapisie bêdzie to SLAVE). Po odczytaniu
ostatniej danej, przed wygenerowaniem wa-
runku STOP, nie nale¿y generowaæ potwier-
dzenia. Brak sygna³u ACK powinien powodo-
waæ przerwanie transmisji.
Rysunek 30 przedstawia sposób przesy³a-
nia potwierdzenia. Na rysunku 31 natomiast
przedstawiam wygl¹d podstawowych ramek
danych stosowanych podczas transmisji.
Rysunek ten na pierwszy rzut oka mo¿e stra-
szyæ swoim wygl¹dem. Postaram siê krótko
rozwiaæ wszelkie w¹tpliwoci. Widzimy tutaj,
¿e zgodnie z powy¿szym opisem, po warunku
START wysy³amy na magistralê siedem
bitów adresu. Ósmy bit w przesy³anym,
pierwszym bajcie musi mieæ wartoæ: 1 - jeli
chcemy odczytaæ dane; 0 - jeli chcemy doko-
naæ zapisu. Uk³ad powinien potwierdziæ pra-
wid³owy adres sygna³em ACK. Niezale¿nie
od tego, czy dokonujemy zapisu, czy odczytu,
to MASTER generuje sygna³ zegarowy. Przy
zapisie na wyprowadzenie SDA podajemy
kolejne bity danych, od najstarszego do naj-
m³odszego. Ka¿dy prawid³owo zapisany bajt
SLAVE potwierdza sygna³em ACK. Jeli
ACK siê nie pojawi, oznaczaæ to mo¿e koniec
rejestrów przeznaczonych do zapisu, zape³-
nienie pamiêci lub b³¹d transmisji. Jeli dane
chcemy odczytywaæ, na linii SDA nale¿y
utrzymywaæ stan wysoki - bêdzie ona wyste-
rowywana przez urz¹dzenie SLAVE. Wczasie
odczytu to na nas spoczywa odpowiedzial-
noæ za generowanie sygna³u ACK. Ostatnie-
go odczytanego bajtu nie potwierdzamy!
Zwykle transmisja koñczy siê warunkiem
STOP. Przyjrzyj siê jednak ostatniej ramce.
Okazuje siê, ¿e transmisja mo¿e byæ tak¿e
zakoñczona kolejnym STARTem. Umo¿li-
wia to, przede wszystkim, zmianê trybu
odczyt/zapis bez zwalniania magistrali.
Uff... Tyle jeli chodzi o sam sposób trans-
misji. Jeli jeszcze co jest dla Ciebie niejas-
ne, spróbuj przyjrzeæ siê ponownie rysunkom,
przeczytaj ponownie opis. Nie piesz siê.
Trudniejsze zagadnienia powinny staæ siê
jasne, gdy pojawi¹ siê kody ród³owe.
Szybkoæ transmisji
Standard definiuje aktualnie trzy mo¿liwe
prêdkoci transmisji: standard (100kbps), fast
(400kbps) oraz high speed (3,4Mbps). W ka¿-
dym przypadku ograniczona jest jedynie mak-
symalna prêdkoæ przesy³anych danych, tak
wiêc bezpiecznie mo¿emy korzystaæ z uk³a-
dów typu high speed, taktuj¹c transmisjê
z czêstotliwoci¹ choæby 1kHz. Jednak próba
transmisji z wiêksz¹ prêdkoci¹ ni¿ ta, do któ-
rej uk³ad zosta³ przystosowany, najpradpo-
dobniej zakoñczy siê b³êdami transmisji. Pod-
sumowuj¹c, w prostym przypadku, mogliby-
my stworzyæ procedury obs³ugi I
2
C dla
najwolniejszego przypadku i nie przejmowaæ
siê reszt¹. Jednak nie zapominajmy o tym,
¿e naszym podstawowym celem nie jest
"
Programowanie
Elektronika dla Wszystkich
Rysunek 29 - generowanie warunku
START i STOP
Rysunek 30
Rysunek 28 - transmisja jednego bitu
Rysunek 31 - ramki danych
odklepanie sprawy i zapomnienie o proble-
mie. Spróbujemy stworzyæ co bardziej uni-
wersalnego.
Powiedzmy sobie najpierw o tym, jak od
strony programu bêdzie wygl¹da³a transmisja
jednego bajtu. Sprawa jest prosta:
1. Miêdzy warunkami START i STOP,
w przerwie miêdzy bajtami, linia zegara znaj-
duje siê w stanie niskim.
2. Zmieniamy stan linii danych i odczekujemy
po³owê okresu wynikaj¹cego z szybkoci
transmisji - odczekanie pewnego czasu jest
konieczne przed wygenerowaniem narastaj¹-
cego zbocza zegara.
3. Generujemy impuls wysoki na linii zegara
o czasie trwania równym pozosta³ej nam
po³owie okresu.
4. Mo¿liwy jest natychmiastowy powrót do
punktu 2 - na magistrali nie trzeba podtrzy-
mywaæ danych.
Jak ³atwo zauwa¿yæ, konieczne bêdzie
obliczenie wystêpuj¹cego wy¿ej po³ówkowe-
go opónienia. W praktyce, aby transmisja
mog³a przebiegaæ z pe³n¹ szybkoci¹, na
potrzebê warunków START i STOP mo¿na
stworzyæ oddzieln¹ funkcjê o opónieniu
æwiartkowym, jednak w tym przyk³adzie
spowolnimy odrobinê szybkoæ dzia³ania
magistrali, uzyskuj¹c w zamian uproszczenie
kodu. Konieczne opónienie mo¿emy obli-
czyæ ze wzoru:
n = ( F_CPU - X ) / I2C_SPEED / 2
Uzyskamy wynik bezporednio w cyklach
maszynowych. F_CPU jest sta³¹ do³¹czan¹ z
poziomu pliku makefile, I2C_SPEED zdefi-
niowalimy na listingu 38. Zapytanie mo¿e
wywo³aæ X. W tym miejscu powinna pojawiæ
siê liczba cykli, jakie zajmuje procesorowi
zmiana stanu linii danych. W najprostszym
przypadku mo¿emy j¹ pomin¹æ. Wp³yw tej
wartoci jest widoczny jedynie dla prêdkoci
transmisji porównywalnych z czêstotliwoci¹
pracy mikroprocesora. W takim przypadku
nasze obliczenia i tak obarczone bêd¹ du¿ym
b³êdem - zmiana stanu linii to nie wszystko
czym musimy siê zaj¹æ.
I
2
C - funkcja opónienia
po³ówkowego
Stwórz i dodaj teraz do projektu plik i2c.c
oraz i2c.h. Plik nag³ówkowy pozostawimy
chwilowo pusty i zajmiemy siê plikiem ród-
³owym.
Na pocz¹tku do³¹czamy wszystkie
potrzebne nag³ówki - patrz listing 41.
Po tej czynnoci obliczymy niezbêdn¹
wartoæ opónienia oraz stworzymy odpo-
wiedni¹ funkcjê. Zale¿y nam na tym, aby
obliczanie opónienia oraz wybór
odpowiedniej wersji funkcji odby-
wa³ siê automatycznie. Informacje
przydatne do zrozumienia tej czê-
ci kodu znajduj¹ siê w trzech kolej-
nych ramkach: Kompilacja
warunkowa jako sposób na
zwiêkszenie
uniwersalnoci
kodu, Funkcje static i inline
oraz Generowanie w³asnych
b³êdów i ostrze¿eñ. Kod przed-
stawiam na listingu 42.
Listing ten mo¿e wygl¹daæ na
skomplikowany. Zatrzymajmy siê
na nim przez chwilê. Jeli przeczy-
ta³e wspomnian¹ na pocz¹tku
dokumentacjê (avr300), po krót-
kim przyjrzeniu kod wyda siê zna-
jomy. W istocie jest to przepisany
na C umieszczony w dokumentacji
opis generowania niezbêdnego
opónienia. Wprowadzone zosta³y
jedynie niewielkie poprawki dosto-
sowuj¹ce kod do poznanej do tej
pory czêci jêzyka C. Pierwsza
linia nie powinna budziæ w¹tpli-
woci - odpowiedni wzór pojawi³
siê w poprzednim ródtytule. Dal-
sz¹ czêæ ³atwiej jest zrozumieæ,
analizuj¹c kod od koñca. Funkcja
i2c_hdelay bêdzie wywo³ywana wszêdzie tam
w programie, gdzie potrzeba opónienia
po³ówkowego. Jest to funkcja rozwijalna w
miejscu wywo³ania. W pierwszym przypadku
(opónienie mniejsze ni¿ jeden cykl) kompi-
lator powinien wszystkie jej wywo³ania pomi-
n¹æ. Zauwa¿, ¿e reszta przypadków zosta³a
zoptymalizowana
w taki sposób, ¿e
umieszczenie w
kodzie programu
odniesienia do
funkcji spowodu-
je dodanie tylko
"#
Programowanie
Elektronika dla Wszystkich
ABC... C
Kompilacja warunkowa
jako sposób na zwiêkszenie
uniwersalnoci kodu
Na samym pocz¹tku przypominam, aby wyjaniæ
wszelkie w¹tpliwoci, ¿e wszystkie instrukcje zaczy-
naj¹ce siê znakiem hash (#) s¹ dyrektywami preproce-
sora. Niektóre z nich mog¹ w szczególnoci odnosiæ
siê do kompilatora. Jednak w ¿adnym razie nie tworz¹
one czêci programu wpisywanego w procesor. Nie
mijaj¹c siê daleko z prawd¹, mo¿na powiedzieæ, ¿e
dyrektywy te tworz¹ program dla naszego kompilato-
ra - pozwalaj¹ nam sterowaæ przebiegiem kompilacji.
#if, #elif, #else, #endif
Wymienione powy¿ej dyrektywy s¹ bardzo podobne
w swojej sk³adni do instrukcji warunkowej C. Podsta-
wowy przyk³ad wykorzystania pokazuje listing:
#if STALA<2
// Wersja 1 programu
#elif STALA<100
// Wersja 2 programu
#else
// Wersja 3 programu
#endif
Zestaw takich instrukcji umo¿liwia skompilowa-
nie ró¿nych wersji programu dla ró¿nych wartoci sta-
³ych. Mo¿na wyobraziæ sobie takie napisanie funkcji
obs³ugi wywietlacza, aby mo¿liwe by³o prze³¹czenie
za pomoc¹ jednej sta³ej miêdzy trybami sterowania
wywietlaczem podpiêtym, tak jak na naszej p³ytce,
a podpiêtym do szyny danych. Konieczne by³oby
umieszczenie miêdzy dyrektywami #if, #elif, #endif
ró¿nych wersji funkcji niskiego poziomu.
Przy tworzeniu wyra¿eñ warunkowych dla dyrek-
tyw kompilatora mo¿emy korzystaæ z wszelkich ope-
ratorów C. Mo¿emy porównywaæ, mno¿yæ, dzieliæ,
dodawaæ... Oczywistym ograniczeniem jest to, ¿e ope-
rowaæ mo¿emy tylko na wartociach sta³ych - takich
które znane s¹ w czasie kompilacji.
#ifdef, #ifndef
W pewien sposób oddzielnymi dyrektywami s¹ tytu³o-
we #ifdef, #ifndef. Mielimy okazjê ju¿ dzisiaj z nich
korzystaæ. Okazuje siê, ¿e w instrukcjach kompilacji
warunkowej mamy mo¿liwoæ sprawdzenia, czy dana
sta³a zosta³a zdefiniowana, podjêcia w zale¿noci od
tego odpowiednich sta³ych. Definicja w stylu:
#define NIC
jest jak najbardziej prawid³owa (wszystkie wyst¹pie-
nia identyfikatora NIC w kodzie zostan¹ zamienione...
na nic - zostan¹ po prostu usuniête). Pamiêtaj o tym -
takie dzia³anie czasami siê przydaje.
Jednak to, na co chcê teraz zwróciæ uwagê, to fakt,
¿e dokonanie takiej (oraz ka¿dej innej) definicji sta³ej
mo¿e byæ wykryte za pomoc¹ instrukcji #ifdef i #ifndef.
Przy tworzeniu wyra¿eñ mo¿na korzystaæ ze s³owa
kluczowego defined. Zwraca ono TRUE, jeli dana
sta³a jest ju¿ zdefiniowana. Tak wiêc #ifdef i #ifndef
mog¹ byæ zast¹pione przez, odpowiednio:
#if defined NIC
#if !defined NIC
S³owa defined mo¿na u¿ywaæ tak¿e w z³o¿onych
warunkach.
Jak zwykle - wszystko bêdzie stawaæ siê coraz ja-
niejsza przy wprowadzaniu tego w ¿ycie.
Listing 42 - opónienie po³ówkowe w i2c.c
#define I2C_nhalf (F_CPU/I2C_SPEED/2)
// Funkcja d³u¿szych opónieñ
#if I2C_nhalf < 3
// Nic
#elif I2C_nhalf < 8
static void
i2c_xdelay(
void
)
{
NOP();
}
#else
#define I2C_delayloops (1+(I2C_nhalf-8)/3)
#if I2C_delayloops > 255
#error Przyspiesz - bo sie nie wyrabiam ;)
#endif
static void
i2c_xdelay(
void
)
{
asm volatile
( \
delayus8_loop%=: \n\t
\
dec %[ticks] \n\t
\
brne delayus8_loop%= \n\t
\
: :[ticks]
r
(I2C_delayloops) );
}
#endif
//I2C_nhalf >= 3
// Opónienia dla I2C
static inline void
i2c_hdelay(
void
)
{
#if I2C_nhalf < 1
return
;
#elif I2C_nhalf < 2
NOP();
#elif I2C_nhalf < 3
asm volatile
(
rjmp exit%=\n\t
exit%=:\n\t
::);
#else
i2c_xdelay();
#endif
}
Listing 41 - nag³ówki w i2c.c
#include <avr\io.h>
#include <inttypes.h>
#include harddef.h
#include makra.h
#include i2c.h
jednego s³owa programu: dla kolejnych przy-
padków bêdzie to kolejno: nop, rjmp, rcall.
Dla opónieñ z zakresu 3-8 cykli wywo³ywa-
na jest funkcja zawieraj¹ca tylko jedn¹
instrukcjê: nop. Wywo³anie podprogramu,
nop, powrót z podprogramu - daje to 5 cykli.
Rozwi¹zanie nie jest super dok³adne, ale te¿
super dok³adnoci nie potrzebujemy. Zwra-
cam uwagê na to, ¿e instrukcja NOP() jest
konieczna - inaczej kompilator usunie ca³¹
funkcjê i jej wywo³ania. Dla opónieñ d³u¿-
szych tworzona jest znana nam ju¿ pêtla - ten
fragment nie powinien sprawiaæ problemów.
Nowoci¹ w tym przypadku jest jedynie
sprawdzenie, czy iloæ powtórzeñ pêtli mo¿e
byæ wykonana. Jeli nie - zg³aszana jest infor-
macja o b³êdzie.
Kod nie jest taki straszny. Przyjrzyj siê
zastosowaniu instrukcji warunkowych - z ele-
mentów tych bêdziemy korzystaæ coraz czê-
ciej, tworz¹c coraz bardziej uniwersalne
modu³y.
I
2
C - reszta funkcji
Na listingu 43 mo¿esz zobaczyæ funkcje
zmieniaj¹ce odpowiednio fizyczne wyprowa-
dzenia portów. Moglibymy siê bez nich
obejæ - wpisuj¹c w programie bezporednio
instrukcje odwo³uj¹ce siê do portów. By³oby
to niewygodne przede wszystkim ze wzglêdu
na liniê SDA - decyduj¹c siê na wewnêtrzne
podci¹ganie musimy siê liczyæ z konieczno-
ci¹ bardziej skomplikowanego sterowania
portem - dla stanu wysokiego konieczne jest
ustawienie portu jako wejcia i w³¹czenia
podci¹gania, dla stanu niskiego wy³¹cze-
nie podci¹gania i prze³¹czenie portu
w tryb wyjcia. Wykonanie analogicznych
funkcji dla linii zegara zwiêksza czytel-
noæ kodu.
Moglibymy w tym miejscu pos³u¿yæ
siê tak¿e makrami - tak jak robilimy to
w poprzednich czêciach. Du¿o zale¿y od
preferencji. Mnie funkcje w tym miejscu
wydaj¹ siê bardziej przyjazne.
Serce obs³ugi I
2
C znajduje siê na listin-
gu 44. Przyjrzyj siê, w jaki sposób genero-
wane s¹ warunki START i STOP. W razie
potrzeby ps³u¿ siê rysunkami 28-31.
Generacja warunku start zosta³a troszkê
skomplikowana ze wzglêdu na mo¿liwoæ
jego wywo³ania tak¿e bez koñczenia trans-
misji. Konieczne jest uwzglêdnienie faktu,
¿e funkcja wysy³aj¹ca i odbieraj¹ca bajt
zostawia zawsze liniê SCL w stanie nis-
kim, jednak stan linii SDA nie jest okre-
lony.
Poród naszych funkcji nie ma takich,
które obs³ugiwa³yby kompletne ramki,
takie jak na rysunku 31. W najprostszym
przypadku nie s¹ one potrzebne - wys³anie
dowolnej ramki mo¿e byæ zrealizowane
przez wy¿sz¹ czêæ programu za pomoc¹
czterech tylko funkcji z listingu 44.
Modu³ i2c zamyka wpisanie do pliku
nag³ówkowego zawartoci listingu 45.
"$
Programowanie
Elektronika dla Wszystkich
i
{
}
Listing 43 - operacje na portach w i2c.c
// Ustawienie i zerowanie wyjcia
static inline void
i2c_sdaset(
void
)
{
DDR(I2C_SDAPORT) &= ~(
1
<<I2C_SDA);
PORT(I2C_SDAPORT) |=
1
<<I2C_SDA;
}
static inline void
i2c_sdaclear(
void
)
{
PORT(I2C_SDAPORT) &= ~(
1
<<I2C_SDA);
DDR(I2C_SDAPORT) |=
1
<<I2C_SDA;
}
// Pobieranie danej z wyprowadzenia portu
static inline
uint8_t i2c_sdaget(
void
)
{
return
PIN(I2C_SDAPORT) & (
1
<<I2C_SDA);
}
// Zerowanie i ustawianie zegara
static inline void
i2c_sclset(
void
)
{
PORT(I2C_SCLPORT) |=
1
<<I2C_SCL;
}
static inline void
i2c_sclclear(
void
)
{
PORT(I2C_SCLPORT) &= ~(
1
<<I2C_SCL);
}
ABC... C
Generowanie w³asnych b³êdów i ostrze¿eñ
Gdy kompilator natknie siê na dyrektywê #error,
zakoñczy swoj¹ pracê wywietlaj¹c opis b³êdu który
podajemy zaraz za dyrektyw¹.
Aby wyjaniæ sposób oraz ideê tworzenia w³as-
nych informacji o b³êdzie pos³u¿my siê troszkê
uproszczon¹ czêci¹ kodu z listingu 42. Posiadamy
tutaj funkcjê opónienia, która dzia³a prawid³owo
dla liczby o wartoci najwy¿ej 255, próba podania
wartoci wiêkszej spowoduje zignorowanie najstar-
szych bitów, o czym nie zostaniemy nawet poinfor-
mowani (jest to jedna z przykroci wstawek asem-
blerowych - kompilator ich nie pilnuje). Jeli opó-
nienie jest wartoci¹ sta³¹, mo¿emy za pomoc¹ pros-
tej konstrukcji z kompilacj¹ warunkow¹ (poprzednia
ramka) sprawdziæ czy zakres nie zosta³ przekroczo-
ny:
#define opoznienie 256
#if opoznienie > 255
#error
za duze opoznienie!
#endif
Oczywicie przedstawiony przyk³ad jest banalny -
zawsze uzyskamy informacjê o b³êdzie wygl¹daj¹c¹
mniej wiêcej jak poni¿ej:
i2c.c:21:3: #error za duze opoznienie!
Klikniêcie na powy¿szy tekst w okienku Output
spowoduje przeniesie kursora na pozycje, gdzie poja-
wi³a siê dyrektywa #error. Przytoczony przyk³ad,
oczywicie w praktyce, nie ma wiêkszego sensu. Jed-
nak, ju¿ jeli wartoæ sta³ej opoznienie bêdzie wyni-
kiem jaki obliczeñ, przeprowadzenie stosownego
sprawdzenia jest ca³kowicie uzasadnione.
Wa¿ne jest to, aby zrozumieæ, ¿e kompilator zg³o-
si b³¹d zawsze gdy natknie siê na dyrektywê #error. W
praktyce wiêc u¿ywamy jej ³¹cznie z dyrektywami
kompilacji warunkowej.
Jeli rozumiesz ju¿ ideê komendy #error, nie
bêdziesz mia³ ¿adnych problemów z komend¹ #war-
ning. Jej dzia³anie jest bardzo zbli¿one. Ró¿ni siê
jedynie tym, ¿e po zg³oszeniu ostrze¿enia kompilacja
jest kontynuowana. Mo¿esz postanowiæ, ¿e b³¹d prze-
pe³nienia zakresu naszego opónienia nie jest tak
wa¿ny aby zatrzymywaæ kompilacjê i pozostawiæ
jedynie generowanie ostrze¿enia. Nic prostszego:
wystarczy zamieniæ s³owo error na warning.
Listing 44 - serce i2c.c
void
i2c_start(
void
)
{
// Jeli start bez stop
i2c_sdaset();
i2c_hdelay();
i2c_sclset();
i2c_hdelay();
// Normalna sekwencja startu
i2c_sdaclear();
i2c_hdelay();
i2c_sclclear();
}
void
i2c_stop(
void
)
{
i2c_sdaclear();
i2c_hdelay();
i2c_sclset();
i2c_hdelay();
i2c_sdaset();
i2c_hdelay();
}
uint8_t i2c_send(uint8_t data)
{
uint8_t n;
for
(n=
8
; n>
0
; n)
{
if
(data &
0x80
)
i2c_sdaset();
else
i2c_sdaclear();
data <<=
1
;
i2c_hdelay();
i2c_sclset();
i2c_hdelay();
i2c_sclclear();
}
// ack
i2c_sdaset();
i2c_hdelay();
i2c_sclset();
i2c_hdelay();
n = i2c_sdaget();
i2c_sclclear();
return
n;
}
uint8_t i2c_get(uint8_t ack)
{
uint8_t n, temp=
0
;
i2c_sdaset();
for
(n=
8
; n>
0
; n)
{
i2c_hdelay();
i2c_sclset();
i2c_hdelay();
temp<<=
1
;
if
(i2c_sdaget())
temp++;
i2c_sclclear();
}
// ack
if
(ack == I2C_ACK)
i2c_sdaclear();
else
i2c_sdaset();
i2c_hdelay();
i2c_sclset();
i2c_hdelay();
i2c_sclclear();
return
temp;
}
Listing 45 - plik i2c.h
#ifndef I2C_H_INCLUDED
#define I2C_H_INCLUDED
#define I2C_ACK 1
#define I2C_NACK 0
void
i2c_start(
void
);
void
i2c_stop(
void
);
uint8_t i2c_send(uint8_t data);
uint8_t i2c_get(uint8_t ack);
#endif
//I2C_H_INCLUDED
Plik g³ówny
Na tym etapie pracy mamy gotowe wszystkie
potrzebne nam modu³y. Pozostaje po³¹czyæ je
w dzia³aj¹cy program. Poniewa¿ zale¿y nam
na szybkim uruchomieniu programu, teraz
wszystkie niezbêdne funkcje wywo³amy z po-
ziomu funkcji main - tylko po to, aby wresz-
cie co siê zaczê³o dziaæ. Utwórz plik main.c.
Powinien pojawiæ siê listing 46. Wiêksza
czêæ zawartego tutaj kodu powinna byæ ju¿ Ci
znana. Nowoci¹ jest jedynie wysy³anie oraz
odbiór danych z uk³adu przetwornika. Na
rysunkach 32 i 33 zamieci³em fragmenty
wyjête z dokumentacji firmy Philips. Pokazuj¹
one, jak powinna przebiegaæ komunikacja
z naszym uk³adem. Jak widaæ na rysunku 32,
zgodnie z opisanymi wczeniej ramkami
transmisji, po podaniu warunku start,
konieczne jest zaadresowanie uk³adu. Adres
PCF8591 sk³ada siê z czterech starszych
bitów ustawionych na sta³e (1001b = 9h) oraz
trzech m³odszych, które mo¿emy wybraæ
poprzez odpowiednie zwarcie wyprowadzeñ
adresowych. P³ytka AVT3500 ustawia na
sta³e w tym miejscu 0. Najm³odszy bit s³owa
adresowego s³u¿y do ustalenia czy odbywa
siê zapis, czy odczyt. Przy zapisie, w pierw-
szej kolejnoci wysy³any jest bajt konfigura-
cyjny. Jego znaczenie pokazuje rysunek 33.
Przyjrzyjmy siê programowi. Po starcie
transmisji wysy³amy bajt 90h - adres uk³adu,
zapis. Nastêpnie bajt konfiguracyjny (0)
- wy³¹czone wyjcie przetwornika CA, kon-
figuracja wejæ na cztery niezale¿ne obwody,
wy³¹czone automatyczne prze³¹czanie kana-
³ów dla ka¿dego odczytu, kana³ 0.
W pêtli g³ównej w³¹czamy transmisjê,
pobieramy bajt danych i wy³¹czamy trans-
misjê. Usuniêcie z pêtli warunku STOP nie
spowoduje b³êdnego dzia³ania uk³adu. Pó-
niej, gdy uda nam siê skompilowaæ program,
spróbuj przenieæ inicjacjê odczytu poza pêtlê
a w pêtli pozostawiæ sam¹ instrukcjê odczytu
danych. Zgodnie z rysunkiem 32 takie dzia-
³anie jest mo¿liwe.
Wywietlanie danych wykorzystuje utwo-
rzon¹ dzi funkcjê lcd_dec. Zauwa¿, ¿e zaraz
po niej wpisywane s¹ dodatkowo dwie spacje.
Poprawia to czytelnoæ wyniku, jeli wcze-
niej wywietlana by³a liczba o wiêkszej iloci
cyfr - w takim przypadku na wywietlaczu
pozostan¹ stare cyfry i trzeba je usun¹æ.
Przedstawiony sposób jest najprostszym
z mo¿liwych i nie wszêdzie mo¿e byæ zasto-
sowany. Dobrze siê spisuje jeli nie zamierza-
my pisaæ nic wiêcej w danej linii.
W pliku main.c napisalimy prociutki
program, którego zadaniem jest wywietlanie
danej odpowiadaj¹cej napiêciu podanemu na
wejcie I0 naszej p³ytki. Zanim przejdziemy
dalej, pod³¹cz na to wejcie ród³o regulowa-
nego napiêcia. Mo¿e byæ to zasilacz albo -
najwygodniej - zwyk³y potencjometr (1-100k)
z przeciwleg³ymi wyprowadzeniami wpiêtymi
miêdzy masê i zasilanie a rodkowym do wej-
cia I0.
Konfiguracja makefile
Gdyby chcia³ skompilowaæ teraz program,
czeka Ciê przykra niespodzianka - w tej chwi-
li nie jest to jeszcze mo¿liwe. Jeli
zmieni³e, tak jak zwykle, pole
TARGET pliku makefile, otrzymasz
komunikat w stylu:
make.exe: *** No rule to make
target `PCF8591.o, needed by
`PCF8591.elf. Stop.
Nic dziwnego - nie istnieje prze-
cie¿ plik PCF8591.c. Skompilowa-
nie programu sk³adaj¹cego siê z
wiêkszej liczby plików wymagaæ
bêdzie niewielkiej modyfikacji
"%
Programowanie
Elektronika dla Wszystkich
ABC... C
Funkcje static i inline
Funkcja statyczna
Umieszczenie przed nazw¹ funkcji s³owa kluczowego
static powoduje utworzenie tak zwanej funkcji sta-
tycznej. Nie ma to wiele wspólnego z poznanymi ju¿
statycznymi zmiennymi lokalnymi. Tworz¹c funkcjê
statyczn¹, informujemy kompilator, ¿e bêdziemy
korzystaæ z niej tylko w bie¿¹cym module - co prak-
tycznie oznacza bie¿¹cy plik ród³owy. Informacja
o danej funkcji nie zostanie umieszczona przez kom-
pilator w pliku obiektowym (*.o), przez co linkier nie
bêdzie mia³ do takiej funkcji dostêpu. Sam kompilator
uzyskuje za to mo¿liwoæ lepszej optymalizacji kodu.
W skrajnym przypadku, jeli funkcja nie zostanie
wykorzystana, nie znajdzie siê ona w kodzie wyniko-
wym programu (jednak zostanie w tym przypadku
wygenerowane ostrze¿enie).
Funkcji static u¿yjemy wszêdzie tam, gdzie pew-
nych elementów modu³u nie chcemy udostêpniaæ na
zewn¹trz. W naszym module wywietlacza mo¿na by
zastosowaæ to do wszystkich funkcji niskiego pozio-
mu.
Funkcja rozwijana w miejscu wywo³ania
Pod t¹ niezwykle brzmi¹c¹ nazw¹ kryje siê mecha-
nizm zbli¿ony do makr. Ma on jednak kilka zalet.
Przede wszystkim, wygodniej siê pisze funkcje ni¿
makro. Drug¹ spraw¹ jest to, ¿e kompilator mo¿e zba-
daæ, czy bardziej optymalne jest rozwiniêcie funkcji
na wzór makra, czy te¿ jej zwyk³e wywo³anie.
Dodatkowo jeli tworzymy funkcjê, kompilator pil-
nuje, czy nasze argumenty s¹ prawid³owe i jeli to
konieczne, dokonuje przekszta³cenia ich typów. Zauwa¿-
my, ¿e z jednej strony zmniejsza to mo¿liwoæ pope³nie-
nia b³êdu, z drugiej jednak sprawia, ¿e w niektórych
zastosowaniach sprawdzaj¹ siê tylko makra. Wemy jako
przyk³ad makro obliczaj¹ce iloæ elementów w tablicy:
#define ELEMS(p)\
(sizeof(p)/sizeof(p[0]))
Twór taki zupe³nie nie ma sensu, jeli p mia³oby
okrelony typ - tak jak ma to miejsce w funkcji. Nale-
¿y zdawaæ sobie sprawê tak¿e, ¿e nawet tak banalne
makro, jak obliczenie sumy dwóch argumentów:
#define DODAJ(a, b) (a+b)
zachowa siê inaczej jako funkcja inline: makro operu-
je na argumentach o oryginalnych typach; Funkcja
inline przekszta³ci je najpierw do typu identycznego
ze swoimi argumentami.
static inline - czemu?
Jeli chcesz zastosowaæ funkcjê inline, spotka Ciê
jeszcze jedna niespodzianka. Kompilator musi za³o-
¿yæ, ¿e poza jej wykorzystaniem w bie¿¹cym pliku,
mog¹ pojawiæ siê zewnêtrzne do niej odwo³ania. Jeli nie
zaznaczysz funkcji inline jako statycznej, oprócz tego, ¿e
jej kod bêdzie rozwiniêty w ka¿dym miejscu wywo³ania,
dodatkowo zostanie w pamiêci utworzona jej zwyk-
³a wersja. W naszym przypadku by³oby to marnowa-
nie zasobów - s³ówko static rozwi¹zuje ten problem.
Dziêki optymalizacji kodu, funkcja static inline
sk³adaj¹ca siê tylko z rozkazu powrotu (return) nie
zostanie w ogóle umieszczona w kodzie.
Listing 46 - plik main.c
#include <avr\io.h>
#include <stdio.h>
#include <inttypes.h>
#include <avr\pgmspace.h>
#include harddef.h
#include delay.h
#include makra.h
#include i2c.h
#include lcd.h
int
main(
void
)
{
// Inicjacja
PORTD =
1
<<I2C_SDA |
1
<<I2C_SCL;
DDRD =
1
<<I2C_SCL;
DDRB =
1
<<LCD_E |
1
<<LCD_RS |
0x0F
<<LCD_D4;
lcd_init();
// Koniec inicjacji
// Tekst informacyjny w górnej linii
lcd_str_P((prog_char*)PSTR(
Dane z I0:
));
lcd_command(LCDC_DDA |
64
);
// Konfiguracja przetwornika
i2c_start();
// bajt adresowy, zapis
i2c_send(
0x90
);
// bajt kontrolny
i2c_send(
0x00
);
i2c_stop();
// Pobieranie danych
for
(;;)
{
lcd_command(LCDC_DDA |
64
);
// Odczyt danych
i2c_start();
// bajt adresowy, odczyt
i2c_send(
0x91
);
// Pobranie i wywietlenie danej
lcd_dec(i2c_get(I2C_NACK));
// Przys³oniêcie reszty napisu
lcd_str_P((prog_char*)PSTR(
));
i2c_stop();
}
return
0
;
}
Rysunek 32 - sposób komunikacji z uk³adem
PCF8591 - wed³ug dokumentacji
dodatkowego pola. Zajrzyj do ostatniej ju¿
dzisiaj ramki ABC... C. Znajdziesz tam
dok³adny opis, co nale¿y zrobiæ, aby program
zacz¹³ dzia³aæ.
Porz¹dkowanie projektu
Jeli zgodnie z opisem dodawa³e kolejno
wszystkie pliki do projektu, panel po lewej
stronie okna Programmers Notepada bêdzie
coraz mniej czytelny. Jak podoba Ci siê spo-
sób organizacji widocznych w nim wpisów
przedstawiony na rysunku 34? Zapewniam
Ciê, ¿e znakomicie uprzyjemnia to pracê.
Mo¿esz tworzyæ katalogi w okienku projek-
tu, klikaj¹c prawym klawiszem na nazwie,
jaka zosta³a mu nadana. Wybierz Add New
Folder. Pliki mo¿esz przenosiæ przez ich
przeci¹gniêcie na ikonkê wybranego katalo-
gu. Aby przenieæ plik z wnêtrza katalogu na
zewn¹trz, przenie jego nazwê na ikonê pro-
jektu. Tworzony katalog i wykonane przeno-
szenie nie ma nic wspól-
nego z katalogami na
dysku. Umo¿liwia jedy-
nie wirtualne zorganizo-
wanie plików w obrêbie
projektu. Fizycznie pliki
pozostan¹ na swoich
miejscach.
Rzeczywiste odwzo-
rowanie struktury katalo-
gów miêdzy dyskiem a
oknem projektu umo¿li-
wia Magic Folder. Nie
polecam Ci jednak jego
stosowania - wymaga³o-
by to dodatkowej inge-
rencji w plik makefile.
Podsumowanie
Jeli uda³o Ci siê napisaæ na podstawie tego
tekstu program ca³kowicie samodzielnie
- bardzo mnie to cieszy. Jeli jednak program
nie chce siê uruchomiæ - zawsze mo¿esz siêg-
n¹æ do plików udostêpnionych na stronie
internetowej Elportalu. Zdajê sobie sprawê
z tego, ¿e dla niektórych takie programowanie
wyda siê niepotrzebnie skomplikowane.
Celowo nie wprowadza³em go na pocz¹tku.
Proste programy mo¿esz pisaæ w jednym
pliku. Jednak im program wiêkszy, tym sen-
sowniejsze staje siê jego dzielenie. Znacznie
³atwiej zapanowaæ nad dwudziestoma plikami
po tysi¹c linii kodu ka¿dy ni¿ nad jednym pli-
kiem z dwudziestoma tysi¹cami linii kodu
(przyk³ad z ¿ycia wziêty).
Od dzisiaj bêdziemy dzieliæ program na
czêci wszêdzie tam, gdzie ma to sens. Mylê,
¿e o ile nie jeste jeszcze przekonany, za-
czniesz dostrzegaæ wkrótce coraz wiêcej zalet
takiego programowania. Spróbuj powróciæ do
programu za dwa dni i zobacz, czy jeszcze
pamiêtasz, gdzie znajduj¹ siê poszczególne
elementy kodu. Zajrzyj do programów, które
pisalimy poprzednio w pojedynczych pli-
kach.
Propozycje zmian
Nasz program mimo tego, ¿e jest coraz bar-
dziej rozwiniêty, temat g³ównego bohatera
dzisiejszego æwiczenia traktuje doæ skróto-
wo. Jeli chcesz poæwiczyæ swoj¹ znajomoæ
C, proponujê Ci rozwiniêcie tej czêci progra-
mu.
1. Podana w dokumentacji szybkoæ transmi-
sji uk³adu PCF8591 to 100kbps. Sprawd co
siê stanie przy wy¿szych czêstotliwociach.
2. Zauwa¿, ¿e w programie nie sprawdzamy,
czy uk³ad odpowiedzia³ zg³oszeniem ACK.
Do badania poprawnoci transmisji wyko-
rzystaj fakt, ¿e funkcja i2c_get zwraca war-
toæ 0 tylko, jeli potwierdzenie zosta³o ode-
brane.
3. Dopisz dwie funkcje do modu³u i2c, które
zajm¹ siê transmisj¹ pe³nych ramek.
Podpowied: wykorzystaj istniej¹ce funkcje
i2c_send oraz i2c_get. Nowe funkcje powin-
ny przyjmowaæ adres, pod jaki dokonaæ zapi-
su/odczytu, wskanik na tablicê, do której
umieciæ/z której pobraæ dane oraz informacjê
o iloci danych do odczytu/zapisu. Pamiêtaj,
aby nie generowaæ potwierdzenia po ostatnim
odczytanym bajcie.
4. Spróbuj uruchomiæ przetwornik cyfrowo-
-analogowy. W tym celu konieczne jest prze-
s³anie kolejno danych 90h, 40h oraz danej,
która zostanie zapisana w rejestrze przetwor-
nika.
5. Mo¿e uda Ci siê napisaæ ca³y, nowy modu³
zajmuj¹cy siê transmisj¹ specjalnie do i z na-
szego przetwornika? Pamiêtaj, ¿e w takim
przypadku musisz zapamiêtaæ gdzie bajt
konfiguracji, tak aby by³o mo¿liwe jego
potwierdzanie przy ka¿dej zmianie wartoci
wpisanej do przetwornika cyfrowo-analogo-
wego. A mo¿e funkcja zapisu wymaga³aby
podania konfiguracji przy wywo³aniu?
Rados³aw Koppel
radoslaw.koppel@edw.com.pl
"&
Programowanie
Elektronika dla Wszystkich
ABC... C
Przygotowanie pliku makefile
do kompilacji wielu plików ród³owych
Potrzebna nam modyfikacja jest bardzo prosta.
W przysz³oci powiniene wprowadzaæ j¹ jednocze-
nie z dodawaniem nowego pliku ród³owego do pro-
jektu... ale do rzeczy.
Otwórz w Programmers Notepadzie plik makefile
naszego projektu. Zaraz poni¿ej pola TARGET powi-
niene znaleæ pole SRC. Na pocz¹tku powinno to
wygl¹daæ jak
w ramce obok:
Pole SRC
powinno za-
wieraæ pliki
ród³owe pro-
jektu oddzielone spacjami. Domylnie w tym miejscu
pojawia siê nazwa projektu z rozszerzeniem *.c. Ma to
sens przy pisaniu ca³ego programu w jednym pliku
ród³owym. Jednak w wiêkszych projektach staraj siê
raczej nie nadawaæ plikom róde³ nazw identycznych
z projektem - nie jest to b³¹d, ale mo¿e utrudniæ orien-
tacjê.
Kasujemy wiêc fragment ostatniej linii i zmienia-
my jej postaæ na nastêpuj¹c¹:
SRC
= main.c
SRC +
= i2c.c \
lcd.c \
delay.c
Po znakach ukoników musi znaleæ siê znak
koñca linii. Wprowadzenie tutaj spacji spowoduje b³¹d
podczas kompilacji.
Ca³oæ móg³by zapisaæ tak¿e w jednej linii.
Mo¿na te¿ przepisaæ wszystkie pliki od razu, bez
wykorzystywania operatora jak w drugiej linii. Jest to
kwestia gustu: przywyk³em do umieszczania na
pocz¹tku pliku g³ównego oraz wyranego zaznacza-
nia, ¿e reszta plików zosta³a dodana. Kolejnoæ wpi-
sania plików nie ma znaczenia. Jeli gdziekolwiek
znajduje siê funkcja, od której program ma zacz¹æ -
linkier j¹ odnajdzie.
W tej chwili jeste gotowy do kompilacji pro-
gramu.
Uwa¿aj, nawet jeli Twój system operacyjny nie
rozró¿nia wielkoci znaków, dla WinAVR ma to zna-
czenie. Wpisz nazwy plików identyczne z tymi, jakie
zosta³y im nadane.
Rysunek 34
- porz¹dek
w projekcie
Rysunek 33 - konfiguracja PCF8591