AVR-GCC kompilator C dla mikrokontroler├│w AVR, cz─Ö┼Ť─ç 4.pdf

(639 KB) Pobierz
ep_06_103-106_avr_gcc_cz4.indd
K U R S
AVR–GCC: kompilator C
mikrokontrolerów AVR, część 4
Kontynuujemy cykl artykułów, których zadaniem jest przedstawienie podstaw
oraz praktycznych zasad programowania mikrokontrolerów AVR w języku C
z użyciem kompilatora avr-gcc. Oczywiście wybór kompilatora AVR-GCC może
się jednym podobać, a innym nie. Postaramy się jednak uzasadnić, że nie
jest to zły wybór.
Dyrektywy kompilacji
warunkowej
Na chwilę przerwiemy omawianie
typów zmiennych i obejrzymy do-
kładniej zastosowane polecenie kom-
pilacji warunkowej #if #else #endif .
Dyrektywy kompilacji warunkowej
nie wchodzą w skład wynikowego
kodu programu. Są analizowane na
samym początku przez preprocesor,
który zgodnie z nimi modyfikuje kod
źródłowy poddawany następnie kom-
pilacji (sięgnijmy do wcześniejszego
ogólnego opisu działania AVR–GCC).
Powyższy zapis powoduje wstawienie
do kodu tylko bloku zgodnego z na-
rzuconym warunkiem (zdefiniowane
makro) i pomija blok niezgodny. Pomi-
mo dosyć podobnej składni dyrektywa
nie ma więc nic wspólnego z progra-
mową instrukcją warunkową if() else ;
wykonywaną dopiero w trakcie działa-
nia programu.
Dyrektywy warunkowe mogą mieć
kilka odmian. Warunek może być
sformułowany jak powyżej: #ifdef
MACRO – wtedy sprawdza po prostu
czy MACRO zostało wcześniej zdefi-
niowane (lub odwrotnie gdy użyjemy
#ifndef MACRO ). Bardziej uniwersalną
formą jest #if WARUNEK gdzie waru-
nek może być stałą liczbową, stałą
znakową albo dowolnym spełniają-
cym reguły C wyrażeniem (operacją
arytmetyczną, logiczną, bitową). If
jest realizowane gdy WARUNEK !=0 .
W wyrażeniach możemy także spraw-
dzać zdefiniowanie makra – służy do
tego oddzielny operator defined. Za-
pis #ifdef MACRO będzie więc rów-
noważny z #if defined(MACRO) .
Dyrektywa może być pojedyncza:
#ifdef MACRO
Blok kodu.
#endif
albo złożona:
#ifdef MACRO
Blok kodu 1 (blok uwzględniany, gdy MA-
CRO zdefiniowane)
#else
Blok kodu 2 (blok uwzględniany, gdy MA-
CRO nie zdefiniowane)
#endif
Nieco bardziej kłopotliwe jest
sprawdzenie kilku oddzielnych wa-
runków. Wymaga to zagnieżdżenia
kolejnych instrukcji if :
#if WARUNEK1
Blok kodu 1 (WARUNEK1 spełniony)
#else // (WARUNEK1 niespełniony)
#if WARUNEK2
Blok kodu 2 (WARUNEK1 niespełniony, WA-
RUNEK2 spełniony)
#else
Blok kodu 3 (WARUNEK1 niespełniony
i WARUNEK2 nie spełniony)
#endif // zakończenie obsługi WARUNEK2
#endif // zakończenie obsługi WARUNEK1
Dla ułatwienia wprowadzono do-
datkowy operator elif – z jego po-
mocą zapiszemy to samo znacznie
prościej:
#if WARUNEK1
Blok kodu1
#elif WARUNEK2
Blok kodu 2
#else
Blok kodu 3
#endif
W naszym przykładzie makro FLO-
AT zdefiniowaliśmy za pomocą dy-
rektywy #define FLOAT na początku
pliku źródłowego. W przypadku ko-
nieczności zastosowania makra w wie-
lu plikach wygodniej będzie umieścić
definicję w jakimś wspólnym pliku
nagłówkowym. Jeszcze inną możliwo-
ścią jest dopisanie definicji do opcji
wywołania kompilatora. W okienku
edycyjnym dodatkowych opcji Avr-
Side (zakładka Kompilator) wpiszmy
–D FLOAT i sprawdźmy, że działanie
będzie takie samo ( rys. 11 ). Po zmia-
nie samych opcji (bez zmiany treści
kodu) dla ponownego przekompilo-
wania użyjmy polecenia Build ( CTR-
L+SHIFT+F9 ) gdyż polecenie Make
( F9 ) pominie kompilację nie zmienio-
nego pliku źródłowego.
Jednym z typowych zastosowań
może być wstawienie do kodu frag-
mentów używanych tylko przy uru-
chamianiu i testowaniu aplikacji. Po-
wszechne jest też stosowanie w pli-
kach nagłówkowych warunku:
#ifndef PLIK
#define PLIK
Treść pliku nagłówkowego.
#endif
Zabezpiecza to przed omyłkowym
wielokrotnym wstawieniem treści pli-
ku do kodu dyrektywami #include ,
po pierwszym wstawieniu makro
PLIK jest już zdefiniowane i każda
następna próba zostaje zablokowana.
Checkboxy DEBUG i HAPSIM po-
wodują zdefiniowanie odpowiednio
makr DEBUG i HAPSIM , które są po-
mocne w kodzie przy uruchamianiu
i symulacji z użyciem Hapsima.
Rys. 11. Definiowanie makra w opcjach wywołania kompilatora
Zmienne logiczne, flagibitowe,
obsługa linii wejść/wyjść
Zmienne logiczne to zmienne
przyjmujące tylko dwie wartości:
prawda i fałsz. Są wygodne pod-
Elektronika Praktyczna 6/2005
103
27840986.008.png
K U R S
ską zanegowaną (z wartością wszyst-
kich bitów zmienioną na przeciw-
ną: ~0000 1000 = 1111 0111):
1100 1010 & ~0000 1000 = 1100 1010 &
1111 0111 = 1100 0010
Rys. 12. Podpowiedź deklaracji makra w AvrSide
Jeśli nie potrzebujemy konkret-
nego stanu bitu, a chcemy tylko
zmienić jego wartość na przeciw-
ną posłużymy się maską w operacji
ex–or:
1100 0010 ex–or 0000 1000 = 1100 1010
1100 1010 ex–or 0000 1000 = 1100 0010
czas rozpatrywania rozmaitych roz-
gałęzień warunkowych. W języku
C wydzielenie tych zmiennych ma
charakter raczej umowny i służący
przejrzystości kodu. Każda, bowiem
wartość niezerowa jest traktowa-
na jako logiczna „prawda”, a zero
jest równoważne z logicznym „fał-
szem”. Np. w nieskończonej pętli
naszego pierwszego testu użyliśmy
po prostu warunku while (1) – je-
dynka jest zawsze prawdą i pętla
będzie zawsze wykonana. Właśnie
dla przejrzystości zdefiniowany zo-
stał dodatkowy typ bool oraz jego
wartości true oraz false. Aby je
wykorzystać musimy dołączyć sys-
temowy plik nagłówkowy stdbool. h
( #include < stdbool.h >). Wtedy mo-
żemy używać bardzo czytelnego
i jednoznacznego zapisu (np. bool
mybool=true; ). Typ bool nie jest
niestety rozumiany przez AvrStudio,
z czego można jednak łatwo wy-
brnąć definiując własny typ zgodny
z 1–bajtowym char (i pozostawiając
nazewnictwo true false ze stdbo-
ol.h ). Użyjemy do tego bardzo po-
żytecznego operatora typedef. Skład-
nia jest bardzo prosta:
typedef określenie_typu własna_
nazwa_nowego_typu;
z listy poprzez zaznaczenie pozycji
i klawisz DEL , całą listę zerujemy
natomiast skrótem CTRL+DEL ). Pa-
miętajmy tylko, że długość bufo-
ra listy jest ograniczona – po jego
wypełnieniu dalsze wpisy nie będą
dokonywane.
Zmienne typu bool są dosyć roz-
rzutne – zajmują cały bajt tylko po
to, aby określić stan jednego bitu
(gdyż true i false to odpowiednio po
prostu 1 i 0). Dlaczego więc nie uży-
wać tak popularnych w rodzinie ‚51
flag (zmiennych jednobitowych), któ-
re maksymalnie oszczędzają pamięć?
Na przeszkodzie stoją następujące
względy:
AVR nie ma przeznaczonego do
ogólnego stosowania obszaru pamię-
ci adresowanej bitowo (nie można
tu uwzględniać rejestrów I/O, które
mają całkiem inne przeznaczenie, po
trzy takie rejestry ogólnego przezna-
czenia mają tylko niektóre najnowsze
Atmegi),
AVR–GCC niestety nie obsługuje
dość popularnej w innych kompilato-
rach wygodnej składni dostępu do
poszczególnych bitów (np. zmienna.
X , PORTA.2 itp.) – wg obecnych
informacji nie zanosi się tutaj na
szybką zmianę.
Jednak wbrew tym ogranicze-
niom wcale nie musimy z flag bi-
towych rezygnować. Wymaga to
tylko zastosowania nieco innych
(ale standardowych dla C) metod.
Działania na pojedynczych bitach
będziemy wykonywać za pomocą
operatorów bitowych (iloczyn and
& , suma or | i suma wyłączna
ex–or ^ – nie pomylmy jej z po-
pularnym zapisem potęgowania!)
oraz tzw. masek bitowych. Maska
to po prostu liczba całkowita o po-
trzebnym rozmiarze ( char , int , long )
z ustawionymi (1) tylko określonymi
bitami. Dla „zapalenia” jednego wy-
branego bitu w zmiennej sumujemy
ją bitowo z maską o zgodnym roz-
miarze, w której tylko ten bit jest
ustawiony, np. dla bitu nr 3:
1100 0010 or 0000 1000 = 1100 1010
Dla zgaszenia tego samego bitu
używamy iloczynu zmiennej z ma-
Maski potrzebne w powyższych
działaniach uzyskujemy przesuwając
w lewo liczbę jeden (czyli z usta-
wionym bitem nr 0) o ilość miejsc
zgodną z lokalizacją bitu poddawa-
nego działaniu:
0000 0001 << 1 = 0000 0010
0000 0001 << 5 = 0010 0000
i ewentualnie poddając je negacji
(użyta w powyższych przykładach
maska będzie więc wyrażona jako
1<<3).
Biblioteka avr–libc przewidziała
dla tworzenia masek pomocnicze
makro _BV(numer_bitu) – jest ono
całkowicie równoważne z zapisem
(1<<numer_bitu) i może być stoso-
wane zamiennie według własnych
preferencji (użycie _BV wymaga
dołączenia na początku kodu syste-
mowego pliku io.h , #include <avr/
io.h> ). Systemowe pliki nagłówkowe
opisu poszczególnych mikrokontrole-
rów zawierają także nazwy poszcze-
gólnych bitów w rejestrach SFR, co
pozwala podczas konfiguracji SFR
na użycie symboli zgodnych z do-
kumentacją Atmela zamiast niewiele
mówiących numerów bitu.
Zróbmy kilka testowych działań
bitowych na zmiennej long z uży-
ciem różnych masek (w oknie pod-
glądu AvrStudio ustawmy format na
hex żeby łatwo ocenić wynik), np.:
volatile unsigned long flagi;
(Ponownie zwróćmy uwagę na
deklarację volatile , która nie pozwa-
la optymalizatorowi na usunięcie
z kodu operacji pośrednich, nie ma-
jących wpływu na końcową wartość
zmiennej).
flagi |= _BV(5);
flagi |= _BV(12);
Zapiszmy więc w naszym kodzie
np.:
typedef char boolean;
W ten sposób określiliśmy wła-
sny nowy typ boolean – całkowicie
zgodny z char ale wyróżniający się
w kodzie oddzielną nazwą – którego
teraz możemy używać do definio-
wania zmiennych logicznych, np.:
boolean mybool = true;
AvrStudio poprawnie identyfi-
kuje typy zdefiniowane za pomocą
typedef . Natomiast AvrSide pozwala
na dołączenie nowej nazwy do li-
sty słów kluczowych, a tym samym
jej odpowiednie kolorowanie w ko-
dzie (po zaznaczeniu nazwy – np.
dwukrotnym kliknięciem na niej
– używamy skrótu CTRL+K ). Li-
sta dodatkowych słów kluczowych
jest dostępna w dialogu Ustawienia
na zakładce AvrSide (w tym miej-
scu można zbędne słowa usuwać
Oczywiście dla czytelności mo-
żemy w każdej chwili zdefiniować
własną nazwę dla konkretnego bitu
i używać jej zamiast liczby:
#define Flaga1 7
flagi |= _BV(Flaga1);
flagi &=~ _BV(Flaga1);
Zwróćmy przy okazji uwagę na
kilka pułapek:
BV zwraca domyślnie typ si-
104
Elektronika Praktyczna 6/2005
27840986.009.png 27840986.010.png 27840986.011.png 27840986.001.png 27840986.002.png
K U R S
Rys. 13. Automatyczna lista pól unii / struktury
wycofane z avr–libc
(rozdział Deprecated
List w podręcznku) –
w zamian pojawiła się
niedostępna wcześniej
możliwość użycia SFR
bezpośrednio w instruk-
cjach przypisania. Nie
będziemy więc pisać
np. outp (0x55, DDRB);
ale po prostu DDR-
B=0x55; (należy za-
uważyć, że ceną tego
postępu mogą być czasem nieste-
ty kłopoty ze starymi projektami).
Zwróćmy uwagę, że kompilator sa-
modzielnie wykonuje rozróżnienie
pomiędzy rejestrami IO a rejestrami
rozszerzonymi i stosuje odpowiednie
instrukcje. Np. jeśli zmienimy typ
procesora na Atmega 128 (domyśl-
na w AvrSide Atmega 8 nie używa
rozszerzonych SFR) i wypróbujemy
zapis do portu B (przestrzeń IO)
oraz G (adres rozszerzony 0x65)
dostaniemy kod:
PORTB=0x55;
252: 25 e5 ldi r18, 0x55 ;
85
254: 28 bb out 0x18, r18 ;
24
PORTG=0x55;
256: 20 93 65 00 sts 0x0065, r18
Dla portu B użyty został skróco-
ny rozkaz out. Dla portu G byłby
on nieprawidłowy i kompilator stosuje
zwykły zapis do pamięci sts. Podob-
nie jest przy obsłudze pojedynczych
linii, np. dla ustawiania i gaszenia:
PORTB |=_BV(PB2);
252: c2 9a sbi 0x18, 2 ; 24
PORTG |=_BV(PG2);
254: 80 91 65 00 lds r24, 0x0065
258: 84 60 ori r24, 0x04 ; 4
25a: 80 93 65 00 sts 0x0065, r24
oraz
PORTB &=~_BV(PB2);
252: c2 98 cbi 0x18, 2 ;
24
PORTG &=~_BV(PG2);
254: 80 91 65 00 lds r24, 0x0065
258: 8b 7f andi r24, 0xFB
; 251
25a: 80 93 65 00 sts 0x0065, r24
albo dla sprawdzania stanu li-
bit_is_set(sfr, bit) sprawdza
ustawienie (1) bitu nr bit w reje-
strze sfr
bit_is_clear(sfr, bit) to samo
tylko dla bitu zgaszonego (0)
loop_until_bit_is_set(sfr, bit) pę-
tla oczekiwania na ustawienie (1)
bitu nr bit w rejestrze sfr
loop_until_bit_is_clear(sfr, bit)
to samo tylko oczekiwanie na zga-
szenie (0) bitu.
Pętle oczekujące należy stosować
rozważnie, – jeśli wprowadzimy
warunek, który nigdy nie zaistnieje,
zatrzymamy cały program (ewen-
tualnie zresetujemy mikrokontroler
o ile jest włączony watchdog).
Przy okazji należy podkreślić, żeby
nie mylić makra z funkcją, chociaż są
często bardzo podobne w składni.
Funkcja jest wywoływana dy-
namicznie w trakcie działania pro-
gramu z chwilowymi, nieznanymi
w chwili kompilacji, wartościami
argumentów. Makro jest wykonane
(rozwinięte) jednorazowo w trakcie
kompilacji przez preprocesor, a wsta-
wiane argumenty muszą być z góry
określone w kodzie.
Dyrektywy definiujące oraz makra
pozwalają bardzo mocno poprawić
czytelność kodu programu. Na przy-
kład podłączmy do linii PB0 i PB1
dwa ledy, zielony i czerwony, zasila-
ne z V cc przez rezystory ograniczające
– czyli zapalane przy niskim stanie
linii. Zamiast cały czas pamiętać
o tej konfiguracji ikażdorazowo uży-
wać uniwersalnych instrukcji opisa-
nych powyżej, po prostu zdefiniujmy
potrzebne operacje odpowiednio je
nazywając (nazwy makr zwyczajowo
pisze się dużymi literami):
#define LED_Z PB0
#define LED_CZ PB1
#define ZAPAL_Z (PORTB &=~_BV(LED_Z))
#define ZGAS_Z (PORTB |= _BV(LED_Z))
#define PRZELACZ_Z (PORTB ^= _BV(LED_
Z))
#define ZAPAL_CZ (PORTB &=~_BV(LED_CZ))
#define ZGAS_CZ (PORTB |= _BV(LED_CZ))
#define PRZELACZ_CZ (PORTB ^= _BV(LED_
CZ))
Wpis w kodzie np. ZGAS_CZ;
jest jednoznaczny i czytelny, a do-
datkowo zmniejsza ryzyko wystą-
pienia błędu przy wielokrotnym
użyciu. Dodatkowym ułatwieniem
stosowania jest system podpowie-
dzi AvrSide: klawisz F1 przy karet-
ce ustawionej na nazwie symbolu
wyświetla okienko z jego deklaracją
( rys. 12 ), natomiast skrót SHIFT
+ F1 wyszukuje wszystkie miejsca
występowania symbolu w kodzie.
Język C dostarcza jeszcze jeden
sposób używania flag – są to pola
bitowe. Zadeklarowanie flag jest
gned int , przesunięcie o 15 pozycji
(ustawiony najstarszy bit) powoduje
potraktowanie wyniku jako liczby
ujemnej – rozwiązaniem jest rzuto-
wanie typu na wymagany: flagi |=
(unsigned int) _BV(15 ;
Przy przesunięciach większych
niż 15 otrzymujemy ostrzeżenie
o przekroczeniu rozmiaru typu.
Dzieje się tak dlatego, że jedynka
w wyrażeniu (1 << n) jest domyśl-
nie traktowana jako int o szeroko-
ści 16 bitów. Makro _BV nie da
sobie już z tym przypadkiem rady,
użyjmy jawnego przesunięcia z od-
powiednim rzutowaniem typu: flagi
|= (unsigned long)1<<16;
Jest to o tyle mniej istotne, że
_BV() zostało w zasadzie przezna-
czone do obsługiwania 8–bitowych
rejestrów SFR gdzie takie problemy
nie wystąpią – jednak dobrze ilu-
struje różnego rodzaju niespodzian-
ki związane z typami i zakresami
zmiennych.
W podobny sposób sprawdzamy
stan bitu: tworzymy iloczyn zmien-
nej i odpowiedniej maski i kontrolu-
jemy czy jest równy zero czy nie.
Na przykład w instrukcji warunko-
wej możemy bezpośrednio skorzy-
stać z reguły, że każda wartość nie-
zerowa odpowiada logicznej praw-
dzie:
if(flagi & _BV(3)) { coś wykonu-
jemy } – wykonanie nastąpi przy
ustawionym bicie 3 w zmiennej
flagi (przy okazji obejrzyjmy wyge-
nerowany kod, przekonamy się, że
dla typu long jest on mocno skom-
plikowany więc w miarę możliwo-
ści w praktyce ograniczajmy rozmiar
zmiennych używanych w takim celu;
dla flagi typu char kod jest już kró-
ciutki i przejrzysty).
Identyczne reguły zalecane są
przy obsłudze linii wejść / wyjść
portów mikrokontrolera (i ogólnie
przy dostępie do rejestrów SFR).
Popularne dawniej pomocnicze ma-
kra sbi, cbi, outp, inp są obecnie
nii:
if(PINB & _BV(PB2)) PORTA |= _BV(PA0);
252: b2 99 sbic 0x16, 2 ; 22
254: d8 9a sbi 0x1b, 0 ; 27
if(PING & _BV(PG2)) PORTA |= _BV(PA1);
256: 80 91 63 00 lds r24, 0x0063
25a: 82 fd sbrc r24, 2
25c: d9 9a sbi 0x1b, 1 ; 27
Powyższe operacje możemy dla
nabrania wprawy prześledzić w Avr-
Studio, które w pełni wspiera obsłu-
gę oraz podgląd linii we/wy portów
(potrzebna będzie oczywiście rów-
nież zmiana procesora albo utwo-
rzenie nowej sesji).
W avr–libc ( \include\avr\sfr_defs.h )
znajdziemy kilka dodatkowych (zre-
alizowanych za pomocą opisanych
powyżej operacji bitowych) makr
obsługi linii:
Elektronika Praktyczna 6/2005
105
27840986.003.png 27840986.004.png
K U R S
w tym przypadku niestety bardziej
skomplikowane (szczegółowe infor-
macje o stosowanych tu strukturach
oraz uniach znajdziemy w każdym
podręczniku C):
Najpierw definiujemy typ struk-
tury z potrzebną liczbą jednobito-
wych flag (np. 8, wtedy flagi zaj-
mują dokładnie jeden bajt:
typedef struct
{
unsigned char Flag1:1;
unsigned char Flag2:1;
unsigned char Flag3:1;
unsigned char Flag4:1;
unsigned char Flag5:1;
unsigned char Flag6:1;
unsigned char Flag7:1;
unsigned char Flag8:1;
} FlagBits;
(zamiast przy każdym polu
wpisywać oddzielnie typ unsigned
możemy w opcjach kompilacji za-
znaczyć pozycję „zmienne z polami
bitowymi domyślnie bez znaku”).
Następnie definiujemy typ unii,
w której jedną z możliwych zawar-
tości jest nasza struktura z polami
bitowymi a drugą (zamienną) zwy-
czajny bajt bez znaku:
typedef union
{
FlagBits Bits;
uchar Byte;
} Flags;
Jeśli teraz z programie zadeklaru-
jemy zmienną typu Flags to może-
my się odwoływać zarówno jednora-
zowo do całego bajtu poprzez pole
Byte (może być to przydatne przy
operacjach wspólnych np. szybkim
wyzerowaniu wszystkich flag) jak
i do poszczególnych flag (bitów) po-
przez pola Bits.FlagX :
Flags MojeFlagi;
//..................
MojeFlagi.Byte=0; // wyzerowanie
wszystkich flag
MojeFlagi.Bits.Flag1 = true;// ustawie-
nie pierwszej flagi
MojeFlagi.Bits.Flag5 = false; // wyze-
rowanie 5. flagi
W takich zapisach znów dopo-
może system podpowiedzi AvrSide
wyświetlający po kropce listę wy-
boru dostępnych w unii/strukturze
pól ( rys. 13 ) (dla działania wymaga
zapisania pliku po zadeklarowaniu
unii lub struktury).
Dodatkowo możemy nazwać każ-
dą flagę w czytelny sposób ( #defi-
ne MOJA_FLAGA MojeFlagi.Bits.Flag1
itp.) i używać jej wtedy jak każdej
innej zmiennej logicznej (MOJA_
FLAGA = true; if(MOJA_FLAGA) {}) .
Jeśli zajrzymy do wygenerowanego
kodu, to stwierdzimy, że pomimo
skomplikowanych deklaracji jest on
bardzo krótki i efektywny:
MojeFlagi.Bits.Flag3=true;
64: 80 91 78 00 lds r24, 0x0078
68: 84 60 ori r24, 0x04 ; 4
6a: 80 93 78 00 sts 0x0078, r24
Jak widać pozornie zawikłany me-
chanizm jest ostatecznie bardzo efek-
tywny i przejrzysty w działaniu jed-
nocześnie maksymalnie oszczędzając
pamięć danych. Ma on jednak jeden
– istotny w naszym środowisku uru-
chomieniowym – mankament: AvrStu-
dio nie potrafi (przynajmniej w chwili
pisania artykułu) obsługiwać pól bito-
wych. Jeśli więc zależy nam na pod-
glądzie zmiennych logicznych w trak-
cie debugowania użyjemy ich w wer-
sji tradycyjnej. Można też ewentualnie
– zanim zespół Atmela wprowadzi
odpowiednie udoskonalenia do Avr-
Studio – oglądać bezpośrednio pole
Byte unii w zapisie heksadecymalnym,
ale nie jest to zbyt wygodne.
Jerzy Szczesiul, EP
jerzy.szczesiul@ep.com.pl
UWAGA!
Środowisko IDE dla AVR-GCC opracowane
przez autora artykułu można pobrać ze
strony http://avrside.ep.com.pl.
PRENUMERATĘ ELEKTRONIKI PRAKTYCZNEJ
NAJWYGODNIEJ ZAMAWIAĆ SMS-EM!
Wyślij SMS o treści PREN na numer 0695458111 ,
my oddzwonimy do Ciebie
i przyjmiemy Twoje zamówienie.
(koszt SMS-a według Twojej taryfy).
106
Elektronika Praktyczna 6/2005
27840986.005.png 27840986.006.png 27840986.007.png
Zgłoś jeśli naruszono regulamin