Skocz do zawartości

Pomocna odpowiedź

(edytowany)

Witam.

AGNES.MK to druga generacja mojego procesora. Po analizie dokumentacji wielu istniejących architektur doszedłem do wniosku, że żadna z nich nie spełnia moich założeń projektowych. Mój procesor ma być tworzony specjalnie dla FPGA, a nie jako kopia układów ASIC.

Podstawowe założenia są proste: minimalna liczba MUX‑ów, krótkie ścieżki sygnałów, brak zbędnych funkcji, prosty kod sterujący, elastyczna, ale minimalistyczna architektura, instrukcje ALU z dodatkowymi bitami sterującymi, rozproszony dekoder zamiast centralnego potwora logicznego.

Nowy procesor nie będzie korzystać z wbudowanej pamięci BRAM. Jedyną pamięcią systemową będzie zewnętrzna kość SRAM 1M × 16 bit, 10 ns, która pełni jednocześnie rolę pamięci programu i danych. Magistrala 16‑bitowa jest dwukierunkowa, a sterowanie odczytem i zapisem odbywa się wyłącznie przez sygnały OE i WE.

Wybrałem SRAM, ponieważ jest statyczna i nie wymaga synchronizacji z zegarem w cyklach odczytu/zapisu. Dzięki temu mogę uprościć sekwencje sterujące i zachować pełną kontrolę nad czasem dostępu. Pierwsze 64 kB przestrzeni adresowej będzie przeznaczone na dane, a pozostała część — na kod programu.

Formaty instrukcji:
Instrukcje mają długość 16 bitów, a każdy bit odpowiada konkretnemu sygnałowi sterującemu w dekoderze. Procesor przetwarza dane 16‑bitowe i operuje na wspólnej przestrzeni pamięci.

INSTRUKCJE ALU:

| bit15    | bit14  | bit13 | bit12  | bit11           | bit10     | bit9    | bit8     | bit7      | bit6      | bit5      | bit4     | bit3      | bit2      | bit1 | bit0 |
| CONT | ALU2 | ALU1 | ALU0 | ADD/SUB | CARRY | SKIP1 | SKIP0 | ALUA1 | ALUA0 | ALUB1 | ALUB0 | DEST1 | DEST0 | FL1 | FL0 |

CONT: 1 = instrukcje kontrolne 0 = instrukcje ALU
ALU2, ALU1, ALU0: wybór 1 z 8 instrukcji jednostki arytmetyczno-logicznej.
000: ADD/SUB
001: XOR
010: AND
011: OR
100: RRC
101: RLC
110: RR
111: RL
ADD/SUB: 1 = odejmowanie 0 = dodawanie
CARRY: operacje z CARRY_BIT = 1. bez CARRY_BIT = 0
SKIP1, SKIP0: bity zezwolenia skoku krótkiego z testem flagi wyniku operacji ALU.
00: no skip
01: SKPC
10: SKPZ
11: SKPOV
ALUA1, ALUA0: wybór danych wejścia ALUA.
00: X”0000”
01: X”FFFF”
10: Literal
11: 64kB RAM address
ALUB1, ALUB0: wybór danych wejścia ALUB.
00: X”0000”
01: X”FFFF”
10: Literal
11: 64kB RAM address
DEST1, DEST0: wybór zapisu wyniku operacji ALU.
00 no destination
01 ALUA address
10 ALUB address
11 no destination
FL1, FL0: zapis flag alu
00 brak zapisu
01 zapis Z
10 zapis C, Z
11 zapis C, Z, N, OV

INSTRUKCJE KONTROLNE:

| bit15   | bit14   | bit13    | bit12      | bit11    | bit10 | bit9    | bit8 |
| CONT | CON1 | LJMP1 | LJMP0 | BRA 2 | BRA1 | BRA0 | N     |

| bit15   | bit14   | bit13        | bit12      | bit11       | bit10 | bit9 | bit8 |
| CONT | CON1 | RETURN | RETFIE | RETARG | -       | -      | -      |

| bit7    | bit6     | bit5    | bit4    | bit3    | bit2    | bit1     | bit0    |
| ARG7 | ARG6 | ARG5 | ARG4 | ARG3 | ARG2 | ARG1 | ARG0 | 


CONT: 1 = instrukcje kontrolne 0 = instrukcje ALU
CON1: 1 = instrukcje kontrolne skoków 0 = instrukcje powrotów
LJMP1, LJMP0: dekoder skoków długich
00: GOTO
01: CALL
10: RCALL
11: BRA
BRA2, BRA1, BRA0: instrukcje skoków z testem bitów rejestru STATUS
000: BC
001: BZ
010: BN
011: BOV
100: BNC
101: BNZ
110: BNN
111: BNOV
GOTO: ARG7:ARG0 + 16 bit (drugie słowo instrukcji) = adres skoku długiego.
Skok długi bez adresu powrotu.
CALL: ARG7:ARG0 + 16 bit (drugie słowo instrukcji) = adres skoku długiego.
Skok długi z adresem powrotu.
RCALL: U2 (N + (ARG7:ARG0)) = adres skoku kodowany w liczbie U2 (+-255).
Skok krótki z adresem powrotu.
BRA: U2 (N + (ARG7:ARG0)) = adres skoku kodowany w liczbie U2 (+-255).
Skok krótki warunkowy bez adresu powrotu.
RETURN:
Powrót z przerwania programu.
RETFIE:
Powrót z przerwania z zewnątrz.
RETARG: ARG7:ARG0 + 16 bit (drugie słowo instrukcji) = adres kopiowanej zmiennej.
Trzecie słowo instrukcji = kopiowanie zmiennej.
Czwarte słowo instrukcji = adres dla zapisania kopii pobranej zmiennej.
Powrót z przerwania + kopiowanie stałej do wybranej komórki pamięci.

 

Nowa architektura AGNES.MK jest zaprojektowana tak, aby maksymalnie wykorzystać właściwości układów FPGA. W poprzedniej wersji procesora wiele sygnałów sterujących było generowanych centralnie, co prowadziło do powstawania szerokich magistral sterujących i długich ścieżek routingu. W FPGA takie konstrukcje są kosztowne czasowo i trudne do zamknięcia przy wysokiej częstotliwości. (Po wielu optymalizacjach kodu i testach stwierdziłem, że zmiana 1 linijki kodu może wydłużyć czas propagacji sygnału. Ciężko jest zoptymalizować dekoder z klasycznymi połączeniami,  bo FPGA nie jest stworzony do takich zadań)

W AGNES.MK każdy bit instrukcji bezpośrednio steruje lokalnymi blokami logiki. Dzięki temu nie powstaje centralny dekoder, nie ma globalnych sygnałów o dużym fan‑out, MUX‑y są ograniczone do minimum, routing jest krótszy i bardziej przewidywalny, procesor może pracować stabilnie przy 100 MHz.

Przykład przewagi nowej architektury

W poprzedniej wersji instrukcja ALU wymagała dekodowania kilku pól i generowania sygnałów sterujących w jednym miejscu. W AGNES.MK każdy bit instrukcji jest już sygnałem sterującym.

Przykład:

bit DEST1..0 bezpośrednio wybiera miejsce zapisu,
bit FL1..0 bezpośrednio wybiera zapis flag,
bit ALUA1..0 bezpośrednio wybiera źródło danych.

Nie ma potrzeby tworzenia dodatkowych MUX‑ów ani logiki pośredniej. To oznacza, że instrukcja sama jest mikrokodem, a FPGA wykonuje ją bez zbędnych połączeń.

W praktyce daje to krótszy czas propagacji, mniejsze zużycie zasobów, większą stabilność czasową, łatwiejszą implementację kolejnych instrukcji.

AGNES.MK jest więc architekturą prostszą, szybszą i bardziej naturalną dla FPGA niż poprzednia wersja — i to jest główny powód, dla którego powstaje.

To dopiero początek prac nad AGNES.MK — teraz przechodzę do implementacji w VHDL i będę na bieżąco opisywać kolejne etapy powstawania procesora.

Edytowano przez kroszkanorber
  • Lubię! 1

Witam.

Dodałem dwa nowe moduły w VHDL: selektor wejść ALU (ALU_IN) oraz zaktualizowaną 16‑bitową jednostkę ALU. ALU_IN to 4‑wejściowy multiplekser 16‑bitowy, wykorzystywany jako ALU_A i ALU_B w module głównym. Jednostka ALU została uproszczona (usunięto sygnał EN) oraz rozszerzona z 8 do 16 bitów. Kod jest opatrzony komentarzami, aby ułatwić analizę i integrację.

Selektor kod:

----------------------------------------------------------------------------------
-- Module Name:    ALU_IN - Behavioral 
-- ALUA1, ALUA0: wybór danych wejścia ALUA.
-- ALUB1, ALUB0: wybór danych wejścia ALUB.
-- 00: X"0000"
-- 01: X"FFFF"
-- 10: Literal
-- 11: 64kB RAM address
--
----------------------------------------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;


entity ALU_IN is
	Port
	(
    SEL : in  STD_LOGIC_VECTOR (1 downto 0);   -- ALU input selector
    LI  : in  STD_LOGIC_VECTOR (15 downto 0);  -- Immediate literal value
    DI  : in  STD_LOGIC_VECTOR (15 downto 0);  -- Data from RAM address bus
    DO  : out STD_LOGIC_VECTOR (15 downto 0)   -- Selected ALU input
	 );
end ALU_IN;

architecture Behavioral of ALU_IN is

begin

	with SEL select
		DO <= X"FFFF" when "01",
				LI      when "10",
				DI      when "11",
				X"0000" when others;

end Behavioral;


Jednostka arytmetyczno-logiczna kod:

	-- S   EN
	-- 000 1  F = ADD/SUB
	-- 001 1  F = A AND B
   -- 010 1  F = A OR  B
	-- 011 1  F = A XOR B
	-- 100 1  F = ROL A
	-- 101 1  F = ROR A
	-- 110 1  F = RCL C < A < Ci
	-- 111 1  F = RCR Ci > A > C
	-- SSS 0  F = 0
	
	--     1  C = Cen
	--     0  C = 0
	
	--     1  Z = Zen
	--     0  Z = 0
	
	--     1  OV = OVen
	--     0  OV = 0
	
	--     AS
	--     1  SUB
	--     0  ADD
	
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity ALU is
	Port (
		 A  : in  STD_LOGIC_VECTOR (15 downto 0);  -- ALU input operand A
		 B  : in  STD_LOGIC_VECTOR (15 downto 0);  -- ALU input operand B
		 S  : in  STD_LOGIC_VECTOR (2 downto 0);   -- Function select (000-111)
		 AS : in  STD_LOGIC;                       -- Add/Sub select: '0' = ADD, '1' = SUB
		 CI : in  STD_LOGIC;                       -- Carry‑in for ADD/SUB and rotate‑through‑carry
		 C  : out STD_LOGIC;                       -- Carry‑out flag (ADD/SUB or rotate)
		 Z  : out STD_LOGIC;                       -- Zero flag (1 when result = 0)
		 N  : out STD_LOGIC;                       -- Negative flag (MSB of result)
		 OV : out STD_LOGIC;                       -- Overflow flag for signed arithmetic
		 F  : out STD_LOGIC_VECTOR (15 downto 0)   -- ALU result output
	);
end ALU;

architecture Behavioral of ALU is

	signal SE : std_logic_vector (7 downto 0);      -- One‑hot function select decoder (8 ALU operations)
	signal E8 : std_logic_vector (16 downto 0);     -- 17‑bit adder result (A ± B + CI), includes carry‑out
	signal BX : std_logic_vector (16 downto 0);     -- Modified B operand (B or NOT B) for ADD/SUB
	signal E7 : std_logic_vector (15 downto 0);     -- 16‑bit arithmetic result (lower part of E8)
	signal A7 : std_logic_vector (15 downto 0);     -- AND operation result (A AND B)
	signal O7 : std_logic_vector (15 downto 0);     -- OR operation result (A OR B)
	signal X7 : std_logic_vector (15 downto 0);     -- XOR operation result (A XOR B)
	signal R7 : std_logic_vector (15 downto 0);     -- Rotate right (ROR) result
	signal L7 : std_logic_vector (15 downto 0);     -- Rotate left (ROL) result
	signal LC : std_logic_vector (15 downto 0);     -- Rotate left through carry (RCL) result
	signal RC : std_logic_vector (15 downto 0);     -- Rotate right through carry (RCR) result
	signal FE : std_logic_vector (15 downto 0);     -- Final ALU output before flags (selected function result)
	signal CX : std_logic;                          -- Modified carry‑in (CI or NOT CI) for ADD/SUB
	signal CR : std_logic;                          -- Carry‑out from RCR operation
	signal CL : std_logic;                          -- Carry‑out from RCL operation
	signal CE : std_logic;                          -- Carry‑out from ADD/SUB operation


begin

	with S select
		SE <= "00000001" when "000", -- 000 ADD/SUB
			   "00000010" when "001", -- 001 AND
				"00000100" when "010", -- 010 OR
				"00001000" when "011", -- 011 XOR
				"00010000" when "100", -- 100 ROL
				"00100000" when "101", -- 101 ROR
				"01000000" when "110", -- 110 RCL
				"10000000" when others;-- 111 RCR
				
	BX <= ('0' & B) xor (16 downto 0 => AS);
	CX <= CI xor AS;
	E8 <= ('0' & A) + BX + CX;
	CE  <= E8(16) and SE(0);
	CR  <= A(0) and SE(7);
	CL  <= A(15) and SE(6);
	E7 <= E8(15 downto 0)         and (15 downto 0 => SE(0));
	A7 <= (A and B)              and (15 downto 0 => SE(1));
	O7 <= (A  or B)              and (15 downto 0 => SE(2));
	X7 <= (A xor B)              and (15 downto 0 => SE(3));
	L7 <= (A(14 downto 0) & A(15)) and (15 downto 0 => SE(4));
	R7 <= (A(0) & A(15 downto 1)) and (15 downto 0 => SE(5));
	LC <= (A(14 downto 0) & CI) and (15 downto 0 => SE(6));
	RC <= (CI & A(15 downto 1)) and (15 downto 0 => SE(7));
	FE <= E7 or A7 or O7 or X7 or R7 or L7 or LC or RC;
	
	F  <= FE;
	C  <= CE or CR or CL;
	Z  <= '1' when FE = X"0000" else '0';
	N  <= FE(15);
	OV <= E8(16) xor E8(15);

end Behavioral;

 

Moduły są stabilne i gotowe do dalszej integracji z AGNES.MK.

Dodałem kolejne dwa moduły odpowiedzialne za dekodowanie miejsca zapisu wyniku ALU oraz zapisu flag ALU.

dekoder zapisu wyniku ALU:
 

----------------------------------------------------------------------------------
-- Module Name:    DEST_DEC - Behavioral 
-- DEST decoding:
-- 00 : no write
-- 01 : write to ALUA
-- 10 : write to ALUB
-- 11 : no write
----------------------------------------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity DEST_DEC is
    Port
    (
        DEST     : in  STD_LOGIC_VECTOR (1 downto 0);   -- DEST field selecting ALU write destination
        ALUA_WE  : out STD_LOGIC;                       -- Write enable for ALUA destination
        ALUB_WE  : out STD_LOGIC                        -- Write enable for ALUB destination
    );
end DEST_DEC;

architecture Behavioral of DEST_DEC is

begin

    ALUA_WE <= DEST(0) and not DEST(1);
    ALUB_WE <= DEST(1) and not DEST(0);

end Behavioral;

Dekoder zapisu flag ALU:
 

----------------------------------------------------------------------------------
-- Module Name:    FLAG_DEC - Behavioral 
-- FL decoding:
-- 00 : no write
-- 01 : write Z
-- 10 : write C, Z
-- 11 : write C, Z, N, OV
----------------------------------------------------------------------------------
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity FLAG_DEC is
    Port
    (
        FL : in  STD_LOGIC_VECTOR (1 downto 0);   -- FL field selecting which ALU flags to update
        C  : out STD_LOGIC;                       -- Write enable for C flag
        Z  : out STD_LOGIC;                       -- Write enable for Z flag
        N  : out STD_LOGIC;                       -- Write enable for N flag
        OV : out STD_LOGIC                        -- Write enable for OV flag
    );
end FLAG_DEC;

architecture Behavioral of FLAG_DEC is

begin

    Z  <= FL(1) or FL(0);        -- Z written for FL = 01, 10, 11
    C  <= FL(1);                 -- C written for FL = 10, 11
    N  <= FL(1) and FL(0);       -- N written only for FL = 11
    OV <= FL(1) and FL(0);       -- OV written only for FL = 11

end Behavioral;

Dekodery są proste, ponieważ część logiki wyboru miejsca zapisu została przeniesiona bezpośrednio do formatu instrukcji.

  • Lubię! 1

Wprowadzenie do mikroinstrukcji procesora

Mikroinstrukcja to zestaw sygnałów sterujących, które mówią procesorowi, co ma zrobić w danym takcie — tak jak światła na skrzyżowaniu mówią, który kierunek ma jechać.

Skrzyżowanie = magistrala danych
Na magistrali danych mamy kilka możliwych „kierunków ruchu”:
ALU → rejestr
rejestr → ALU
pamięć → rejestr
rejestr → pamięć
PC → magistrala
magistrala → PC

Każdy z nich to osobny „pas ruchu”.

Światła = sygnały sterujące
Każde światło to jeden bit mikroinstrukcji:
zielone → enable
czerwone → disable
żółte → przygotowanie do kolejnego kroku

Sekwencja świateł = sekwencja mikroinstrukcji
Na skrzyżowaniu nie można puścić ruchu we wszystkich kierunkach naraz — byłby chaos. Tak samo w CPU:
najpierw pobierasz instrukcję,
potem ustawiasz źródła,
potem wykonujesz operację,
potem zapisujesz wynik,
na końcu aktualizujesz PC.
Każdy z tych kroków to inna mikroinstrukcja.

Mikroinstrukcja = „kto ma teraz zielone”
W danym takcie mikroinstrukcja określa: które rejestry wystawiają dane,
które rejestry mają je przyjąć,
czy ALU ma liczyć,
czy PC ma się zwiększyć,
czy ma nastąpić skok,
czy zapisujemy coś do pamięci.
To dokładnie tak, jakby powiedzieć: „Teraz jedzie kierunek północ–południe, reszta stoi.”

Maszyna stanów jako sterownik mikroinstrukcji
Moim zdaniem najprościej takie sekwencje zrealizować w maszynie stanów. Maszyna stanów wysyła tylko sygnały enable (zezwolenia), a cała reszta dzieje się w logice.
Maszyna stanów nie bierze udziału w przetwarzaniu danych. Ona tylko steruje przepływem.
Sygnał enable działa jak zawór: kiedy odkręcamy zawór, nie dotykamy wody — tylko pozwalamy jej płynąć.
Dokładnie tak samo mikroinstrukcja „odkręca” odpowiedni kierunek przepływu danych.

Podsumowanie

Myślę, że takie obrazowe wprowadzenie do „magii sterowania” dobrze pokazuje, jak złożone procesy zachodzą podczas przetwarzania danych w procesorze.
W następnym wpisie wyjaśnię, jak reguluję ruchem danych.

W tym wpisie przedstawiam pierwszy moduł odpowiedzialny za skoki relatywne.
Kod obsługuje skoki w programie które są liczone jako offset licznika programu.
Nazwy skoków zapożyczyłem z instrukcji kontrolerów PIC.

Na obrazie zaznaczyłem które bity instrukcji wpływają na polecenia
               image.thumb.png.c02314ea975cb612e23e291a822c921a.png

Czerwone obramowanie bez zaznaczenia pokazuje bity które są odpowiedzialne za wybór instrukcji skoków.
Niebieskie obramowanie wybiera charakter skoku w programie.
Czerwone obramowanie z zaznaczeniem jest wyborem skoków z warunkiem.
Czarne obramowanie zaznacza N czyli znak liczby dodawanej do Licznika programu a ARG jest liczbą 8 bitową reprezentującą wartość 0 do 255.
Zajmiemy się dekodowaniem liczby i wystawieniem sygnału enable dla zapisu offset do licznika programu, uwzględniając wszystkie warunki na wykonanie operacji.

kod VHDL:
 

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

entity BRANCH_DEC is
    Port
    (
        PC        : in  STD_LOGIC_VECTOR (19 downto 0);  -- Current program counter value
        LIT       : in  STD_LOGIC_VECTOR (8 downto 0);   -- 8-bit signed branch offset
        RCALL_EN  : in  STD_LOGIC;                       -- Relative call enable signal
        BRANCH_EN : in  STD_LOGIC;                       -- Branch enable signal
        BRANCH    : in  STD_LOGIC_VECTOR (2 downto 0);   -- Branch type + invert flag
        FLAG_C    : in  STD_LOGIC;                       -- Carry flag
        FLAG_Z    : in  STD_LOGIC;                       -- Zero flag
        FLAG_N    : in  STD_LOGIC;                       -- Negative flag
        FLAG_OV   : in  STD_LOGIC;                       -- Overflow flag
        OFFSET_EN : out STD_LOGIC;                       -- Branch or RCALL condition result
        PC_OFFSET : out STD_LOGIC_VECTOR (19 downto 0)   -- Computed relative target address
    );
end BRANCH_DEC;

architecture Behavioral of BRANCH_DEC is

   signal Se      : std_logic_vector (3 downto 0);          -- One-hot decoded branch type
   signal Bc      : std_logic;                              -- Branch-on-Carry condition
   signal Bz      : std_logic;                              -- Branch-on-Zero condition
   signal Bn      : std_logic;                              -- Branch-on-Negative condition
   signal Bov     : std_logic;                              -- Branch-on-Overflow condition
   signal B_En    : std_logic;                              -- Combined branch/RCALL enable signal
   signal Sum     : std_logic_vector (19 downto 0);         -- PC + sign-extended offset
   signal Offset9 : std_logic_vector (11 downto 0);         -- sign-extended offset

begin

    -- One-hot decode of BRANCH function (lower 2 bits select condition)
    with BRANCH(1 downto 0) select
      Se <= "0001" when "00",   -- 00 = BC  (branch on carry)
            "0010" when "01",   -- 01 = BZ  (branch on zero)
            "0100" when "10",   -- 10 = BN  (branch on negative)
            "1000" when others; -- 11 = BOV (branch on overflow)
            
   -- Evaluate each branch condition with optional inversion (BRANCH(2))
   Bc  <= (FLAG_C  xor BRANCH(2)) and Se(0);  -- Carry condition
   Bz  <= (FLAG_Z  xor BRANCH(2)) and Se(1);  -- Zero condition
   Bn  <= (FLAG_N  xor BRANCH(2)) and Se(2);  -- Negative condition
   Bov <= (FLAG_OV xor BRANCH(2)) and Se(3);  -- Overflow condition

   -- Final enable: branch taken OR RCALL active
   B_En <= Bc or Bz or Bn or Bov;
   
   -- Compute relative target: PC + sign-extended 9-bit offset
   Offset9 <= (11 downto 0 => LIT(8));
   Sum <= PC + (Offset9 & LIT(7 downto 0));
   
   -- Output relative target
   PC_OFFSET <= Sum;
   
   -- Output branch/RCALL condition
   OFFSET_EN <= (B_En and BRANCH_EN) or RCALL_EN;

end Behavioral;

1. Krótki opis sygnałów wejściowych/wyjściowych

PC — aktualna wartość licznika programu

LIT — 9‑bitowy offset (bit 8 = znak)

BRANCH_EN — sygnał aktywacji skoku

RCALL_EN — sygnał aktywacji skoku z zapisem powrotu

OFFSET_EN — sygnał informujący, że skok ma zostać wykonany

PC_OFFSET — nowy adres PC po dodaniu offsetu

2. Krótki przykład instrukcji

Instrukcja BNZ -4 oznacza: jeśli Z=0, to PC = PC - 4.

3. Mały diagram instrukcji

PC ----+
       +----[ adder ]----> PC_OFFSET
offset +
 

Nie napisałem wcześniej wstępu do projektu, więc nadrabiam zaległości. Mój pomysł na CPU nie wziął się znikąd — powstał na bazie własnej płytki deweloperskiej, którą zaprojektowałem specjalnie pod ten projekt.

Poniżej zdjęcie mojej płytki PCB. Na tej platformie pracuje cały projekt AGNES.

                                                                                                  IMG_20260228_215145318.thumb.jpg.1cd9877fa41499bcfcc3834116e75bb5.jpg
 

Zamontowałem na niej układ Xilinx Spartan‑3 XC3S400‑4 PQ208 oraz cztery pamięci SRAM 512k×8, połączone parami równolegle, co daje łącznie 1M×16 bit. Każda kość ma osobne linie OE i WE podłączone do dedykowanych pinów FPGA.

Dodatkowo płytka ma:

pamięć 512k×8 jako VRAM dla VGA,

porty PS/2 dla klawiatury i myszy,

GPIO,

slot SD,

dwa porty USB,

USB‑C do zasilania,

RS232 na FTDI,

moduł Bluetooth,

wyjście audio na filtrach RC,

Pamięć EEPROM I2C 64kB.

Płytka jest „drutowana” , bo przy projektowaniu pominąłem linie do układu konfiguracyjnego i VRAM — po poprawkach działa i wystarczy mi pewnie na lata.

Poniżej schemat blokowy AGNES.MK — odzwierciedla dokładnie mój hardware.


                                                                                   image.thumb.png.e6b065e49ff70b23fae6a181ab98a386.png

Płytkę testowałem wieloma programami i wygląda na poprawnie działającą platformę. Na dzień dzisiejszy wykluczam błędy i zimne luty ponieważ nie zauważyłem niepoprawnych sygnałów.
Dodałem na liniach sygnałowych między FPGA i SRAM rezystory 50 ohm w szeregu na kazdej linii by odkłócić sygnały sterujące. Wiem z doświadczenia, że to dobra praktyka która  poprawia sygnały (odbicia, interferencje i inne przypadłości). Piszę o tym ponieważ uważam to za ważną sugestię w projektowaniu PCB zwłaszcza w niskich napięciach i z różną długością ścieżek dla sygnałów równoległych o wysokich prędkościach (+20MHz). Rezystory to nie rozwiązanie, a jedynie kompromis. Warto pamiętać o tym, bo nie załatwiają problemu, ale umożliwiają komunikację na liniach sygnałowych, względnie czytelną. 

  • Lubię! 1

Cześć wszystkim,

Pracuję dalej nad moim procesorem AGNES i chciałem wrzucić mały update z tego, co udało mi się ostatnio ogarnąć. Projekt jest cały czas w fazie rozwoju, a po drodze wyszło kilka ciekawych problemów związanych z opisem VHDL i długością instrukcji.

Pisząc dekoder instrukcji skoków warunkowych zrozumiałem, że nie mogę po prostu „przeskoczyć jednej instrukcji”. Moje instrukcje mają różną długość — są takie, które zajmują jedno słowo, ale są też 2‑słowowe, a instrukcje ALU potrafią mieć nawet 3 słowa.

Rozwiązania są dwa:

1. Ujednolicić wszystkie instrukcje do 3 słów (proste, ale marnuje pamięć i zabija klimat retro)

2. Zrobić ILD (Instruction Length Decoder), dodać AP (auto precharge) dla kilku instrukcji i dorzucić IP (instruction pointer)

Wybrałem opcję 2, bo lubię wyzwania. Można powiedzieć, że sam stworzyłem sobie problem i sam go rozwiązuję — czy powinienem być z tego dumny? Trudno powiedzieć, ale przynajmniej jest ciekawie i czegoś się uczę.

Na razie mam fragment odpowiedzialny za cykl instrukcji oraz mikrokod ALU. Całość pracuje w trzech stanach: FETCH → DECODE → EXECUTE, a przejście do kolejnej instrukcji następuje dopiero po zakończeniu mikrocyklu ALU. Mikrokod ALU zostanie jeszcze poprawiony, gdy ogarnę wyżej wymienione dekodery — głównie pod kątem optymalizacji. Obecnie mikrokod zastępuje kody wyboru danych wejściowych dla ALU i wyboru zapisu wyniku (te, które pokazywałem wcześniej).

Poniżej wrzucam aktualny kod FSM i mikrokod ALU z komentarzami:

   -------------------------------------------------------------------------
   -- FSM for instruction update
   -- Controls the main instruction cycle: FETCH → DECODE → EXECUTE
   -- A new instruction is fetched only when the ALU microcycle finishes.
   -------------------------------------------------------------------------

   -- Fetch enable: start fetching the next instruction when ALU signals completion
   Fetch_En <= AluEnd;

   process(CLK, RST)
   begin

      if RST = '1' then
         Ir          <= (others => '0');   -- Clear instruction register
         instr_state <= fetch;             -- Start in FETCH state

      elsif rising_edge(CLK) then

         case instr_state is

            -- FETCH: read next instruction word from program memory
            when fetch =>
               Ir <= PROG_DATA;            -- Load instruction register
               instr_state <= decode;      -- Move to DECODE phase

            -- DECODE: instruction is interpreted by decoders
            when decode =>
               instr_state <= execute;     -- Proceed to EXECUTE phase

            -- EXECUTE: wait until ALU microcode finishes
            when execute =>
               if Fetch_En = '1' then      -- ALU finished → ready for next instruction
                  instr_state <= fetch;    -- Restart instruction cycle
               end if;

         end case;
      end if;
   end process;

   -------------------------------------------------------------------------
   -- ALU microcode controller
   -------------------------------------------------------------------------

   -- Start ALU microcycle only for ALU instructions, during DECODE phase,
   -- and only when no interrupt is pending
   Start_Alu <= '1' when (Alu_En = '1' and instr_state = decode and Interrupt_En = '0') else '0';

   -- ALU microcycle shift register (8-step microcode engine)
   process(CLk, RST)
   begin
       if RST = '1' then
           Mikro_Alu <= "00000001";   -- Idle state (LSB = 1)

       elsif rising_edge(CLK) then

           if Start_Alu = '1' then
               Mikro_Alu <= "10000000";   -- Start ALU microcycle (Fetch step)

           elsif Mikro_Alu /= "00000001" then
               Mikro_Alu <= '0' & Mikro_Alu(7 downto 1);  -- Shift right through micro-steps
           end if;

       end if;
   end process;

   -- Load RAM address for operand A (from instruction stream)
   process(CLK, RST)
   begin
       if RST = '1' then
           RamAddrA <= (others => '0');
       elsif rising_edge(CLK) then
           if LoadAddrA = '1' then
               RamAddrA <= PROG_DATA;     -- Address for operand A
           end if;
       end if;
   end process;

   -- Load RAM address for operand B (from instruction stream)
   process(CLK, RST)
   begin
       if RST = '1' then
           RamAddrB <= (others => '0');
       elsif rising_edge(CLK) then
           if LoadAddrB = '1' then
               RamAddrB <= PROG_DATA;     -- Address for operand B
           end if;
       end if;
   end process;

   -- Load operand A value based on instruction field Ir(7:6)
   process(CLK, RST)
   begin
       if RST = '1' then
           AluA <= (others => '0');
       elsif rising_edge(CLK) then
           if LoadDataA = '1' then
               case Ir(7 downto 6) is
                   when "00"   => AluA <= X"0000";     -- Constant 0
                   when "01"   => AluA <= X"FFFF";     -- Constant -1
                   when others => AluA <= ROM_DATA;    -- Literal or RAM data
               end case;
           end if;
       end if;
   end process;

   -- Load operand B value based on instruction field Ir(5:4)
   process(CLK, RST)
   begin
       if RST = '1' then
           AluB <= (others => '0');
       elsif rising_edge(CLK) then
           if LoadDataB = '1' then
               case Ir(5 downto 4) is
                   when "00"   => AluB <= X"0000";     -- Constant 0
                   when "01"   => AluB <= X"FFFF";     -- Constant -1
                   when others => AluB <= ROM_DATA;    -- Literal or RAM data
               end case;
           end if;
       end if;
   end process;

   -- Destination address and write-enable decoder
   -- Selects RAM address A or B as ALU result destination
   process(Ir, RamAddrA, RamAddrB, WriteRes)
   begin
       AluDestAddr <= (others => '0');   -- Default: no destination
       AluDestWe   <= '0';               -- Default: no write

       case Ir(3 downto 2) is
           when "01" =>                  -- DEST = A
               AluDestAddr <= "0000" & RamAddrA;
               AluDestWe   <= WriteRes;  -- Write only during WriteRes micro-step

           when "10" =>                  -- DEST = B
               AluDestAddr <= "0000" & RamAddrB;
               AluDestWe   <= WriteRes;

           when others =>
               null;                     -- No write
       end case;
   end process;

   -- Skip conditions (based on SKIP field Ir(9:8))
   -- Skip on Carry
   Skip_C  <= '1' when ((ALU_C  = '1') and (Ir(9 downto 8) = "01")) else '0';

   -- Skip on Zero
   Skip_Z  <= '1' when ((ALU_Z  = '1') and (Ir(9 downto 8) = "10")) else '0';

   -- Skip on Overflow
   Skip_OV <= '1' when ((ALU_OV = '1') and (Ir(9 downto 8) = "11")) else '0';

   -- Combined skip enable (any condition true)
   Skip_En <= Skip_C or Skip_Z or Skip_OV;

   -- Microcycle control signals
   LoadAddrA  <= Mikro_Alu(7) and Ir(7) and Ir(6);   -- Load RAM address for operand A
   LoadAddrB  <= Mikro_Alu(6) and Ir(5) and Ir(4);   -- Load RAM address for operand B
   LoadDataA  <= Mikro_Alu(5);                       -- Load operand A value
   LoadDataB  <= Mikro_Alu(4);                       -- Load operand B value
   WriteRes   <= Mikro_Alu(3);                       -- Write ALU result + flags
   SkipDo     <= Mikro_Alu(2) and Skip_En;           -- Perform instruction skip
   AluEnd     <= Mikro_Alu(1);                       -- End of ALU microcycle
   -- AluIdle <= Mikro_Alu(0);                       -- Idle state (unused)

Dodatkowo wrzucam kod ALU — to jest struktura, której na razie nie zamierzam zmieniać.

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

entity ALU is
    Port (
         A  : in  STD_LOGIC_VECTOR (15 downto 0);  -- ALU input operand A
         B  : in  STD_LOGIC_VECTOR (15 downto 0);  -- ALU input operand B
         S  : in  STD_LOGIC_VECTOR (2 downto 0);   -- Function select (000-111)
         AS : in  STD_LOGIC;                       -- Add/Sub select: '0' = ADD, '1' = SUB
         CI : in  STD_LOGIC;                       -- Carry-in for ADD/SUB and rotate-through-carry
         C  : out STD_LOGIC;                       -- Carry-out flag (ADD/SUB or rotate)
         Z  : out STD_LOGIC;                       -- Zero flag (1 when result = 0)
         N  : out STD_LOGIC;                       -- Negative flag (MSB of result)
         OV : out STD_LOGIC;                       -- Overflow flag for signed arithmetic
         F  : out STD_LOGIC_VECTOR (15 downto 0)   -- ALU result output
    );
end ALU;

architecture Behavioral of ALU is

    signal SE : std_logic_vector (7 downto 0);      -- One-hot function select decoder (8 ALU operations)
    signal E8 : std_logic_vector (16 downto 0);     -- 17-bit adder result (A ± B + CI), includes carry-out
    signal BX : std_logic_vector (16 downto 0);     -- Modified B operand (B or NOT B) for ADD/SUB
    signal E7 : std_logic_vector (15 downto 0);     -- 16-bit arithmetic result (lower part of E8)
    signal A7 : std_logic_vector (15 downto 0);     -- AND operation result (A AND B)
    signal O7 : std_logic_vector (15 downto 0);     -- OR operation result (A OR B)
    signal X7 : std_logic_vector (15 downto 0);     -- XOR operation result (A XOR B)
    signal R7 : std_logic_vector (15 downto 0);     -- Rotate right (ROR) result
    signal L7 : std_logic_vector (15 downto 0);     -- Rotate left (ROL) result
    signal LC : std_logic_vector (15 downto 0);     -- Rotate left through carry (RCL) result
    signal RC : std_logic_vector (15 downto 0);     -- Rotate right through carry (RCR) result
    signal FE : std_logic_vector (15 downto 0);     -- Final ALU output before flags (selected function result)
    signal CX : std_logic;                          -- Modified carry-in (CI xor AS) for ADD/SUB
    signal CR : std_logic;                          -- Carry-out from RCR operation
    signal CL : std_logic;                          -- Carry-out from RCL operation
    signal CE : std_logic;                          -- Carry-out from ADD/SUB operation

begin

    -- One-hot decode of ALU function select S
    with S select
      SE <= "00000001" when "000", -- 000 ADD/SUB
            "00000010" when "001", -- 001 AND
            "00000100" when "010", -- 010 OR
            "00001000" when "011", -- 011 XOR
            "00010000" when "100", -- 100 ROL
            "00100000" when "101", -- 101 ROR
            "01000000" when "110", -- 110 RCL
            "10000000" when others;-- 111 RCR
                
    -- Prepare B operand for ADD/SUB (invert B when AS = 1)
    BX <= ('0' & B) xor (16 downto 0 => AS);

    -- Modified carry-in for ADD/SUB (CI xor AS)
    CX <= CI xor AS;

    -- 17-bit arithmetic result (A ± B + CI)
    E8 <= ('0' & A) + BX + CX;

    -- Carry-out from ADD/SUB (valid only when SE(0) = 1)
    CE  <= E8(16) and SE(0);

    -- Carry-out from RCR (bit shifted out from LSB)
    CR  <= A(0) and SE(7);

    -- Carry-out from RCL (bit shifted out from MSB)
    CL  <= A(15) and SE(6);

    -- Masked arithmetic result
    E7 <= E8(15 downto 0) and (15 downto 0 => SE(0));

    -- Masked logical results
    A7 <= (A and B) and (15 downto 0 => SE(1));
    O7 <= (A  or B) and (15 downto 0 => SE(2));
    X7 <= (A xor B) and (15 downto 0 => SE(3));

    -- Masked rotate results
    L7 <= (A(14 downto 0) & A(15)) and (15 downto 0 => SE(4)); -- ROL
    R7 <= (A(0) & A(15 downto 1))  and (15 downto 0 => SE(5)); -- ROR
    LC <= (A(14 downto 0) & CI)    and (15 downto 0 => SE(6)); -- RCL
    RC <= (CI & A(15 downto 1))    and (15 downto 0 => SE(7)); -- RCR

    -- OR all masked results to get final ALU output
    FE <= E7 or A7 or O7 or X7 or R7 or L7 or LC or RC;
    
    -- Output result
    F  <= FE;

    -- Combined carry-out from ADD/SUB or rotate operations
    C  <= CE or CR or CL;

    -- Zero flag (1 when result = 0)
    Z  <= '1' when FE = X"0000" else '0';

    -- Negative flag (MSB of result)
    N  <= FE(15);

    -- Signed overflow flag (MSB carry xor MSB result)
    OV <= E8(16) xor E8(15);

end Behavioral;

Komentarze w kodzie piszę od niedawna po angielsku, korzystając z pomocy SI — tak jest mi łatwiej zachować porządek w kodzie i nie mieszać dwóch języków. Poza tym większość dokumentacji CPU i VHDL i tak jest po angielsku, a angielski w programowaniu jest jednoznaczny i każdy od razu wie, o co chodzi.

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...