Porty mikrokontrolera - Przegląd rozwiązań

Pozostałe układy mikrokontrolerów, układy peryferyjne i inne, nie mieszczące się w powyższych kategoriach.
Awatar użytkownika
ZbeeGin
User
User
Posty: 270
Rejestracja: sobota 08 lip 2017, 17:16
Lokalizacja: GOP
Kontaktowanie:

Porty mikrokontrolera - Przegląd rozwiązań

Postautor: ZbeeGin » niedziela 26 sie 2018, 09:01

W tym artykule, chciałbym nieco przybliżyć wszystkim podstawowe zagadnienie związane z rozpoczynaniem pracy z każdym mikrokontrolerem: porty zewenętrzne i ich konfiguracja. Ma to być przegląd rozwiązań, zatem nie będziemy się skupiać na jednym tylko typie mikrokontrolera, i przejdziemy płynnie od najmniej zaawansowanego do najbardziej zaawansowanego.

Wstęp

Jak wiadomo mikrokontroler komunikuje się ze światem znajdującym się poza własną strukturą za pomocą swoich wyprowadzeń. Z reguły większość z nich to uniwersalne porty wejścia-wyjścia, zwane GPIO (General Pheripherial Input-Output). Zwykle ich konstrukcja opiera się na wykorzystaniu układu tranzystorowego typu push-pull. Uproszczony schemat takiego wyjścia portu mamy poniżej:

rys1.png

Jak widać na rysunku, mamy tu dwa komplementarne tranzystory, które będą się otwierać w zależności od stanu wewnętrznej linii SYGNAŁ. Jeśli na końcówce portu ma zostać wystawiona jedynka logiczna to będzie przewodzić górny tranzystor i wymusi on napięcie mniej więcej równe VCC. Jeśli zaś na port miałby zostać wystawione zero logiczne to dolny tranzystor będzie nam ściągał pin wyjściowy do poziomu GND.
Diody jakie widzicie bezpośrednio na wyjściu to diody zabezpieczające przed pojawianiem się na wyjściu napięć wyższych niż VCC lub niższych od GND. Zwane one są diodami "clamp" i są już stosowane jako standard. O istnieniu tych diod można się przekonać czytając noty katalogowe, gdzie np. określa się maksymalne napięcie wejściowe w stanie wysokim jako VCC+0,5V. Te pół wolta to właśnie spadek na diodach clamp.

Pewnie zauważyliście, że w takim układzie mamy tylko wyjście, a przecież mówimy tu o portach wejścia-wyjścia. Oczywiście o tym też pomyślano. Do końcówki wyjściowej podłączono jeszcze bufor wejściowy, którzy może przekazać aktualny stan końcówki.

rys2.png

W takim prostym układzie bieżący stan wyjścia jaki wystawiają tranzystory będzie przez bufor powtarzany. Odpowiedni układ na bramkach tranzystorów może na przykład ustawić stan równowagi poza zakresem poziomów logicznych i nie zakłócający wejścia bufora. Wtedy niepodłączona końcówka będzie określana jako pływająca, a bufor nie będzie jednoznacznie mógł zinterpretować stanu jaki wystawiają tranzystory. Dopiero doprowadzając sygnał z zewnątrz będziemy wymuszać określony stan.

To tyle wstępnej teorii. Teraz czas na rozwiązania praktyczne.

INFO: W tekście będę operować często na pełnej szerokości portów w przykładach. Wiedzcie jednak, że współczesne kompilatory pozwalają na wyłuskiwanie poszczególnych bitów. Zatem konfiguracja portu nie musi się odbywać dla wszystkich jego linii. Można na przykład operować tylko na bicie 1. W poszczególnych mikrokontrolerach jest to różnie definiowane, stąd takie uproszczenie.

Mikrokontrolery MCS-51 - czyli staruszek i8051.

Zacznę może od nieco historycznego już układu. i8051 to w pełni 8-bitowy mikrokontroler, który posiada w podstawowej wersji dość proste cztery porty GPIO. Każdy z nich obsługuje jeden rejestr portu znajdujący się w przestrzeni SFR (Special Function Registers). Skupimy się na razie tylko na porcie P1 pomijając pozostałe, które posiadają odrobinę inną konstrukcję.

rys3.png

Widać tu duże podobieństwo do naszego uproszczonego układu. Zamiast górnego tranzystora mamy rezystor pull-up, który wymusza nam stan wysoki jeśli dolny tranzystor nie jest otwarty. W zasadzie to nie jest rezystor, a odpowiednio wysterowany tranzystor. Do tego mamy dwa bufory odczytu: jeden czyta bezpośrednio z końcówki, drugi czyta z zatrzasku portu.

INFO: To dlatego zaleca się by np. diody sterowane bezpośrednio z końcówki mikrokontrolera sterować katodą od strony masy. Wtedy możemy uzyskać większy prąd wpływający. Praca diody przy sterowaniu anodą była by tu sporym obciążeniem dla tego rezystora pull-up.

Konfiguracja takiego porty w układach '51 jest uproszczona do minimum. Widzimy tylko jeden uniwersalny rejestr zwany tak samo jak port. Jeśli do tego rejestru wpiszemy zera to nasz przerzutnik uruchomi dolne tranzystory ściągając końcówki do zera. Jeśli zaś wpiszemy jedynki to rolę wymuszacza stanu wysokiego będzie pełnił rezystor pull-up.

A co z wejściem? Tutaj konstruktorzy wymyślili dość prosty mechanizm. Jeśli wpiszemy 1 do rejestru to dolny tranzystor pozwoli na działanie pull-up-a i mamy po prostu wejście z podciąganiem do plusa, które to bezpiecznie możemy ściągnąć do poziomu masy w przypadku doprowadzenia stanu niskiego. M.in. dlatego wydajność prądowa wyjścia w stanie wysokim jest dość niska.

Zapis stanu portu jak i odczyt odbywają się przez ten sam rejestr. Oczywiście odpowiednie układy będą dbać o to by czytać przez właściwy bufor. Przy operacjach wejścia będzie czytany dolny bufor bezpośredni, a przy operacjach typu: odczytaj-zmień-zapisz (Read-Modify-Write) górny bufor, który czyta stan przerzutnika.

Dlatego też jeśli chcemy zapisać stan portu to piszemy po prostu:
P1 = 0b00001111; // połowa portu będzie wystawiać zera, druga połowa jedynki


Zaś jeśli chcemy odczytywać to najpierw musimy wpisać jedynki, a potem możemy już odczytywać stan aktualny:
P1 = 0x0FF;
(...)
if (P1 == 0x01) {
/* instrukcje do wykonania */
};

Nieco odmiennie zbudowany jest zaś port P0, który może pracować jako GPIO lub jako przedłużenie szyny adresowej/danych podczas współpracy z zewnętrzną pamięcią programu.

rys3_1.png

Widzimy na rysunku oba tranzystory układu push-pull. Niestety górny tranzystor jest wyłączany gdy port ten pracuje jako zwykłe GPIO. Dlatego w przypadku takiej pracy tego portu musimy dodać zewnętrzne rezystory pull-up. Chyba, że świadomie chcemy mieć wyjście typu Open-Drain.

Podobna sytuacja, ale z nieco innych powodów zachodzi w małych układach rodziny '51 produkcji Atmel-a: AT89C2051. Tam piny P1.0 i P1.1 pełnią rolę wejść wewnętrznego komparatora, stąd też nie posiadają wewnętrznego podciągania.

Mikrokontrolery Atmel AVR.

Firma Atmel rozpoczęła swoją przygodę od produkcji układów zgodnych z 8051, ale z pamięcią wewnętrzną typu Flash. Był to dość poważny skok jakościowy i szybko te modele stały się popularne. Jednak firma nie osiadła na laurach i przy projektowaniu swojej własnej rodziny mikrokontrolerów: AVR zauważono pewne niedogodności związane z posiadaniem tylko jednego rejestru dla portu we-wy. Dlatego w tych procesorach mamy już trzy rejestry związane z portami:

1. DDRx - Rejestr ustalający w którym kierunku działa port.
2. PORTx - Rejestr wyjściowy portu, albo rejestr sterujący podciąganiem wejścia.
3. PINx - Rejestr wejściowy do bezpośredniego czytania stanu końcówki lub zmiany jej stanu na przeciwny.

Popatrzmy teraz na strukturę potu.

rys4.png

Od razu widać, że jest o wiele bardziej skomplikowany, choć struktura dalej przypomina tą jaką poznaliśmy na początku.

Praca jako wyjście.

W przypadku pracy jako wyjście musimy uaktywnić część znajdującą się w środkowej części tego rysunku. Mamy tam dwa przerzutniki i bufor pracujący trójstanowo. Górny przerzutnik odpowiada za załączenie bufora wyjściowego i jest połączony z rejestrem portu DDRx. Dlatego aby praca jako wyjście była możliwa trzeba wpisać jedynki do DDRx. Wtedy stan drugiego przerzutnika połączonego z rejestrem PORTx będzie przekazywany na końcówkę portu.
Tu można zauważyć też pewien sprytny układ ze sprzężeniem zwrotnym za pomocą multipleksera. Układ ten jest połączony z rejestrem PINx. W niektórych procesorach AVR - zwłaszcza w tych nowszych - rejestr PINx może też pełnić rolę szybkiego przełącznika stanu portu. Przy pracy jako wyjście, zapisanie jedynki do rejestru PINx spowoduje szybkie przełączenie stanu przerzutnika. Stąd nie potrzebujemy trzech operacji Read-Modify-Write. Wystarczy tylko jeden szybki zapis!

Podsumowując, praca jako wyjście będzie realizowana tak:
DDRA  = 0xFF;  // aktywacja wyjścia
PORTA = 0xFF; // wymuszamy stany wysokie na wyjściach

PINA = 0xFF; // tak możemy szybko przełączyć stan wyjścia na przeciwny


Ponieważ układ odczytu jest ciągle aktywny przy aktywnym stanie procesora, to odczyt z rejestru PINx będzie odczytywał rzeczywisty stan portu. Aby nie doszło jednak do pewnych konfliktów i gry na zboczach dodano prosty układ synchronizacji, który opóźnia o dwa takty ten sygnał. Przy pisaniu programów w językach wysokiego poziomu ten mechanizm jest ukrywany i kod jest generowany tak by nie doszło do pewnych perturbacji przy natychmiastowym odczycie wystawionego stanu. Przy programowaniu w kodzie maszynowym trzeba o tym pamiętać i odpowiednio napisać kod dodając np. pustą instrukcję.

INFO: W przeciwieństwie do 8051 układy AVR mogą sterować - w ograniczonym jednak stopniu - diody LED bezpośrednio z końcówek. Dzieje się tak, ponieważ deklarowana wydajność prądowa portu w stanie wysokim to umożliwia.

Praca jako wejście, z podciąganiem lub bez.

W przypadku pracy jako wejście mamy stale uaktywnioną część znajdującą się na dole rysunku i ewentualnie część górną z tranzystorem. Nasz górny przerzutnik jeśli nie będzie aktywny i w DDRx będzie 0 zablokuje nam bufor wyjściowy, a dolny przerzutnik będzie kontrolował wtedy tylko tranzystor podciągający przez stan rejestru PORTx. Wpisując jedynkę do przerzutnika górna bramka załączy tranzystor.

Podsumowując, praca jako wejście będzie realizowana tak:
DDRA  = 0x00;  // aktywacja wejścia
PORTA = 0xFF; // możemy wymusić podciągnięcie wpisując jedynki lub nie wpisując zera

if (PINA == 0xFF) {
/* instrukcje do wykonania */
};


Mikrokontrolery Texas Intruments MSP430.

Mikrokontrolery MSP430 - mało u Nas znane - to 16-bitowe jednostki. Do niedawna były one liderem w energooszczędności i są dalej wykorzystywane w takich aplikacjach. Pomimo, iż rdzeń pracuje w domenie 16-bitów to dostęp do urządzeń peryferyjnych zwykle odbywa się już magistralą 8-bitową. Stąd dla każdego portu GPIO przeznaczono cztery 8-bitowe rejestry związane z pracą portu:

1. PxDIR - Rejestr ustalający w którym kierunku działa port.
2. PxOUT - Rejestr wyjściowy portu.
3. PxIN - Rejestr wejściowy do czytania stanu końcówki.
4. PxSEL - Rejestr wyboru pracy portu jako GPIO bądź układu peryferyjnego.

Popatrzmy teraz na strukturę potu.

rys5.png

Doszedł nowy rejestr i układ portu musi zostać dodatkowo obudowany logiką kombinacyjną w postaci dwóch multiplekserów. Tutaj od razu powinno się Wam rzucić w oczy to, że dany pin może pracować tylko w jednym kierunku. Odpowiednie bufory w prawej części są blokowane lub aktywowane i nie ma możliwości wystawiania stanu i jednoczesnego jego czytania wprost z końcówki.

Praca jako wyjście zwykłego portu.

W przypadku pracy jako zwykłe wyjście musimy uaktywnić drugi od góry MUX podłączony do rejestru PxOUT oraz bufory z prawej przełączyć w tryb wyjściowy. Odbywa się to przez odpowiednie ustawienie rejestru PxDIR. Podobnie jak w przypadku AVR musimy wpisać do PxDIR jedynki by taki stan uzyskać. Wtedy stan rejestru PxOUT będzie przekazywany na końcówkę portu.

P1SEL = 0x00;  // praca jako zwykły GPIO
P1DIR = 0xFF; // praca jako wyjście
P1OUT = 0xFF; // wymuszenie stanów wysokich na wyjściach
(...)
P1OUT = 0x00; // wymuszenie stanu niskiego na wyjściach


Ponieważ układ buforów jest przełączony na wyjście to odczyt z rejestru PxIN będzie odczytywał tylko stan wyjściowy MUX-a a nie rzeczywisty stan końcówki, którą np. urządzenie peryferyjne może ściągnąć do masy.

Praca jako wejście zwykłego portu.

W przypadku pracy jako zwykłe wejście musimy przełączyć bufory z prawej w tryb wejściowy. Odbywa się to przez wyzerowanie rejestru PxDIR. Wtedy stan końcówki będzie mógł być przekazany do rejestru PxIN. Nie mamy tutaj możliwości włączenia wewnętrznego podciągania. Trzeba je dodać na zewnątrz.

P1SEL = 0x00;  // praca jako zwykły GPIO
P1DIR = 0x00; // praca jako wejście

if (P1IN == 0xFF) {
/* instrukcje do wykonania */
};


W nowszych mikrokontrolerach MSP430 konstruktorzy rozwiązali problem braku podciągania dodając kolejny rejestr PxREN. Zatem nie każdy mikrokontroler z rodziny MSP430 tą funkcję posiada i trzeba uważnie przeczytać Reference Manual dla danej rodziny. Za jego pomocą można włączyć podciąganie pull-up, lub pull-down jeśli zapiszemy do niego jedynki a stan rejestru PxDIR wskazuje na pracę jako wejście. Kontrolą nad tym, czy będzie to pull-up czy pull-down zajmuje się wtedy rejestr PxOUT.

Praca jako wyjście lub wejście układu peryferyjnego.

Kierunkowość pracy portu obowiązuje nas także jeśli zamiast zwykłego GPIO na danej końcówce ma się pojawić sygnał z lub do urządzenia peryferyjnego, np. USART. Dlatego ta część odpowiedzialna za kierunek jest taka sama jak w powyższych przypadkach. Jedyną zmianą jaką należy wykonać to przestawić rejestr PxSEL by przełączyć oba MUX-y odłączając rejestry PxIN/PxOUT. Zmiany rejestru PxOUT nie będą odzwierciedlane na wyjściach, a PxIN będzie powtarzał stan rejestru wyjściowego lub stan wejścia układ peryferyjnego.

Kod: Zaznacz cały

P1SEL = 0xFF;  // praca jako peryferia
P1DIR = 0xFF;  // praca jako wyjście

P1DIR = 0x00;  // praca jako wejście


Rejestrami sterującymi komutacją pinów dla układu przerwań - część dolna struktury portu - nie będziemy się na razie tu zajmować.

Mikrokontrolery Microchip PIC.

Mikrokontrolery Microchip PIC w przypadku portów we-wy mają sporą gamę rozwiązań układowych do sterowania końcówkami portów. Trzeba zatem pamiętać, że prawie każdy port jest tu skonstruowany inaczej. Co więcej, niektóre linie jednego portu wykazują tu również pewne różnice.

ProTip: Dlatego zawsze należy zajrzeć do noty katalogowej danego procesora, jakie porty są dostępne i czy nie ma tam wyjątków.

PORTA lub GPIOx
Generalnie PORTA/porty GPIOx mają konstrukcję typu push-pull. Wyjątek stanowi pin 4 PORTA gdzie pracuje on tylko jako open-drain.

rys6.png
rys6_1.png

Mamy tu trzy przerzutniki. Przerzutnikiem TRIS ustala się, czy dany port będzie pracował jako port wejścia czy jako port wyjścia. W przypadku wpisania do niego stanu 1 stan jego wyjść będzie blokował tranzystory wyjściowe, pozwalając na niezakłóconą pracę bufora wejściowego. Jeśli zaś wpiszemy do niego stan 0 to tranzystory zostaną odblokowane i ich stanem będzie zarządzać przerzutnik Data. Tutaj podobnie jak w przypadku AVR rzeczywisty stan końcówki można zawsze odczytać i stan ten jest powielany w dolnym przerzutniku.

INFO: Bramka OR dla logiki "ujemnej" - aktywne zera - zachowuje się jakby realizowała funkcję AND. I tu to wykorzystano.

PORTB
PORTB ma nieco inną konstrukcję. Pojawił się dodatkowy sygnał ~RBPU, który może wymusić na pinach tego portu tzw. słabe podciągnięcie do Vdd. Ten sygnał pochodzi z bitów konfiguracyjnych, więc pracę podciągnięcia ustala się sprzętowo w momencie startu mikrokontrolera.

rys7.png
rys7_1.png

Aby to podciągnięcie działało port musi pracować jako wejście, stąd sygnał ~RBPU jest bramkowany wyjściem przerzutnika TRIS.
Jak widać na drugim obrazku piny 4..7 mają dodatkowe obwody logiczne, ponieważ mogą pracować też jako źródła przerwań. Ogólna zasada pracy portu jest taka sama.

PORTF, PORTG
Ciekawym przypadkiem są porty PORTF/PORTG, które mogą pracować wyłącznie jako wejścia lub jako wyjścia do sterowania wyświetlaczy LCD (bez sterownika). Nie ma tu żadnych przerzutników konfiguracyjnych, stąd też nie ma rejestru konfiguracji kierunku pracy portu, ale jest sygnał LCDSE pochodzący z układu sterownika LCD blokujący bufor wejściowy.

rys8.png


Inne specyficzne rozwiązania
W niektórych nowszych procesorach PIC występuje możliwość odczytywania samego stanu przerzutnika wyjściowego, bez względu na to jaki aktualnie stan aktualnie występuje na samej końcówce - jest on aktywowany sygnałem Read LAT. Taka procedura jest potrzebna do prawidłowej pracy przy operacjach Read-Modify-Write na portach. Dlatego też przy samym odczycie rejestru portu nie czyta się sygnału zza bufora tylko przed buforem, co gwarantuje uzyskanie informacji o wartości wpisanej do portu, a nie zastanej. Przy zapisie nic się nie zmienia - w dalszym ciągu zapisujemy stan przerzutnika sterującego stanem.

rys9.png


Od strony programowej
Od strony programowej są dostępne dwa lub trzy główne rejestry związane z pracą portów:

1. TRISx - służący do określenia w jakim kierunku dany port ma działać, o ile jest on dwukierunkowy,
2. PORTx - służący do odczytywania stanu końcówek, a w przypadku braku rejestru LATx również do wystawiania stanu na port,
3. LATx - służący do wystawiania stanu na port lub, w celu jego modyfikacji, w operacjach Read-Modify-Write.

Proste operacje ustawiania stanu portu

Kod: Zaznacz cały

TRISB = 0x00;  // praca jako wyjście

PORTB = 0xFF;  // wymuszenie stanów wysokich na wyjściach
(...)
PORTB = 0x00;  // wymuszenie stanu niskiego na wyjściach


Proste operacje ustawiania stanu portu w przypadku obecności rejestru LATx

Kod: Zaznacz cały

TRISB = 0x00;  // praca jako wyjście

LATB = 0xFF;   // wymuszenie stanów wysokich na wyjściach
(...)
LATB = 0x00;   // wymuszenie stanu niskiego na wyjściach


Sprawdzenie stanu wejść

Kod: Zaznacz cały

TRISB = 0xFF;  // praca jako wejście

if (PORTB == 0xF0) {
/* akcja przy stanach niskich na RB3...RB0 */
};


Operacja zmiany stanu pinów na przeciwny z wykorzystaniem rejestru LATx poddanego operacji XOR.

Kod: Zaznacz cały

TRISB = 0x00;        // praca jako wyjścia
(...)
LATB ^= 0b00001111;  // zmiana stanu na przeciwny najmłodszych bitów


Mikrokontrolery Atmel XMEGA.

Atmel po sukcesie rodziny AVR zaprojektował i wprowadził na rynek ich nowe wcielenie w postaci procesorów XMEGA. Zostały one znacznie rozbudowane w stosunku do swoich starszych braci pod względem układów peryferyjnych. Niestety wprowadzono je nieco zbyt późno, stąd nie zyskały już takiej popularności.

Układ portów GPIO jest już jak najnardziej "wpółczesny" i stąd jego konstrukcja jest jeszcze bardziej rozbudowana. Za to dostajemy sporo funkcji dodatkowych, a to jeszcze nie koniec niespodzianek.

rys10.png

Można tu wydzielić trzy zasadnicze bloki:
- Część górna to układ konfiguracji opcji dodatkowych portu,
- Część środkowa to klasyczny układ bufora wyjściowego znanego już z rodziny AVR,
- Część dolna to również klasyczny układ bufora wejściowego również znanego z rodziny AVR.

Bufor wejściowy i towarzyszące mu układy to niemal 100% kopia układu z procesorów AVR. Również mamy tu bufor dołączony bezpośrednio do pinu - choć można zauważyć dodatkowy układ łącznikowy, o którym później - oraz synchronizator w postaci krótkiego rejestru przesuwającego, którego ostatni człon stanowi dostępny dla programu przerzutnik IN.

Bufor wyjściowy to klasyczny układ push-pull połączony z dwoma przerzutnikami. Przerzutnik DIR określa kierunek pracy portu i blokuje układ wyjściowy jeśli port ma pracować jako wejście, pozwalając na swobodną pracę układu bufora wejściowego. Przerzutnik OUT zaś to rejestr wyjściowy portu, którym możemy wstawić odpowiedni stan. Tutaj odmiennie niż w układach AVR nie służy on do sterowania podciąganiem. Tym zajmuje się już układ konfiguracji, który wymaga osobnego podrozdziału.

Układ konfiguracji pracy portu
Układ konfiguracji to dość pokaźna struktura logiczna pozwalająca na aktywowanie kilka niespotykanych jak dotąd funkcjonalności portu. Pierwsze co powinno się nam rzucić w oczy to sygnały Pull Enable razem z Pull Direction, pozwalające na naprzemienne włączanie dwóch tranzystorów łączących "rezystor" podciągający do zasilania - realizując funkcję pull-up, albo do masy - realizując funkcję pull-down:

rys11.png

Tranzystory te mogą też pełnić rolę układu podtrzymywania stanu - bus keeper. Wtedy sygnał Pull Keep pozwala na przełączenie multiplekserem obwodu sprzężenia zwrotnego. Układ taki pozwala na podtrzymywanie ostatniego stanu pinu, nawet gdy nie jest on już wymuszany z zewnątrz. Przy takiej konfiguracji stan pinu nigdy nie będzie pływający - wysoka impedancja, zawsze będzie podciągnięty do 1 lub 0.

rys12.png

Następną ciekawą funkcją związaną już z konfiguracją tranzystorów bufora wyjściowego jest sumowanie lub mnożenie "na drucie" - Wired-OR, Wired-AND. Odpowiada za to dodatkowy sygnał Wired OR/AND, który może wyłączać jeden z tranzystorów układu push-pull. Gdy wyłączony zostanie dolny tranzystor to końcówka może pracować w konfiguracji Wired-OR. Jeśli zaś wyłączony zostanie górny tranzystor to wyjście będzie pracować w konfiguracji Wired-AND.
Dodatkowo sterując sygnałami Pull-Enable, Pull-Direction można tym dwóm konfiguracjom dodawać również wewnętrzne rezystory podciągające.

rys13.png

Także unikalną funkcją jest przełączanie logiki pracy wyjścia na logikę "ujemną" - gdzie stanem aktywnym jest stan niski. Funkcją tą zarządza sygnał Inverted I/O, który aktywuję bramki EX-OR zarówno w obwodzie bufora wyjściowego jak i wejściowego. Dzięki temu można normalnie operować bitami w rejestrze portów na standardowych poziomach i dopiero w module GPIO sygnały zostaną odwrócone.

INFO: Odwrócona logika obowiązuje zarówno na wyjściu jak i na wejściu. Dlatego trzeba uważać by nie popełnić błędu w programie, np. oczekując nie tego stanu logicznego co potrzeba.

Strona programowa - Podstawowa
Jak już pewnie się domyślacie, takie skomplikowanie portów odbije się na większym skomplikowaniu rejestrów portu. I tak, i nie. Gdybyśmy porty w układach XMEGA traktowali tylko jak zwykłe GPIO to tak jak w przypadku AVR mamy trzy główne rejestry:
- DIR - służący do określania kierunku pracy portu,
- OUT - służący do określania stanu wyjściowego portu,
- IN - służący do określania bieżącego stanu końcówki portu.

Dlaczego nie napisałem DIRx albo OUTx - gdzie "x" oznaczałby nazwę portu: A, B, C, itd.? Otóż w przypadku urządzeń peryferyjnych w układach XMEGA obowiązują nieco inne zasady dostępu do rejestrów. Każde urządzenie lub grupa ustawień ma tzw. adres bazowy i poszczególne rejestry są po prostu przesunięte o określony offset względem tego adresu. Daje to pewne możliwości dla projektanta kodu by w razie zmian układowych - np. przeniesienie wyświetlacza z portu PORTB na PORTD bez zmiany kolejności połączeń - mógł szybko zmieniać kod dokonując wyłącznie zmiany adresu bazowego.
Takie podejście do rejestrów pozwala też na utworzenie specjalnych struktur, rzutowanych pod konkretne adresy. Dzięki temu kod źródłowy staje się również bardziej przejrzysty. Dlatego też chcąc się odwołać do rejestru OUT portu PORTA moglibyśmy napisać PORTA.OUT. Właśnie taki sposób jest to rozwiązane w przypadku stosowania gotowych plików z definicjami przygotowanymi przez Atmel-a.

Dlatego proste operacje na portach będą wyglądać następująco:

Kod: Zaznacz cały

PORTA.DIR = 0xFF;  // aktywacja wyjścia
PORTA.OUT = 0xFF;  // wymuszamy stany wysokie na wyjściach
(...)
PORTA.OUT = 0x00;  // wymuszamy stany niskie na wyjściach

Kod: Zaznacz cały

PORTA.DIR = 0x00;  // aktywacja wejścia

if (PORTA.IN == 0x00) {
/* akcja do wykonania */
};


Strona programowa - Zaawansowana
By dopełnić opisu musimy powinniśmy poznać jeszcze kilka rejestrów należących do grupy rejestrów danego portu. Spójrzmy na pełną mapę rejestrów. W tej chwili będą nas interesować tylko te zaznaczone w czerwonych ramkach:

rys14.png

Część rejestrów z górnej ramki już poznaliśmy. Pozostałe 6 rejestrów służy do szybkich operacji bitowych na odpowiadających im rejestrach.
- DIRSET/OUTSET - służą do szybkiego - bez operacji Read-Modify-Write - nadawania stanu wysokiego na danych bitach rejestru DIR/OUT.
- DIRCLR/OUTCLR - służą do szybkiego - bez operacji Read-Modify-Write - nadawania stanu niskiego na danych bitach rejestru DIR/OUT.
- DIRTGL/OUTTGL - służą do szybkiego - bez operacji Read-Modify-Write - nadawania stanu przeciwnego na danych bitach rejestru DIR/OUT.
Aby zatem dokonać szybkiej zmiany stanu zgodnej z przeznaczeniem danego rejestru, wystarczy tylko wpisać 1 na danym bicie.

Na przykład kod:

Kod: Zaznacz cały

PORTA.DIRSET = PIN1_bm;  // aktywacja wyjścia na pinie PA1
PORTA.OUTSET = PIN1_bm;  // i wymuszamy stan wysoki
(...)
PORTA.OUTTGL = PIN1_bm;  // tak możemy szybko przełączyć stan PA1
                         // na przeciwny


Rejestry z dolnej ramki służą do konfiguracji opcji dodatkowych dla poszczególnych pinów, dlatego też każdy pin ma osobny rejestr PINxCTRL - gdzie "x" to tym razem numer pinu od 0 do 7.

rys15.png

Przy obsłudze portów jako zwykłe wejścia-wyjścia będą nas interesować bity:
- INVEN - kontrolujący sygnał Inverted I/O i pozwalający na włączenie logiki ujemnej dla danego pinu. Gdybyśmy chcieli by PB2 pracował w tym trybie to możemy napisać:

Kod: Zaznacz cały

PORTB.PIN2CTRL |= (1 << PORT_INVEN_bm);


- OPC[2..0] - to trzy bity kontrolujące dodatkowe funkcje danego portu. Poszczególne kombinacje tworzą tabelkę z możliwymi ustawieniami:

Kod: Zaznacz cały

OPC[2:0] | Nazwa zdefiniowana       | Funkcja    | Podciąganie
---------------------------------------------------------------
   0b000 | PORT_OPC_TOTEM_gc        | Push-pull  | Brak
   0b001 | PORT_OPC_BUSKEEPER_gc    | Push-pull  | Bus-keeper
   0b010 | PORT_OPC_PULLDOWN_gc     | Push-pull  | Pull-down (Wejście)
   0b011 | PORT_OPC_PULLUP_gc       | Push-pull  | Pull-up (Wejście)
   0b100 | PORT_OPC_WIREDOR_gc      | Wired-OR   | Brak
   0b101 | PORT_OPC_WIREDAND_gc     | Wired-AND  | Brak
   0b110 | PORT_OPC_WIREDORPULL_gc  | Wired-OR   | Pull-down
   0b111 | PORT_OPC_WIREDANDPULL_gc | Wired-AND  | Pull-up


Na przykład, gdybyśmy szybko chcieli skonfigurować pin PE4 do pracy jako wejście z podciągnięciem do plusa, możemy napisać:

Kod: Zaznacz cały

PORTE.DIRCLR   = PIN4_bm; 
PORTE.PIN4CTRL = PORT_OPC_PULLUP_gc;


Porty wirtualne
Konstruktorzy rodziny XMEGA dodali jeszcze jedną niespodziankę: tzw. porty wirtualne. Porty te to cztery grupy rejestrów VPORTx, które można skonfigurować tak, by ich poszczególne rejestry IN, OUT, DIR zostały zmapowane do rzeczywistych portów GPIO. Struktura rejestrów wirtualnych portów:
rys16.png

Zapis do rejestru portu wirtualnego będzie się równał zapisowi rejestru w połączonym z nim portem GPIO.

Oczywiście wcześniej należy skonfigurować ich przynależność do realnych portów korzystając z rejestrów VPCTRLA i VPCTRLB z grupy PORTCFG.
rys17.png


Kod: Zaznacz cały

/* mapujemy PORTC do VPORT0 */
PORTCFG.VPCTRLA = PORTCFG_VP0MAP_PORTC_gc;

VPORT0.DIR = 0xFF;  // aktywacja wyjścia
VPORT0.OUT = 0x00;  // wymuszamy stany niskie na wyjściach


Mikrokontrolery ST - STM32 Cortex M0/M3/M4.

Skoro poradziliśmy sobie z układami XMEGA i ich zaawansowanymi funkcjami portów GPIO, to możemy prawie bezboleśnie przejść do 32-bitowych układów STM32 z rdzeniem Cortex M0/M3/M4. Tutaj struktura portu wydaje się nieco prostsza, ale to tylko pozory. W dalszym ciągu mamy spore możliwości konfiguracji.
Jak dotąd wcześniej poznane układy charakteryzowały się portami składającymi się z 8-bitów. W układach STM32 porty są już 16-bitowe. Dlaczego nie 32-bitowe, skoro układ jest 32-bitowy? Otóż wynika to z konstrukcji pewnych rejestrów konfiguracji portu, gdzie jednemu pinowi przydzielono po 2 bity dla danej opcji.

rys18.png

Widzimy, że układ portu jest logicznie podzielony na część wejściową i wyjściową. Część wejściowa to klasyczny trójstanowy bufor współpracujący z rejestrem wejściowym, z którego możemy tylko czytać. Na wejściu bufora mamy też przełączalne rezystory pull-up i pull-down.
Część wyjściowa również jest dość prosta. Typowy układ push-pull zarządzany blokiem logiki, przełącznik do przekierowywania pinu portu do urządzeń peryferyjnych oraz dwa tym razem rejestry wyjściowe połączone w kaskadę. Pierwszy z nich Bit Set/Reset Register obsługuje atomowe polecenia zmiany pinów (bez udziału mechanizmu Read-Modify-Write). Stąd można do niego tylko wpisywać dane. Drugi z nich to rzeczywisty rejestr danych portów: Output Data Register.

Strona programowa - Wstęp
Zanim zaczniemy opisywać stronę programową portów GPIO osobno dla rodzin M0 i M3 - gdyż istnieją tu pewne różnice - musimy sobie zdać sprawę z jednej istotnej funkcjonalności układów STM32. Otóż każde peryferia, poza pewnymi wyjątkami bez których procesor by po prostu nie wystartował, nie są domyślnie włączone. Tuż po resecie układ zegarowy po prostu nie dostarcza im żadnego przebiegu zegarowego i by użyć danego peryferium musimy go obowiązkowo włączyć. Wiele początkujących o tym zapomina.

Samą konfigurację wykonuje się w module RCC - Reset and Clock Controller gdzie mamy odpowiednie rejestry służące do aktywacji wyjść sygnałów zegarowych. Przy czym należy pamiętać, że peryferia w mogą być podłączone do jednej z kilku wewnętrznych magistral występujących w procesorach ARM. I tak, porty w rodzinie M0 są połączone z magistralą AHB - Advanced High-performance Bus, zaś w układach M3/M4 do jednej z "niższych" magistral APB - Advanced Peripheral Bus.

Strona programowa - Rodzina Cortex M0
Podobnie jak w przypadku procesorów XMEGA firmy Atmel, rejestry portów też zostały połączone w grupy. Każdy port nazywany po kolei: GPIOA, GPIOB, GPIOC, itd ma swój adres bazowy pozwalający na rzutowanie na niego pewnej struktury ze wszystkimi rejestrami odnoszącymi się do danego portu. Na przykład grupa dla GPIOA wygląda następująco:

rys19.png

W grupie tej znajdują się następujące rejestry:

  • Rejestr trybu pracy portu GPIOx_MODER - 32 bity, gdzie każde dwa bity ustalają tryb pracy danej końcówki,
  • Rejestr typu wyjścia portu GPIOx_OTYPER - 16 młodszych bitów, gdzie każdy ustala czy pracuje górny tranzystor i wyjście jest typu push-pull,
  • Rejestr szybkości pracy portu w trybie wyjściowym GPIOx_OSPEEDR - 32 bity, gdzie każde dwa określają dostępną szybkość,
  • Rejestr konfiguracji podciągania GPIOx_PUPDR - 32 bity, gdzie każde dwa określają sposób i aktywność podciągania,
  • Rejestr wejściowy portu GPIOx_IDR - 16 młodszych bitów, gdzie każdy zwraca aktualny stan pinu,
  • Rejestr wyjściowy portu GPIOx_ODR - 16 młodszych bitów, gdzie każdy określa jaki stan ma się pojawić na końcówce,
  • Rejestr dostępu atomowego GPIOx_BSRR - 16 młodszych bitów pozwala na szybkie ustawienie stanu portu, a 16 starszych na szybkie ich wyzerowanie,
  • Rejestr blokowania zmian konfiguracji portu GPIOx_LCKR -16 młodszych bitów określa czy dana konfiguracja ma być niezmienna do ponownego resetu mikrokontrolera,
  • Rejestry wyboru funkcji alternatywnej portu GPI Ox_AFRL / GPIOx_AFRH - Ustawiając odpowiedni bit można przekierować inne peryferium do danej końcówki portu, wyłączając ją tym samym z funkcji GPIO,
  • Rejestr dostępu atomowego do resetowania GPIOx_BRR - 16 młodszych bitów pozwala na szybką zmianę stanu na niski danego pinu.

Można tu łatwo wydzielić pewne bloki rejestrów. Blok konfiguracyjny: MODER, OTYPER, OSPEEDR, LCKR, AFRL/H; blok dostępu wejścia-wyjścia: IDR, ODR; blok dostępu atomowego: BSRR, BRR.

Najważniejszy teraz dla Nas będzie blok konfiguracyjny. Tabela dostępnych opcji jest dość spora:

rys20.png

By dokonać konfiguracji danego pinu/portu, najpierw powinniśmy określić stan bitów MODER. To one decydują czy dany pin/port będzie wejściem, wyjściem i czy będzie realizował przy tym funkcję alternatywną, albo niezakłócony urządzeniami cyfrowymi przenosił wartość analogową. Następnie w OTYPER określamy czy praca danego pinu/portu będzie realizowana przez układ push-pull czy tylko Open-Drain. Stan bitów OSPEEDR pomoże nam w przypadku, gdy szybkość narastania sygnału - nachylenie zboczy - będzie krytyczna, kosztem oczywiście nieco większego zużycia prądu.

ProTip: Domyślnie porty pracują z niską szybkością narastania, co da oszczędność energii, i taka prędkość np. do zaświecenia diody LED będzie wystarczająca. Gdyby jednak końcówka pracowała jako wyjście alternatywne z pozostałych układów peryferyjnych, będzie trzeba obowiązkowo tą prędkość zwiększyć.

Na koniec ustawiając bity PUPDR będziemy mogli decydować o tym czy będzie działać wewnętrzne podciąganie.

Pora na parę prostych przykładów. Będę się w nich posługiwał pewnymi definicjami zawartymi w plikach nagłówkowych oraz pojedynczymi pinami w celu uproszczenia tych przykładów.

Na początek proste operacje ustawiania stanu końcówek portu:

Kod: Zaznacz cały

/* włączamy sygnał zegarowy dla GPIOA */
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
(...)

/* zerowanie bitów MODER dla PA0 i PA1 */
GPIOA->MODER   &= ~(GPIO_MODER_MODER0_Msk | GPIO_MODER_MODER1_Msk); 

/* ustawiamy je jako wyjścia */
GPIOA->MODER   |=  (GPIO_MODER_MODER0_0 | GPIO_MODER_MODER0_1); 

/* praca jako push-pull */
GPIOA->OTYPER  &= ~(GPIO_OTYPER_OT_0 | GPIO_OTYPER_OT_1);

/* ustawimy im największą prędkość narastania */
GPIOA->OSPEEDR |=  (GPIO_OSPEEDER_OSPEEDR0 | GPIO_OSPEEDER_OSPEEDR1); 

/* i pracę bez podciągania */
GPIOA->PUPDR   &= ~(GPIO_PUPDR_PUPDR0_Msk | GPIO_PUPDR_PUPDR1_Msk);

GPIOA->ODR &= ~(GPIO_ODR_0);    // wymuszenie stanu niskiego na PA0
GPIOA->ODR |=   GPIO_ODR_1;     // wymuszenie stanu wysokiego na PA1

/*  To samo z użyciem rejestrów atomowych */
GPIOA->BRR  = GPIO_BRR_BR_0;    // wymuszenie stanu niskiego na PA0
GPIOA->BSRR = GPIO_BSRR_BS_1;   // wymuszenie stanu wysokiego na PA1


Konfiguracja PA2 jako wejście z pull-up-em i sprawdzenie jego stanu:

Kod: Zaznacz cały

/* włączamy sygnał zegarowy dla GPIOA */
RCC->AHBENR  |= RCC_AHBENR_GPIOAEN;   
(...)
/* zerowanie bitów MODER dla PA2 tym samym będzie wejściem cyfrowym */
GPIOA->MODER &= ~(GPIO_MODER_MODER2_Msk); 

/* zerowanie bitów odpowiedzialnych za pracę podciągania */
GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPDR2_Msk);

/* PA2 podciągniemy do VDD */
GPIOA->PUPDR |=   GPIO_PUPDR_PUPDR2_0;

if ((GPIOA->IDR & GPIO_IDR_2) == 0) {

/* akcja przy stanie niskim na PA2 */
};


Strona programowa - Rodzina Cortex M3/M4
Tu nadal obowiązuje model dostępu znany z rodziny M0, ale jest on nieco bardziej zwarty. Można by powiedzieć, że w stosunku do rodziny M3 jest on nieco prostszy.

INFO: Historycznie rzecz biorąc rodzina M3 pojawiła się wcześniej niż rodzina M0. I dlatego nie można tego traktować jak progresywne uproszczenie w stosunku do M0.

rys21.png

W grupie rejestrów danego portu znajdziemy:

  • Rejestr trybu pracy portu GPIOx_CRL / GPIO_CRH - dwa razy po 32 bity, gdzie każde cztery bity ustalają tryb pracy danej końcówki,
  • Rejestr wejściowy portu GPIOx_IDR - 16 młodszych bitów, gdzie każdy zwraca aktualny stan pinu,
  • Rejestr wyjściowy portu GPIOx_ODR - 16 młodszych bitów, gdzie każdy określa jaki stan ma się pojawić na końcówce,
  • Rejestr dostępu atomowego GPIOx_BSRR - 16 młodszych bitów pozwala na szybkie ustawienie stanu portu, a 16 starszych na szybkie ich wyzerowanie,
  • Rejestr blokowania zmian konfiguracji portu GPIOx_LCKR -16 młodszych bitów określa czy dana konfiguracja ma być niezmienna do ponownego resetu mikrokontrolera,
  • Rejestr dostępu atomowego do resetowania GPIOx_BRR - 16 młodszych bitów pozwala na szybką zmianę stanu na niski danego pinu.

Zatem tu podział funkcjonalny będzie jeszcze bardziej widoczny. Blok konfiguracyjny: CRL / CRH; blok dostępu wejścia-wyjścia: IDR, ODR; blok dostępu atomowego: BSRR, BRR.
Nie ma tu rejestrów dotyczących funkcji alternatywnych pinów. Są one w odrębnym peryferium zwanym AFIO. Stąd przy wykorzystaniu końcówek portów jako wejścia-wyjścia innych układów peryferyjnych, dodatkowo musimy też osobno włączyć sygnał zegarowy układowi AFIO.

Z uwagi na to, że konfiguracja pinu odbywa się tylko w dwóch rejestrach, tabela dostępnych opcji jest nieco mniejsza i łatwiejsza do opanowania:

rys22.png


By dokonać konfiguracji danego pinu/portu, po prostu określamy stan bitów CNF oraz MODE. Dla pinów od 7..0 będzie się to odbywać w rejestrze CRL, a dla 15..8 w rejestrze CRH. To one zadecydują czy dany pin/port będzie wejściem z podciąganiem lub bez, wyjściem lub przenosił wartość analogową.

ProTip: Domyślnie porty pracują jako wejścia i nie jest dla nich ustalona szybkość narastania. Dlatego tu, odmiennie niż w rodzinie M0 zawsze trzeba wybrać jaką prędkość narastania będziemy chcieli wykorzystać przy pracy pinu jako wyjście. Dla funkcji alternatywnych obowiązują tu te same zasady co w M0.

Jak zwykle przykłady. Oczywiście na początek proste operacje ustawiania stanu końcówek portu:
/* włączamy sygnał zegarowy dla GPIOB umieszczonego w 
domenie magistrali APB2
*/

RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
(...)

/* zerowanie bitów CRL dla PB0 i PB1 */
GPIOB->CRL &= ~(GPIO_CRL_CNF0_Msk | GPIO_CRL_CNF1_Msk);

/* ustawimy im prędkość ograniczoną do 2MHz i skoro w CNF mamy zera
daje to nam wyjście push-pull
*/

GPIOB->CRL |= (GPIO_CRL_MODE0_1 | GPIO_CRL_MODE1_1);

GPIOB->ODR &= ~(GPIO_ODR_ODR0); // wymuszenie stanu niskiego na PB0
GPIOB->ODR |= GPIO_ODR_ODR1; // wymuszenie stanu wysokiego na PB1

/* To samo z użyciem rejestrów atomowych */
GPIOB->BRR = GPIO_BRR_BR0; // wymuszenie stanu niskiego na PB0
GPIOB->BSRR = GPIO_BSRR_BS1; // wymuszenie stanu wysokiego na PB1


Konfiguracja PB2 jako wejście z pull-up-em i sprawdzenie jego stanu:
/* włączamy sygnał zegarowy dla GPIOA umieszczonego 
w domenie magistrali APB2
*/

RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
(...)

/* zerowanie bitów CRL i MODE dla PB2 co da nam pracę jako wejście */
GPIOB->CRL &= ~(GPIO_CRL_CNF2_Msk | GPIO_CRL_MODE2_Msk);

/* teraz włączymy podciąganie do VDD */
GPIOB->CRL |= GPIO_CRL_CNF2_1;

if ((GPIOB->IDR & GPIO_IDR_IDR2) == 0) {

/* akcja przy stanie niskim na PA2 */
};


Koniec ?

W zasadzie to już koniec tego nieco dłuższego posta. Starałem się w nim jak najprościej opisać jak działają i jak skonfigurować porty GPIO w kilku najbardziej popularnych rodzinach procesorów. Oczywiście nie wyczerpuje to całkowicie tematu, bo jest jeszcze parę innych rodzin oraz szczegółów, które z oczywistych względów zostały pominięte bo wykraczałyby znacznie poza główny temat.

Mam nadzieję, że ta rozprawka w takiej formie będzie dla kogoś użyteczna. Może powstaną następne części opisujące w formie przeglądu rozwiązań kolejnych układów peryferyjnych najczęściej występujących w mikrokontrolerach. "Only time will tell".

p.s. Fragment o mikrokontrolerach PIC powstał we współpracy z kol. Antystatyczny.
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
Ostatnio zmieniony wtorek 28 sie 2018, 06:13 przez ZbeeGin, łącznie zmieniany 1 raz.

Awatar użytkownika
PROTON
User
User
Posty: 460
Rejestracja: czwartek 08 paź 2015, 18:35
Lokalizacja: Warszawa

Re: Porty mikrokontrolera - Przegląd rozwiązań

Postautor: PROTON » niedziela 26 sie 2018, 18:30

Like.png
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
Gott weiß ich will kein Engel sein.

Awatar użytkownika
xor
User
User
Posty: 137
Rejestracja: poniedziałek 05 wrz 2016, 21:44

Re: Porty mikrokontrolera - Przegląd rozwiązań

Postautor: xor » poniedziałek 27 sie 2018, 22:26

/* To samo z użyciem rejestrów atomowych */
GPIOB->BRR |= GPIO_BRR_BR0; // wymuszenie stanu niskiego na PB0
GPIOB->BSRR |= GPIO_BSRR_BS1; // wymuszenie stanu wysokiego na PB1


Rejestry BRR i BSRR są "Write Only", a więc zapis do nich bez sumy bitowej:

Kod: Zaznacz cały

/* To samo z użyciem rejestrów atomowych */
GPIOB->BRR  = GPIO_BRR_BR0;     // wymuszenie stanu niskiego na PB0
GPIOB->BSRR = GPIO_BSRR_BS1;    // wymuszenie stanu wysokiego na PB1

Awatar użytkownika
piotrek
Newb
Newb
Posty: 77
Rejestracja: niedziela 05 lis 2017, 02:46

Re: Porty mikrokontrolera - Przegląd rozwiązań

Postautor: piotrek » sobota 01 wrz 2018, 18:12

Przydatna wiedza dla kogoś kto lubi eksperymentować z różnymi rodzinami MCU. Wiedza w pigułce. Przydałby się podobny opis poruszający kwestię środowiska rozwojowego dla poszczegółnych rodzin (IDE, programatory, debugery, biblioteki).


Wróć do „Inne mikroklocki, również peryferyjne”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 0 gości