Will man mit FPGAs Motoren ansteuern oder Analogsignale erzeugen, so bietet sich die Verwendung einer PWM-Einheit an. Das ist im einfachsten Fall ein Zähler, der mit dem FPGA-Takt hochgezählt und mit einem Umschaltwert verglichen wird.
Für PWM-Eingangswerte pwmvalue von 0-255 nehme ich einen 8-Bit-Zähler cnt. Dieser Zähler wird mit dem pwmvalue verglichen, und als Ergebnis dieses Vergleichs pwmout entsprechend auf '0' oder '1' gesetzt.
Der Zähler cnt läuft von 0 bis 254. Auf diese Art werden auch die beiden Grenzfälle pwmvalue = 0 und pwmvalue = 255 korrekt abgebildet. Würde cnt bis 255 zählen, wäre ein Dauer-High bei pwmvalue = 255 nicht möglich.
Also gilt:
pwmvalue = 0 ==> Ausgang pwmout dauernd LOW
pwmvalue = 1 ==> Ausgang pwmout 254 Takte LOW, 1 Takt HIGH
pwmvalue = 127 ==> Ausgang pwmout 50% (128 Takte) LOW, 50% (127 Takte) HIGH
pwmvalue = 128 ==> Ausgang pwmout 50% (127 Takte) LOW, 50% (128 Takte) HIGH
pwmvalue = 254 ==> Ausgang pwmout 1 Takt LOW, 254 Takte HIGH
pwmvalue = 255 ==> Ausgang pwmout dauernd HIGH
In dem folgenden (recht übersichtlichen) Code ist dieses Verhalten abgebildet:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity PWM is
Port ( clk : in STD_LOGIC;
pwmvalue : in STD_LOGIC_VECTOR (7 downto 0);
pwmout : out STD_LOGIC := '0');
end PWM;
architecture Behavioral of PWM is
signal cnt : integer range 0 to 254 := 0;
begin
process begin
wait until rising_edge(clk);
-- Zähler
if (cnt<254) then cnt <= cnt+1;
else cnt <= 0;
end if;
end process;
-- Vergleicher
pwmout <= '0' when (cnt>=to_integer(unsigned(pwmvalue))) else '1';
end Behavioral;
Zur direkten Ausgabe auf einen IO-Pin ist es allerdings ratsam, den PWM-Ausgang zu registrieren, damit keine Glitches auftreten können.
process begin
wait until rising_edge(clk);
-- Zähler
if (cnt<254) then cnt <= cnt+1;
else cnt <= 0;
end if;
-- Vergleicher
if (cnt>=to_integer(unsigned(pwmvalue))) then pwmout <= '0';
else pwmout <= '1';
end if;
end process;
Hier die Waveform dazu:
Mit dem oben gezeigten Code hängt die PWM direkt an der Taktfrequenz des FPGAs. In der Praxis ist das zu unflexibel und nur selten so direkt verwendbar.
Will ich eine niederfrequentere PWM, dann ist ein Vorteiler für den PWM-Zähler nötig. Und dann kann das Ganze natürlich auch noch generisch beschrieben werden, damit die Zählerwerte nicht von Hand ausgerechnet werden müssen. Das wird in diesem Code gemacht:
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity PWM is
Generic ( width: natural := 8; -- Breite
fclk : integer := 50000000; -- Taktfrequenz
fpwm : integer := 1000 -- PWM-Frequenz
);
Port ( clk : in std_logic;
pwmvalue : in std_logic_vector (width-1 downto 0);
pwmout : out std_logic
);
end PWM;
architecture Behavioral of PWM is
signal cnt : integer range 0 to 2**width-2 := 0;
signal pre : integer range 0 to fclk/(fpwm*(2**width-2)) := 0;
begin
-- Vorteiler teilt FPGA-Takt auf PWM-Frequenz*Zählschritte
process begin
wait until rising_edge(clk);
if (pre<fclk/(fpwm*(2**width))) then
pre <= pre+1;
else
pre <= 0;
end if;
end process;
-- PWM-Zähler
process begin
wait until rising_edge(clk);
if (pre=0) then
if (cnt<2**width-2) then cnt <= cnt+1;
else cnt <= 0;
end if;
end if;
end process;
-- Vergleicher, registriert für Ausgabe auf IO-Pin ohne Glitches
process begin
wait until rising_edge(clk);
if (cnt>=to_integer(unsigned(pwmvalue))) then pwmout <= '0';
else pwmout <= '1';
end if;
end process;
end Behavioral;
Mit der Testbench unten ergibt sich dann folgende Waveform:
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.numeric_std.ALL;
ENTITY tb_PWM_vhd IS
END tb_PWM_vhd;
ARCHITECTURE behavior OF tb_PWM_vhd IS
-- Component Declaration for the Unit Under Test (UUT)
COMPONENT PWM
PORT(
clk : IN std_logic;
pwmvalue : IN std_logic_vector(7 downto 0);
pwmout : OUT std_logic
);
END COMPONENT;
--Inputs
SIGNAL clk : std_logic := '0';
SIGNAL pwmvalue : std_logic_vector(7 downto 0) := (others=>'0');
--Outputs
SIGNAL pwmout : std_logic;
BEGIN
-- Instantiate the Unit Under Test (UUT)
uut: PWM PORT MAP(
clk => clk,
pwmvalue => pwmvalue,
pwmout => pwmout
);
clk <= not clk after 10 ns; -- 50 MHz
tb : PROCESS BEGIN
pwmvalue <= x"7F";
wait for 10 ms;
pwmvalue <= x"01";
wait for 10 ms;
pwmvalue <= x"FE";
wait for 10 ms;
pwmvalue <= x"00";
wait for 10 ms;
pwmvalue <= x"FF";
wait for 10 ms;
END PROCESS;
END;
Wer eine symmetrische PWM braucht, der braucht statt des Sägezahnzählers erst mal einen Dreieckszähler. Und der Rest ist wieder einfach (bzw. er bleibt gleich):
signal dir : std_logic := '0'; -- 0=up : : -- PWM-Zähler process begin wait until rising_edge(clk); if (pre=0) then if(dir = '0') then -- up if (cnt<2**width-2) then cnt <= cnt+1; else dir <= '1'; cnt <= cnt-1; end if; else -- down if (cnt>0) then cnt <= cnt-1; else dir <= '0'; cnt <= cnt+1; end if; end if; end if; end process;