Computer engineering tutorial - Μέρος 3ο: Προσομοίωση ψηφιακών κυκλωμάτων

gnu_labis | Παρ, 08/20/2010 - 17:25 | 19' | 0

Διαβάστε το πρώτο μέρος της σειράς εδώ, κ το δεύτερο εδώ.

Έπειτα από μακρά απουσία, επιστρέφω με το τρίτο μέρος της σειράς που έχει κάνει μικρούς κ μεγάλους να μην ξεκολλάνε από το φόρουμ μας, κ έχει γεμίσει το σπίτι μου με στοίβες δώρα κ κάρτες ευχαριστιών (όχι άλλες καφετιέρες ρε παιδιά, φτάνει!) :P

Κ για πρώτη φορά στη μακρόχρονη ιστορία αυτών των tutorial, προκειμένου να τιμήσω τον τίτλο που τους έδωσα, θα μιλήσω για κάτι που πραγματικά αφορά μόνο όσους ασχολούνται με ψηφιακά κυκλώματα, είτε ερασιτεχνικά, ή επαγγελματικά: προσομοίωση ψηφιακών κυκλωμάτων σε περιβάλλον Linux (φυσικά).

Πράγματι, ο λόγος που πριν από αρκετό καιρό ξεκίνησα αυτή την ιδέα ήταν ότι είχα αφιερώσει πάρα πολύ χρόνο κ κόπο στο να μάθω πώς να κάνω τη δουλειά μου μόνο με ανοιχτά εργαλεία, κ κατά προτίμηση σε περιβάλλον Linux, κ σκέφτηκα ότι καλό θα ήταν να μαζέψω τις εμπειρίες μου κάπου.

Πριν μπούμε στο ζουμί, μια προειδοποίηση: (δυστυχώς) δεν είμαι εδώ για να δείξω τί είναι ψηφιακά κυκλώματα ή πώς τα προσομοιώνουμε. Αυτό θα ήθελε πολύ κόπο κ χρόνο, κ είναι κ λίγο εκτός της θεματολογίας του φόρουμ μας. Αντίθετα, υποθέτω ότι ήδη ξέρετε τί είναι προσομοίωση ψηφιακών, κ θέλετε απλά κάποιον να σας δείξει πώς θα το κάνετε με ανοιχτά εργαλεία σε περιβάλλον Linux.


Ο λόγος λοιπόν περί "γλωσσών περιγραφής υλικού" (Hardware Description Languages, HDL), κ πιο συγκεκριμένα περί VHDL, μιας κ αυτή η γλώσσα επικρατεί στα Ελληνικά κ λοιπά Ευρωπαϊκά πανεπιστήμια.

Οι περισσότερες HDL γλώσσες έχουν ήδη αρκετές δεκαετίες ζωής στη πλάτη τους, σαν ένας πολύ αποτελεσματικός τρόπος προσομοίωσης κυκλωμάτων. Τα τελευταία 20 χρόνια, γνωρίζουν νέα άνθηση, λόγω των Field Programmable Gate Arrays (FPGA). Μια FPGA είναι ένα προγραμματιζόμενο τσιπ, κ μπορούμε να πούμε τί θέλουμε να κάνει το τσιπ, περιγράφοντας το σε κάποια HDL.

Ο κάθε κατασκευαστής FPGA (Xilinx, Altera, Actel, Atmel, Lattice, κλπ) προσφέρει τα δικά του εργαλεία που μετατρέπουν (ανάλογο του compile στο software) τον HDL κώδικα μας σε κάτι που καταλαβαίνει το τσιπάκι (FPGA bitstream).

Συνήθως η λογική της σχεδίασης ενός ψηφιακού κυκλώματος για υλοποίηση σε FPGA είναι:

  1. Γράφουμε τον κώδικα σε κάποια HDL
  2. Προσομοιώνουμε τον κώδικα για να βεβαιωθούμε ότι κάνει αυτό που θέλουμε
  3. Χρησιμοποιούμε το εργαλείο του κατασκευαστή της FPGA που στοχεύουμε για να παράγουμε το τελικό αρχείο
  4. Προγραμματίζουμε την FPGA με το αρχείο από το προηγούμενο βήμα, κ ελπίζουμε να δουλέψει :)

Εμείς εδώ θα συγκεντρωθούμε στο δεύτερο βήμα, αυτό της προσομοίωσης.

Για το σκοπό αυτό θα χρειαστούμε ένα απλό κομμάτι κώδικα το οποίο κ παραθέτω εδώ (σε VHDL, η οποία δυστυχώς δεν έχει syntax highlighting στον editor του φόρουμ μας). Πρόκειται για έναν απλό 8-bit ripple-carry αθροιστή, που αποτελείται από 3 αρχεία:

  1. fadder.vhd:Ένα αρχείο με ένα βασικό 1-bit full adder
  2. padder.vhd:Ένα αρχείο που ενώνει στη σειρά πολλούς full adder, ενώνοντας το carry out τους ενός με το carry in του επόμενου
  3. padder_tb.vhd:Ένα αρχείο για τη δοκιμή (testbench) που αρχικοποιεί ένα padder μήκους 8 bit κ του δίνει μερικές τιμές για να δούμε αν δουλεύει

fadder.vhd

library ieee;
use ieee.std_logic_1164.all;

entity fadder is
  port (i0   : in  std_logic;
        i1   : in  std_logic;
        cin  : in  std_logic;
        sum  : out std_logic;
        cout : out std_logic);
end fadder;

architecture Behavioral of fadder is

begin
  sum  <= i0 xor i1 xor cin;
  cout <= (i0 and i1) or (cin and i1) or (cin and i0);
end Behavioral;

padder.vhd

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity padder is
  generic(width : natural);
  port(
    cin      : in  std_logic;
    in0, in1 : in  std_logic_vector (width-1 downto 0);
    sum      : out std_logic_vector (width-1 downto 0);
    cout     : out std_logic
    );
end padder;

architecture Structural of padder is
  component fadder
    port (i0   : in  std_logic;
          i1   : in  std_logic;
          cin  : in  std_logic;
          sum  : out std_logic;
          cout : out std_logic
          );
  end component;

  signal carry_chain : std_logic_vector (width downto 0);

begin

  padder0 : for i in 0 to width-1 generate
    comp : fadder
      port map (
        i0   => in0(i),
        i1   => in1(i),
        cin  => carry_chain(i),
        sum  => sum(i),
        cout => carry_chain(i+1)
        );
  end generate padder0;

  carry_chain(0) <= cin;
  cout           <= carry_chain(width);
  
end Structural;

padder_tb.vhd

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

use std.textio.all;

entity padder_tb is
  
  generic (
    width : natural := 8);              -- adder width

end padder_tb;

architecture TB of padder_tb is

  component padder
    generic (
      width : natural);
    port (
      cin      : in  std_logic;
      in0, in1 : in  std_logic_vector(width-1 downto 0);
      sum      : out std_logic_vector(width-1 downto 0);
      cout     : out std_logic);
  end component;

  signal TB_input0, TB_input1, TB_output : std_logic_vector (width-1 downto 0);
  signal TB_carryin, TB_carryout         : std_logic;

begin  -- TB

  padder_test : padder
    generic map (
      width => width)
    port map (
      cin  => TB_carryin,
      in0  => TB_input0,
      in1  => TB_input1,
      sum  => TB_output,
      cout => TB_carryout);

  process
  begin
    report " -> Ripple-carry adder testbench start" severity note;

    TB_carryin <= '0';
    TB_input0  <= X"00";
    TB_input1  <= X"00";

    wait for 50 ms;

    TB_carryin <= '0';
    TB_input0  <= X"C8";
    TB_input1  <= X"38";

    wait for 50 ms;

    report " --> The 1st operand is: " & integer'image(TO_INTEGER(unsigned(TB_input0)));

    report " --> The 2nd operand is: " & integer'image(TO_INTEGER(unsigned(TB_input1)));

    wait for 50 ms;

    report " --> The sum is: " & integer'image(TO_INTEGER(unsigned(TB_output)));

    if TB_carryout = '1' then
      report " --> Carry = 1" severity note;
    else
      report " --> Carry = 0" severity note;
    end if;

    wait for 50 ms;
    
    TB_carryin <= '0';
    TB_input0  <= X"17";
    TB_input1  <= X"2E";

    wait for 50 ms;

    report " --> The 1st operand is: " & integer'image(TO_INTEGER(unsigned(TB_input0)));

    report " --> The 2nd operand is: " & integer'image(TO_INTEGER(unsigned(TB_input1)));

    wait for 50 ms;

    report " --> The sum is: " & integer'image(TO_INTEGER(unsigned(TB_output)));

    if TB_carryout = '1' then
      report " --> Carry = 1" severity note;
    else
      report " --> Carry = 0" severity note;
    end if;

    wait for 50 ms;

    report " -> Ripple-carry adder testbench end" severity note;

    wait;
    
  end process;
  
end TB;

Αυτός λοιπόν είναι ο κώδικας, οπότε φτιάξτε κάπου ένα ΝΕΟ φάκελο κ σώστε αυτά τα 3 αρχεία εκεί μέσα. Για τους σκοπούς του tutorial, εφώ έφτιαξα ένα φάκελο με το όνομα cetutorial03 πάνω στην επιφάνεια εργασίας μου:

dimitris@mercury:~$ mkdir -p Desktop/cetutorial03
dimitris@mercury:~$ cd Desktop/cetutorial03/
dimitris@mercury:~/Desktop/cetutorial03$ ls
fadder.vhd  padder_tb.vhd  padder.vhd

Στη συνέχεια, φτιάχνουμε ένα φάκελο (εγώ τον ονομάζω work) που θα χρησιμοποιήσουμε για τα ενδιάμεσα αρχεία που παράγει η προσομοίωση:

dimitris@mercury:~/Desktop/cetutorial03$ mkdir work
dimitris@mercury:~/Desktop/cetutorial03$ ls
fadder.vhd  padder_tb.vhd  padder.vhd  work

Το πρόγραμμα που θα χρησιμοποιήσουμε ακούει στο όνομα ghdl, κ πρόκειται για ένα VHDL simulator βασισμένο πάνω στο δοξασμένο GCC compiler. Τι άλλο θέλετε, τα καλύτερα σας μαθαίνω! ;)

Αφού λοιπόν εγκαταστήσετε το σχετικό πακέτο στη διανομή σας (με όνομα "ghdl"), είστε έτοιμοι. Πηγαίνετε στο φάκελο cetutorial03 (ή όπως τον ονομάσατε) κ δίνετε:

dimitris@mercury:~/Desktop/cetutorial03$ ghdl -i --workdir=work *.vhd
dimitris@mercury:~/Desktop/cetutorial03$ ghdl -m --workdir=work padder_tb
analyze padder_tb.vhd
analyze padder.vhd
analyze fadder.vhd
elaborate padder_tb
dimitris@mercury:~/Desktop/cetutorial03$ ls
fadder.vhd  padder_tb  padder_tb.vhd  padder.vhd  work

Αν δώσατε άλλο όνομα για τον προσωρινό σας φάκελο (work στην περίπτωση μου), φυσικά αλλάξτε ανάλογα τις 2 αυτές εντολές.

Η πρώτη εντολή "σκανάρει" τα 3 αρχεία μας κ δημιουργεί κάτι σαν πίνακα περιεχόμενων (μέσα στο φάκελο work), τον οποίο κ χρησιμοποιεί η δεύτερη εντολή που ακολουθεί.

Η δεύτερη εντολή κάνει όλη τη σοβαρή δουλειά, κ πέρνει σαν όρισμα το όνομα του "top level entity" που θέλουμε να προσομοιώσουμε (padder_tb στην περίπτωση μας).

Αν όλα πάνε καλά, ένα νέο executable αρχείο με το όνομα "padder_tb" πρέπει να εμφανιστεί δίπλα στα αρχεία VHDL. Τι πιο φυσικό λοιπόν από το να το τρέξουμε:

dimitris@mercury:~/Desktop/cetutorial03$ ./padder_tb 
padder_tb.vhd:43:5:@0ms:(report note):  -> Ripple-carry adder testbench start
padder_tb.vhd:57:5:@100ms:(report note):  --> The 1st operand is: 200
padder_tb.vhd:59:5:@100ms:(report note):  --> The 2nd operand is: 56
padder_tb.vhd:63:5:@150ms:(report note):  --> The sum is: 0
padder_tb.vhd:66:7:@150ms:(report note):  --> Carry = 1
padder_tb.vhd:79:5:@250ms:(report note):  --> The 1st operand is: 23
padder_tb.vhd:81:5:@250ms:(report note):  --> The 2nd operand is: 46
padder_tb.vhd:85:5:@300ms:(report note):  --> The sum is: 69
padder_tb.vhd:90:7:@300ms:(report note):  --> Carry = 0
padder_tb.vhd:95:5:@350ms:(report note):  -> Ripple-carry adder testbench end

K "βουαλά"! Μόλις προσομοιώσατε ένα ψηφιακό αθροιστή 8 θέσεων. Τα μυνήματα που τυπώνει η προσομοίωση είναι εκεί γιατί εγώ τα έβαλα στον κώδικα του testbench μου. Βασικά, αυτό που κάνει το testbench είναι να δώσει 2 ζευγάρια νούμερα για πρόσθεση.

Το πρώτο ζευγάρι είναι το 200+56, το οποίο μας κάνει φυσικά 256. Με 8 bits μπορούμε να δείξουμε τιμές από 0 εώς 255, οπότε το 256 υπολογίζεται ως 0 κ 1 κρατούμενο. Το δεύτερο ζευγάρι είναι το 23+46, το οποίο πολύ σωστά η προσομοίωση το υπολογίζει σε 69 με 0 κρατούμενο.

Τώρα θα μου πείτε (όσοι ακόμα διαβάζετε), μα εγώ αλλιώς τις ξέρω τις προσομοιώσεις (βλ. το πανάκριβο εμπορικό πρόγραμμα modelsim), με γραφικές παραστάσεις, κουμπάκια κλπ κλπ

Καταρχήν να σας πω ότι δεν υπάρχει τίποτα πιο σημαντικό (στο χώρο της VHDL) από το να μάθει κάποιος να γράφει σωστά testbench, ακόμα κ αν το software του δίνει τη δυνατότητα να το παραλήψει σαν βήμα. Κατά δεύτερον, φυσικά κ μπορείτε να δείτε τα αποτελέσματα γραφικά, χωρίς να χρειάζεται να τυπώνετε στο τερματικό (όπως έκανα εγώ) όποια πληροφορία σας ενδιαφέρει.

Η λύση ακούει στο όνομα gtkwave, κ είναι ένα πρόγραμμα που ουδεμία σχέση έχει με προσομοίωση. Στην πατροπαράδοτη κ αγαπημένη τάση του Linux, άλλο είναι το πρόγραμμα για την προσομοίωση (ghdl) κ άλλο για την αναπαράσταση των σημάτων γραφικά (gtkwave).

Αφού λοιπόν βάλετε το πακέτο gtkwave στη διανομή σας, πάτε πάλι στο φάκελο που δουλεύαμε κ τρέξτε άλλη μια φορά το testbench, αυτή τη φορά με μια παράμετρο που του λέει να σώσει όλες τις πληροφορίες σε ένα αρχείο που είναι κατανοητό από το gtkwave:

dimitris@mercury:~/Desktop/cetutorial03$ ./padder_tb --wave=padder_tb.ghw
padder_tb.vhd:43:5:@0ms:(report note):  -> Ripple-carry adder testbench start
padder_tb.vhd:57:5:@100ms:(report note):  --> The 1st operand is: 200
padder_tb.vhd:59:5:@100ms:(report note):  --> The 2nd operand is: 56
padder_tb.vhd:63:5:@150ms:(report note):  --> The sum is: 0
padder_tb.vhd:66:7:@150ms:(report note):  --> Carry = 1
padder_tb.vhd:79:5:@250ms:(report note):  --> The 1st operand is: 23
padder_tb.vhd:81:5:@250ms:(report note):  --> The 2nd operand is: 46
padder_tb.vhd:85:5:@300ms:(report note):  --> The sum is: 69
padder_tb.vhd:90:7:@300ms:(report note):  --> Carry = 0
padder_tb.vhd:95:5:@350ms:(report note):  -> Ripple-carry adder testbench end
dimitris@mercury:~/Desktop/cetutorial03$ ls
fadder.vhd  padder_tb  padder_tb.ghw  padder_tb.vhd  padder.vhd  work

Τέλος τρέξτε το gtkwave δίνοντας του σαν παράμετρο το αρχείο που δημιουργήθηκε:

dimitris@mercury:~/Desktop/cetutorial03$ gtkwave padder_tb.ghw 

στο παράθυρο που θα σας ανοίξει, αριστερά μπορείτε να πλοηγηθείτε στα διάφορα επίπεδα του σχεδίου κ να επιλέξετε τα σήματα που θέλετε (drag n drop στη δεξιά πλευρά).

Τα επιλεγμένα σήματα εμφανίζονται στη δεξιά πλευρά της οθόνης, κ με τα κουμπιά στο πάνω μέρος του παραθύρου μπορείτε μεταξύ άλλων να κάνετε zoom in/zoom out/zoom to fit κλπ. Απλά πράγματα.

Στα σήματα που έχετε επιλέξει μπορείτε να αλλάξετε τις παραμέτρους τους, όπως με τη "βάση" θα γίνεται η αναπαράσταση τους (δυαδικό, δεκαδικό, κλπ). Επίσης, μπορείτε να προσθέσετε κενά, επικεφαλίδες κλπ κλπ στη λίστα με τα επιλεγμένα σήματα.

Αφού φτιάξετε μια διάταξη που σας αρέσει, μπορείτε να τη σώσετε (File->Write Save File) ώστε να μη χρειάζεται κάθε φορά να ξαναστήνετε τα σήματα που θέλετε να δείτε.

Ορίστε κ μια εικόνα από το gtkwave να δείχνει τα βασικά σήματα της προσομοίωσης του αθροιστή μας (με τα σήματα in0, in1 κ sum σε δεκαδική μορφή):

Καλή επιτυχία, κ για απορίες, εδώ είμαστε φυσικά :)

Δώσε αστέρια!

MO: (ψήφοι: 0)