Základní konstrukce ve VHDL

Nedávno jsem se tady zabýval programovatelnými obvody CPLD. K implementaci jednoduchého příkladu jsem zvolil jazyk VHDL. V tomto článku bych se chtěl tímto jazykem zabývat podrobněji. Ukážu některé základní a nejpoužívanější konstrukce a pokusím se vysvětlit základní pojmy používané při programování PLD.

Základem jsou bloky

Je důležité si uvědomit, že zdrojovým kódem napsaným ve VHDL se snažíme popsat chování hardware. Nejedná se o žádný kód, který bude zpracovávat procesor, ale o kód, který popisuje chování digitálního obvodu. Na ten můžeme nahlížet jako na různě pospojované bloky. Každý blok má své vstupy a výstupy a vykonává nějakou předem danou činnost.

Každý projekt, který budeme ve VHDL psát, si tedy rozdělíme na základní bloky. Dejme tomu, že chceme napsat SPI slave. Základem bude blok SPI, který bude mít vstupy SCK, SS a MOSI a jeden výstup MISO. To máme hlavní blok. Ten se dá rozdělit na dva podbloky – posuvný registr a čítač. Ty napíšeme ve VHDL a spojíme je tak, aby fungovaly jako SPI slave.

Tímto způsobem lze rozdělit libovolně složitý projekt na množství jednoduchých bloků, které lze ve VHDL implementovat na několika málo řádcích. Jeden blok ve VHDL vypadá následovně:

entity NÁZEV_BLOKU is
  Port (
    --
    -- Definice vstupů a výstupů
    --
  );
end NÁZEV_BLOKU;

architecture Behavioral of NÁZEV_BLOKU is
  --
  -- Definice proměnných a signálů
  --
begin
  --
  -- Vlastní kód
  --
end Behavioral;

To, jak blok vypadá zvenku, definuje část v entity. Zde jsou všechny vstupy a výstupy, které lze použít. Samotný kód, který definuje chování bloku je v sekci architecture, která je ještě rozdělena na definici proměnných a signálů a na vlastní kód. Tímto definujeme jak se má blok chovat. To, jak bude doopravdy vypadat hardware pak záleží na překladači, který náš kód interpretuje.

Základní datové typy

Mezi základní datové typy, které se používají téměř pořád patří std_logic a std_logic_vector. Prvně jmenovaný typ popisuje jeden bit, zatímco druhý popisuje více bitů. Slovo bit berte s rezervou, použil jsem jej pouze pro představu. Doopravy se jedná o signálovou cestu. Aritmetika nad těmito typy je definována v knihovnách IEEE.STD_LOGIC_ARITH.ALL a IEEE.STD_LOGIC_UNSIGNED.ALL nebo IEEE.STD_LOGIC_SIGNED.ALL.

Uveďme si pár příkladů:

signal A: std_logic := '1';
signal B: std_logic := 'Z';
--
signal C: std_logic_vector(7 downto 0) := X"FE";
signal D: std_logic_vector(3 downto 0) := "01ZZ";
signal E: std_logic_vector(31 downto 0) := (others => '0');

Signály A a B jsou typu std_logic. Zároveň v definici signálu jsme provedli i deklaraci (přiřadili jsme signálům jejich počáteční hodnoty hned během jejich vytvoření). Signál A nabývá hodnoty log. jedničky a signál B je ve stavu vysoké impedance.

Signály C, D a E jsou typu std_logic_vector. Signál C je 8 bitový a jeho počáteční hodnota je nastavena na 0xFE. Signál D je 4 bitový a nejvyšší dva bity jsou nastaveny na 01 a nižší dva bity jsou ve stavu vysoké impedance. Poslední signál, E, je 32 bitů široký a všechny jsou nastaveny na log. nulu.

Tyto definice patří do sekce architecture, před klíčové slovo begin. V samotném kódu se s nimi pracuje následovně:

A <= '0';  -- Přiřazení log. nuly signálu A.
D <= X"F";

if B = '1' then
  -- Spojení dvou signálů.
  C <= A & C(7 downto 1);
else
  C <= C
end if;

E <= (others => 'U'); -- 'U' je nedefinovaný stav.

U signálů A a D provedeme obyčejné přiřazení. U signálu C je spojení dvou signálů pomocí operátoru &. To znamená, že k horním 7 bitům signálu C připojíme signál A. V tomto našem případě jsme provedli bitový posun o jeden bit doleva/doprava. Signálu E jsme přiřadili všechny bity na U, což je nedefinovaný stav (většinou nežádoucí).

Klíčové slovo process

Již v minulém článku jsem upozorňoval na fakt, že všechny řádky se vykonávají okamžitě, bez ohledu na pořadí. To znamená, že tento kus kódu

C <= A + B;
Z <= C;

je funkčně stejný jako

Z <= C;
C <= A + B;

Pokud potřebujeme, aby se určitá část kódu změnila jen někdy za určité podmínky, můžeme použít klíčové slovo process:

process (A) begin
  if A = '1' then
    B <= '1';
  end if;
end process;

Pokud signál A jakkoliv změní svůj stav, vykoná se i obsah celého bloku process. V našem případě se provede přiřazení log. jedničky do signálu B v případě, že signál A nabývá hodnoty log. jedničky.

Často se process používá k implementaci sekvenční logiky, která je řízena hodinovým signálem:

signal (clk) begin
  if rising_edge(clk) then
    counter <= counter + '1';
    if counter = X"FE" then
      overflow <= '1';
    else
      overflow <= '0';
    end if;
  end if;
end signal;

Tímto jsme vytvořili jednoduchý čítač, jehož hodnota se inkrementuje s každou náběžnou hranou hodinového signálu a při přetečení se krátce nastaví signál overflow.

Základní konstrukce

V této části ukážu některé základní nejpoužívanější bloky. Každý kus kódu stručně okomentuji. Nicméně vše by mělo být jasné, půjde o jednoduché příklady a budou obsahovat jen to, co jsem již vysvětlil.

Multiplexor

Multiplexor bude obsahovat 4 vstupy (A, B, C, D), které budou na výstup (Y) vybírány pomocí dvou bitů (sel).

library ieee;
use ieee.std_logic_1164.all;

entity Mux is
  port (
    sel: in std_logic_vector(1 downto 0);
    A, B, C, D: in std_logic;
    Y: out std_logic
  );
end Mux;

architecture behavior of Mux is
  -- Žádné signály nejsou potřeba.
begin
  process (sel, A, B, C, D) begin
    case sel is
      when "00" => Y <= A;
      when "01" => Y <= B;
      when "10" => Y <= C;
      when "11" => Y <= D;
      when others => Y <= A;
    end case;
  end process;
end behavior;

Proces se spustí při každé změně některého z 5 signálů (sel, A, B, C a D). Pro rozhodnutí, který vstup půjde na výstup Y, jsem použil konstrukci case - when. Jedná se o známé switch - case z jiných programovacích jazyků.

Co je zde velmi důležité, je poslední přepínač when others. Ten ošetřuje jakýkoliv jiný stav, který by mohl na vstupu sel nastat. Pokud bychom jej nenapsali, obvod by se buď choval v některých případech nedefinovaně nebo by překladač interpretoval kód mnohem méně efektivně.

Kód je možné podle potřeby rozšířit na multiplexor 8:1 i více.

Čítač

Osmi bitový čítač, jehož aktuální hodnotu lze číst na výstupu. Při přetečení se na výstupu objeví logická jedna po dobu jedné periody hodinového signálu. Čítač jde vynulovat asynchronním signálem reset.

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

entity Counter is
  port (
    rst : in std_logic;
    clk : in std_logic;
    count: out std_logic_vector(7 downto 0),
    overflow: out std_logic,
  );
end Counter;

architecture behavioral of Counter is
  signal temp: std_logic_vector(count'range) := (others => '0');
begin
  process (clk, rst) begin
    if rst = '1' then
      temp <= (others => '0');
    elsif rising_edge(clk) then
      temp <= temp + '1';
      if temp = X"FE" then
        overflow <= '1';
      else
        overflow <= '0';
      end if;
    end if;
  end process;

  count <= temp;
end behavioral;

Zde je jedna věc, o které jsem se ještě nezmiňoval – count'range. Tato konstrukce vytvoří signál temp o stejné šířce jako je count. Klíčové slovo range je tak nahrazeno za 7 downto 0. Další klíčová slova, která lze využít jsou 'high (pozice MSB), 'low (pozice LSB), 'length (celková delka signálu) a 'reverse_range (vrátí to samé jako 'range, ale s opačným pořadím).

Dalším důležitým poznatkem, kterého byste si měli v tomto kódu všimnout, je použití pomocné proměnné temp. Tato proměnná je vlastně identická s výstupem count. Tak proč jsem použil nový signál a ne přimo výstup count? Důvod je prostý, výstupní signály nelze za žádných okolností číst. Slouží pouze jako výstup. Pokud nastane situace, kdy potřebujeme číst stav výstupního signálu, pak je nutné použít pomocnou proměnnou/signál (to platí i pro výstup použitý v podmínce if – nelze).

Dekodér

Dekodér binárního kódu na 1z8. Obecně se ale může jednat o libovolný dekodér, např. na 7 segmentový displej.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL, IEEE.NUMERIC_STD.all;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity Decoder is
  port (
    A : in std_logic_vector (2 downto 0);
    Y : out std_logic_vector(7 downto 0)
  );
end Decoder;

architecture behavioral of Decoder is
begin
  process (A) begin
    case A is
      when "000" => Y <= "00000001";
      when "001" => Y <= "00000010";
      when "010" => Y <= "00000100";
      when "011" => Y <= "00001000";
      when "100" => Y <= "00010000";
      when "101" => Y <= "00100000";
      when "110" => Y <= "01000000";
      when "111" => Y <= "10000000";
      when others => Y <= "00000000";
    end case;
  end process;
end behavioral;

Posuvný registr

Posuvný registr, kterému lze přepnutím vstupu dir změnit směr posunu.

library IEEE;
use IEEE.STD_LOGIC_1164.ALL, IEEE.NUMERIC_STD.all;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity Shift is
  port (
    clk: in std_logic;
    dir: in std_logic;
    input: in std_logic;
    Y : out std_logic_vector(7 downto 0)
  );
end Shift;

architecture behavioral of Shift is
  signal temp: std_logic_vector(Y'range) := (others => '0');
begin
  process (clk) begin
    if rising_edge(clk) then
      if dir = '1' then
        temp <= input & temp(temp'high downto temp'low-1);
      else
        temp <= temp(temp'high-1 downto temp'low) & input;
      end if;
    end if;
  end process;

  Y = temp;
end behavioral;

Tento kód by se dal ještě vylepšit jedním výstupním signálem data_ready, který by signalizoval, že se registr již celý přetočil a nový bajt je nachystaný na výstupu, ale to už byste měli zvládnout sami (chce to jeden signál, který bude čítat počet hodinových cyklů).

Závěr

V článku jsme si ukázali některé základní postupy a konstrukce jazyka VHDL, které se mohou hodit při práci s PLD obvody. Je důlěžité si uvědomit, že vaším kódem popisujete chování reálného hardware. Vy sami ale nemáte možnost jak výsledný hardware ovlivnit, to za vás dělá překladač. Ten může váš kód pochopit dobře a výsledek optimalizovat, ale také nemusí a výsledná struktura bude velmi neefektivní. Hodně záleží na každém řádku i procesu. Více než kde jinde zde platí, že je potřeba dvakrát přemýšlet, než něco napíšete.

Pokud máte k VHDL jakoukoliv otázku, můžete se ptát na fóru qa.uart.cz. Jsem si vědom, že jsem vůbec neprobral jak použít VHDL k simulacím, jak na cykly (for a while, ty jde použit pouze při simulacích, do reálného hardware jsou syntetizovat pouze těžko) a mnoho dalšího. Tímto odkazuji na některou z mnoha učebnic, případně na některý budoucí článek.

Články v sérii<< Úvod do PLD a jazyka VHDLCPLD a první aplikace >>