Nie takie USB straszne jak je malują, cz.3 - endpointy

Wszystko co dotyczy płytek z rodziny Discovery firmy STM
ODPOWIEDZ
Awatar użytkownika
elvis
Użytkownik
Posty: 35
Rejestracja: 30 lis 2018, 17:50

Nie takie USB straszne jak je malują, cz.3 - endpointy

Post autor: elvis » 05 lip 2019, 16:49

Poprzednio starałem się poznać działanie interfejsu USB patrząc niejako z góry - oglądając jak działa gotowa implementacja oraz czym są deskryptory. Teraz przeskok na sam dół stosu, czyli to co bezpośrednio komunikuje się ze sprzętem. Jak wspominałem na początku jako przykład używam układu STM32L053, w tym odcinku postaram się wyjaśnić jak wygląda komunikacja z modułem peryferyjnym tego mikrokontrolera.

Co to jest endpoint?
Każdy kto trochę się interesował USB pewnie chociaż raz usłyszał to pojęcie. Ja już też go używałem, chociaż bez słowa wyjaśnienia. Otóż endpoint to po prostu bufor.
Komunikacja przez USB odbywa się między hostem, a urządzeniem. Urządzenie jest wyposażone w bufory, czyli właśnie endpointy. Takich buforów może być maksymalnie 32, po 16 do wysyłania danych z hosta do urządzenia i 16 w przeciwnym kierunku. Większość urządzeń używa znacznie mniejszej liczby buforów. Wymagane są tylko dwa: endpoint zerowy do wysyłania i zerowy do odbierania.
Jeśli endpointy wydają się dziwne, najłatwiej porównać je z buforami znanymi chociażby z interfejsu UART. Mamy bufor do wysyłania, czyli TXR oraz RXR do odbierania danych. USB jest tylko minimalnie bardziej skomplikowane - można mieć więcej buforów no i nazewnictwo jest przekombinowane.
Projektanci USB siedzieli sobie wygodnie przed komputerem PC i świat widzieli przez okienka windows, przyjęli więc że to co wysyła host do urządzenia jest określane jako dane wyjściowe, czyli OUT. Natomiast to co odbiera - IN. Niestety gdy piszemy sterownik dla urządzenia, nie najlepiej takie nazwy się sprawdzają.
Jak wspominałem wcześniej endpointy mają numery (0 - 15) oraz kierunek (IN/OUT). Każde urządzenie musi mieć więc endpoint 0 OUT do odbierania danych oraz 0 IN do wysyłania.

Podsumowując - komunikacja przez USB jest prawie tak prosta jak przez UART. To co wstawimy do endpointa typu IN zostanie wysłane do hosta. A to co host przysyła do nas znajdziemy w buforze typu OUT. Całą resztę wykona sprzęt, czyli moduł peryferyjny naszego STM32.

Gdybyśmy jednak przyjrzeli się komunikacji nieco dokładniej zobaczylibyśmy że jest ona podobna do i2c. Po pierwsze więcej niż jedno urządzenie może być podłączone do komputera. W związku z tym urządzenia mają swoje adresy. Po podłączeniu urządzenie używa adresu 0, który jest następnie zmieniany przez hosta na inny z zakresu 1 - 127.
Drugie podobieństwo do i2c polega na tym, że każda transmisja jest inicjowana przez hosta. Więc nawet jeśli wysyłamy dane, nie zostają one nadane od razu. Urządzenie musi poczekać, aż host o nie zapyta. Zajmuje się tym sprzęt, my jako programiści musimy tylko zapisać dane do bufora i czekać na przerwanie, które poinformuje nas że dane zostały wysłane. Podobnie jest z odbieraniem danych. My tylko konfigurujemy bufor, sprzęt załatwia wszystko i zgłasza przerwanie gdy nowe dane są gotowe.

Bufory USB mogą być znacznie większe niż te znane z UART-a. Typowe endpointy mają od 8 do nawet 1024 bajtów. Ponieważ zapisanie tak dużych ilości danych od razu byłoby trudne, endpointy mają też status:
  • DISABLED - endpoint jest wyłączony i nie odpowiada na zapytania wysyłane przez hosta
  • STALL - informuje hosta, że funkcja którą wywołał jest niedostępna. Takie "pieniędzy nie ma i nie będzie".
  • NAK - bufor nie jest gotowy, host ponowi transmisję za jakiś czas
  • VALID - bufor gotowy do transmisji
Rozwijajac nieco przykład z wysyłaniem danych - po zapisanu danych do bufora musimy zmienić jego stan na VALID. Będzie to sygnał dla sprzętu że dane można wysłać. Po wysłaniu danych status zostanie automatycznie zmieniony na NAK, więc jeśli host zapyta o kolejne dane, dostanie informację że jeszcze nic nie ma i ma spróbować ponownie za chwilę.
W przypadku odbierania danych wystarczy że ustawimy stan na VALID, wówczas gdy pojawią się nowe dane zostaną zapisane w buforze, a status zostanie zmieniony na NAK, dzięki czemu kolejne dane nie zostaną stracone - host po prostu będzie ponawiał próby transmisji.

Poza statusem endpointy mają jeszcze typ. Znowu są 4 możliwości:
  • CONTROL - typ używany właściwie tylko przez endpoint zero. Służy do konfiguracji urządzenia, odpytywania od deskryptory
  • INTERRUPT - pozwala na przesyłanie małych ilości danych co ściśle określony czas
  • BULK - przesyła dużo danych, ale bez gwarancji czasowych
  • ISOCHRONOUS - specjalny typ dla danych strumieniowych, nie zawiera korekcji błędów
Zapomniałem dodać, że wszystkie typy poza ISOCHRONOUS obsługują retransmisje i sprawdzanie sumy kontrolnej.
Endpoint kontrolny to wspomniany, obowiązkowy endpoint zero - do niego jeszcze wrócimy. INTERRUPT nie ma nic wspólnego z przerwaniami, to po prostu dane, których jest mało, ale muszą być dostarczone na czas. Przykładem są chociażby informacje z klawiatury o naciskanych klawiszach. BULK służy do przesyłania dużych ilości danych np. do drukarki, czy pamięci Flash. Host wysyła dane bulk jak nie ma nic pilniejszego. ISOCHRONOUS to specjalny tryb dla miłośników kamerek, mikrofonów i głośników USB - dane są wysyłane w ściśle określonych chwilach, nie ma retransmisji ani kontroli błędów.

Trochę się rozpisałem... więc o rejestrach i samym STM32 napiszę w kolejnej części

Dodano po 2 godzinach 36 minutach 46 sekundach:
Skoro wiemy już wszystko o endpointach, czas na zapoznanie się z implementacją USB na STM32L053. W sumie dokładnie tak samo wyglądają rejestry na STM32F103, raz przez pomyłkę otworzyłem złą dokumentację i zorientowałem się dopiero jak zobaczyłem tytuł - pewnie inne stm32 mają co najmniej podobnie.

Na początek niestety przykra uwaga - implementacja USB na STM32 wygląda jakby była zrobiona przez zdolnego studenta, który wygrał konkurs "dam radę zaprojektować moduł USB po tygodniu picia"... Jest straszna, koszmarna i potworna. Ale chociaż ma mało rejestrów, więc da się ją opanować - chociaż inni producenci znacznie mniej komplikują programistom życie.

Na początek kilka informacji marketingowych: STM32L053 obsługuje 8 endpointów, ma też 1024 bajtów dedykowane pamięci dla USB. Tyle marketingu.
Technicznie patrząc do całej obsługi USB wystarcza raptem 7 rejestrów sprzętowych ogólnego przeznaczenia oraz dodatkowo po jednym na endpoint. Jest też wspomniana pamięć RAM oraz jedno przerwanie.
Więc nie może to być aż tak skomplikowane jak wiele osób uważa :)

Postaram się krótko opisać wykorzystywane przeze mnie rejestry oraz ich funkcje. Jeśli to zbyt szczegółowy opis, można sobie go spokojnie darować. Później pokażę gotowe funkcje, które ukrywają wszystkie szczegóły operacji na rejestrach.
Chciałbym przede wszystkim pokazać, że USB wcale nie jest trudniejsze do obsługi niż USART, czy I2C.

Bity wspólne dla całego modułu USB

Obrazek

W rejestrze USB_CNTR znajdziemy bity odblokowujące przerwania. CTRM uruchamia przerwanie po odebraniu i wysłaniu danych, RESETM włącza przerwanie po wykryciu resetu interfejsu USB (co widzieliśmy w pierwszym wpisie).
Wyzerowanie bitu PDWN uruchamia moduł USB, natomiast FRES resetuje ustawienia.

Obrazek

Rejestr USB_ISTR będzie wykorzystywany podczas procedury obsługi przerwania. Jego bity zawierają informacje o źródle przerwania: CTR oznacza zakończenie wysyłania lub odbierania danych, RESET wykrycie resetu interfejsu. Dodatkowo bit DIR oraz EP_ID[3:0] pozwalają określić którego endpointa dotyczy zgłoszone przerwanie.

Obrazek

Siedem bitów ADD[6:0] w rejestrze USB_DADDR określa adres naszego urządzenia. Więc na początku wystarczy wstawić tutaj zero, a po otrzymaniu komunikatu SET_ADDRESS zmienić na przydzielony adres.
Dla zmyłki znajdziemy tutaj też bit EF, który włącza moduł USB - tak jeszcze raz na wszelki wypadek. Ale pewnie student - projektant zapomniał że taki bit już był...

Obrazek

Ten rejestr odnosi się do dedykowanej pamięci. Musimy w niej umieścić bufory do komunikacji (czyli endpointy) oraz "spis treści". Wrócę do tego później. W każdym razie ten rejestr określa gdzie ten spis treści będzie. Wstawienie tutaj zera jest całkiem rozsądnym rozwiązaniem, a sama obecność tego rejestru dowodzi że projektant miał zły dzień.

Obrazek

Tem rejestr jest kolejnym dowodem na pijacko-imprezowy rodowód implementacji USB w stm32. Nazwa tego rejestru to "Battery charging detector" i właściwie mało on nas interesuje... poza bardzo ważnym bitem DPPU, który włącza rezystor podciągający na linii D+. Bez niego host nie wykryje nawet że podłączyliśmy urządzenie. Mała rzecz a ile czasu potrafi zmarnować - szczególnie ukryta w tak niezwiązanym tematycznie rejestrze.

Dodano po 21 minutach 9 sekundach:
Rejestry związane z endpointami
Teraz czas na najgorsze. Jeśli myślicie że widzieliście już wszystkie źle zaprojektowane moduły sprzętowe to macie szansę zmienić zdanie. Ja przynajmniej znalazłem pierwsze miejsce na mojej liście koszmarków.
Rejestr wygląda niepozornie:

Obrazek

Jak widzimy mamy po jednym rejestrze dla każdego z 8 endpointów. Przy okazji - w sumie stm32 obsługuje 8 par endpointów, czyli faktycznie jest ich 16. Implementacja obsługuje je w parach, co niby ma sens chociaż nie jest wcale wymagane przez standard USB.
Każdy bit tego rejestru jest istotny, a dostęp do nich jest potwornie niewygodny. Na szczęście ten cały koszmar można schować w odpowiednich funkcjach - ale nadal to nie świadczy dobrze o tym nieszczęsnym projektancie sprzętu.
Opis bitów rejestru USB_EPnR:
  • CTR_RX - ten bit jest ustawiany gdy endpoint odbierze dane. Program musi go wyzerować w procedurze obsługi przerwania
  • DTOG_RX - aby uniknąć duplikacji pakietów, USB transmituje dane naprzemiennie oznaczane jako DATA0 i DATA1. Ten bit pozwala na zresetowanie transmisji i rozpoczęcie od DATA0
  • STAT_RX - status endpointu odbiorczego, przyjmuje jedną z wartości: DISABLED, STALL, NAK, VALID
  • SETUP - gdy host wysyła zapytanie do endpointu 0 (kontrolnego), pierwszy pakiet jest wyróżniony, przy okazji zapalana jest flaga SETUP. Dzięki temu stos USB wie że powinien przerwać inne transmisje (o ile trwają)
  • EP_TYPE - określa typ endpointu: CONTROL, INTERRUPT, BULK albo ISOCHRONOUS
  • EP_KIND - tego bitu nie używałem, ma on znaczenie zależne od typu endpointu
  • CTR_TX - ten bit jest ustawiany gdy endpoint zakończy wysyłanie danych. Program musi go wyzerować w procedurze obsługi przerwania
  • DTOG_TX - działa jak DTOG_RX, ale dla endpointa nadawczego
  • STAT_TX - status endpointu nadawczego, przyjmuje jedną z wartości: DISABLED, STALL, NAK, VALID
  • EA - przypisuje numer do endpointu. Można tutaj wstawić n czyli indeks rejestru.

Awatar użytkownika
elvis
Użytkownik
Posty: 35
Rejestracja: 30 lis 2018, 17:50

Re: Nie takie USB straszne jak je malują, cz.3 - endpointy

Post autor: elvis » 06 lip 2019, 11:57

Na zakończenie omawiania interfejsu sprzętowego jeszcze dedykowana pamięć RAM. Mamy do dyspozycji 1024 bajty takiej pamięci, ale dostęp do niej możliwy jest tylko w postaci 16-bitowych słów. Jeśli spróbujemy adresować pamięć bajtami lub 32-bitowymi wartościami otrzymamy niepoprawne wartości. Tutaj projektant sprzętu po prostu przeszedł sam siebie...
W każdym razie w dedykowanej pamięci musimy umieścić "spis treści" - przykład znajdziemy w dokumentacji mikrokontrolera:

Obrazek

Jeśli do rejestru USB_BTABLE zapisaliśmy zero to mamy sytuację jak na przykładowym rysunku. Początek pamięci zawiera spis buforów, na każdy wpis składają się 4 wartości (każda to 16-bitowe słowo):
  • ADDRn_TX - adres początku bufora nadawczego n-tego endpointa
  • COUNTn_TX - liczba bajtów do wysłania
  • ADDRn_RX - adres początku bufora odbiorczego
  • COUNTn_RX - wielkość bufora odbiorczego oraz liczba odebranych bajtów
Rejestry wymienione w spisie treści są używane jak zwykłe rejestry sprzętowe. Więcej informacji o nich znajdziemy w dokumentacji układy, ja nie będę zanudzał wszystkimi szczegółami.

W każdym razie znamy już cały interfejs jaki oferuj sprzęt. Czas na napisanie własnych bibliotek, które ułatwią do niego dostęp - bo wcale nie jest to takie proste jak się wydaje.

ODPOWIEDZ