Um aus einem Drehgebersignal (Quadratursignal) mit den A und B Spuren ein Postitionssignal zu machen, bedient man sich am einfachsten eines Zustandsautomaten, der aus jedem Wechsel des Graycodes einen Zählschritt erzeugt.
Der von Drehgebern und Drehencodern ausgegebene Gray-Code hat den angenehmen Effekt, dass sich pro Zählschritt nur 1 Bit ändert. Damit ergibt sich folgende Zählweise
0 | 00 |
1 | 01 |
2 | 11 |
3 | 10 |
Die Übergänge stellen sich grafisch so dar:
Man sieht, dass die Flanken einer Spur und der zugehörige Pegel der anderen Spur die Zählrichtung und den Zählimpuls liefern. Anhand der entsprechenden Flanken und der Zustände des jeweils anderen Signals könnte man jetzt folgende Auswertung bauen:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity EncoderSim is
Port ( A : in STD_LOGIC;
B : in STD_LOGIC;
Position : out STD_LOGIC_VECTOR (31 downto 0));
end EncoderSim;
architecture Behavioral of EncoderSim is
signal p : integer := 0;
begin
process (A,B) begin
if rising_edge(A) then
if (B='1') then p<=p+1; else p<=p-1; end if;
end if;
if falling_edge(A) then
if (B='0') then p<=p+1; else p<=p-1; end if;
end if;
if rising_edge(B) then
if (A='0') then p<=p+1; else p<=p-1; end if;
end if;
if falling_edge(B) then
if (A='1') then p<=p+1; else p<=p-1; end if;
end if;
end process;
Position <= std_logic_vector(to_signed(p,32));
end Behavioral;
Wie zu erwarten, reagiert diese Beschreibung sofort auf alle Flankenwechsel:
Leider taugt dieser Quadraturdecoder wegen der Flankenauswertungen nur für die Simulation. Denn in einem programmierbaren Baustein gibt es kein Flipflop, das zwei Takteingänge hat, die sowohl auf die steigende wie auch die fallende Flanke reagieren könnten.
Für die Synthese muß deshalb ein Zustandsautomat hergenommen werden, der nach den Pegelwechslen an den A und B Eingängen weiterschaltet und dabei einen Zähler mitzählt. Mit dem globalen FPGA-Takt wird der Zustand ausgewertet und ein Positionszähler entsprechend dem Folgesignal inkrementiert oder dekrementiert.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity Encoder is
Port ( clk : in STD_LOGIC;
A : in STD_LOGIC;
B : in STD_LOGIC;
Position : out STD_LOGIC_VECTOR (31 downto 0));
end Encoder;
architecture Behavioral of Encoder is
type zustaende is (Z00, Z01, Z11, Z10);
signal z : zustaende := Z00;
signal p : integer := 0;
signal i : std_logic_vector(1 downto 0);
signal e : std_logic_vector(1 downto 0);
begin
process begin -- Eintakten der asynchronen Signale
wait until rising_edge(clk);
i <= A & B; -- Zusammenfassen der Eingänge A und B
e <= i;
end process;
process -- Weiterschalten und Zählen
variable cu, cd : std_logic := '0';
begin
wait until rising_edge(clk);
cu := '0'; -- lokale Werte
cd := '0';
case z is
when Z00 => if (e = "01") then z <= Z01; cu := '1';
elsif (e = "10") then z <= Z10; cd := '1';
end if;
when Z01 => if (e = "11") then z <= Z11; cu := '1';
elsif (e = "00") then z <= Z00; cd := '1';
end if;
when Z11 => if (e = "10") then z <= Z10; cu := '1';
elsif (e = "01") then z <= Z01; cd := '1';
end if;
when Z10 => if (e = "00") then z <= Z00; cu := '1';
elsif (e = "11") then z <= Z11; cd := '1';
end if;
end case;
if (cu='1') then p <= p+1;
elsif (cd='1') then p <= p-1;
end if;
end process;
Position <= std_logic_vector(to_signed(p,32)); -- Position ausgeben
end Behavioral;
Diese Beschreibung ist sauber synthetisierbar, und kann dank der eingebauten Synchronisationsstufen direkt an einen Drehgeber angeschlossen werden.
Zum Verständnis der obigen Simulationen hier die Testbench:
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.numeric_std.ALL;
ENTITY tb_Encoder_vhd IS
END tb_Encoder_vhd;
ARCHITECTURE behavior OF tb_Encoder_vhd IS
-- Component Declaration for the Unit Under Test (UUT)
COMPONENT Encoder
PORT(
clk : IN std_logic;
A : IN std_logic;
B : IN std_logic;
Position : OUT std_logic_vector(31 downto 0)
);
END COMPONENT;
--Inputs
SIGNAL clk : std_logic := '0';
SIGNAL A : std_logic := '0';
SIGNAL B : std_logic := '0';
--Outputs
SIGNAL Position : std_logic_vector(31 downto 0);
type steprom is array (0 to 3) of std_logic_vector (1 downto 0);
constant steps : steprom := ("00","01","11","10");
signal counter : integer := 0;
BEGIN
-- Instantiate the Unit Under Test (UUT)
uut: Encoder PORT MAP(
clk => clk,
A => A,
B => B,
Position => Position
);
clk <= not clk after 5 ns;
A <= steps(counter mod 4)(1);
B <= steps(counter mod 4)(0);
tb : PROCESS
BEGIN
for i in 0 to 7 loop
counter <= counter+1;
wait for 33 ns;
end loop;
wait for 31 ns;
assert counter=to_integer(signed(Position)) report "Verzählt..." severity error;
for i in 0 to 4 loop
counter <= counter-1;
wait for 22 ns;
end loop;
wait for 31 ns;
assert counter=to_integer(signed(Position)) report "Verzählt..." severity error;
for i in 0 to 3 loop
counter <= counter+1;
wait for 3 ns;
counter <= counter-1;
wait for 13 ns;
end loop;
wait for 31 ns;
assert counter=to_integer(signed(Position)) report "Verzählt..." severity error;
counter <= counter+1;
wait for 30 ns;
for i in 0 to 3 loop
counter <= counter+1;
wait for 11 ns;
counter <= counter-1;
wait for 3 ns;
end loop;
wait for 31 ns;
assert counter=to_integer(signed(Position)) report "Verzählt..." severity error;
for i in 0 to 10 loop
counter <= counter+1;
wait for 13 ns;
end loop;
wait for 20 ns;
assert counter=to_integer(signed(Position)) report "Verzählt..." severity error;
END PROCESS;
END;
Langfristig muß position also gleich counter sein.
Wer es noch kompakter will, der kann diese Beschreibung verwenden:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity Encoder is
Port ( clk : in STD_LOGIC;
A : in STD_LOGIC;
B : in STD_LOGIC;
Position : out STD_LOGIC_VECTOR (31 downto 0));
end Encoder;
architecture Behavioral of Encoder is
signal p : integer := 1;
signal h : std_logic_vector(1 downto 0);
signal s : std_logic_vector(1 downto 0);
signal e : std_logic_vector(1 downto 0);
begin
h <= A & B; -- Zusammenfassen der Eingänge A und B
process begin -- Eintakten der asynchronen Signale
wait until rising_edge(clk);
s <= h;
e <= s;
end process;
process -- Zählen
variable i : signed(1 downto 0);
variable os : signed(1 downto 0) := "01";
begin
wait until rising_edge(clk);
i := '0' & e(0); -- Umwandeln in Gray-Code
i := i XOR e(1)&e(1); -- i enthält neuen Zustand
i := i-os; -- neuer Zustand - alter Zustand
if (i(0)='1') then -- Bit 0 ist Zählimpuls
os := os+i; -- Zustand merken
if (i(1)='1') then p <= p-1; -- Zählen
else p <= p+1;
end if;
end if;
end process;
Position <= std_logic_vector(to_signed(p,32)); -- Position ausgeben
end Behavioral;
Es wird hier im Spartan 3 eine LUT weniger verbraucht als im oberen Beispiel. Allerdings ist meines Erachtens der Code nicht mehr so leicht nachvollziehbar. Insbesondere, wenn mal ein halbes Jahr vergangen ist