Wykład 7: Komunikacja z procesorem

Data: 08.12.2020

O rejestrach MMIO i szynach systemowych

Częścią praktycznie każdego bardziej skomplikowanego układu cyfrowego jest procesor (a często wiele procesorów), który steruje pracą całości bądź części układu. Procesor musi mieć możliwość komunikacji z każdą istotniejszą częścią takiego układu.

Istnieją dwa podejścia do podłączenia urządzeń do procesora:

  • ciasna integracja: urządzenie ma swój dedykowany interfejs i staje się w pewnym sensie częścią specyfikacji procesora (np. dedykowane instrukcje czy specjalne rejestry procesora do obsługi danego urządzenia)

  • luźna integracja: urządzenie jest podłączone do procesora przez standardową szynę (zazwyczaj współdzieloną z innymi urządzeniami) i jest widoczne dla procesora za pomocą zwykłych instrukcji dostępu do pamięci — pewna część fizycznej przestrzeni adresowej jest wydzielona dla urządzenia i odpowiada ono na instrukcje procesora które piszą bądź czytają ten obszar; podejście takie nazywa się MMIO (memory-mapped I/O)

Ciasna integracja jest używana rzadko (najczęściej gdy projektujemy jednocześnie procesor i jego urządzenia peryferyjne) i w przypadku procesorów ogólnego przeznaczenia używa się wyłącznie luźnej integracji.

Szyną systemową nazywamy zbiór połączeń, który łączy ze sobą procesory (i inne urządzenia będące w stanie inicjować transakcje) z urządzeniami peryferyjnymi. Może to być dość prosty bądź bardzo skompilokowany byt — może składać się z naprawdę wielu segmentów wykonanych w wielu technologiach, może być zawarta w ramach jednego układu scalonego, bądź też składać się z wielu układów i połączeń (być może kablowych).

Charakteryzuje się następującymi cechami:

  • szyna ma jednego lub więcej inicjatorów — urządzenia, które mogą inicjować transakcje na szynie; oczywistym przykładem inicjatora jest procesor, ale inicjatorami są też np. urządzenia peryferyjne będące w stanie wykonywać operacje bezpośredniego dostępu do pamięci (DMA)

  • szyna ma jeden bądź więcej targetów — urządzeń, które mogą odpowiadać na transakcje

    • główna pamięć systemu również jest targetem (pokrywającym duży zakres adrwsów)

  • standardowe transakcje na szynie to transakcje odczytu i zapisu (ale zdarzają się dziwniejsze typy transakcji na niektórych szynach); transakcje odczytu i zapisu składają się z co najmniej:

    • adresu docelowego (wysyłanego przez inicjatora)

    • rozmiaru (w bajtach) — jeśli jest większy niż 1, transakcja pokrywa wiele kolejnych adresów

    • danych (wysyłane przez inicjatora w przypadku zapisu, przez target w przypadku odczytu)

— target dla danej transakcji jest wybierany na podstawie jej adresu — każdemu

targetowi jest przydzielony jakiś zakres (bądź zakresy) adresów; w ramach swoich zakresów target może dowolnie interpretować resztę adresu

  • mapowanie adresów na urządzenia może być kompletnie ustalone w momencie produkcji układu (w przypadku szyn mieszczących się w całości w jednym układzie) bądź mniej lub bardziej konfigurowalne (w przypadku bardziej skomplikowanych szyn, jak te w komputerach klasy PC)

  • choć zazwyczaj każdy inicjator na szynie widzi takie samo mapowanie adresów, nie zawsze jest to prawda — mogą istnieć targety niedostępne z części szyny

  • zdarzają się szyny z tzw. IOMMU — układami tłumaczącymi adresy; w takim wypadku, transakcje od niektórych inicjatorów przechodzą przez tłumaczenie adresów bardzo podobne do stronicowania przez procesor

  • szyna może składać się z wielu różnych pod-szyn wykonanych w różnych techonologiach i połączonych mostkami, bądź switchami

Układy SoC i AMBA

Układy SoC (system on a chip) to układy, które zawierają procesor wraz z dość kompletnym zestawem urządzeń peryferyjnych, pozwalając na złożenie kompletnego systemu z minimalną liczbą zewnętrznych układów (zazwyczaj tylko DRAM + flash). Taki układ jest sercem każdego współczesnego telefonu czy tableta. Takim układem jest również używany przez nas Zynq.

AMBA to zbiór standardów stworzonych przez ARM opisujących technologie komunikacji pomiędzy urządzeniami zawartymi w jednym układzie scalonym, w tym technologie służące do realizacji szyny systemowej. W skład AMBA wchodzą między innymi:

  • szyna AXI (Advanced eXtensible Interface) — główny interfejs używany przez współczesne duże procesory ARM i układy SoC na nich oparte; jest to interfejs point-to-point: komunikacja odbywa się pomiędzy jednym inicjatorem a jednym targetem i należy użyć switcha AXI, by móc spiąć ze sobą więcej urządzeń; jest oparty o 5 strumieni valid-ready i pozwala na jednoczesne wykonywanie wielu transakcji w potoku; ma wiele wersji:

    • AXI3: pierwsza wersja (używana na Zynq)

    • AXI4

    • AXI5

    • AXI4-Lite: uproszczone AXI4

    • AXI5-Lite

    • ACE (AXI Coherency Extensions): wersja AXI4 z dodatkowymi operacjami pozwalającymi na śledzenie stanu pamięci cache

    • ACE-Lite

    • ACE5: ACE, ale na bazie AXI5

  • szyna AHB (Advanced High-performance Bus) — poprzednik AXI, oparty o zupełnie inny schemat; jest to szyna wielodostępna (jest wielu inicjatorów, w każdym cyklu jeden z nich ma dostęp do szyny, wybrany przez arbitra), z wieloma targetami; używana w starszych (bądź mniejszych) procesorach ARM; istnieje też wariant AHB-Lite, w którym istnieje tylko jeden inicjator

  • szyna APB (Advanced Peripherial Bus) — dość prosty interfejs; ma jednego inicjatora i wiele targetów, nie ma żadnej możliwości przetwarzania potokowego ani równoległego; jest używana na „końcówkach” szyny systemowej, które nie wymagają zbyt dużej wydajności (czyli wolne urządzenia peryferyjne, bądź rzadko używane rejestry MMIO)

Zynq 7000

Zynq jest (poza częścią z FPGA) dość typowym układem typu SoC bazującym na procesorze ARM. Zawiera:

  • procesor ARM Cortex-A9

  • 256kB wewnętrznego RAMu

  • kontroler pamięci DDR pozwalający na podłączenie do 1GB zewnętrznego RAMu

  • dość nietrywialną sieć szyn AXI i switchy stanawiącą szkielet szyny systemowej

  • kilka szyn APB, do których podłączone są układy peryferyjne

  • kilka szyn AXI łączących szynę systemową z FPGA i pozwalających na rozszerzenie jej przez układ użytkownika:

    • SAXI_HP[0-3]: 4 szybkie i szerokie (64-bit) szyny AXI, w których FPGA jest inicjatorem, podłączone do switcha, który widzi tylko RAM (ten wbudowany i ten zewnętrzny) — pozwala na wydajny dostęp do pamięci

    • SAXI_GP[0-1]: 2 32-bitowe szyny AXI, w których FPGA jest inicjatorem, podłączone do switcha, z którego dostępna jest cała główna przestrzeń adresowa urządzenia — pozwala na dostęp do (prawie) wszystkich targetów

    • SAXI_ACP: 64-bitowa szyna AXI, w której FPGA jest inicjatorem, podłączone do kontrolera pamięci cache w procesorze — pozwala na dostęp do całej przestrzeni adresowej widzianej przez procesor w sposób spójny z jego cachem (pozostałe szyny SAXI_* pomijają cache)

    • MAXI_GP[0-1]: 2 32-bitowe szyny AXI, w których FPGA jest targetem; pozwala innym urządzeniom na wysyłanie transakcji do FPGA; szyna MAXI_GP0 ma przydzielony zakres adresów 0x40000000:0x80000000, a szyna MAXI_GP10x80000000:0xc0000000

  • kilka układów peryferyjnych, które mają moce DMA i mają szynę AXI łączącą je (jako inicjator) z resztą systemu

  • dwie szyny AHB-Lite, którymi podłączone są do głównej sieci kontrolery SDIO (bo akurat takie kupili)

  • wolnostojący kontroler DMA, pozwalający na przesył danych między pamięcią a peryferiami (w przypadku Zynq, jedynym podłączonym urządzeniem peryferyjnym jest FPGA)

Szyna AXI

Pełny opis szyny AXI można znaleźć w dokumentacji: https://developer.arm.com/documentation/ihi0022/e/AMBA-AXI3-and-AXI4-Protocol-Specification?lang=en . Tutaj opowiemy tylko o najważniejszych elementach.

Szyna AXI3 (w wersji zaimplementowanej w Zynq) składa się z następujących połączeń:

  • sygnały sterujące: ACLK (zegar) i ARESETN (zanegowany reset), wspólne dla wszystkich 5 strumieni; w przypadku interfejsów FPGA na Zynq, są sterowane ze strony FPGA (Zynq zawiera odpowiedni mostek, który przeniesie dane z/do wewnętrznych domen zegarowych i możemy użyć dowolnego zegara nie większego niż 250 MHz)

  • strumień żądań odczytu (od inicjatora do targetu) — służy do rozpoczynania transakcji odczytu:

    • ARVALID, ARREADY

    • ARADDR (32-bitowy): adres żądanego odczytu

    • ARID (różnej długości): identyfikator inicjatora transakcji — pełny identyfikator inicjatora w całym Zynq ma 12 bitów, ale tylko część jest przydzielona do wyboru przez naszą szynę (pozostałe bity są wypełniane przez sieć połączeń i identyfikują port, z którego wysłaliśmy transakcję)

      • MAXI_GP*: 12 bitów (jako target widzimy cały identyfikator inicjatora)

      • SAXI_ACP: 3 bity (pozostałe bity identyfikatora wskazują na procesor, a wewnątrz procesora na port ACP)

      • SAXI_GP*: 6 bitów (pozostałe bity identyfikatora wskazują na naszą szynę)

      • SAXI_HP*: 6 bitów (pozostałe bity identyfikatora wskazują na naszą szynę)

    • ARBURST (2-bitowy): typ transakcji:

      • 0: FIXED — wszystkie transfery w transakcji mają ten sam adres

      • 1: INCR — kolejne transfery w transakcji mają kolejno rosnące (o rozmiar transferu) adresy

      • 2: WRAP — dość skomplikowany tryb, podobny do INCR, ale ze specjalnymi zasadami zawijania; używany do wczytywania całej linii cache zaczynając od wybranego słowa

    • ARSIZE (2-bitowy): rozmiar jednego transferu danych, dekodowany następująco:

      • 0: 1 bajt

      • 1: 2 bajty

      • 2: 4 bajty

      • 3: 8 bajtów (tylko na szynach 64-bitowych)

    • ARLEN (4-bitowy): rozmiar transakcji w transferach pomniejszony o 1 (czyli 0 w tym polu oznacza 1 transfer; 5 oznacza 6 transferów)

    • ARCACHE (4-bitowy): atrybuty cacheowalności transakcji (można tu podać 0 jako inicjator)

    • ARLOCK (2-bitowy): atrybuty atomowości transakcji (należy tu podać 0 jako inicjator)

    • ARPROT (3-bitowy): atrybuty zabezpieczeń transakcji (można tu podać 0 jako inicjator)

    • ARQOS (4-bitowy): atrybuty priorytetu transakcji (można tu podać 0 jako inicjator)

    • ARUSER (5-bitowy, tylko na SAXI_ACP): specjalne atrybuty transakcji (należy to podać 0 jako inicjator)

  • strumień danych odczytu (od targetu do inicjatora) — służy do przesyłania odczytanych danych w transakcji odczytu i kończenia transakcji odczytu:

    • RVALID, RREADY

    • RDATA (32-bitowe lub 64-bitowe): właściwe dane

    • RID (różnej długości): równe ARID odczytu, którego dane dotyczą

    • RLAST (1-bitowe): prawda, jeśli to ostatni transfer danych z transakcji, kończy transakcję

    • RRESP (2-bitowe): typ odpowiedzi:

      • 0: OKAY — udana transakcja

      • 1: EXOKAY — udana transakcja z wyłącznością

      • 2: SLVERR — transakcja odrzucona przez target

      • 3: DECERR — transakcja odrzucona przez szynę (adres nie odpowiada żadnemu targetowi)

  • strumień żądań zapisu (od inicjatora do targetu) — służy do rozpoczynania transakcji zapisu:

    • AWVALID, AWREADY

    • AWADDR (32-bitowy): adres żądanego zapisu

    • AWID (różnej długości): analogicznie do ARID

    • AWBURST (2-bitowy): analogicznie do ARBURST

    • AWSIZE (2-bitowy): analogicznie do ARSIZE

    • AWLEN (4-bitowy): analogicznie do ARLEN

    • AWCACHE (4-bitowy): analogicznie do ARCACHE

    • AWLOCK (2-bitowy): analogicznie do ARLOCK

    • AWPROT (3-bitowy): analogicznie do ARPROT

    • AWQOS (4-bitowy): analogicznie do ARQOS

    • AWUSER (5-bitowy, tylko na SAXI_ACP): analogicznie do ARUSER

  • strumień danych zapisu (od inicjatora do targetu) — służy do przesyłania zapisywanych danych w transakcji zapisu:

    • WVALID, WREADY

    • WDATA (32-bitowe lub 64-bitowe): właściwe dane

    • WID (różnej długości): równe ARID odczytu, którego dane dotyczą

    • WLAST (1-bitowe): prawda, jeśli to ostatni transfer danych z transakcji, kończy transakcję

    • WSTRB (4-bitowe dla 32-bitowej szyny, 8-bitowe dla 64-bitowej szyny): sygnały byte enable dla poszczególnych bajtów, wybierają które bajty w słowie faktycznie należy zapisać

  • strumień odpowiedzi na zapisy (od targetu do inicjatora) — kończy transakcję zapisu:

    • BVALID, BREADY

    • BID (różnej długości): identyfikator inicjatora transakcji — równe AWID zapisu, na który odpowiada

    • BRESP (2-bitowe): typ odpowiedzi, analogiczne do RRESP

Strumienie w AXI są wariantem opisanych wcześniej interfejsów valid-ready z następującymi wymaganiami:

  • gdy wysyłający ustawi sygnał valid na 1, nie wolno mu ustawić go na 0 ani zmienić pakietu dopóki pakiet nie zostanie zaakceptowany przez odbierającego (czyli gdy ready będzie równe 1) — nie wolno „zrezygnować” z przesyłu pakietu

  • nie wolno mieć żadnych ścieżek kombinacyjnych między żadną parą sygnałów w interfejsie

Zasady działania transakcji AXI są następujące:

  1. Transakcja składa się z jednego lub więcej transferów, gdzie transfer ma dowolną długość będącą potęgą dwójki od 1 bajtu do szerokości szyny.

  2. Bajt o adresie X jest zawsze transferowany na bitach (X * 8 % DATA_WIDTH) : (X * 8 % DATA_WIDTH) + 8 szyny danych.

  3. Wszystkie adresy w ramach jednej transakcji muszą zawierać się w tym samym wyrównanym bloku 4kB.

  4. Jeśli adres transakcji nie jest wyrównany do rozmiaru transferu, tylko część bajtów w pierwszym transferze zostanie przetransferowana; w pozostałych transferach zostanie już przetransferowany pełny rozmiar.

  5. Targety będące pamięcią muszą wspierać wszystkie typy i rozmiary transakcji. Targety będące rejestrami MMIO mogą ograniczyć się do wybranego podzbioru.

  6. Zawsze przesyłane jest dokładnie tyle danych, ile zostało wcześniej zadeklarowane (nawet w przypadku błędu).

  7. Zapis wygląda następująco:

    1. Inicjator wysyła na strumieniu AW* pakiet z adresem i metadanymi transakcji.

    2. Inicjator wysyła na strumieniu W* pakiety z danymi transakcji (tyle pakietów, ile jest transferów w transakcji). Ostatni pakiet w transakcji (i tylko on) ma ustawiony bit WLAST. Przesył danych może odbywać się równolegle z przesyłem adresu.

    3. Wszystkie pakiety na strumieniu W* muszą być wysyłane w takiej samej kolejności co odpowiadające pakiety na strumieniu AW*.

    4. Gdy target odbierze wszystkie pakiety odpowiadające jednej transakcji, wysyła na strumieniu B* jeden pakiet z odpowiedzią (nie wolno zrobić tego przed otrzymaniem wszystkich danych).

    5. Inicjator nie musi czekać na odpowiedź przed rozpoczęciem kolejnego zapisu — tempo wysyłania transakcji ograniczone jest tylko sygnałami gotowości targetu.

    6. W ramach jednego identyfikatora AWID/WID/BID, odpowiedzi będą zawsze przychodzić w takiej samej kolejności, w jakiej zapisy zostały wysłane.

    7. Switchom i targetom wolno jednak dowolnie przestawiać zapisy o różnych identyfikatorach AWID/WID/BID — odpowiedzi mogą też przyjść w dowolnej kolejności.

  8. Odczyt wygląda następująco:

    1. Inicjator wysyła na strumieniu AR* pakiet z adresem i metadanymi transakcji.

    2. Target wysyła na strumieniu R* pakiety z danymi transakcji (tyle pakietów, ile ma być transferów w transakcji). Ostatni pakiet w transakcji (i tylko on) ma ustawiony bit RLAST.

    3. Inicjator nie musi czekać na odpowiedź przed rozpoczęciem kolejnego odczytu — tempo wysyłania transakcji ograniczone jest tylko sygnałami gotowości targetu.

    4. W ramach jednego identyfikatora ARID/RID, odpowiedzi będą zawsze przychodzić w takiej samej kolejności, w jakiej odczyty zostały wysłane.

    5. Switchom i targetom wolno jednak dowolnie przestawiać odczyty o różnych identyfikatorach ARID/RID — odpowiedzi mogą też przyjść w dowolnej kolejności.

  9. Odczyty i zapisy są niezależnymi strumieniami i mogą być dowolnie przestawiane między sobą — jeśli inicjator chce, by miały ustaloną kolejność, powinien poczekać na odpowiedź na pierwszą transakcję przed wysłaniem drugiej.