.. _w03-ram:

==================================
Wykład 3: RAM w układach cyfrowych
==================================

Data: 03.11.2020

.. toctree::

.. contents::


Czym jest RAM
=============

RAM jest ogólną nazwą na pamięć o dowolnym dostępie — typ pamięci, w której
możemy we w miarę podobnym czasie dostać się do dowolnej komórki (czyli ją
odczytać lub zapisać).  Jest to tak naprawdę niezbyt dobra definicja
(można by się kłócić, że DRAM tak naprawdę nie pasuje do tej definicji).

RAMem będziemy określać tablicę bitów, która ma:

- szerokość: ile bitów ma jednostka pamięci, którą czytamy/piszemy
- głębokość: ile różnych adresów możemy wybrać do odczytu/zapisu
- rozmiar: szerokość × głębokość
- porty odczytu: zbiory sygnałów, których używamy do odczytu zawartości RAMu.
  Pamięć może mieć jeden lub więcej niezależnych portów odczytu.
- porty zapisu: zbiory sygnałów, których używamy do zapisu zawartości RAMu.
- porty odczytu i zapisu (robiące jedno i/lub drugie pod tym samym adresem).

RAM jest bardzo często przydatnym elementem w logice cyfrowej.  W zależności
od potrzeb stosuje się różne rozmiary i technologie RAMu.  Te najważniejsze to:

1. Najprostszym rodzajem RAMu jest po prostu zespół przerzutników połączonych
   przez multipleksery (do odczytu) i dekodery adresu zapisu.  Szybko staje
   się to jednak nieefektywne (szczególnie w przypadku logiki programowalnej).

2. SRAM (pamięć statyczna): tablica bitów zrealizowanych jako tzw. zatrzaski
   (po 6 tranzystorów każdy w najczęstszej realizacji), zorganizowana fizycznie
   w wiersze i kolumny.  Pamięć ta sama podtrzymuje swój stan dopóki ma zasilanie
   i może w dowolnym momencie przeczytać lub zapisać dowolny bit.

   Pamięć SRAM może być osadzona w układzie logicznym (jeśli jest mała), bądź
   umieszczona w osobnym układzie scalonym (jeśli jest duża).  W przypadku
   układów ASIC, możemy zaprojektować pamięć o dość dowolnych parametrach.
   W przypadku układów FPGA, mamy dostępne gotowe bloki pamięci o z góry
   ustalonych rozmiarach, zazwyczaj podzelonych na kilka rodzajĂłw:

   - LUT RAM albo distributed RAM: małe (rzędu dziesiątek bądź setek bitów),
     są nieco przerobionymi LUTami do których dorobiona została funkcjonalność
     zapisu.  Zazwyczaj mają jeden port synchroniczny port zapisu i jeden lub
     więcej asynchroniczny port odczytu.  Jeśli nie są akurat używane jako
     pamięć, mogą służyć za zwykłe LUTy.

   - block RAM: średniego rozmiaru (rzędu dziesiątek kilobitów).  Dedykowany
     blok.  Zazwyczaj mają dwa niezależne synchroniczne porty odczytu+zapisu.

   - (czasem w niektórych FPGA) ultra RAM: jak block RAM, ale większe (rzędu setek
     kilobitĂłw).

   Układy FPGA (w przeciwieństwie do ASIC) zazwyczaj dają nam możliwość wyboru
   stanu początkowego pamięci (w szczególności możemy nie używać żadnych portów
   zapisu i traktować pamięć jako ROM).

   Jeżeli bloki pamięci dostępne w FPGA są dla nas za małe, można połączyć wiele
   z nich w jedną pamięć.

3. DRAM (pamięć dynamiczna): tablica bitów zrealizowanych w najtańszy możliwy sposób:
   kaĹźdy bit to jeden tranzystor i jeden kondensator.  RĂłwnieĹź fizycznie zrealizowana
   w wiersze i kolumny.  Ze względu na sposób realizacji, pamięć ta cały czas powoli
   zapomina swój stan, nawet gdy jest zasilana — gwarantowany czas przechowywania
   informacji to tylko 64ms.  Co więcej, dostęp jest bardzo skomplikowany — trzeba
   najpierw przeczytać cały wiersz pamięci do specjalnego bufora (co zajmuje trochę
   czasu) przed przeczytaniem/odczytaniem danej komórki, a następnie odłożyć go
   z powrotem (co również zajmuje czas).  Użycie takiej pamięci wymaga skomplikowanego
   kontrolera pamięci, który będzie pilnował wykonania wszystkich czynności w odpowiedniej
   sekwencji i z odpowiednimi odstępami czasowymi, a także zapewniał odświeżanie pamięci
   — regularne czytanie i zapisywanie z powrotem każdego wiersza pamięci, by utrzymać
   jej stan.

   Ponieważ efektywna konstrukcja pamięci DRAM wymaga trochę innego procesu niż
   efektywna konstrukcja logiki (i ze względu na jej rozmiar), pamięć DRAM jest
   zawsze osobnym układem.  Kontroler pamięci znajduje się jednak razem z logiką.
   W przypadku FPGA, kontroler pamięci realizuje się zazwyczaj używając programowalnej
   logiki (wyjątkiem są FPGA z wbudowanym kontrolerem pamięci, w tym te z wbudowanym
   procesorem jak Zynq).


RAM osadzony w układach cyfrowych
=================================

Aby użyć osadzonego RAMu w FPGA, możemy:

1. Przeczytać dokumentację FPGA, wybrać odpowiedni rodzaj RAMu i zinstancjonować
   ręcznie dany prymityw, bądź
2. Opisać wymiary i funkcjonalność pożądanego RAMu w języku opisu sprzętu
   i zdać się na automatyczny wybór przez narzędzia syntezy.

Automatyczny wybór jest oczywiście prostszy, lecz często ma ograniczone możliwości
— prymitywy dostępne w FPGA miewają wiele funkcji ciężkich do opisania w języku
sprzętu i ich użycie w ten sposób będzie niemożliwe (np. funkcje ECC czy hardware
FIFO).

Opis RAMu w nMigen
------------------

Aby opisać swój RAM w nMigen, najpierw tworzymy obiekt typu ``Memory`` z odpowiednimi
wymiarami i danymi początkowymi, a następnie wywołujemy jego metody ``read_port``
i ``write_port``, by stworzyć jego porty, i dodajemy je jako podmoduły naszego modułu::

    m = Module()

    ...

    # Pamięć o 16 komórkach, każda komórka ma 8 bitów.  Dane początkowe
    # to 0x12, 0x33, 0x44, 0, 0, ..., 0.
    mem = Memory(width=8, depth=16, init=[0x12, 0x33, 0x44])

    # Tworzymy synchroniczny port odczytu.
    rdport = m.submodules.rdport = mem.read_port()
    raddr = Signal(4)
    rdata = Signal(8)
    m.d.comb += [
        # podłączamy adres z którego ma czytać (gdzieś powinno być przypisanie do raddr)
        rdport.addr.eq(raddr),
        # podłączamy dane
        rdata.eq(rdport.data),
        # [opcjonalnie] podłączamy sygnał enable — gdy ten sygnał będzie równy 0,
        # port nie będzie pracował (wartość rdata się nie zmieni).  Jeśli go nie
        # podłączymy, domyślnie będzie to zawsze 1.
        rdport.en.eq(cośtam == coś),
    ]

    # Tworzymy synchroniczny port zapisu.
    wrport = m.submodules.wrport = mem.write_port()
    waddr = Signal(4)
    wdata = Signal(8)
    wen = Signal()
    m.d.comb += [
        wrport.addr.eq(waddr),
        wrport.data.eq(wdata),
        # Gdy ten sygnał będzie równy 1 na zboczu zegara, nastąpi zapis danych
        # wdata pod adres waddr.  Gdy będzie równy 0, nic się nie stanie.
        wrport.en.eq(wen),
    ]

Jeśli zamiast tego chcielibyśmy mieć asynchroniczny port odczytu::

    # Tworzymy asynchroniczny port odczytu.
    rdport = m.submodules.rdport = mem.read_port(domain='comb')
    raddr = Signal(4)
    rdata = Signal(8)
    m.d.comb += [
        # podłączamy adres z którego ma czytać (gdzieś powinno być przypisanie do raddr)
        rdport.addr.eq(raddr),
        # podłączamy dane
        rdata.eq(rdport.data),
        # w asynchronicznych portach odczytu nie wolno mieć sygnału en — nie miałoby to sensu
    ]

Czasem przydaje się port zapisu z osobnymi sygnałami enable dla różnych grup
bitów danych (w szczególności, osobno dla każdego bajtu w słowie)::

    mem = Memory(width=32, depth=0x1000)

    # Tworzymy synchroniczny port zapisu z osobną kontrolą zapisu każdego bajtu.
    wrport = m.submodules.wrport = mem.write_port(granularity=8)
    waddr = Signal(12)
    wdata = Signal(32)
    wen = Signal(4)
    m.d.comb += [
        wrport.addr.eq(waddr),
        wrport.data.eq(wdata),
        # bit 0 wen mówi, czy zapisać bity 0-7 wdata
        # bit 1 wen mówi, czy zapisać bity 8-15 wdata
        # bit 2 wen mówi, czy zapisać bity 16-23 wdata
        # bit 3 wen mówi, czy zapisać bity 24-31 wdata
        wrport.en.eq(wen),
    ]


W nMigen nie możemy bezpośrednio opisać portu odczytu+zapisu — zamiast tego,
tworzymy osobne porty odczytu i zapisu współdzielące sygnał adresu.  Narzędzia
syntezy scalą je w jeden port, jeśli będzie to potrzebne do realizacji RAMu.


Realizacja RAMĂłw w FPGA
-----------------------

Należy pamiętać, że nasz opis RAMu musi być realizowalny w docelowym układzie
FPGA.  Synteza spróbuje wielu transformacji by móc zrealizować nasz RAM, lecz
nie zawsze będzie to możliwe:

1. Szerokość RAMu zostanie dopasowana do szerokości sprzętowej.  Jeśli nasz
   sprzęt ma tylko szerokość 8, a prosimy o RAM o szerokości 5, zostanie
   on odpowiednio rozszerzony (marnując ⅜ dostępnych bitów).  Jeśli prosimy
   o RAM o szerokości 24, syntezator wykorzysta 3 równoległe bloki sprzętu
   by poskładać nasz RAM.

2. Głębokość RAMu również zostanie dopasowana, marnując RAM jeśli nasz RAM
   jest mniejszy od bloku, bądź wykorzystując wiele bloków (i odrobinę dodatkowej
   logiki sterującej) gdy nasz RAM jest większy.

3. Jeśli nasze porty nie dają się zapakować do portów bloku sprzętowego przez
   nadmiar portów odczytu, synteza zduplikuje cały RAM dla każdego portu odczytu
   (bądź grupy portów odczytu).  Jeśli nie da się zapakować z innych powodów
   (nadmiar portów zapisu, brak możliwości poscalania portów odczytu z portami
   zapisu, bądź brak wymaganej funkcjonalności portów), RAMu w ogóle nie da się
   zrealizować.

Zaleca się przestrzegać następujących reguł:

1. UĹźywamy maksymalnie jednego portu zapisu.
2. Jeżeli pamięć jest mała: staramy się minimalizować liczbę portów odczytu.
3. Jeżeli pamięć jest duża (kilka kilobitów i więcej): minimalizujemy liczbę
   portĂłw odczytu, uĹźywamy tylko synchronicznych portĂłw odczytu.


Do czego uĹźywany jest RAM
=========================

Przykładowe użycia RAMu:

1. Plik rejestrĂłw procesora
2. Kolejka przychodzących danych / zleceń / pakietów ...
3. Kod / pamięć operacyjna mikrokontrolera
4. Pamięć cache
5. Tabele stałych (np. stablicowane wartości sinusa)
6. Tablica aktualnie przetwarzanych danych (część mnożonej macierzy, itp)