Skocz do zawartości

Pomocna odpowiedź

Napisano

 

witam

Projekt: AGNES — 8‑bitowy procesor na FPGA (wersja poglądowa) Wątek prezentuje kompletną, działającą implementację procesora 8‑bitowego w VHDL. Model obejmuje architekturę jednostki wykonawczej, pamięci RAM/ROM, zestaw instrukcji oraz logikę sterującą. Procesor jest testowany na płytce PCB z układem Xilinx Spartan‑3 mojego autorstwa. Przedstawiona wersja pełni funkcję referencyjną — pokazuje poprawność struktury CPU i organizacji bloków.

1. Architektura pamięci

ROM — 16 bit × 2048 Format słowa: 5 bit instrukcji + 11 bit adresu/danych. Tryby: INSADDR, ROMADDR, ROMDATA.

RAM — 8 bit × 2048 Pamięć danych, zapis sterowany sygnałem RAMWE.

2. Cykl pracy procesora (4 takty)

00 — pobranie instrukcji z ROM 01 — pobranie danych z RAM 10 — wykonanie operacji 11 — zapis wyników / aktualizacja PC

3. Zestaw instrukcji (skrót)

Transfer danych: MOVLB, MOVEB, MOVRB, MOVBR, MOVER, NOP Logika: ANDRB, IORRB, XORRB, COMR Arytmetyka: ADDRB, SUBRB, MULRB Rotacje: RRR, RLR, SWAPR Flagi: SETC, CLRC Skoki warunkowe: SKIPC, SKIPDC, SKIPZ, SKIPE, SKIPU, SKIPL Skoki: GOTO, CALL, ADDPC Przerwania: RETURN, RETLB, RETFIE RAM: SETR, CLRR

4. Jednostka wykonawcza (ALU)

Obsługuje operacje logiczne, arytmetyczne, rotacje, flagi oraz zapis wyników. Wynik: 9 bitów (8 danych + CARRY).

5. Rejestry i sygnały

B, R, PC, TOS, INTPC, flagi C/DC/Z, sygnały porównania REB/RUB/RLB, sygnały skoków SKIP*.

6. Obsługa przerwań

Przerwanie zewnętrzne (INT_KBD), zapis adresu powrotu w ROM (od końca przestrzeni), powrót przez RETURN, RETLB, RETFIE.

7. Status projektu

Wersja poglądowa — prezentuje działającą strukturę CPU: ROM → dekoder → ALU → RAM → PC, pełny cykl 4‑taktowy, skoki, przerwania, integracja pamięci. Wątek ma charakter referencyjny; kolejne wersje CPU będą rozwijane osobno.

8. Cel wątku

Dokumentacja kompletnej, działającej architektury 8‑bitowego procesora w VHDL.

library IEEE;
use ieee.std_logic_1164.all;
use ieee.std_logic_UNSIGNED.all;

entity RAM is
    Port ( Clk : in  STD_LOGIC;
           WE : in  STD_LOGIC;
           DI : in  STD_LOGIC_VECTOR (7 downto 0);
           ADDR : in  STD_LOGIC_VECTOR (10 downto 0);
           DO : out  STD_LOGIC_VECTOR (7 downto 0)
			  );
end RAM;

architecture Behavioral of RAM is

	type ram_type is array (0 to 2047) of std_logic_vector (7 downto 0);
	signal RAM: ram_type;

begin

	process (Clk)
	begin
		if falling_edge (Clk) then
			if WE = '1' then
			  RAM(conv_integer(ADDR)) <= DI;
			else
				DO <= RAM(conv_integer(ADDR)) ;
			end if;
		end if;
	end process;


end Behavioral;

-----------------------------------------------------------------------------------
--
-- 
-----------------------------------------------------------------------------------
--
-- pamięć ROM 16b x 2048 
--
-- mapa pamięci ROM
-- "SEL" + "INSADDR" = "iiiii" + "aaa aaaa aaaa" = pamięć ROM (16 bit) [5bit instrukcje|11bit adres instrukcji]
-- "SEL" + "ROMADDR" = "iiiii" + "rrr rrrr rrrr" = pamięć ROM (16 bit) [5bit instrukcje|11bit adres pamięci RAM]
-- "SEL" + "ROMDATA" = "iiiii" + "xxx bbbb bbbb" = pamięć ROM (16 bit) [5bit instrukcje|3bit nieużywane|8bit dane]
--
-- licznik taktów kontrolera (4 takty na 1 istrukcję)
-- Cnt = "00" <- pobieranie instrukcji z pamięci ROM
-- Cnt = "01" <- pobieranie danych z pamięci RAM
-- Cnt = "10" <- wykonanie instrukcji
-- Cnt = "11" <- wykonanie instrukcji, zmiana zawartości licznika instrukcji, zapis danych do pamięci RAM|rejestru B|PORT_O
--
-----------------------------------------------------------------------------------
--
--  instrukcje
--	
--  NOP  	"00000" -- instrukcja pusta
--  MOVLB	"00001" -- instrukcja kopiująca dane ROM do rejestru B
--  MOVEB   "00010" -- instrukcja kopiująca dane prortu zewnętrznego do rejestru B
--  MOVRB   "00011" -- instrukcja kopiująca dane RAM do rejestru B
                   
--  ANDRB   "00100" -- instrukcja RAM and B wynik zapisywany w RAM pod tym samym adresem
--  IORRB   "00101" -- instrukcja RAM or B wynik zapisywany w RAM pod tym samym adresem
--  XORRB   "00110" -- instrukcja RAM xor B wynik zapisywany w RAM pod tym samym adresem
--  COMR    "00111" -- instrukcja not RAM wynik zapisywany w RAM pod tym samym adresem
                   
--  ADDRB   "01000" -- instrukcja (CARRY & RAM) + B
--  SUBRB   "01001" -- instrukcja (CARRY & RAM) - B
--  RRR     "01010" -- instrukcja CARRY & RAM rotacja w prawo
--  RLR     "01011" -- instrukcja CARRY & RAM rotacja w lewo
                   
--  SWAPR   "01100" -- instrukcja zamiane 4 bit starszych z młodszymi wynik zapisywany w RAM pod tym samym adresem
--  MULRB   "01101" -- instrukcja monożenie RAM i B wynik (7 downto 0) zapisywany w RAM pod tym samym adresem reszta w B
--  SETC    "01110" -- instrukcja ustawienie bitu CARY w stanie wysokim i przepisanie zawartości RAM bez zmian
--  CLRC    "01111" -- instrukcja ustawienie bitu CARY w stanie niskim i przepisanie zawartości RAM bez zmian
                   
--  RETLB   "10000" -- instrukcja powrotu z przerwania programowego kopiująca dane ROM do rejestru B
--  RETURN  "10001" -- instrukcja powrotu z przerwania programowego
--  RETFIE  "10010" -- instrukcja powrotu z przerwania z zewnątrz
--  MOVBE   "10011" -- instrukcja kopiująca B lub RAM do portu zewnętrznego
                   
--  SKIPC   "101000" -- instrukcja skoku warunkowego CARRY
--  SKIPDC  "101001" -- instrukcja skoku warunkowego DIGITAL CARRY
--  SKIPZ   "101001" -- instrukcja skoku warunkowego ZERO
--  SKIPE   "101010" -- instrukcja skoku warunkowego R = B
--  SKIPU   "101011" -- instrukcja skoku warunkowego R > B
--  SKIPL   "101100" -- instrukcja skoku warunkowego R < B
                   
--  GOTO    "11000" -- instrukcja skoku do wybranego adresu w programie
--  CALL    "11001" -- instrukcja skoku do wybranego adresu w programie z zapisem adresu powrotu i zmianą licznika przerwań
--  WEEN    "11010" -- instrukcje dla urządzeń zewnętrznych (zakodowany sygnał WE dla 1 z 2048 adresów)
--  ADDPC   "11011" -- instrukcja skoku do tabeli zapisane w pamięci ROM (256 adresów)
                   
--  MOVBR   "11100" -- instrukcja kopiująca rejestr B do pamięci RAM
--  MOVER   "11101" -- instrukcja kopiująca port zewnętrzny do pamięci RAM
--  SETR    "11110" -- instrukcja ustawiająca wszystkie bity w wybranej komórce RAM na 1
--  CLRR    "11111" -- instrukcja ustawiająca wszystkie bity w wybranej komórce RAM na 0
--
-------------------------------------------------------------------------

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity CORE2 is
    Port ( Clk       : in  STD_LOGIC;								-- 100 MHz
           Reset     : in  STD_LOGIC;								-- reset aktywny = 0
           PROG_WE   : in  STD_LOGIC;								-- sygnał zapisu pamięci programu
           ROM_WE    : in  STD_LOGIC;								-- sygnał dla programatora zewnętrznego
           ROM_DI    : in  STD_LOGIC_VECTOR (15 downto 0);	-- dane wysyłane do pamięci ROM
           ROM_ADDR  : in  STD_LOGIC_VECTOR (10 downto 0);	-- adres pamięci ROM
           INT_KBD   : in  STD_LOGIC;								-- sygnał przerwania od klawiatury
           PORT_I    : in  STD_LOGIC_VECTOR (7 downto 0);	-- port zewnętrzny wejściowy
           PORT_O    : out  STD_LOGIC_VECTOR (7 downto 0);	-- port zewnętrzny wyjściowy
           INSTR     : out  STD_LOGIC_VECTOR (10 downto 0)	-- instrukcje procesora dla urządzeń zewnętrzych
			  );
end CORE2;

architecture Behavioral of CORE2 is
	
signal Cnt    : std_logic_vector(1 downto 0); --licznik procesów instrukcji
signal Sel    : std_logic_vector(4 downto 0); --dekoder instrukcji
signal SkipSel: std_logic_vector(5 downto 0); --dekoder instrukcji skoków warunkowych

signal PROD   : std_logic_vector(15 downto 0);--wynik mnożenia
signal PRODL  : std_logic_vector(8 downto 0); --wynik mnożenia rejestr pomocniczy
signal ADNRB  : std_logic_vector(8 downto 0); --wynik AND
signal IORRB  : std_logic_vector(8 downto 0); --wynik OR
signal XORRB  : std_logic_vector(8 downto 0); --wynik XOR
signal COMR   : std_logic_vector(8 downto 0); --wynik NEGACJI BITÓW
signal ADDRB  : std_logic_vector(8 downto 0); --wynik SUMY
signal SUBRB  : std_logic_vector(8 downto 0); --wynik RÓŻNICY
signal RRR    : std_logic_vector(8 downto 0); --wynik PRZESUNIĘCIE W PRAWO + CARRY BIT
signal RLR    : std_logic_vector(8 downto 0); --wynik PRZESUNIĘCIE W LEWO + CARRY BIT
signal SWAPR  : std_logic_vector(8 downto 0); --wynik ZAMIANA 4 BITY MŁODSZE Z 4 BITY STARSZE
signal SETC   : std_logic_vector(8 downto 0); --wynik AND
signal CLRC   : std_logic_vector(8 downto 0); --wynik AND
signal MOVBR  : std_logic_vector(8 downto 0); --wynik AND
signal MOVER  : std_logic_vector(8 downto 0); --wynik AND
signal SETR   : std_logic_vector(8 downto 0); --wynik AND
signal CLRR   : std_logic_vector(8 downto 0); --wynik AND
signal B      : std_logic_vector(7 downto 0); --rejestr B
signal R      : std_logic_vector(7 downto 0); --rejestr R
signal Q      : std_logic_vector(8 downto 0); --wynik operacji jednostki ALU

signal C     : std_logic;									--CARRY bit
signal CQ    : std_logic;
signal DC    : std_logic;									--DIGITAL CARRY bit
signal DCQ   : std_logic;
signal Z     : std_logic;									--ZERO bit
signal REB   : std_logic;									--R equal B bit
signal RUB   : std_logic;									--R upper B bit
signal RLB   : std_logic;									--R lower B bit
signal SKIPC : std_logic;
signal SKIPDC: std_logic;
signal SKIPZ : std_logic;
signal SKIPE : std_logic;
signal SKIPU : std_logic;
signal SKIPL : std_logic;
signal SKIP  : std_logic;

signal ROMADDR  : std_logic_vector(10 downto 0);	--adres wysyłany do pamięci ROM
signal ROMDI    : std_logic_vector(15 downto 0);	--dane wysyłane do pamięci ROM
signal ROMDO    : std_logic_vector(15 downto 0);	--dane odczytywane z pamięci ROM
signal ROMWE 	 : std_logic;								--sygnał zapisu dla pamięci ROM
signal RAMWE 	 : std_logic;								--sygnał zapisu dla pamięci RAM
signal RAMADDR  : std_logic_vector(10 downto 0);	--adres pamięci RAM
signal RAMDI    : std_logic_vector(7 downto 0);		--dane wysyłane do pamięci RAM
signal RAMDO    : std_logic_vector(7 downto 0);		--dane odczytywane z pamięci RAM

signal PC    : std_logic_vector(10 downto 0);		--licznik programu
signal TOS   : std_logic_vector(10 downto 0);		--adres stosu przerwań
signal INTPC : std_logic_vector(10 downto 0);		--adres powrotu z przerwania zewnętrznego
signal INTEN : std_logic;									--sygnał zezwolenia dla przerwania z zewnątrz

	COMPONENT RAM
	PORT(
		Clk  : IN std_logic;
		WE   : IN std_logic;
		DI   : IN std_logic_vector(7 downto 0);
		ADDR : IN std_logic_vector(10 downto 0);          
		DO   : OUT std_logic_vector(7 downto 0)
		);
	END COMPONENT;

	COMPONENT ROM
	PORT(
		Clk  : IN std_logic;
		WE   : IN std_logic;
		DI   : IN std_logic_vector(15 downto 0);
		Cnt  : IN std_logic_vector(1 downto 0);
		ADDR : IN std_logic_vector(10 downto 0);
		PC   : IN std_logic_vector(10 downto 0);          
		DO   : OUT std_logic_vector(15 downto 0)
		);
	END COMPONENT;

begin

-------------------------------------------------------------------------
--licznik taktów instrukcji (4 takty na 1 instrukcję)
process(Clk, Reset, Cnt, ROM_WE)
begin
	if ((Reset = '0') or (ROM_WE = '1')) then
		Cnt <= (others => '0');
	else
		if falling_edge (Clk) then
			Cnt <= Cnt + 1;
		end if;
	end if;
end process;
-------------------------------------------------------------------------
--dekoder poleceń dla urządzeń zewnętrznych
INSTR <= ROMDO(10 downto 0) when ((Sel = "11010") and (Cnt = "11")) else (others => '0');
-------------------------------------------------------------------------
--jednostka arytmetyczno logiczna
ADNRB <= ('0' & (R and B))                     when (Sel = "00100") else (others => '0');
IORRB <= ('0' & (R or  B))                     when (Sel = "00101") else (others => '0');
XORRB <= ('0' & (R xor B))                     when (Sel = "00110") else (others => '0');
COMR  <= ('0' & (not R))                       when (Sel = "00111") else (others => '0');
ADDRB <= ((C & (R)) + ('0' & (B)))             when (Sel = "01000") else (others => '0');
SUBRB <= ((C & (R)) - ('0' & (B)))             when (Sel = "01001") else (others => '0');
RRR   <= (R(0) & C & R(7 downto 1))            when (Sel = "01010") else (others => '0');
RLR   <= (R(7 downto 0) & C)                   when (Sel = "01011") else (others => '0');
SWAPR <= ('0' & R(3 downto 0) & R(7 downto 4)) when (Sel = "01100") else (others => '0');
PROD  <= (R(7 downto 0) * B(7 downto 0))       when (Sel = "01101") else (others => '0');
SETC  <= ('1' & R)                             when (Sel = "01110") else (others => '0');
CLRC  <= ('0' & R)                             when (Sel = "01111") else (others => '0');
MOVBR <= ('0' & B)                             when (Sel = "11100") else (others => '0');
MOVER <= ('0' & PORT_I)                        when (Sel = "11101") else (others => '0');
SETR  <= ('0' & "11111111")                    when (Sel = "11110") else (others => '0');
CLRR  <= ('0' & "00000000")                    when (Sel = "11111") else (others => '0');
PRODL <= ('0' & PROD(7 downto 0));
--przypisanie wyników działań do wyjścia Q
Q <= ADNRB or IORRB or XORRB or COMR or ADDRB or SUBRB or RRR or RLR or SWAPR or SETC or CLRC
			  or PRODL or MOVBR or MOVER or SETR or CLRR;
-------------------------------------------------------------------------
--zapis danych do rejestru B
process(Clk, Reset, Sel, Cnt, PORT_I, R, ROMDO, PROD, Sel)
begin
	if Reset = '0' then
		B <= (others => '0');
	else
		if falling_edge (Clk) then
			if (Cnt = "11") then
				if (Sel = "00001") 		--MOVLB
				or (Sel = "10000") then	--RETLB		kopiowanie stałej do rejestru B
					B <= ROMDO(7 downto 0) ;
			elsif (Sel = "00010") then	--MOVEB 		kopiowanie danych z portu EXTI do rejetru B
					B <= PORT_I ;
			elsif (Sel = "00011") then	--MOVRB		kopiowanie zawartości pamięci RAM do rejestru B
					B <= R ;
			elsif (Sel = "01101") then	--MULRB		kopiowanie PRODH do rejestru B 
					B <= PROD(15 downto 8) ;
				end if;
			end if;
		end if;
	end if;
end process;
-------------------------------------------------------------------------
-- zapis danych z rejestru B lub RAM do portu EXTO.
process(Clk, Reset, Cnt, B, Sel, R)
begin
	if Reset = '0' then
		PORT_O <= (others => '0');
	else
		if falling_edge (Clk) then
			if (Cnt = "11") then
				if (Sel = "10011") then
					PORT_O <= (B or R);
				end if;
			end if;
		end if;
	end if;
end process;
-------------------------------------------------------------------------
--dekodery segnałów skoków warunkowych
REB <= '1' when (R = B)              else '0';
RUB <= '1' when (R > B)              else '0';
RLB <= '1' when (R < B)              else '0';
Z   <= '1' when (R(7 downto 0) = 0)  else '0';
DCQ <= '1' when (R(3 downto 0) > 9)  else '0';
CQ  <= Q(8);
--zapis sygnałów skoków warunkowych
process(Clk, Reset, CQ, RAMWE, Sel)
begin
	if Reset = '0' then
		C  <= '0';
		DC <= '0';
	else
		if falling_edge (Clk) then
			if (RAMWE = '1') then
				if ((Sel(4 downto 2) = "010") or (Sel(4 downto 1) = "0111")) then
					C  <= CQ;	--wynik dodawania, odejmowania, przesunięcie w lewo i prawo
				end if;
				if (Sel(4 downto 1) = "0100") then
					DC <= DCQ;	--wynik dodawania, odejmowania
				end if;
			end if;
		end if;
	end if;
end process;
--dekoder skoku warunkowego
SKIPC  <=  '1' when ((C = '1')   and (SkipSel = "101000")) else '0';
SKIPDC <=  '1' when ((DC = '1')  and (SkipSel = "101001")) else '0';
SKIPZ  <=  '1' when ((Z = '1')   and (SkipSel = "101010")) else '0';
SKIPE  <=  '1' when ((REB = '1') and (SkipSel = "101011")) else '0';
SKIPU  <=  '1' when ((RUB = '1') and (SkipSel = "101100")) else '0';
SKIPL  <=  '1' when ((RLB = '1') and (SkipSel = "101101")) else '0';
SKIP   <= SKIPC or SKIPDC or SKIPZ or SKIPE or SKIPU or SKIPL;
-------------------------------------------------------------------------
--adresy powrotu z przrewań zapisywane są w pamięci ROM 
--zaczynają się od końca adresowanej pamięci tzn. X"7FF", a zapis adresowany jest licznikiem TOS
--licznik TOS jest zmniejszany o 1 w instrukcji CALL (przykład [ przed X"7FF" | po X"7FF" - 1 = X"7FE" ] )
--licznik TOS jest zwiększany o 1 w instrukcji RETURN, RETLB (przykład [ przed X"7F2" | po X"7F2" + 1 = X"7F3" ] )

--licznik programu z instrukcjami skoków, przerwań
process(Clk, Reset, Cnt, SKIP, ROMDO, Sel, INT_KBD, R, ROM_WE, TOS)
begin
	if ((Reset = '0') or (ROM_WE = '1'))then
		PC <= (others => '0');
		TOS <= (others => '1'); -- adres powrotu z pierwszego przerwania
		INTPC <=  (others => '0');
		INTEN <= '0';
	else
		if falling_edge (Clk) then
			if (Cnt = "01") then 
			
				if ((Sel = "10001") or (Sel = "10000")) then	-- RETURN, RETLB
					PC <= TOS; 	-- adres TOS przypisany do licznika PC 
					TOS <= TOS + 1;   -- w celu odczytu adresu przed przerwaniem
				end if;
			end if;
			
			if (Cnt = "11") then
			
				if ((INT_KBD = '1') and (INTEN = '0')) then	-- INTERUPT
					PC <= "00000010000"; -- skok do adresu "10000"
					INTPC <= PC;
					INTEN <= '1';
					
			elsif (SKIP = '1') then	-- SKIP
					if (ROMDO(7) = '1') then
						PC <= PC - ROMDO(6 downto 0);
					else
						PC <= PC + ROMDO(6 downto 0);
					end if;
					
			elsif (Sel = "11000") then -- GOTO
					PC <= ROMDO(10 downto 0);
					
			elsif (Sel = "11001") then -- CALL
					PC <= ROMDO(10 downto 0);
					TOS <= TOS - 1;		-- zmniejszenie adresu licznika przerwań po odczycie instrukcji
					
			elsif (Sel = "11011") then -- ADDPC
					PC <= PC + (ROMDO(10 downto 8) & B); -- tablica w pamięci ROM o długości 256 adresów
					
			elsif ((Sel = "10010") and (INTEN = '1')) then	-- RETFIE
					PC <= INTPC;
					INTEN <= '0';
					
				else
					PC <= PC + 1;
					
				end if;
			end if;
		end if;
	end if;
end process;
-------------------------------------------------------------------------
ROMADDR <= ROM_ADDR when (ROM_WE = '1') else TOS;
--GOTO + PC + 1 zapis instrukcji powrotu z przerwania z adresem następnego polecenia
ROMDI <= ROM_DI when (ROM_WE = '1') else ("11000" & (PC + 1));
--CALL dekoder zapisu adresu powrotu z przerwania w pamięci ROM
ROMWE <= '1' when ((Sel = "11001") and (Cnt = "10")) else (ROM_WE or PROG_WE);
--przypisanie instrukcji zapisanych w pamięci ROM do wskaźnika SEL
Sel   <= ROMDO(15 downto 11);
SkipSel   <= ROMDO(15 downto 10);

	Inst_ROM: ROM PORT MAP(
		Clk => Clk,				-- sygnał taktujący
		WE  => ROMWE,			-- sygnał zezwolenia na zapis do pamięci ROM
		DI  => ROMDI,			-- dane zapisywane w pamięci ROM
		Cnt => Cnt,				-- zegar systemowy kontrolera
		ADDR => ROMADDR, 		-- adres zapisu danych do pamięci ROM
		PC  => PC,				-- adres odczytu danych z pamięci ROM
		DO  => ROMDO			-- dane odczytywane z pamięci ROM
	);
-------------------------------------------------------------------------
--dekoder sygnału zapisu dla pamięci RAM
RAMWE   <= Cnt(0) and Cnt(1) and (((not Sel(4)) and (Sel(3) or Sel(2))) or (Sel(4) and Sel(3) and Sel(2)));
--dane do zapisania w pamięci RAM (wynik ALU)
RAMDI   <=  Q(7 downto 0);
--adres zapisu i odczytu pamięci RAM
RAMADDR <= ROMDO(10 downto 0);
-- zapis danych z pamięci RAM do rejestru R
process (Clk, RAMDO, Cnt)
begin
	if falling_edge (Clk) then
		if (Cnt = "10") then
			R <= RAMDO;
		end if;
	end if;
end process;

	Inst_RAM: RAM PORT MAP(
		Clk =>  Clk,
		WE =>  RAMWE,
		DI =>  RAMDI,
		ADDR =>  RAMADDR,
		DO => RAMDO
	);

end Behavioral;

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity ROM is
    Port (
        Clk  : in  STD_LOGIC;
        WE   : in  STD_LOGIC;
        DI   : in  STD_LOGIC_VECTOR (15 downto 0);
        Cnt  : in  STD_LOGIC_VECTOR (1 downto 0);
        ADDR : in  STD_LOGIC_VECTOR (10 downto 0); -- Adres zapisu
        PC   : in  STD_LOGIC_VECTOR (10 downto 0); -- Adres odczytu
        DO   : out STD_LOGIC_VECTOR (15 downto 0)
    );
end ROM;

architecture Behavioral of ROM is

	type ram_type is array (0 to 2047) of std_logic_vector (15 downto 0);
	signal ROM0: ram_type;
	
	signal EN : std_logic;

begin

	EN <= '1' when Cnt = "00" else '0';

	process (CLK)
	begin
		if falling_edge (Clk) then
			if WE = '1' then
			  ROM0(conv_integer(ADDR)) <= DI;
			end if;
			if EN = '1' then
				DO <= ROM0(conv_integer(PC)) ;
			end if;
		end if;
	end process;

end Behavioral;

 

(edytowany)

W następnym wpisie podam przykłady poprawnego używania języka assembler w moim procesorze.

Edytowano przez kroszkanorber

@Sylbaw kolejnych wpisach wytłumaczę jak działa kod i jak jest skonstruowany. Rozłożę kod na małe bloki (funkcje) które będą bardziej zrozumiałe.

  • Lubię! 1

witam.

Opis architektury układu:

Działanie procesora jest powiązane z kodem zapisanym w pamięci ROM. Pamięć ROM ma zapisany kod wykonujący się w układzie. Procesor pobiera dane na podstawie sygnałów elektrycznych występujących na wyjściu danych pamięci ROM. Czyli pamięć ROM nie wpływa na działanie procesora a jedynie wysyła "bezmyślnie" dane. Pamięć ROM można zapisać dowolnie i nie ma to znaczenia dla użytkownika dopóki procesor robi co mu się każe. Wielu się na pewno nie zgodzi z moim tokiem myślenia, ale moim celem jest postawienie granicy między kodem a architekturą procesora. Chcę wykazać, że kody nie są własnością procesora, ale twórczością programisty i tak chcę być zrozumiany. Umiejętność kodowania dla mnie jest porównywalna do operowania obcym językiem. Trzeba zrozumieć zasady, metodę wyrażania intencji i sposób w jaki możemy przekazać to dla odbiorcy by zinterpretował to tak jak tego oczekujemy.

Procesor ma 5 bitowe instrukcje. w 5 bitach można zapisać 32 różne stany i są one instrukcjami procesora. (Procesor posiada 32 instrukcje, a suma wszystkich instrukcji jest produktem "SEL"). W procesorze jedno słowo instrukcji ma długość 16 bitów. Po odjęciu 5 bitów na instrukcję dla dekodera SEL zostaje 11 bitów. Te ostanie bity są używane dla adresu (np. skoki, wywołania, zapis do pamięci ram). Wyjątkiem są operacje kopiowania danych z pamięci ROM gdzie wykorzystujemy tylko 8 bitów a pozostałe zapisuję zerami.

Procesor ma cykl wykonania instrukcji = 4 tkty. W pierwszym takcie pobiera dane, w następnych przetwarza, a w 4 takcie zapisuje wynik i inkrementuje PC(Licznik programu), lub wykonuje skok warunkowy, lub odpowiada na przerwanie z zewnątrz, w zależności od instrukcji i stanu procesora.

To co piszę może wydawać się skomplikowane, ale tak nie jest. Myśląc o procesorze należy zwrócić uwagę na zależność (akcja-reakcja). Procesor nie myśli, tylko reaguje na prąd na liniach adresowych i danych. Zapewniam że nic więcej nie potrafi...

Podsumowując. Budowę procesora można zrozumieć tylko, gdy poznamy budowę bramek logicznych. Zrozumienie jak przepływają sygnały w bramkach logicznych odtajni zrozumienie kodów nawet w cpp i innych językach wyższego poziomu.

Na początek podaje zestaw instrukcji. W następnych postach wyjaśnię jak zaimplementowałem te instrukcje.

-----------------------------------------------------------------------------------
--
-- 
-----------------------------------------------------------------------------------
--
-- pamięć ROM 16b x 2048 
--
-- mapa pamięci ROM
-- "SEL" + "INSADDR" = "iiiii" + "aaa aaaa aaaa" = pamięć ROM (16 bit) [5bit instrukcje|11bit adres instrukcji]
-- "SEL" + "ROMADDR" = "iiiii" + "rrr rrrr rrrr" = pamięć ROM (16 bit) [5bit instrukcje|11bit adres pamięci RAM]
-- "SEL" + "ROMDATA" = "iiiii" + "xxx bbbb bbbb" = pamięć ROM (16 bit) [5bit instrukcje|3bit nieużywane|8bit dane]
--
-- licznik taktów kontrolera (4 takty na 1 istrukcję)
-- Cnt = "00" <- pobieranie instrukcji z pamięci ROM
-- Cnt = "01" <- pobieranie danych z pamięci RAM
-- Cnt = "10" <- wykonanie instrukcji
-- Cnt = "11" <- wykonanie instrukcji, zmiana zawartości licznika instrukcji, zapis danych do pamięci RAM|rejestru B|PORT_O
--
-----------------------------------------------------------------------------------
--
--  instrukcje
--    
--  NOP      "00000" -- instrukcja pusta
--  MOVLB    "00001" -- instrukcja kopiująca dane ROM do rejestru B
--  MOVEB   "00010" -- instrukcja kopiująca dane prortu zewnętrznego do rejestru B
--  MOVRB   "00011" -- instrukcja kopiująca dane RAM do rejestru B
                   
--  ANDRB   "00100" -- instrukcja RAM and B wynik zapisywany w RAM pod tym samym adresem
--  IORRB   "00101" -- instrukcja RAM or B wynik zapisywany w RAM pod tym samym adresem
--  XORRB   "00110" -- instrukcja RAM xor B wynik zapisywany w RAM pod tym samym adresem
--  COMR    "00111" -- instrukcja not RAM wynik zapisywany w RAM pod tym samym adresem
                   
--  ADDRB   "01000" -- instrukcja (CARRY & RAM) + B
--  SUBRB   "01001" -- instrukcja (CARRY & RAM) - B
--  RRR     "01010" -- instrukcja CARRY & RAM rotacja w prawo
--  RLR     "01011" -- instrukcja CARRY & RAM rotacja w lewo
                   
--  SWAPR   "01100" -- instrukcja zamiane 4 bit starszych z młodszymi wynik zapisywany w RAM pod tym samym adresem
--  MULRB   "01101" -- instrukcja monożenie RAM i B wynik (7 downto 0) zapisywany w RAM pod tym samym adresem reszta w B
--  SETC    "01110" -- instrukcja ustawienie bitu CARY w stanie wysokim i przepisanie zawartości RAM bez zmian
--  CLRC    "01111" -- instrukcja ustawienie bitu CARY w stanie niskim i przepisanie zawartości RAM bez zmian
                   
--  RETLB   "10000" -- instrukcja powrotu z przerwania programowego kopiująca dane ROM do rejestru B
--  RETURN  "10001" -- instrukcja powrotu z przerwania programowego
--  RETFIE  "10010" -- instrukcja powrotu z przerwania z zewnątrz
--  MOVBE   "10011" -- instrukcja kopiująca B lub RAM do portu zewnętrznego
                   
--  SKIPC   "101000" -- instrukcja skoku warunkowego CARRY
--  SKIPDC  "101001" -- instrukcja skoku warunkowego DIGITAL CARRY
--  SKIPZ   "101001" -- instrukcja skoku warunkowego ZERO
--  SKIPE   "101010" -- instrukcja skoku warunkowego R = B
--  SKIPU   "101011" -- instrukcja skoku warunkowego R > B
--  SKIPL   "101100" -- instrukcja skoku warunkowego R < B
                   
--  GOTO    "11000" -- instrukcja skoku do wybranego adresu w programie
--  CALL    "11001" -- instrukcja skoku do wybranego adresu w programie z zapisem adresu powrotu i zmianą licznika przerwań
--  WEEN    "11010" -- instrukcje dla urządzeń zewnętrznych (zakodowany sygnał WE dla 1 z 2048 adresów)
--  ADDPC   "11011" -- instrukcja skoku do "tabeli" zapisanej w pamięci ROM (256 adresów)
                   
--  MOVBR   "11100" -- instrukcja kopiująca rejestr B do pamięci RAM
--  MOVER   "11101" -- instrukcja kopiująca port zewnętrzny do pamięci RAM
--  SETR    "11110" -- instrukcja ustawiająca wszystkie bity w wybranej komórce RAM na 1
--  CLRR    "11111" -- instrukcja ustawiająca wszystkie bity w wybranej komórce RAM na 0
--
-------------------------------------------------------------------------

pozdrawiam.

  • Lubię! 2

Czuje się zobowiązany wyjaśnić co to jest właściwie FPGA zanim wyjaśnię strukturę kodów.

Ogólna budowa FPGA

FPGA to matryca powtarzalnych bloków logicznych, połączonych ze sobą programowalną siecią routingu. Każdy blok logiczny (CLB) składa się z:

LUT — programowalnych tablic prawdy
rejestrów D — do przechowywania stanu
carry‑chain — sprzętowego toru przeniesienia
multiplekserów — do wyboru ścieżek sygnału

Dodatkowo FPGA zawiera bloki specjalne:
BRAM — pamięci blokowe
DSP — szybkie mnożniki i akumulatory
PLL/MMCM — generatory i modyfikatory zegara

Najważniejsze jest zrozumienie, że FPGA to układ programowalny, a nie zestaw gotowych bramek logicznych.

LUT — programowalna tablica prawdy
LUT (Look‑Up Table) to mała pamięć SRAM, która implementuje dowolną funkcję logiczną. Wejścia LUT są adresami, a wyjście to 1‑bitowa wartość odczytana z pamięci.

Przykład: LUT o 4 wejściach ma 16 komórek pamięci:

Kod
adres = ABCD dane = wynik funkcji logicznej
Jeśli chcemy zbudować bramkę AND:
programujemy komórkę 1111 = 1
wszystkie pozostałe komórki = 0

To wszystko. FPGA nie liczy funkcji logicznych — FPGA odczytuje wynik z pamięci.

Dlatego LUT może być:
AND, OR, XOR, NAND, NOR, dowolną kombinacją powyższych, fragmentem dekodera, fragmentem ALU

To jest fundament działania całego FPGA.

Carry‑chain — sprzętowy tor przeniesienia
To jeden z najważniejszych elementów FPGA.
Carry‑chain to dedykowana, sprzętowa ścieżka, która realizuje:
dodawanie, odejmowanie, rotacje z przeniesieniem, operacje na flagach

I robi to szybciej niż LUT, bo:
ma własne połączenia, omija sieć routingu, działa równolegle, jest zoptymalizowany sprzętowo

Dlatego:

W FPGA dodawanie nie jest realizowane przez LUT, tylko przez sprzętowy carry‑chain.
Wystarczy w VHDL napisać A + B, a syntezator sam użyje carry‑chain. Nie trzeba budować sumatora ręcznie.

Rejestry D — pamięć stanu

Każdy CLB zawiera rejestry D. Służą one do:
przechowywania wyników, synchronizacji sygnałów, budowania pipeline, stabilizacji logiki sekwencyjnej

Rejestry są integralną częścią FPGA — nie są opcjonalne. To one decydują o:
szybkości układu, stabilności czasowej, poprawności działania logiki

W AGNES rejestry odpowiadają m.in. za:

licznik programu, rejestr B, rejestry RAM, rejestry stanu (CARRY, ZERO itd.)

CLB — kompletny blok logiczny
CLB to „kafelek”, z którego zbudowane jest FPGA.
CLB = LUT + rejestry + carry‑chain + mux

CLB potrafi:
realizować logikę kombinacyjną (LUT)
realizować logikę sekwencyjną (rejestry)
wykonywać szybkie operacje arytmetyczne (carry‑chain)
przełączać ścieżki sygnałów (mux)

To właśnie CLB jest podstawą implementacji:
dekodera instrukcji, ALU, logiki sterującej, liczników, rejestrów, magistral danych

Routing — programowalne połączenia
Routing to najbardziej wrażliwy element FPGA.

To on decyduje o:
czasie propagacji sygnału, opóźnieniach, stabilności czasowej, zużyciu zasobów

Kod napisany nieumiejętnie może:
wymusić długie ścieżki sygnału, przeciążyć sieć routingu, spowodować błędy czasowe, wygenerować komunikat „brak zasobów”, mimo że LUT są wolne

Dlatego styl pisania VHDL ma ogromne znaczenie.

Podsumowanie

FPGA to nie procesor i nie układ TTL. To programowalna matryca logiczna, która:
działa równolegle, jest szybka, jest elastyczna, pozwala tworzyć własne architektury CPU, pozwala implementować logikę dokładnie tak, jak chcemy

Zrozumienie LUT, carry‑chain, rejestrów i CLB jest konieczne, zanim przejdziemy do:
struktury kodu VHDL, budowy ALU, implementacji instrukcji AGNES

Informacje które podałem nie są pełne, ale wystarczają dla większości projektów. Świadomość co programuje pozwala mi szybciej zdiagnozować błędy w kodzie.

pozdrawiam.

(edytowany)

Witam.

Instrukcje ALU mojego procesora.

------------------------------------------------------------------------- --

jednostka arytmetyczno logiczna
ADNRB <= ('0' & (R and B)) when (Sel = "00100") else (others => '0');
IORRB <= ('0' & (R or B)) when (Sel = "00101") else (others => '0');
XORRB <= ('0' & (R xor B)) when (Sel = "00110") else (others => '0');
COMR <= ('0' & (not R)) when (Sel = "00111") else (others => '0');
ADDRB <= ((C & (R)) + ('0' & (B))) when (Sel = "01000") else (others => '0');
SUBRB <= ((C & (R)) - ('0' & (B))) when (Sel = "01001") else (others => '0');
RRR <= (R(0) & C & R(7 downto 1)) when (Sel = "01010") else (others => '0');
RLR <= (R(7 downto 0) & C) when (Sel = "01011") else (others => '0');
SWAPR <= ('0' & R(3 downto 0) & R(7 downto 4)) when (Sel = "01100") else (others => '0');
PROD <= (R(7 downto 0) * B(7 downto 0)) when (Sel = "01101") else (others => '0');
SETC <= ('1' & R) when (Sel = "01110") else (others => '0');
CLRC <= ('0' & R) when (Sel = "01111") else (others => '0');
MOVBR <= ('0' & B) when (Sel = "11100") else (others => '0');
MOVER <= ('0' & PORT_I) when (Sel = "11101") else (others => '0');
SETR <= ('0' & "11111111") when (Sel = "11110") else (others => '0');
CLRR <= ('0' & "00000000") when (Sel = "11111") else (others => '0');
PRODL <= ('0' & PROD(7 downto 0));
--przypisanie wyników działań do wyjścia Q
Q <= ADNRB or IORRB or XORRB or COMR or ADDRB or SUBRB or RRR or RLR or SWAPR or SETC or CLRC or PRODL or MOVBR or MOVER or SETR or CLRR;
-------------------------------------------------------------------------
Jednostka ALU jest sercem operacji każdego procesora. W nim wykonywane są obliczenia a wynik  ALU jest ostatecznie podawany na wyjściu Q.
ALU nie posiada rejestrów i składa się z sygnałów o nazwach instrukcji (poprawia czytelność).
wszystkie sygnały są połączone równolegle bramkami OR.
Przykład: Q <= ANDRB or IORRB;
oznacza dla Q = 9 bit, ANDRB = 9 bit, IORRB = 9 bit :
Q(0) <= ANDRB(0) or IORRB(0); -- zerowy bit na wyjściu Q jest sumą zerowego bitu na wyjściu ANDRB i IORRB.

 .
 .
Q(8) <= ANDRB(8) or IORRB(8);
funkcja OR (Q) daje na wyjściu 1 gdy jeden lub więcej bitów zadanych w tej funkcji jest = 1. Czyli wyjście OR jest sumą wszystkich sygnałów wchodzących do funkcji.

Zajmijmy się zapisem wyboru z maskowaniem sygnału.
R i B są sygnałami dla jednostki ALU. Są to dwa słowa na których wykonujemy operacje. Operacje można też wykonać na większej liczbie elementów, ale nigdy na jednym, ponieważ wynik się nie zmieni. na jednym argumencie nie widziałem operacji 🙂
przykład:
ADNRB <= ('0' & (R and B)) when (Sel = "00100") else (others => '0');
zapis ten oznacza że mamy funkcję AND, dekoder(Sel) i maskę sygnału.
Sygnał R i B są = 8 bit. by na wyjściu otrzymać wynik 9 bitowy potrzebny w dalszej części kodu dokleiłem jeden bit o wartości 0 ('0' &) i dodałem funkcję AND dla R i B. fukcja z doklejonym bitem ma taką postać '0' & (R and B).
dekoder Sel ma za zadanie maskowanie sygnału funkcji. Sel jest złożony z 5 bitów. Mozna go zapisać też w inny sposób: ANDRB_EN <= not Sel(4) and not Sel(3) and Sel(2) and not Sel(1) and not Sel(0), albo tak: ANDRB_EN <= (not (Sel(4) or Sel(3) or Sel(1) or Sel(0)) and Sel(2);.
Pokazałem inny zapis dekodera Sel by wytłumaczyć maskowanie sygnału.
Maskowanie sygnału jest wykonywane funkcją AND. Funkcja ta daje wynik = 1 gdy wszystkie sygnały są też = 1. W ten sposób uzyskujemy wynik na wyjściu. Przykład:
Wynik(10101010) and Maska(11111111) = Wyjście(10101010). 
Wynik(10101010) and Maska(00000000) = Wyjście(00000000). 
To jest MUX zrealizowany przez maskowanie + OR., synteza robi korzystniejsze połączenia (routing). Czyli kod jest optymalnie syntezowany pod układ i jest szybszy niż bym użył case. Kod przepisywałem parę razy i za każdym razem otrzymuje lepsze wyniki pisząc w tym stylu.
Ostatni wyjątek to PROD. Jest to 16 bitowy wynik mnożenia i dolne 8 bitów wyniku jest podłączone do wyjścia ALU pod nazwą PRODL a górne jest zapisywane w innym rejestrze. Musiałem zachować pełny wynik mnożenia, a ze względu na 8 bitową architekturę i możliwość przetworzenia danych 8 bit jednocześnie, podzieliłem wynik mnożenia na dwie połowy.
Dlaczego 9 bit gdy przetważam tylko 8? Na początku to mylące ale w 9 bicie używam Carry(przeniesienie). Czyli operacje dodawania, odejmowania i rotacji dla pozostałych bitów wpisuje 0.

Tak działa w mojej interpretacji jednostka arytmetyczno-logiczna.
W następnych wpisach przejdziemy do kolejnej części kodu. Opiszę działanie rejestrów i jak dane trafiają do ALU.
pozdrawiam.

 

Edytowano przez kroszkanorber
  • Lubię! 1

Witam. 
Przepływ sygnałów (rejestry).
W tym wpisie opiszę, jak dane przechodzą przez rejestry i pamięci w trakcie 4‑taktowego cyklu procesora.
Przepływ sygnałów jest w moim procesorze synchroniczny. Przepływem sygnałów steruje licznik 2 bitowy o nazwie Cnt.


-- Cnt = "00" <- pobieranie instrukcji z pamięci ROM
-- Cnt = "01" <- pobieranie danych z pamięci RAM
-- Cnt = "10" <- wykonanie instrukcji
-- Cnt = "11" <- wykonanie instrukcji, zmiana zawartości licznika instrukcji, zapis danych do pamięci RAM|rejestru B|PORT_O


Pamięć programu i danych jest zaimplementowana w wbudowanej pamięci BRAM w układzie FPGA.
Mój układ posiada 16 banków pamięci, którą możemy konfigurować (pamięć BRAM nie jest dowolnie konfigurowalna i najważniejsze jest użycie zegara ponieważ jest synchroniczna). BRAM nie dopuszcza konfiguracji jako pamięć statyczna ponieważ nie będzie użyta, a konsekwencją takiej konfiguracji będzie zużycie LUT.
Pamięć BRAM można interpretować jako pole kwadratu z małymi pamięciami 1 bit (tylko przykład). W tym kwadracie są zaimplementowane pamięci 1 bitowe o stałej liczbie w osi X i osi Y. To wymusza konfigurację X * Y. Czyli w moim przypadku 1 BRAM = 2048 adresów * 8 bitów = 16384. Jeśli wpisałbym inne wartości, pamięć nie zostałaby rozpoznana jako BRAM i synteza utworzy połączenia LUT + Rejestry D. 
Pamięć BRAM w mojej architekturze wymusza odstęp 1 taktu zegara między pobraniem kodu w którym jest zapisany adres RAM, a odczytem zaadresowanej komórki pamięci RAM.
Drugi takt zatrzaskuje dane wysłane z komórek pamięci RAM do rejestru RAMDO.
W trzecim takcie RAMDO jest kopiowany do rejestru R.
Rejestr R łapie dane z RAM tylko w trzecim takcie, dzięki czemu ALU zawsze dostaje stabilne dane.
-- zapis danych z pamięci RAM do rejestru R
process (Clk, RAMDO, Cnt)
begin
    if falling_edge (Clk) then
        if (Cnt = "10") then
            R <= RAMDO;
        end if;
    end if;
end process;
Rejestr R jest połączony do wejścia ALU.
Drugie wejście ALU jest połączone z rejestrem B (rejestr roboczy 8 bitowy).
Między trzecim i czwartym taktem przez ALU przepływają dane, a wszystkie operacje wykonywane są równolegle. Wynik jednej z tych operacji jest podawany na wyjście Q, O tym, który wynik ma być wybrany decyduje MUX którego działanie opisałem w poprzednim poście.
W czwartym takcie dane wystawione na wyjściu Q są zapisywane w rejestrze B, lub pamięci BRAM, lub na wyjściu EXTO w zależności od użytej instrukcji.

Instrukcje kopiujące pozwalają mi dostać się do rejestrów. To nie jest idealne rozwiązanie, ale działa i na pierwszy procesor wystarcza.
--zapis danych do rejestru B
process(Clk, Reset, Sel, Cnt, PORT_I, R, ROMDO, PROD, Sel)
begin
    if Reset = '0' then (Globalny sygnał RESET kasuje rejestr ustawiając zera dla wszystkich bitów)
        B <= (others => '0');
    else
        if falling_edge (Clk) then
            if (Cnt = "11") then (Zapis tylko w czwartym takcie)
                if (Sel = "00001")         --MOVLB
                or (Sel = "10000") then    --RETLB        kopiowanie stałej do rejestru B (Stała to liczba zapisana w pamięci ROM)
                    B <= ROMDO(7 downto 0) ;
            elsif (Sel = "00010") then    --MOVEB         kopiowanie danych z portu EXTI do rejetru B (Port EXTI to dane z zewnątrz)
                    B <= PORT_I ;
            elsif (Sel = "00011") then    --MOVRB        kopiowanie zawartości pamięci RAM do rejestru B (Bez użycia ALU)
                    B <= R ;
            elsif (Sel = "01101") then    --MULRB        kopiowanie PRODH do rejestru B (Pisałem w poprzednim poście że wynik mnożenia podzieliłem na pół a górna połowa zapisywana jest właśnie tutaj czyli do rejestru roboczego)
                    B <= PROD(15 downto 0) ;
                end if;
            end if;
        end if;
    end if;
end process;

Poprawiłem jedną rzecz: PRODH to bity 15..8, nie 15..0.
elsif (Sel = "01101") then -- MULRB
     B <= PROD(15 downto 8);

Pamięć programu (ROM).
Wykorzystuję możliwość jednoczesnego zapisu i odczytu BRAM.
Adres zapisu i odczytu mogą być różne.
Ważne: nie wolno czytać i pisać tej samej komórki w tym samym takcie.

architecture Behavioral of ROM is

    type ram_type is array (0 to 2047) of std_logic_vector (15 downto 0); (Zostaną wykorzystane dwa bloki BRAM)
    signal ROM0: ram_type;
    
    signal EN : std_logic;

begin

    EN <= '1' when Cnt = "00" else '0';

    process (CLK)
    begin
        if falling_edge (Clk) then
            if WE = '1' then (Zezwolenie na zapis do pamięci)
              ROM0(conv_integer(ADDR)) <= DI; (Adres BRAM to sygnał 11 bitowy o nazwie  ADDR)
            end if;
            if EN = '1' then (Zezwolenie na odczyt)
                DO <= ROM0(conv_integer(PC)) ; (Adres BRAM to sygnał 11 bitowy o nazwie  PC)
            end if;
        end if;
    end process;

end Behavioral;

Sygnał EN blokuje odczyt w trakcie wykonywania instrukcji.

pozdrawiam

  • Lubię! 1

Czytam sobie trochę wybiórczo bo tylko część mnie interesuje (nie znam się na FPGA ale o procesorach coś tam słyszałem). Ciekawe.

  • Lubię! 1
9 godzin temu, ethanak napisał:

Czytam sobie trochę wybiórczo bo tylko część mnie interesuje (nie znam się na FPGA ale o procesorach coś tam słyszałem). Ciekawe.

Od młodych lat fascynowała mnie elektronika. Niestety miałem możliwości rozwijać swojej pasji zawodowo, więc postanowiłem wziąć sprawy w swoje ręce. Jestem pasjonatem i hobbystą. Moje formy wypowiedzi mogą z tego powodu być niezrozumiałe ludziom po studiach, ponieważ nie kształciłem się w tym kierunku. Wiedzę pozyskuję z wielu źródeł i przekazuję tak jak ją rozumiem. O procesorach i ich funkcjonowaniu dowiedziałem się czytając książkę w niemieckim wydaniu o procesorze 8051 Francisa. Później poznałem mikrochip i stronę sprut.de na której opisane są te kontrolery z przykładami. Następnie próbowałem moich sił z układami cpld i finalnie fpga. Chcę przez to powiedzieć że nie jestem znawcą FPGA i napewno nie wiem więcej niż inni. Wrzucam posty by przybliżyć uniwersalność układów FPGA w ich zastosowaniach.

Witam.

W tym poście opiszę działanie stosu przerwań.
Na początek wyjaśnię jak działa mój stos adresów przerwań, co to są przerwania i po co to komu...

Stos przerwań jest używany w celu weryfikacji danych. Działanie ma cel nawiązania połączenia z urządzeniem które do nas chce przesłać dane, lub obliczeniem funkcji...
Przykład: Klawiatura komputera chce przekazać procesorowi, że został wciśnięty klawisz. Procesor jest zajęty nie zna tego urządzenia, ponieważ kod który wykonuje przetwarza inne procesy. Procesor jest jak pociąg jadący po szynach w jedną stronę. Przerwanie może przekierować taki pociąg na inny tor, zapisuje adres, z którego został przekierowany, a po zakończonej akcji kieruje go w to samo miejsce przed przerwaniem. Takie przerwanie jest nazywane INTERRUPT.
Inną formą przerwań jest przerwanie programowe. Przerwanie programowe jest przerwaniem wywołującym funkcję.
Przykład: dobrze znana pętla for. for (int i = 0; i < 7; i++). Gdy program odnajdzie takie polecenie oznacza to skok pod adres, pod którym procesor wykonuje tę instrukcję (instrukcja ta  jest zapisana w formie Assebler dla procesora o stałym adresie do którego się odwołujemy) ze składnikami, czyli liczbą dla "i" w tym przypadku i zapisze na stosie adres powrotu z przerwania programowego. Magia przerwania programowego polega na tym że procesor ma zmienną do której się odwołuje i wykonuje tą operację dopóki wynik będzie prawdziwy "i < 7".
Gdy wynik w naszym przykładzie osiągnie liczbę 8 to procesor wróci do adresu w którym przerwał wykonywanie instrukcji main(kod główny). Takie przerwania opisujemy w procesorach microchip instrukcją CALL ( Wykonuję pracę. Dzwonię do znajomego i pytam się o wynik 2+2. Odkładam telefon. Wracam do pracy i zapisuję wynik 4 który mi podał znajomy).
Różnica między przerwaniami z zewnątrz i programowymi jest następująca. Przerwania programowe są zaplanowane w kodzie, a przerwania z zewnątrz są nieprzewidywalne dla procesora. Przerwania z zewnątrz moim zdaniem mimo, że są nieprzewidywalne są łatwiejsze do zrozumienia niż przerwania programowe.

Obsługa przerwania z zewnątrz w mim procesorze:
                if ((INT_KBD = '1') and (INTEN = '0')) then    -- INTERUPT
                    PC <= "00000010000"; -- skok do adresu "10000"
                    INTPC <= PC;
                    INTEN <= '1';
Przerwanie z zewnątrz jest sygnałem elektrycznym, który wymusza w liczniku programu (PC) zapisanie stałej wartości (skok do adresu "10000"). To wszystko. Zadaniem programisty jest napisanie kodu obsługującego te urządzenie, dla którego przypisaliśmy ten sygnał w programie zaczynającym się pod tym adresem. Wydaje się proste i tak jest. W takich przerwaniach ważny jest hardware, a software to kod obsługujący przerwania z zewnątrz.
Adres powrotu w moim procesorze zapisywany jest automatycznie w rejestrze i wywoływany jest instrukcją powrotu z przerwania z zewnątrz:
            elsif ((Sel = "10010") and (INTEN = '1')) then    -- RETFIE
                    PC <= INTPC;
                    INTEN <= '0';
INTPC jest rejestrem w którym przechowywany jest adres powrotu z przerwania z zewnątrz. Instrukcja wymusza zapis adresu zawartego w rejestrze do licznika programu (PC).

Przerwania programowe opiszę w następnym wątku, ponieważ zastosowałem mechanizm zapisu adresów stosu przerwań mojego autorstwa. To wymaga obszerniejszego wyjaśnienia które mogło by być nadmiarowe w tym wpisie..

pozdrawiam

(edytowany)
6 godzin temu, kroszkanorber napisał:

Niestety miałem możliwości rozwijać swojej pasji zawodowo, więc postanowiłem wziąć sprawy w swoje ręce.

Brzmi niejednoznacznie. Zakładam, że nie miałeś możliwości łączenia hobby z pracą zawodową.

 

6 godzin temu, kroszkanorber napisał:

Jestem pasjonatem i hobbystą. Moje formy wypowiedzi mogą z tego powodu być niezrozumiałe ludziom po studiach

Pasja i hobby nie mają nic wspólnego z formą wypowiadania się. Piszesz obszerne posty, wyjaśniasz co robisz, a to rzadkość. Cenne są informacje w jaki sposób pozyskujesz wiedzę.

Dla mnie brakuje tylko wstępu, w którym byłoby opisane co jest celem prezentacji tego projektu. Po co realizujesz ten projekt? Czy jest to tylko etap nauki technologii FPGA i chcesz się pochwalić ile już osiągnąłeś, czy może jest to etap realizacji większego projektu, który masz już w głowie? 

Edytowano przez Sylba
  • Lubię! 1
7 godzin temu, Sylba napisał:

Dla mnie brakuje tylko wstępu, w którym byłoby opisane co jest celem prezentacji tego projektu. Po co realizujesz ten projekt? Czy jest to tylko etap nauki technologii FPGA i chcesz się pochwalić ile już osiągnąłeś, czy może jest to etap realizacji większego projektu, który masz już w głowie?

Widzę zainteresowanie projektem i wasze odpowiedzi zobowiązują mnie do wyjaśnień.

Kod procesora, który podałem, jest kodem referencyjnym i nie jest celem tego postu. Wrzuciłem go i wyjaśniam jego działanie, ponieważ niektóre rozwiązania przeniosę do projektu docelowego.

Opisany procesor w VHDL (konfiguracja układu) jest relatywnie prosty i dla wielu czytelników może być dobrym wstępem do mojej architektury, nad którą pracuję. Podsumowując — moje wpisy są wprowadzeniem do projektu docelowego.

Przypuszczam, że tak niskopoziomowe podejście nie jest oczywiste dla wszystkich. Dlatego staram się wyjaśniać działanie mojego procesora obszernie, nie zakładając, że wszyscy wszystko rozumieją.

Procesor opisywany w poście ma swoje słabe strony, głównie ze względu na ubogą liczbę instrukcji. Kod assemblera dla tego procesora posiada dużo instrukcji pomocniczych, co utrudnia pisanie oprogramowania. To jest główny powód, dla którego rozwijam i przebudowuję kod konfiguracyjny.

Celem projektu jest stworzenie procesora przyjaznego w obsłudze, napisanie małego IDE/kompilatora (aplikacja okienkowa w C++), programowanie przez złącze RS232 oraz dopisanie modułów VGA i KBC. Czyli — mały mini‑komputer do zabawy.

  • Pomogłeś! 1
(edytowany)

Witam.

Przerwania programowe.
Przerwania programowe działają podobnie do przerwań od urządzeń zewnętrznych.  Przerwania programowe są zaimplementowane w kodzie w celu wywołania funkcji zapisanej pod adresem w którym się zaczyna. 
Przerwanie programowe wywołujemy instrukcją CALL. Instrukcja posiada ukryte działanie które polega na zapisaniu adresu powrotu na stosie przerwań, a następnie skoku pod podany adres w kodzie.
Przypisanie adresu zapisanego w instrukcji do licznika programu (PC) jest wykonywane w 4 takcie.
Przy okazji zmniejszamy licznik adresów stosu przerwań (adresy stosu nie są adresami powrotu z przerwania, a adresem kolejnej komórki pamięci w której zapisany jest adres powrotu).
elsif (Sel = "11001") then -- CALL
         PC <= ROMDO(10 downto 0);
         TOS <= TOS - 1; -- zmniejszenie adresu licznika przerwań po odczycie instrukcji.
Po uruchomieniu procesora lub po użyciu sygnału Reset, licznik TOS jest ustawiany na adres ostatniej komórki pamięci programu ROM.
Pamięć programu jest zapisywana instrukcjami od początku (PC = 0 po uruchomieniu), a stos przerwań jest zapisywany od końca pamięci programu (ROMADDR = TOS = 2047).
Za każdym razem gdy wywołujemy polecenie CALL, wskaźnik TOS wskazuje na aktualną komórkę pamięci w której zapisujemy adres powrotu i w 4 takcie wykonuje skok pod podany adres, równocześnie zmniejszając licznik TOS.. 
By przed zmianami zapisać adres powrotu z przerwania w komórce pamięci wymagane jest przetworzenie sygnałów we wcześniejszych taktach.

Tutaj przypisany jest wskaźnik do adresu zapisu w pamięci programu 
         ROMADDR <= TOS;

W następnej linijce kodu zastosowałem trick zapisu adresu powrotu który powiązałem z instrukcją GOTO.
Instrukcja GOTO jest poleceniem skoku do następnego adresu wskazanego w tej instrukcji i po wywołaniu wykona skok bez adresu powrotu.
         --GOTO + PC + 1 zapis instrukcji powrotu z przerwania z adresem następnego polecenia
         ROMDI <= "11000" & (PC + 1); 

W kolejnej linijce kodu steruję sygnałem zapisu ROMWE. Zapis jest aktywny tylko gdy wywołujemy instrukcję CALL (Sel = "11001") i licznik cyklu pokazuje 3 takt (Cnt = "10"). Czyli zapis adresu powrotu jest wykonywany 1 takt przed wykonaniem instrukcji CALL.
        --CALL dekoder zapisu adresu powrotu z przerwania w pamięci ROM
        ROMWE <= '1' when ((Sel = "11001") and (Cnt = "10")) else '0';

Powrót z przerwania programowego wykonywany jest instrukcją RETURN, RETLB.
if (Cnt = "01") then 
                if ((Sel = "10001") or (Sel = "10000")) then    -- RETURN, RETLB
                    PC <= TOS;     -- adres TOS przypisany do licznika PC 
                    TOS <= TOS + 1;
                end if;
            end if;
Po wykryciu polecenia powrotu z przerwania w 2 takcie, przypisany zostaje adres wskaźnika do licznika programu, który odpowiada za adres, pod którym odczytujemy pamięć programu. Jednocześnie zwiększony zostaje licznik TOS (wskaźnik).
W 3 takcie wykrywane jest polecenie GOTO z adresem powrotu z przerwania a w 4 takcie jest wykonane.

Podsumowując - TOS jest wskaźnikiem adresów powrotu z przerwań, Adresy są zapisywane w pamięci programu od końca. Możemy wywoływać instrukcję CALL wiele razy bez obawy, że zabraknie nam kolejnego poziomu w naszym drzewie programu. Możemy zrobić tyle poziomów przerwań na ile nam pozwoli wolne miejsce w pamięci programu (dopóki stos nie nadpisze instrukcji zawartej w pamięci programu).

Moim zdaniem jest to najmocniejsza strona mojego procesora. Daje pełną swobodę tworzenia głębokich wywołań funkcji bez ograniczeń sprzętowych.

Edytowano przez kroszkanorber

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...