Moduł Memory
Moduł Memory jest najdłuższym i najbardziej zawiłym modułem w tym projekcie. Długo zastanawiałem się, czy nie byłoby lepiej podzielić go na pamięć czcionki i pamięć danych do wyświetlenia, lecz ostatecznie stwierdziłem, że te wszystkie obszary danych będą w jednym miejscu.
Moduł składa się z dwóch funkcjonalnych części, tworzących jedną logiczną całość. Pierwszą z nich jest odbieranie bajtów, pochodzących z interfejsu UART. Bajty te mogą być znakami sterującymi kursorem – enter, backspace i escape (który umieszcza kursor na pozycji (0,0), czyli w lewym górnym rogu). Mogą być też kodem ASCII znaku lub informacjami o kolorach. Druga część to obsługiwanie zapytań z VGA, co wiąże się z odczytywaniem danych z pamięci obrazu i czcionki.
W tym momencie musimy przyjrzeć się dokładniej pamięci RAM, w której zapisywany jest tekst oraz kolory. Musimy w niej przechowywać 2400 bajtów tekstu i 2400 bajtów informacji o kolorach, czyli razem 4800 bajtów. Wygodnie byłoby mieć osobne pamięci na tekst i kolory. Wtedy bajt tekstu i bajt koloru w obu pamięciach miałyby te same adresy, a ponadto operacje odczytu/zapisu z/do obu pamięci mogłyby wykonywać się jednocześnie. Potrzebowalibyśmy trzech bloków EBR na tekst i kolejnych trzech na kolor. Problem tylko w tym, że mamy do dyspozycji 5 bloków EBR o pojemności 1024 bajty każdy.
Zatem musimy scalić wszystko w jedną pamięć, aby pomieścić się w pięciu blokach EBR. Kod znaku i kolory umieścimy w następujących po sobie bajtach. Tworzy to ciekawą właściwość – jeżeli najmłodszy bit adresu ma wartość 0 to adres wskazuje na bajt tekstu, a jeżeli 1 to na bajt koloru. Ta cecha jest wspólna dla wszystkich znaków w pamięci. Pozostałą część adresu można łatwo obliczyć znając numer kolumny i wiersza. Wystarczy numer wiersza przemnożyć przez 80 i dodać numer kolumny – a te właśnie informacje kontroler pamięci otrzymuje od modułu VGA.
Aby zaadresować 4800 bajtów potrzebujemy 13-bitowego adresu. I tutaj pojawia się problem, bo Lattice Synthesis Engine, mając 13-bitowy adres, syntezuje pamięć o rozmiarze, jaki maksymalnie można zaadresować wykorzystując adres o takiej szerokości, tzn 8192 bajty. Na etapie mapowania dostaniemy błąd, bo taka pamięć potrzebuje 8 bloków EBR, a mamy tylko 5. Poza tym nawet, gdybyśmy mieli jeszcze trzy wolne bloki EBR do użytku, to zostałyby one bezsensownie zmarnowane.
Rozwiązaniem tego problemu jest skorzystanie z dodatku IP Express lub rozbicie pamięci na osobne bloki i scalenie ich przy pomocy własnego dekodera adresu. Wybrałem tę drugą opcję aby pokazać, jak można to zrobić.
Wejście adresowe bloku EBR jest 10-bitowe, ponieważ w pojemność bloku EBR to 1024 bajty (210=1024). Skoro potrzebujemy pięciu takich bloków, to musimy wstawić dodatkową zmienną 3-bitową, aby wskazywać który blok EBR chcemy odczytać lub zapisać. W ten sposób tworzymy 13-bitowy adres, który składa się z numeru bloku EBR i adresu wewnątrz EBR.
Pierwsza część kodu odpowiedzialna jest za odbieranie znaków z UART i ich analizowanie. Na potrzeby tego zadania musimy utworzyć kilka zmiennych pomocniczych, zaczynając od linii 1.
- WriteStep1 i WriteStep2 to rejestry ułatwiające zapisywanie kolejno po sobie bajtów koloru i tekstu. Omówimy je dokładniej później.
- WriteRequest to rejestr sterujący wejściami WriteEnable_i wszystkich pamięci RAM. Ustawienie tego rejestru w stan wysoki spowoduje, że przy najbliższym zboczu rosnącym pamięć zapisze dane z wejścia Data_i pod adresem, wskazywanym przez wartość znajdującą się na wejściu WriteAddress_i.
- WriteBuffer – 8-bitowy rejestr przechowujący dane do zapisania w pamięci RAM. W zależności od fazy zapisu, jest to bajt koloru lub bajt tekstu.
- ColorBuffer to 8-bitowy rejestr przechowujący ostatnio odebrany bajt koloru, domyślnie ustawiony jest w taki sposób, aby wyświetlać biały tekst na czarnym tle. Po odebraniu bajtu koloru ten rejestr zostanie zaktualizowany, a następnie wszystkie znaki odebrane później będę pokolorowane tak samo do czasu, aż zostanie odebrany inny bajt koloru.
- WriteAddress – adres pod którym ma zostać zapisany bajt tekstu lub koloru.
- CursorX i CursorY – współrzędne znaku, do którego ma zostać zapisany następny bajt tekstu, odebrany przez UART.
- WriteCharNum – numer znaku, obliczony metodą z linii 2 (a zarazem bity [12:1] adresu do zapisania).