kursC_czesc016.pdf
(
1204 KB
)
Pobierz
215235030 UNPDF
Programowanie
P r o g r a m o w a n i e
p r o c e s o r ó w
w
j
ę
ę
ęz y k u
C
część 16
Dziś czeka nas dużo pracy połączonej z dobrą
zabawą. Poznaliśmy już pamięć programu
procesora oraz prostszą część zagadnienia
umieszczania zmiennych w pamięci danych.
Dziś zajmiemy się zewnętrzną przestrzenią
adresową, wspomnimy o działaniu pamięci
EEPROM zawartej w mikrokontrolerze AVR,
a na końcu zapoznamy się z interesującym za-
gadnieniem dynamicznego przydzielania pa-
mięci. Przy okazji przedstawię kilka nowych
„sztuczek” dotyczących programowania w C
jako takim i GCC w szczególności, jednak ze
względu na rozmiary kodów przedstawiam
przede wszystkim fragmenty ciekawe
oraz te, które dotyczą bezpośrednio za-
gadnienia. Reszta zostanie omówiona
ogólnie, tak aby samodzielne ich napi-
sanie było możliwe, jednak nie wystar-
czy już tylko przepisanie podanych li-
stingów, aby całość zaczęła działać.
Pełne kody, jak zwykle, dostępne są w
Elportalu.
12 bitów na każdy piksel. Ma to dwie ogrom-
ne zalety: możemy dowolnie ustawić, pod
którym indeksem kryje się jaki kolor oraz da-
je nam możliwość wybrania dowolnych 256
kolorów spośród 4096. Ma także wady: trans-
misja jest wolniejsza niż w trybie 8 bitów,
a sama paleta zajmie dodatkowe 512B pamię-
ci. Jak się okaże, w świetle naszego pierwsze-
go programu, zalety przeważają.
Listing 202
pokazuje dodany bufor wy-
świetlacza oraz paletę kolorów. Paleta zostaje
w pamięci wewnętrznej, będzie przez to ob-
sługiwana szybciej.
Dodajemy teraz prostą funkcję inicjacji pa-
lety na podstawie danych z pamięci progra-
mu. Pokazuje ją
listing 203.
Potrzebna nam
będzie także funkcja czyszczenia wyświetla-
cza – skorzystamy z funkcji
memset –
iden-
tycznie robiliśmy to już w przypadku po-
przedniego wyświetlacza.
Bardziej rozbudowana będzie funkcja od-
świeżania wyświetlacza. Przedstawiam ją na
listingu 204.
W jednym przebiegu pętli prze-
syłamy dwa piksele. Jest to związane z tym,
Listing 202 Zmienne w module wyświetlacza
lcd_pixel lcd_buffer
[
LCD_SX
*
LCD_SY
]
EXMEM
;
uint16_t lcd_rgb
[
256
];
// XXXXRRRRGGGGBBBB
Listing 200 Dodanie sekcji. exram w pliku makefile
LDFLAGS +
= -Wl,--section-start=.exram=0x800500
Listing 203 Inicjacja palety
void
lcd_loadRGB_P
(
const
prog_uint16_t
*
pRGB
,
uint16_t size
)
Listing 201 Makro upraszczające dostęp do. exram
#define EXMEM __attribute__ ((section (".exram")))
{
memcpy_P
(
lcd_rgb
,
pRGB
,
size
);
}
Pamięć zewnętrzna
w sekcji .exram
Zanim przejdziesz dalej, polecam przeczytanie
małej ramki na tej stronie, dotyczącej opcji
linkera oraz dwóch dużych omawiających
pamięć zewnętrzną od strony technicznej oraz
jej obsługi przez AVR-GCC. Po ich przeczyta-
niu wszystko, co będziemy robić dalej, powin-
no być zrozumiałe.
Nasz program powstaje na bazie napisane-
go w poprzedniej części sterownika wyświe-
tlacza. Rozwiniemy go o potrzebną nam funk-
cjonalność.
Pierwsze, co zrobimy, to dodamy sekcję
pamięci, zawierającą zmienne w pamięci ze-
wnętrznej. Odpowiednią opcję pokazuje
li-
sting 200.
Następnie w naszym pliku
makra.h
dodamy nową definicję, widoczną na
listingu
201.
Dzięki temu tworzenie zmiennej w pa-
mięci zewnętrznej będzie bardziej intuicyjne.
Teraz jesteśmy gotowi do dodawania ele-
mentów do modułu lcd3310i. Wyświetlacz
będziemy obsługiwać troszkę nietypowo.
Tworzony bufor wyświetlacza będzie składał
się z elementów ośmiobitowych. Ponieważ
8 bitów jest naturalną wielkością zmiennej dla
AVR-a, będziemy mogli na takim buforze
dość szybko wykonywać obliczenia. Jednak
wprowadzimy dodatkową tablicę, która bę-
dzie zawierała naszą paletę kolorów. Wartość
w buforze wyświetlacza będzie indeksem dla
naszej palety. Do wyświetlacza prześlemy
ABC... GCC
Opcja linkera, opcja kompilatora...
co, gdzie i jak
ABC... C
Wartość pseudolosowa
– funkcja rand i srand
Dziś będę często posługiwał się określeniami „zmień
opcje linkera, dodaj opcje kompilatora”. Aby nie opisy-
wać za każdym razem, gdzie odpowiednich opcji szu-
kać, już na wstępie wyjaśniam zagadnienie:
Wszystkie opcje znajdują się w pliku
makefile
.
W oryginalnym pliku opcje kompilatora zaczynają się
w okolicy 114 linii, natomiast opcje linkera w linii 184.
Wszystkie opcje kompilatora zebrane są w zmiennej
CFLAGS, natomiast opcje linkera w zmiennej
LDFLAGS. Przykładowy fragment ciągu opcji kompila-
tora pokazuje listing niżej:
CFLAGS
= -g
$(DEBUG)
CFLAGS +
=
$(CDEFS) $(CINCS)
CFLAGS +
= -O
$(OPT)
(...)
Dodanie opcji polega na dopisaniu linii na końcu:
CFLAGS +
=
opcja1 opcja2
Troszkę inaczej ma się sprawa z opcjami przezna-
czonymi dla linkera. Opcje zapisujemy jak niżej:
LDLAGS +
= -Wl,
opcja1
,
opcja2
Ciąg opcji rozpoczynamy przez „-Wl,” co informu-
je, że występujące dalej opcje przeznaczone są dla linke-
ra. Poszczególne opcje oddzielamy przecinkami albo
przed każdą stawiamy ponownie ciąg “-Wl,”. Zauważ,
że po ostatniej opcji nie ma przecinka! Opcje są interpre-
towane jako kolejne parametry linkera tak długo, jak od-
dzielane są one przecinkami.
Istnieje analogiczna komenda: „-Wa,”, informująca,
że dalsze opcje przeznaczone są dla kompilatora asem-
blera.
Możesz skorzystać z narzędzia „znajdź”, aby zoba-
czyć, gdzie nasze zmienne CFLAGS oraz LDFLAGS są
później używane.
W bibliotece
stdlib
znajdują się, między innymi, funkcje
służące do generowania liczb pseudolosowych. Liczba
jest generowana na podstawie zarodka. Po starcie pro-
gramu ma on wartość 1. Zmieniamy go za pomocą funk-
cji
srand
. Każde wywołanie funkcji
rand
spowoduje wy-
generowanie kolejnej liczby oraz ustawienie nowego za-
rodka.
Wynik funkcji
rand
to liczba 0-RAND_MAX, przy
czym standard ANSI-C określa, że minimalna wartość
RAND_MAX to 32767 (0x7fff) i taka wartość jest też
używana w AVR-GCC. Aby wylosować liczbę w zakre-
sie a <= x < b, stosujemy zapis:
rand () % (b-a) + a
Wartość (b-a) powinna być dużo mniejsza od
RAND_MAX.
W normie ANSI podane są
zalecane
równania do
generowania kolejnych liczb. Jednak przy ich stosowa-
niu wartość zarodka niewiele zmienia generowany ciąg.
W WinAVR stosuje się inne, niż zalecane równania, cze-
go norma nie zabrania.
Ważne jest, aby zrozumieć, że jedyne, co liczby
pseudolosowe mają wspólnego z liczbami losowymi,
to fakt, że nie znając zarodka trudno, na podstawie kilku
następujących po sobie liczb, określić, jaka będzie na-
stępna. Jednak jeśli zaczniemy generację dwa razy od ta-
kiego samego zarodka, otrzymamy identyczne ciągi
liczb (generowany ciąg może zależeć od kompilatora).
Ważne staje się więc odpowiednie ustawienie zarodka
przed rozpoczęciem korzystania z funkcji
rand
. Najle-
piej, jeśli będzie to liczba rzeczywiście losowa. W mi-
krokontrolerze liczbą taką często może być suma modu-
lo 2 wszystkich komórek pamięci.
Przykłady: listing 207 i 208.
Elektronika dla Wszystkich
39
Programowanie
Szczegóły techniczne
Pamięć zewnętrzna
w procesorach AVR
RAM oraz wolniejszego
wyświetlacza alfanume-
rycznego czy graficzne-
go.
Magistrala zewnętrz-
na konfigurowana jest za
pomocą bitów trzech re-
jestrów opisanych na
ry-
sunku B.
W tym miejscu
rozwinięcia wymaga opis
bitu
XMBK.
Współdzie-
lone linie młodszej części
adresu i danych (ozna-
czone
AD0..7
) tworzone
są przez wyprowadzenia
portu A.
Przy domyślnie
skonfigurowanej magi-
strali, w czasie jej nieak-
tywności, na szynach da-
nych utrzymywany jest
stan wysokiej impedan-
cji. Nie jest to najlepsze
możliwe wyjście. Spra-
wia to, że wszelkie wej-
ścia CMOS (a takimi są
także wyprowadzenia da-
nych pamięci) znajdują
się w nieokreślonym sta-
nie. Często ich stan bę-
dzie zmieniał się pod
wpływem powstających
na płytce zakłóceń, cza-
sem znajdą się w stanie
zabronionym. W przy-
padku niektórych ukła-
dów, w tym samego kon-
trolera, utrzymywanie ta-
kiego stanu może prowa-
dzić do niepotrzebnego
wzrostu poboru prądu.
Nasz procesor oferuje
dwie metody zaradzenia
temu problemowi:
1. Włączenie podciągania
na wyprowadzeniach
portu A. W tym celu wpi-
sujemy jedynki do reje-
stru PORTA.
2. Utrzymywanie po-
przedniego stanu na wyprowadzeniach portu A. Po usta-
wieniu bitu
XMBK
, gdy wyprowadzenia portu A zostaną
ustawione w stan wysokiej impedancji, port zostanie wy-
sterowany tak, aby utrzymany był poprzednio ustawiony
stan. Jest to optymalne energetycznie rozwiązanie. Funk-
cja ta odnosi się do portu A i działa niezależnie od tego,
czy interfejs pamięci zewnętrznej jest włączony.
Przestrzeń pamięci danych
W procesorach AVR pamięć zewnętrzna jest umieszczona
w przestrzeni pamięci danych. Oznacza to, że odwołują
się do niej identyczne instrukcje jak te, z których kompi-
lator korzysta w przypadku zwyczajnych zmiennych.
W praktyce dostęp do zewnętrznej pamięci danych może
być z punktu widzenia programisty niewidoczny.
Spójrz na
rysunek A
w ramce. Pokazuje on przestrzeń
adresową pamięci danych procesora. Przestrzenie reje-
strów uniwersalnych oraz rejestrów wejścia/wyjścia,
razem z rozszerzoną przestrzenią wejścia/wyjścia, istnieją
także w innych przestrzeniach adresowych, co oznacza,
że odwołują się do nich specjalne instrukcje. Rejestry uni-
wersalne adresujemy często bezpośrednio w rozkazie
(R0, R1, ...) i zgodnie z zasadami procesorów RISC są one
docelowym punktem wszelkich obliczeń oraz pośredniczą
w operacjach przesłania. Specjalna przestrzeń wej-
ścia/wyjścia posługuje się ośmiobitowym adresem i może
być obsługiwana przez dwie oddzielne instrukcje IN
i OUT. W stosunku do odpowiadających im instrukcji
operujących na przestrzeni danych: LDS i STS, zajmują
one dwa razy mniej miejsca i wymagają tyle samo mniej
cykli zegara.
Dostęp do pełnej zewnętrznej
przestrzeni adresowej
Dla naszego procesora ATmega162 adresowanie pamięci
zewnętrznej rozpoczyna się od adresu 0x0500. Pierwsze
1280 bajtów przestrzeni adresowej to odwołanie do ele-
mentów wewnątrz mikrokontrolera. W naszym układzie
modelowym pamięć została podłączona w taki sposób,
że jest aktywna, gdy stan na linii adresowej A15 jest niski
– okupuje więc niższą połowę przestrzeni adresowej. Jak
w takim przypadku umożliwić dostęp do wszystkich ko-
mórek pamięci? Przyjrzyjmy się bitom XMM2..0 na
ry-
sunku B.
Jeśli wpiszemy do nich wartość ‘001’, najstar-
szy bit adresu zostanie odpięty od portu C i odpowiadają-
ce mu wyprowadzenie stanie się zwykłym portem wej-
ścia/wyjścia. Teraz możemy w tym miejscu ustawić na
sztywno niski stan logiczny. Dzięki temu najniższa część
pamięci będzie widoczna na samej górze obszaru adreso-
wego, co ilustruje
rysunek C.
Warto zaznaczyć, że podobna technika umożliwia do-
stęp do wszystkich komórek, gdy mamy do czynienia
z pamięcią 64KB, wtedy posługiwalibyśmy się tylko adre-
sami wyższymi niż 0x0500, a maskowanie najstarszego
bitu umożliwiałoby dostęp do „ukrytego” obszaru pamię-
ci, przenosząc go pod adres 0x8000 do 0x84FF. W tym,
normalnie ukrytym, obszarze powinniśmy umieszczać da-
ne, do których nie wymagamy szybkiego dostępu i któ-
rych kompilator nie musi obsługiwać automatycznie.
Uruchomienie pamięci zewnętrznej
Uzgodniliśmy już, że z punktu widzenia programowania
pamięć zewnętrzna nie sprawi nam trudności. Jednak jako
rasowi elektronicy musimy wejrzeć w sprawę odrobinę
głębiej.
Pierwsza sprawa, jaką będziemy musieli się zająć, to
włączenie i konfiguracja pamięci zewnętrznej. Jak widać
na
rysunku A,
przestrzeń pamięci zewnętrznej możemy
podzielić na dwa sektory. Miejsce podziału ustalamy
z krokiem 8KB. Gdy w bity SRL2.. 0 wpiszemy wartość
0, cały obszar pamięci będzie sektorem górnym. W obu
sektorach mamy możliwość oddzielnej konfiguracji szyb-
kości działania pamięci. Umożliwia nam to, przykładowo,
podłączenie na jednej magistrali danych szybkiej pamięci
Szybkość działania
Sposób działania zewnętrznej magistrali danych sprawia,
że podłączona do niej pamięć jest wolniejsza od pamięci
wewnętrznej mikrokontrolera. Instrukcje LD, ST, LDS,
STS, PUSH oraz POP zajmują o 1 cykl więcej czasu, jeśli
ich wykonanie dotyczy pamięci zewnętrznej. Należy doli-
czyć kolejny cykl na każdy wprowadzony cykl oczekiwa-
nia (bity SRWn0..1). Instrukcje PUSH i POP dotyczą tyl-
ko sytuacji, gdy stos znajduje się w pamięci zewnętrznej.
Ponadto, jeśli umieścimy stos w pamięci zewnętrznej, mu-
simy się liczyć z tym, że wolniejsze będzie wykonanie in-
strukcji CALL, ICALL, RCALL, RET oraz RETI. Zajmu-
ją one o 3 cykle więcej + 2 cykle zegarowe na każdy cykl
oczekiwania (dane dla 16-bitowego licznika rozkazów).
Ponieważ kompilator języka C dość intensywnie korzysta
ze stosu, należy unikać jego umieszczania w pamięci ze-
wnętrznej. Ponadto, w erratach można znaleźć informacje,
że niektóre procesory nie mogą działać ze stosem w pa-
mięci zewnętrznej.
40
Elektronika dla Wszystkich
Programowanie
Listing 204 Odmalowanie wyświetlacza
z wykorzystaniem wewnętrznej palety
void
lcd_Update(
void
)
{
// Wys
ł
anie danych
pbuf = lcd_buffer;
for
(n=
0
; n<(ELEMS(lcd_buffer)/
2
); n++)
{
pix1
<<=
4
;
lcd_Data
((
uint8_t
)(
pix1
>>
8
));
pix1
|= (
pix2
>>
8
) &
0x0F
;
lcd_Data
((
uint8_t
)
pix1
);
lcd_Data
((
uint8_t
)
pix2
);
uint16_t n;
lcd_pixel* pbuf;
// Wczytuj
ę
warto
ść
koloru
uint16_t pix1 = lcd_rgb[*pbuf++];
uint16_t pix2 = lcd_rgb[*pbuf++];
// Wysy
ł
am dane
}
lcd_Command
(
LCD_NOP
);
}
lcd_Command(LCD_RAMWR);
ABC... GCC
Konfiguracja sekcji w pamięci RAM
oraz dostęp do pamięci zewnętrznej
Z ramki o działaniu zewnętrznej magistrali pamięci da-
nych procesora AVR dowiedzieliśmy się, że z punktu wi-
dzenia programisty pamięć zewnętrzna tworzy przedłuże-
nie pamięci wewnętrznej. Mimo tego, jeśli piszemy pro-
gram w GCC, nie powinniśmy w zwykły sposób tworzyć
dużych zmiennych, zakładając, że ten ich fragment, który
nie mieści się w pamięci wewnętrznej, zostanie przenie-
siony na zewnątrz. Rzeczywiście, duże zmienne w taki
właśnie sposób zostaną umieszczone w przestrzeni adre-
sowej. Jednak tracimy kontrolę nad tym które zmienne
znajdą się w szybkiej pamięci wewnętrznej, a które w wol-
niejszej pamięci zewnętrznej. Ponadto, między obszarem
„zwykłych” zmiennych a pamięcią zewnętrzną mamy jed-
ną przeszkodę: stos.
Z ideą sekcji pamięci zapoznaliśmy się już w
części
12.
Teraz spójrz na rysunek w ramce. Pokazuje on domyśl-
ny sposób, w jaki poszczególne sekcje zostaną umieszczo-
ne w obszarze pamięci danych.
Przypomnę znaczenie widocznych na obrazku sekcji:
.data:
W tej sekcji znajdują się dane które są inicjowane
wartością, inną niż zero. W pamięci programu znajduje się
obszar zawierający dane inicjujące obszar sekcji
.data
.
Automatycznie ustawiana na początek obszaru pamięci
RAM.
.bss:
Jest to obszar zmiennych nieinicjowanych wartością.
Zawarte tutaj zmienne są zerowane podczas inicjacji pro-
gramu.
.noinit
: jej obszar nie jest w żaden sposób modyfikowany
po starcie programu. Ponieważ w ANSI C sekcja .noinit
nie istnieje, czasem sekcje .noinit traktujemy jako frag-
ment sekcji
.bss
.
sterta:
Obszar przeznaczony na pamięć alokowaną dyna-
micznie. Jej opis pojawi się w dalszej ramce.
stos:
Sprzętowy stos mikrokontrolera. Wykorzystywany
do odkładania adresu powrotu z podprogramu lub prze-
rwania oraz, w razie konieczności, do pamiętania stanu
zmiennych lokalnych. Stos przyrasta w kierunku zmniej-
szających się adresów. Automatycznie ustawiany na koń-
cu pamięci wewnętrznej.
Widoczne na rysunku mnemoniki wskazujące począt-
ki i końce poszczególnych obszarów można zmieniać
przez odpowiednie opcje kompilatora. Zostanie to omó-
wione przy pamięci dynamicznej.
Przemieszczanie istniejących sekcji
Pierwszy raz przemieszczaliśmy sekcję zawierającą kod
programu (.text) przy okazji pisania bootloadera w
części
12
. Przesuwanie sekcji w pamięci danych odbywa się
w identyczny sposób. Konieczne jest jedynie dodanie do
wybranego adresu 0x800000 dla oznaczenia, z jakim ty-
pem pamięci mamy do czynienia. I uwaga: jeśli przesunie-
my sekcję .data, sekcje .bss, .noinit i sterta zostanią prze-
sunięte automatycznie. Możemy także przenieść samą
sekcję .bss albo sekcję .noinit do pamięci zewnętrznej.
Którąkolwiek sekcję przesuniemy, wszystkie elemen-
ty leżące w kierunku rosnących adresów przesuną się ra-
zem z nią. Nie dotyczy to stosu, który domyślnie jest usta-
wiany na koniec pamięci wewnętrznej. Natomiast przesu-
nięcie sekcji. noinit, leżącej na końcu sekcji .bss, przesu-
nie także pozycję początku sterty.
Oprócz przesuwania sekcji opcją poznaną w
części
12:
–section-start=.data=nnnn
(tylko) w przypadku sekcji
.data, .bss, oraz .text, możemy posłużyć się prostszym
zapisem:
-T
nazwa_sekcji
=nnnn
%. hex
: %. elf
@echo
@echo
$ (MSG_FLASH)
$@
$ (OBJCOPY)
-O
$ (FORMAT)
-R. eeprom $< $@
W tym momencie umieszczenie zmiennej w pamięci
zewnętrznej ogranicza się do dopisania zapisu jak niżej
w chwili jej deklaracji:
int
zmienna
__attribute__
( (
section
('
.exram'
)));
Uwaga: zmiennej takiej nie możemy przypisać warto-
ści początkowej, ponieważ możliwe jest to tylko dla
zmiennych w sekcji .data. Kompilator nie zgłosi błędu, ale
też nic nie ustawi.
-R. exram
Jawne podanie adresu
Skoro już mówimy o dostępie do zewnętrznego obszaru
adresowego, trudno zapomnieć omożliwości pozornie
najprostszej. Można oczywiście podać bezpośrednio adres
do odpowiedniej wstawki asemblerowej albo rzutować
odpowiednią liczbę na wskaźnik. Pierwsze rozwiązanie
jest dobre w przypadku, gdy chcemy obsłużyć jakieś urzą-
dzenie wejścia/wyjścia, a nie pamięć. Pamiętaj jednak, że
nawet w takim przypadku można utworzyć dodatkową
sekcję i umieścić w niej strukturę odpowiadającą reje-
strom sterowanego układu. Aby zapewnić, że kompilator
nie będzie optymalizował dostępu do takich rejestrów, na-
leży im nadać atrybut
volatile
.
Natomiast jedyne, co można powiedzieć o bezpośred-
nim rzutowaniu liczby wskazującej adres na wskaźnik to
tyle, że w AVR-GCC jest to możliwe i działa. Jednak,
z punktu widzenia ANSI-C, wynik takiego działania jest
nieokreślony. Nie będziemy więc rozwijać dalej tej idei.
Tworzenie własnej sekcji
Pełną kontrolę nad rozmieszczeniem danych da nam
utworzenie nowej, specjalnej sekcji obejmującej pamięć
zewnętrzną. Jeśli umieścimy w niej zmienną, automatycz-
nie znajdzie się ona w pamięci zewnętrznej. Uzyskany
w ten sposób mechanizm jest praktycznie tak samo wy-
godny w stosowaniu, jak oznaczanie miejsca umieszcze-
nia zmiennej znane z BASCOM-a!
Tutaj dobra wiadomość. Tworzenie nowej sekcji jest
tak samo proste, jak przesuwanie sekcji istniejącej. Ko-
nieczny będzie jednak dodatkowy krok. Tę opcję przero-
bimy dokładniej.
Tworzymy nową sekcję przez dodanie poniższej opcji
linkera:
–section-start=.exram=0x800500
Już w tej chwili możemy korzystać z nowej sekcji.
Jednak gdy skompilujemy program z jakąkolwiek zmien-
ną w sekcji
.exram,
czeka nas przykra niespodzianka: Pod-
czas wczytywania pliku
hex
może się okazać, że program
nie mieści się wukładzie albo nawet sam plik nie daje się
wczytać. Jest to związane z tym, że dane z nowej sekcji są
kopiowane do pliku wyjściowego, bez przesunięcia do ze-
ra adresu 0x800000.
Tworzeniem plików
hex
oraz
eep
na podstawie pliku
elf zajmuje się programik
avr-objcopy.
W naszym pliku
makefile
jest on zdefiniowany jako zmienna OBJCOPY.
Prawie na końcu pliku odnajdujemy regułę tworzącą plik
hex
i wprowadzamy opcję, która usunie z pliku wyniko-
wego niechcianą sekcję:
Przesunięcie stosu
Kompilator zawarty w pakiecie WinAVR umożliwia dość
swobodne przemieszczanie sekcji w pamięci danych.
Pierwszym pomysłem, jaki może się pojawić, jest przesu-
nięcie przeszkadzającego nam stosu na koniec naszej pa-
mięci zewnętrznej. Można to zrobić na dwa sposoby:
1. Tworzymy funkcję umieszczoną w sekcji. init2, nadpi-
sując tym samym domyślną funkcję inicjacji stosu. Pamię-
tamy o zerowaniu rejestru
__zero_reg__
(r1).
2. Przesuwamy stos przez opcję kompilatora (w pliku ma-
kefile), wprowadzamy:
-minit-stack=nnnn
gdzie nnnn to adres, od którego rozpocznie się nasz stos
(
nie
dodajemy 0x800000, piszemy: dziesiętnie – 5000,
albo szesnastkowo: 0x5000).
Pamiętaj jednak, że zgodnie z poprzednią ramką, prze-
sunięcie stosu do pamięci zewnętrznej zwalnia działanie
całego programu i ogólnie nie jest zalecane. Zabierzmy się
raczej do reszty sekcji.
Włączyć zewnętrzną magistralę
bardzo wcześnie
Pamiętaj, że jeśli w pamięci zewnętrznej znajdą się dane
potrzebne już w chwili inicjacji, trzeba dokonać jej
włączenia, jeszcze zanim program zechce z nich skorzy-
stać. Jest to ważne, gdy dokonamy przesunięcia sekcji .da-
ta albo .bss. Sekcje te są inicjowane wartościami począt-
kowymi, zanim program przejdzie do funkcji
main
. Spra-
wa robi się jeszcze poważniejsza, jeśli na zewnątrz znaj-
dzie się stos. W takich przypadkach pamięć zewnętrzną
bezpiecznie można włączyć, umieszczając odpowiednią
funkcję w sekcji
.int1
– przed inicjacją stosu lub
.init3 –
stos jest już zainicjowany, rejestr zera (__zero_reg) przy-
gotowany.
Elektronika dla Wszystkich
41
Programowanie
co już wskazaliśmy w poprzedniej części –
dwa piksele wymagają trzech bajtów i dane
w nich rozłożone są w taki sposób, że łatwiej
przesyłać je jako jeden element. Na koniec
wysyłamy instrukcję NOP, o czym także była
mowa przy okazji omawiania wyświetlacza.
To wszystko, czego w tej chwili potrzebu-
jemy w naszej bibliotece. Do bufora wyświe-
tlacza „dobierzemy się” bezpośrednio w pro-
gramie.
odwołanie się bezpośrednio do bufora wy-
świetlacza. Później znajduje się tablica kolo-
rów naszego płomienia. Makro
LCD_palRGB
zamienia poszczególne składowe na 12-bito-
wy kolor. Podobne makra mamy już przero-
bione.
Na
listingu 206
widzimy, co dzieje się
przed funkcją
main
. To umożliwia nam włą-
czenie pamięci zewnętrznej bardzo wcześnie.
Na
listingu 207
przedstawiam zawartość pętli
rysowania płomienia. Nie przedstawiam ini-
cjacji portów, włączenia wyświetlacza oraz
ładowania palety.
Na początku generowanych jest 80 (dobra-
no eksperymentalnie) punktów o maksymal-
nej jasności. Pozycja zmienia się tak, aby
punkt zmieścił się w odstępie 2 pikseli od
bocznych brzegów wyświetlacza i na wyso-
kości od 2 do 6 pikseli od jego dolnej krawę-
dzi.
W ten sposób przekonaliśmy się, że obsłu-
ga zewnętrznej pamięci danych przebiega
praktycznie identycznie jak pamięci wewnętrznej
Listing 206 Przed funkcją main()
void
before_main(
void
)
__attribute__((naked))
__attribute__((section(
„.init3“
)));
void
before_main(
void
)
{
MCUCR =
1
<<SRE;
SFIOR =
1
<<XMBK |
1
<<XMM0
/*A15*/
;
// Ustawienie A15 na 0
DDRC =
0x80
;
PORTC &= ~(
1
<<
7
);
„Podpalanie” wyświetlacza
Za pomocą naszej nowej biblioteki stworzy-
my program realizujący realistyczną animację
płomienia. Wbrew pozorom, stworzenie reali-
stycznie wyglądającego płomienia nie wyma-
ga wielkich mocy obliczeniowych ani żad-
nych zdolności obsługi programów graficz-
nych. Udowodnimy to, generując ładną wizu-
alizację ognia za pomocą naszego zestawu.
Nasz płomień będzie korzystał ze specy-
ficznej palety, która poszczególnym tempera-
turom będzie przypisywać odpowiedni kolor.
Najniższa temperatura będzie miała indeks 0,
będzie to kolor czarny. Im wyższa temperatu-
ra, tym większe natężenie czerwonego, póź-
niej rośnie składowa zielona – da to przejście
od czerwieni do koloru żółtego, ostatecznie
dodajemy składową niebieską – nasz ogień
w „najcieplejszym” miejscu będzie miał kolor
biały. Ogień jest „podsycany” na spodzie wy-
świetlacza. Podsycanie nie jest równomierne,
tylko rozbłyska i przygasa w sposób z grubsza
losowy. Jest to pole do wykorzystania oma-
wianej w jednej z ramek funkcji
rand
. Ciepło
płomienia rozchodzi się w górę, przy czym
następuje jego stopniowe rozmycie. Odpo-
wiedni efekt da nam obliczanie koloru pikse-
la zgodnie z
rysunkiem 71.
Zauważ, że dzię-
ki sprytnej sztuczce z paletą, obliczenia prze-
prowadzamy niejako na wartościach tempera-
tury, a nie na poszczególnych składowych ko-
loru. Co więcej, jeśli ograniczymy ilość kolo-
rów do 64, obliczenia będzie można wykony-
wać na liczbach ośmiobitowych – to bardzo
przyspiesza działanie programu. Dzielenie
przez 4 także jest bardzo wygodne – kompila-
tor zoptymalizuje ją na przesunięcie logiczne.
Dla uproszczenia obliczeń pozostawiamy
zkażdej strony generowanego obrazka 1 pik-
sel czarny – tutaj nie rysujemy, tylko korzy-
stamy z tych pikseli przy obliczeniach.
Listing 205
pokazuje początek programu
w pliku
main.
Pierwsza linia umożliwia nam
}
Listing 207 Zawartość pętli rysowania
lcd_Update();
// Podsycanie ognia
uint8_t n;
for
(n=
0
; n<
80
; n++)
{
Algorytm rozmywania wydaje się oczywi-
sty. W pierwszej chwili może dziwić dodawa-
nie 2 do wskaźnika
ppix
na koniec zewnętrz-
nej pętli. Jest to związane z tym, że nie obli-
czamy wartości pierwszego i ostatniego pik-
sela w linii (przypominam, że są one cały czas
czarne) i musimy je pominąć.
Dodatkowo, przy rozmywaniu, jeśli nie
mamy do czynienia z minimalną temperaturą,
minimalnie obniżamy temperaturę punktu
płomienia. Ten element został wprowadzony
po pierwszych eksperymentach i zaowocował
bardziej postrzępionym szczytem „płomie-
nia”.
// Losowanie pozycji
uint8_t x = rand() % (LCD_SX-
4
) +
2
;
uint8_t y = LCD_SY – rand() %
5
-
3
;
// Wypisanie piksela
lcd_Pixel(x, y, FIRE_MAX);
}
// Rozmywanie obrazu
lcd_pixel *ppix = lcd_buffer+LCD_SX+
1
;
for
(uint8_t y=
1
; y<LCD_SY-
1
; y++)
{
for
(uint8_t x=
1
;
x<LCD_SX-
1
; x++, ppix++)
{
lcd_pixel temp;
temp = *ppix + *(ppix+LCD_SX+
1
) +
*(ppix+LCD_SX-
1
) +
*(ppix+LCD_SX);
if
(temp !=
0
)
temp -=
1
;
*ppix = temp /
4
;
}
ppix +=
2
;
}
Listing 205 Zmienne w pliku main.c
extern
lcd_pixel lcd_buffer[];
prog_uint16_t g_fireRGB[] =
{
}
;
LCD_palRGB(
0
,
0
,
0
),
...
LCD_palRGB(
15
,
0
,
0
),
...
LCD_palRGB(
15
,
15
,
0
),
...
LCD_palRGB(
15
,
15
,
15
),
ABC... GCC
Maksymalny rozmiar
obsługiwanej zmiennej
Fot. 11 Płomień na wyświetlaczu
ABC... GCC
Pętla do-while (0)
– alternatywa dla goto
Przy tworzeniu zmiennych trzeba pilnować, aby nie
przekroczyć maksymalnego rozmiaru zmiennej, jaki
AVR-GCC jest w tanie obsłużyć. Maksymalny rozmiar
pojedynczej zmiennej wynosi 32767B. Ograniczenie to
wynika z przyjętego typu zmiennej wskazującej rozmiar
size_t
. Jest to liczba 16-bitowa ze znakiem. Mimo tego,
że rozmiar nie może być ujemny, niektóre funkcje zwra-
cają wartość ujemną, aby zaznaczyć wystąpienie błędu.
Zaznaczam, że ograniczenie dotyczy rozmiaru pojedyn-
czej zmiennej. W programie mogą pojawić się, na przy-
kład, dwie tablice o rozmiarze 30kB każda i zostaną one
prawidłowo obsłużone (jeśli tylko mamy fizycznie od-
powiednią ilość pamięci).
W pliku
<stdint.h>
znajduje się definicja stałej
SIZE_MAX oznaczającej maksymalny rozmiar poje-
dynczej zmiennej.
Część wczytująca dane przesyłane przez komputer zo-
stała umieszczona we wnętrzu pętli
do-while.
Jednak jej
warunek został ustawiony na 0. Jaki to ma sens? Ciało
takiej funkcji będzie
zawsze
wykonane tylko raz. Jednak
zyskujemy jedną znakomitą możliwość: możemy w do-
wolnym momencie wyskoczyć z ciała pętli (jeśli, na
przykład, komputer nie przesłał żądania programowa-
nia) bez użycia instrukcji goto (patrz
część 11
). Po pro-
stu użyjemy instrukcji
break.
Ta ciekawa sztuczka jest
często stosowana i warto o niej pamiętać. Ostatecznie,
często tak napisany kod jest bardziej przejrzysty.
Listing nie jest prezentowany w artykule. Kod do-
stępny na stronie Elportalu.
Rys. 71 Zasada rozmywania płomienia
42
Elektronika dla Wszystkich
– ze względu na architekturę AVR miejsce
umieszczenia zmiennej od strony kodu nie ma
znaczenia. Zmienia się jedynie czas dostępu.
Uzyskany efekt wygląda naprawdę bardzo
atrakcyjnie, o czym przekonuje częściowo
fo-
tografia 11.
Jednak ruchoma animacja spra-
wia znacznie lepsze wrażenie.
Pamięć EEPROM – dodaje-
my możliwość konfiguracji
Nasz płomień wygląda bardzo efektownie, ale
fajnie byłoby mieć możliwość jego dowolnej
konfiguracji bez konieczności każdorazowe-
go kompilowania programu. Pomoże nam
w tym nieulotna pamięć danych typu
EEPROM. Przejrzyj mówiącą o niej ramkę...
i zabieramy się do pracy.
Idea, od strony programowania, będzie jak
najprostsza: jeśli przy uruchamianiu układu
przytrzymamy którykolwiek przycisk, proce-
sor będzie czekał na transmisję. Odbywa się
ona z prędkością 1200 bodów. Komputer mu-
si przesłać ciąg „fire”, na co program odpowie
potwierdzeniem „@”. Teraz przesyłamy in-
formację o ilości danych do nowej palety i za-
raz po niej 16-bitowe wartości palety. Ilość
danych i same dane przesyłane są prosto –
w postaci binarnej. Jednocześnie są one odsy-
łane w celu kontroli.
Pierwsze, co zmienimy, to zamiast tablicy
umieszczonej w pamięci programu, stworzy-
my specjalną strukturę w pamięci EEPROM.
Będzie ona zawierała dane palety oraz maksy-
malną temperaturę płomienia (najwyższy in-
deks palety). Jeśli strukturę tę zainicjujemy
danymi, zostaną one umieszczone w pliku
.eep
. Modyfikację pokazuje
listing 209.
Do funkcji obsługujących wyświetlacz do-
damy funkcję umożliwiającą wczytanie no-
wej palety – patrz
listing 210.
Porównaj wi-
doczny tutaj kod z
listingiem 203.
Sposoby
obsługi pamięci programu i EEPROM są bar-
dzo podobne.
W celu obsługi transmisji użyjemy modu-
łu
rs
, który pojawił się po raz pierwszy już
w
części 8.
Dalej nie będę pisał o inicjacji
portu UART ani dokładnie omawiał sposobu
transmisji – wszystko to już znamy. Skupiam
się natomiast na obsłudze pamięci EEPROM.
Ważny dla nas fragment pokazuje
listing
211.
Przy korzystaniu z biblioteki
<avr/eeprom.h>
dostęp do pamięci EEPROM
jest dość intuicyjny. Na łamach Elporatalu
znajdzie się nie tylko pełny kod programu, ale
także program służący do tworzenia i przesy-
łania palety płomienia z poziomu komputera
PC.
Gdyby podczas działania programu poja-
wiły się problemy z komunikacją, pomóc mo-
że przełączenie procesora na zewnętrzny
kwarc 8MHz. W czasie eksperymentów z we-
wnętrznym generatorem RC problemów ta-
kich nie stwierdzono.
ABC... GCC
Dostęp do pamięci EEPROM
Z punktu widzenia AVR-GCC obszar pamięci EEPROM
jest specjalną sekcją. Aby zaznaczyć, że mamy do czy-
nienia z kolejnym obszarem adresowym, do adresu sek-
cji umieszczonej w tej pamięci dodajemy przesunięcie
0x810000. Zmienną możemy umieścić w obszarze pa-
mięci EEPROM jak niżej:
int
zmienna __attribute__
( (section ('
.eeprom'
))) =
12
;
albo prościej, korzystając z makra znajdującego się
w pliku
<avr/eeprom.h>
:
int
zmienna EEMEM =
12
;
Przykładowa wartość 12 zostanie zapisana w odpo-
wiednim miejscu pliku
eep
. Jeśli nie nadamy zmiennej
wartości, będzie ona domyślnie wyzerowana.
Przy obszarze pamięci EEPROM natkniemy się na
podobny problem, jaki mieliśmy przy umieszczaniu ta-
blic w pamięci programu. Zmienne takie obsługujemy za
pomocą specjalnych funkcji. Proste funkcje umożliwia-
jące obsługę obszaru EEPROM deklarowane są w pliku
<avr/eeprom.h>.
Wszystkie dostępne tutaj funkcje dzia-
łające na pamięci EEPROM przed swoim wykonaniem
czekają na zakończenie poprzedniej operacji zapisu.
Może to zatrzymać wykonywanie programu na około
8,5ms.
eeprom_is_ready()
– zwraca 0, jeśli pamięć EEPROM
jest zajęta,
eeprom_busy_wait()
– czeka, aż pamięć EEPROM za-
kończy zapis,
eeprom_read_byte (addr), eeprom_read_word (addr)
–
funkcje zwracają bajt oraz słowo dostępne pod podanym
adresem w pamięci EEPROM,
eeprom_read_block (ram, eeprom, n)
– odczytuje
n
baj-
tów z pamięci EEPROM i zapisuje je w buforze w pa-
mięci RAM,
eeprom_write_byte (addr, value), eeprom_write_word
(ddr, value)
– funkcje zapisują bajt oraz słowo pod poda-
ny adres w pamięci EEPROM,
eeprom_write_block (ram, eeprom, n)
– zapisuje
n
baj-
tów z pamięci RAM do pamięci EEPROM
.
Listing 208 Zarodek zmiennej pseudolosowej
Pamięć przydzielana
dynamicznie
Odstawiamy już naszą animację płomienia.
Można oczywiście nad nią pracować i uzy-
skać jeszcze ciekawszy efekt. Jednak wyczer-
paliśmy możliwości dydaktyczne tego przy-
kładu. Zajmiemy się teraz bardzo ciekawym
zagadnieniem. Stworzymy prostą przeglądar-
kę obrazków. Obrazki będziemy przesyłać
z komputera, i zakładamy, że pamięć o nich
ma być zachowana tylko do chwili wyłącze-
nia zasilania. Co jednak dla nas istotne, każdy
obrazek będzie mógł mieć inny rozmiar.
Oznacza to, że do naszego układu możemy
przesłać, na przykład, 3 obrazki o rozmiarze
całego wyświetlacza albo 10 o rozmiarze
50x50 każdy. Jak sobie z tym poradzić?
W tym przypadku wspomoże
nas wbudowana w AVR-GCC
możliwość dynamicznej aloka-
cji pamięci. Wszystko, co ko-
nieczne, opisują dwie kolejne
ramki. Tutaj od razu zabieramy
się do działania.
uint16_t *pmem;
uint16_t seed =
0
;
uint16_t n;
pmem = (uint16_t*)
0
;
for
(n=
0
; n<(RAMEND+
1
)/
2
; n++)
{
seed ^= *pmem++;
}
srand(seed);
Listing 209 Dane w pamięci EEPROM
struct
{
uint8_t max_power
;
uint16_t fireRGB
[
64
];
}
eeprom EEMEM
=
{
45
,
{
LCD_palRGB
(
0
,
0
,
0
),
...
}
Struktury w pamięci EEPROM
a pojedyncze zmienne
Dobrym zwyczajem jest tworzenie w pamięci EEPROM
tylko jednej struktury zawierającej wszystkie dane. Jeśli
umieścimy w pamięci pojedyncze zmienne, nie mamy
pewności, czy zostaną one rozmieszczone w kolejności
zgodnej z kolejnością ich definicji. Co więcej, kolejna
wersja WinAVR może rozłożyć je w inny sposób. Staje
się to ważne, jeśli stworzymy nowszą wersję oprogramo-
wania i zechcemy umożliwić jego wgranie bez kasowa-
nia nastaw zawartych w pamięci EEPROM. Jeśli w pa-
mięci EEPROM znajdzie się tylko jedna struktura,
mamy pewność, że rozmieszczenie danych się nie zmie-
ni. Jeśli pojawi się konieczność dodania nowych opcji –
dopisujemy je na końcu struktury, tak aby nie kolidowa-
ły ze starymi ustawieniami:
struct
{
}
;
Listing 210 Wczytanie palety z pamięci EEPROM
void
lcd_loadRGB_EE(
const
uint16_t* peeRGB, uint16_t size)
{
}
eeprom_read_block(lcd_rgb, peeRGB, size);
Listing 211 Zapis konfiguracji do pamięci EEPROM
uint8_t count
;
count
=
rs_get
();
eeprom_write_byte
(&
eeprom
.
max_power
,
count
-
1
);
rs_put
(
count
);
Program „przeglądarka”
Nasz nowy program będzie działał z wy-
świetlaczem w trybie ośmiobitowym.
W module wyświetlacza rezygnujemy
więc ze zmiennej zawierającej paletę. Ko-
nieczne jest jednak dodanie kilku linii na
końcu procedury inicjacji naszego modu-
łu. Pokazuje je
listing 212.
Uprości się
znacznie funkcja odmalowywania wy-
świetlacza – wystarczy teraz wysłać za-
wartość bufora (listingu nie pokazuję).
// Pobieranie danych
uint8_t n
;
for
(
n
=
0
;
n
<
count
;
n
++)
{
uint16_t rgb
= (
uint16_t
)
rs_get
()<<
8
;
rs_put
(
rgb
>>
8
);
rgb
|=
rs_get
();
eeprom_write_word
(&
eeprom
.
fireRGB
[
n
],
rgb
);
rs_put
((
uint8_t
)
rgb
);
int
dana1=
0
, dana2=
5
;
...
}eeprom EEMEM;
Przykłady: listing 209 do 211.
}
Elektronika dla Wszystkich
43
Plik z chomika:
tomaszsmaz3
Inne pliki z tego folderu:
kursC_czesc018.pdf
(401 KB)
kursC_czesc017.pdf
(1126 KB)
kursC_czesc016.pdf
(1204 KB)
kursC_czesc015.pdf
(2765 KB)
kursC_czesc014.pdf
(1115 KB)
Inne foldery tego chomika:
Dokumenty
Galeria
inne
Instrukcje Lego
Noty katalogowe
Zgłoś jeśli
naruszono regulamin