Trochę historii
Możliwość wyświetlania tekstów na typowym monitorze VGA pochodzących z systemów mikroprocesorowych, w których istnieje możliwość „posiadania” kawałka zewnętrznej pamięci RAM (ot choćby takich jak Z80, M6800, I8085 czy wręcz I8051, niektóre modele AVR lub ATMEGA) jest bardzo nęcąca. Bo któżby nie chciał podłączyć do takiego systemu takiego monitora. Ja w swojej historii zabaw z prockami mam już takie doświadczenie. W czasach, gdy pasjami oddawałem się zabawom z Z80, zbudowałem sobie coś na ten kształt. Wtedy był to dosyć prosty kontroler, mający 1kB pamięci RAM wpiętej w przestrzeń adresową procka, który generował obraz na zwykły telewizor (generował zespolony sygnał wizyjny).
Na ekranie było wyświetlanych 32 wiersze po 32 znaki w wierszu, gdyż zależało mi na jak największej liczbie wierszy. Konstrukcja, pomimo sporych wad, jakoś działała. Cóż, z niektórymi problemami to człowiek dopiero się stykał i wcześniej nie był ich świadomy, jednak każde doświadczenie jest cenne (jak się z jego wyciąga wnioski). Z racji dużej prostoty w rozwiązaniu podstawową wadą było to, że obraz „śnieżył”. Spowodowane jest to tym, że...
Zanim wyjaśnię muszę przedstawić koncepcję rozwiązania. Było to: Układ sterujący to była banda przerzutników, która generowała adres do pamięci RAM (wynikający z pozycji na ekranie, w tym część adresu do generatora znaków), impulsy synchronizacji, stroby do wszystkich rejestrów. W tym znajdował się arbitraż dostępu do pamięci, gdyż jak widać, istnieje możliwość konfliktu dostępu. Układ generowania sygnału wizyjnego musi ciągle być napędzany i czasami procek ma ochotę coś zapisać do pamięci. Wtedy uznałem, że pierwszeństwo będzie należeć do procka a generator nie dostanie danych. Właściwie, to nie ma sytuacji, że nie dostanie, układ wizyjny dostał coś (już nie pamiętam, czy to były same jedynki, czy zera). No i to powodowało wyświetlenie krótkiej kreski poziomej (na szerokość jednego znaku). Jak procek chciał przewinąć ekran, to na ekranie był koszmar. Później dorabiałem jakieś patenty ze sterowaniem typu WAITSTATE czy wręcz HOLD. Tam było jeszcze coś takiego (już nie ująłem tego na rysunku), że najstarsze dwa bity (bo generator zawierał tylko wielkie litery – kod znaku był 6-bitowy) atrybutem, to znaczy znak miał się wyświetlić normalnie lub w negatywie oraz był mrugający (na przemian był wyświetlany znak lub spacja). Wtedy „poznałem” istnienie propagacji sygnału przez bramki. Logika kombinacyjna miała różne długości propagacji (różną liczbę funktorów w torze) i jak były wyświetlane kolejne znaki z różnymi atrybutami, to troszkę atrybuty danego znaku wjeżdżały na następny.
Dzisiaj, mając do dyspozycji bardziej nowoczesne układy można pokusić się o redesign. Widząc ile monitorów zaczyna walać się po kontach, to żal bierze, jak one stoją i „rdzewieją”. Kilka razy rozmyślałem nad jakimś sensownym rozwiązaniem. Tu koncepcja zaczęła zmierzać w stronę pamięci RAM z podwójnym dostępem. Robiąc kolejny jakiś porządek w pracowni w łapy wlazł mi zestaw z BTC z układem SPARTAN 3, no i w mózgu coś się przestawiło (no chyba to nie jest przypadek). Przecież FPGA SPARTAN ma coś co się nazywa „Block RAM” i jest tego całkiem sporo. Tutaj potrzeba 4 kilobajty i można uzyskać cechy jakie miał DOS'owy ekran w trybie tekstowym. Jak by tego było mało, to zestaw ten (ZL9PLD + ZL10PLD z KAMAMI) zawiera już na pokładzie gniazdu jak w kartach VGA z opornikami dającymi możliwość prostej (jednak wystarczającej) konwersji z cyfry na analog.
No to trzeba zawalczyć o nią. Poczyniłem pierwszy eksperyment i utworzyłem komponent w VHDL'u modelujący dwuportową pamięć RAM o wymaganej wielkości. Pierwsze eksperymenty są bardzo obiecujące.
Kod: Zaznacz cały
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;
entity dualport_ram is port ( Clock : in std_logic ;
WriteStrobe : in std_logic ;
WriteAddress : in integer range 0 to 4095 ;
DataIn : in std_logic_vector ( 7 downto 0 ) ;
ReadStrobe : in std_logic ;
ReadAddress : in integer range 0 to 4095 ;
DataOut : out std_logic_vector ( 7 downto 0 ) ) ;
end dualport_ram ;
architecture Behavioral of dualport_ram is
type Memory is array ( 0 to 4095 ) of std_logic_vector ( 7 downto 0 ) ;
signal Memory_Array : Memory ;
signal WrAdrReg : integer range 0 to 4095 ;
signal RdAdrReg : integer range 0 to 4095 ;
signal DInReg : std_logic_vector ( 7 downto 0 ) ;
signal DOutReg : std_logic_vector ( 7 downto 0 ) ;
signal WrFlag : std_logic ;
signal RdFlag : std_logic ;
begin
process ( Clock )
begin
if Clock'event and Clock = '1' then
WrAdrReg <= WriteAddress ;
RdAdrReg <= ReadAddress ;
DInReg <= DataIn ;
WrFlag <= WriteStrobe ;
RdFlag <= ReadStrobe ;
end if ;
end process ;
process ( WrFlag )
begin
if WrFlag'event and WrFlag = '0' then
Memory_Array ( WrAdrReg ) <= DInReg ;
end if ;
end process ;
process ( RdFlag )
begin
if RdFlag'event and RdFlag = '0' then
DOutReg <= Memory_Array ( RdAdrReg ) ;
end if ;
end process ;
DataOut <= DOutReg ;
end Behavioral;
Kod: Zaznacz cały
INFO:Xst:2691 - Unit <dualport_ram> : The RAM <Mram_Memory_Array> will be implemented as a BLOCK RAM, absorbing the following register(s): <DOutReg>.
-----------------------------------------------------------------------
| ram_type | Block | |
-----------------------------------------------------------------------
| Port A |
| aspect ratio | 4096-word x 8-bit | |
| mode | write-first | |
| clkA | connected to signal <WrFlag> | fall |
| weA | connected to internal node | high |
| addrA | connected to signal <WrAdrReg> | |
| diA | connected to signal <DInReg> | |
-----------------------------------------------------------------------
| optimisation | speed | |
-----------------------------------------------------------------------
| Port B |
| aspect ratio | 4096-word x 8-bit | |
| mode | write-first | |
| clkB | connected to signal <RdFlag> | fall |
| addrB | connected to signal <RdAdrReg> | |
| doB | connected to signal <DataOut> | |
-----------------------------------------------------------------------
| optimisation | speed | |
-----------------------------------------------------------------------
No to poeksperymentowałem z tym komponentem. Moduł testowy jest następujący:
Kod: Zaznacz cały
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
use ieee.numeric_std.all;
entity dualport_ram_tb_vhd is
end dualport_ram_tb_vhd ;
architecture behavior of dualport_ram_tb_vhd is
component dualport_ram is port ( Clock : in std_logic ;
WriteStrobe : in std_logic ;
WriteAddress : in integer range 0 to 4095 ;
DataIn : in std_logic_vector ( 7 downto 0 ) ;
ReadStrobe : in std_logic ;
ReadAddress : in integer range 0 to 4095 ;
DataOut : out std_logic_vector ( 7 downto 0 ) ) ;
end component ;
--Inputs
signal Clock : std_logic := '1';
signal WriteStrobe : std_logic := '1';
signal WriteAddress : integer range 0 to 4095 ;
signal DataIn : std_logic_vector ( 7 downto 0 ) := ( others => '0' ) ;
signal ReadStrobe : std_logic := '1' ;
signal ReadAddress : integer range 0 to 4095 ;
--Outputs
signal DataOut : std_logic_vector ( 7 downto 0 ) ;
begin
-- Instantiate the Unit Under Test (UUT)
uut: dualport_ram port map ( Clock => Clock ,
WriteStrobe => WriteStrobe ,
WriteAddress => WriteAddress ,
DataIn => DataIn ,
ReadStrobe => ReadStrobe ,
ReadAddress => ReadAddress ,
DataOut => DataOut ) ;
tb : process
begin
wait for 100 ns ;
ReadAddress <= 0 ;
WriteAddress <= 0 ;
WriteStrobe <= '1' ;
ReadStrobe <= '1' ;
for Inx in 0 to 7 loop
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
end loop ;
DataIn <= "10100101" ;
WriteAddress <= 0 ;
for Inx in 0 to 31 loop
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= WriteAddress + 1 ;
end loop ;
WriteAddress <= 0 ;
DataIn <= "10100000" ;
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= 1 ;
DataIn <= "10100001" ;
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= 2 ;
DataIn <= "10100010" ;
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= 3 ;
DataIn <= "10100011" ;
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= 4 ;
DataIn <= "10100100" ;
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= 5 ;
DataIn <= "10100101" ;
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= 6 ;
DataIn <= "10100110" ;
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= 7 ;
DataIn <= "10100111" ;
WriteStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= 0 ;
WriteStrobe <= '1' ;
for Inx in 0 to 31 loop
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteAddress <= WriteAddress + 1 ;
end loop ;
ReadAddress <= 0 ;
for Inx in 0 to 31 loop
ReadStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
ReadStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
ReadAddress <= ReadAddress + 1 ;
end loop ;
DataIn <= "10111101" ;
WriteAddress <= 5 ;
ReadAddress <= 7 ;
WriteStrobe <= '0' ;
ReadStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
ReadStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
for Inx in 0 to 7 loop
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
end loop ;
DataIn <= "10100101" ;
WriteAddress <= 9 ;
ReadAddress <= 9 ;
WriteStrobe <= '0' ;
ReadStrobe <= '0' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
WriteStrobe <= '1' ;
ReadStrobe <= '1' ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
for Inx in 0 to 7 loop
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
Clock <= '1' ;
wait for 100 ns ;
Clock <= '0' ;
wait for 100 ns ;
end loop ;
wait; -- will wait forever
end process ;
end;
Później jest kontrolny odczyt zapisanych danych. No jest to co powinno być. Przy rozdzielonych zapisach i odczytach, zachowuje się w sposób przewidywany. To zasymulowałem jednoczesny zapis i odczyt różnych danych pod różne adresy. Układ zrobił co należy i nie pogubił się. No dodatkowo zasymulowałem jednoczesny zapis i odczyt danych spod tego samego adresu. Wcześniej w pamięci w tym miejscu było zapisane coś. Symulator pokazał, że zaistniał zapis i jednocześnie zostały odczytane nowe dane. Tak jakby przy jednoczesnym zapisie i odczycie w tą samą komórkę priorytetem był zapis.
Mając tak podstawowy komponent (pdwuportową pamięć), szanse ciągle rosną. No z nadzieją patrzę na przyszłość i wierzę, że akcja zakończy się powodzeniem.