Redakcja: Dondu
Artykuł jest fragmentem cyklu: Kurs mikrokontrolerów XMega by Leon-Instruments
Porty są najprostszym układem peryferyjnym każdego mikrokontrolera. Mimo to, w XMEGA do obsługi portu mamy aż… 21 rejestrów na każdy port! Wszystkich rejestrów konfiguracyjnych w procesorze może być kilkaset lub nawet ponad tysiąc! W tym artykule pokażę jak to ogarnąć i nie zwariować. Choć początek tego artykułu może wydawać się trochę mętny – proszę się nie zniechęcać, bo w dalszej części zamieściłem proste praktyczne przykłady.
Wartości można wpisywać do rejestrów w sposób znany ze starszych mikrokontrolerów AVR, czyli:
NAZWA_REJESTRU = (1<<BIT1) | (1<<BIT2);
Jednak mając do dyspozycji kilkaset rejestrów, taki sposób staje się mało wygodny. Inżynierowie Atmela wpadli na pomysł, by do konfiguracji rejestrów wykorzystać struktury, przez co kod wygląda nico inaczej:
NAZWA_PERYFERIUM.NAZWA_REJESTRU = ...;
Tak jak pisałem w poprzednim artykule, układy peryferyjne mikrokontrolerów XMEGA są wielokrotnie powielone, a różnią się jedynie adresami rejestrów w pamięci oraz nazwą peryferium (PORTA, PORTB, PORTC…). Poza tym wszystko jest identyczne. Można zatem wpisać jakąś wartość do rejestrów portów w ten sposób:
PORTA.DIR = ...; PORTB.DIR = ...; PORTC.DIR = ...; PORTA.OUT = ...; PORTB.OUT = ...; PORTC.OUT = ...;
Pisanie kodu programu przy użyciu struktur niesie ze sobą bardzo ważną zaletę – raz napisany kod dla jakiegoś peryferium może być użyty do obsługi wszystkich jego kopii. Tak więc jeśli mamy do dyspozycji 8 interfejsów USART, to w przypadku starych AVR¬-ów funkcje obsługujące USART należałoby skopiować osiem razy i pozmieniać w nich nazwy rejestrów.
W przypadku XMEGA wystarczy napisać funkcję raz, a jako argument podać jaki konkretnie układ peryferyjny nas interesuje.
Zobaczmy przykład, w jaki sposób można sterować różnymi portami przy pomocy jednej funkcji:
void UstawPort(PORT_t *nazwaportu) { nazwaportu->DIR = ...; nazwaportu->OUT = ...; }
Funkcja jako argument przyjmuje nazwę portu. Sposób jej użycia wygląda następująco:
UstawPort(&PORTA); UstawPort(&PORTB); UstawPort(&PORTC);
Dzięki zastosowaniu funkcji operującej na strukturach, można znacząco zmniejszyć rozmiar programu. Choć w przypadku portów, sposób ten może wydawać się trochę bez sensu, to zapewniam, że przy bardziej skomplikowanych peryferiach taki sposób zdecydowanie przyspiesza pisanie programu.
Do rejestrów można wpisywać wartości heksadecymalne lub binarne, jednak jest to proszenie się o błędy i marnowanie czasu. Takich metod lepiej nie stosować!
Dopuszczalny jest sposób znany ze starych AVR-ów, wykorzystujący operator przesunięcia bitowego << oraz predefiniowane symbole z końcówką _bp, czyli bit posiotion, określające numer bitu w rejestrze.
PORTA.DIR = (1<<PIN1_bp) | (1<<PIN0_bp);
Dostępna jest nowa metoda, wykorzystująca predefiniowane symbole z końcówką _bm, czyli bit mask. Dzięki wyeliminowaniu znaczków-krzaczków zapis staje się bardziej czytelny.
PORTA.DIR = PIN1_bm | PIN0_bm
Bardziej skomplikowane peryferia mogą mieć kilka bitów odpowiedzialnych za realizację jakiegoś procesu. Dobrym przykładem jest tu źródło taktowania timera, wybierane przy pomocy czterech bitów. Stosujemy w takim przypadku symbole z końcówką _gc (group configuration). Aby zilustrować przykład, zobaczmy, jakie mamy możliwości ustawienia źródła taktowania i preskalera w timerze.
Group configuration |
Zatem, by ustawić preskaler timera TCC0 na wartość 64, musimy do rejestru CTRLA wpisać odpowiednią grupę konfiguracyjną CLKSEL. Wszystko wyjaśnia przykład:
TCC0.CTRLA = TC_CLKSEL_DIV64_gc;
Atmel Studio posiada bardzo przydatną funkcję przewidywania, co programista zamierza wpisać, przez co program podpowiada, jakie są dostępne możliwości.
Pomoc Atmel Studio |
PERYFERIUM.REJESTR = CONFIG1_gc | // komentarz CONFIG2_gc | // komentarz CONFIG3_bm | // komentarz CONFIG4_bm ; // komentarz
Nic nie stoi na przeszkodzie, by predefiniowane symbole były argumentami funkcji. Na przykład, można napisać funkcję konfigurującą jakiś układ peryferyjny i wywoływać ją w ten sposób:
TimerInit(&TCCO, TC_CLKSEL_DIV64_gc, inne argumenty...); TimerInit(&TCC1, TC_CLKSEL_DIV2_gc, inne argumenty...); TimerInit(&TCD0, TC_CLKSEL_EVCH0_gc, inne argumenty...); TimerInit(&TCD1, TC_CLKSEL_OFF_gc, inne argumenty...);
W ten sposób przy pomocy jednej funkcji TimerInit skonfigurowaliśmy cztery timery o nazwach TCC0, TCC1, TCD0, TCD1.
Więcej opisów i przykładów jest w materiale szkoleniowym dostępnym na stronie firmy Atmel AVR1000: Getting Started Writing C-code for XMEGA (kopia 2013r.) oraz w książce Tomasza Francuza AVR. Praktyczne projekty.
Dominik Leon Bieczyński
leon-instruments.pl
Dondu dziękuję za to co robisz. Jesteś wielki. Twój blog staje sie kopalnią wiedzy.
OdpowiedzUsuńFredy
W tym wypadku to zasługa Dominika :-)
UsuńKopalnią to już jest. Teraz powoli robi się z tego biblia!
UsuńRównież dziękuję wszystkim autorom.
Jedyny sensowny polski portal o tej tematyce, poszczególne peryferia i funkcje opisane świetnie, krok po kroku. Jedynie życzyć aby dalej się rozwijał.
UsuńDziękuję :)
OdpowiedzUsuń