FPGA-basierten RC-Servo-Controller mit präzisem PWM entwerfen
Ferngesteuerte (RC) Servos sind das Herzstück von Hobby-Flugzeugen, -Autos und -Booten. Sie übersetzen einen Fernfunkbefehl in eine präzise Winkelposition. Da RC-Systeme seit Jahrzehnten existieren, ist der Industriestandard Pulsweitenmodulation (PWM) statt eines rein digitalen Protokolls. Mit dem deterministischen Timing, das ein Field-Programmable Gate Array (FPGA) bietet, ist die Implementierung hochpräziser PWM unkompliziert. Dieser Artikel führt durch einen generischen VHDL-Servo-Controller, der mit jedem RC-Servo per PWM funktioniert, inklusive Simulations-Testbench und Realwelt-Implementierung auf einem Lattice iCEstick FPGA-Board.
Wie PWM-Steuerung für einen RC-Servo funktioniert
Während PWM-Grundlagen anderswo behandelt werden, erfordern RC-Servos einen leicht anderen Ansatz. Der Servo ignoriert den vollen Duty Cycle; er wertet nur die Dauer der High-Phase jedes Pulses aus. Das Standardintervall zwischen Pulsen beträgt 20 ms, was einer Frequenz von 50 Hz entspricht. Innerhalb dieses 20-ms-Fensters liest der Servo einen Puls, dessen Breite typischerweise von 1 ms (Minimalposition) bis 2 ms (Maximalposition) reicht, wobei die genauen Werte je nach Hersteller variieren.

Die Abbildung veranschaulicht die Timing-Beziehung: Eine 20-ms-Periode wiederholt sich, und die High-Time jedes Pulses bildet direkt die Winkelposition des Servos ab.
Der VHDL-Servo-Controller
Wir bauen ein generisches VHDL-Modul, das Generics für Taktfrequenz, PWM-Pulsfrequenz, Minimal- und Maximalpulsbreite (in Mikrosekunden) und die Anzahl der diskreten Positionen zwischen den Extremen akzeptiert. Da der FPGA-Takt im Megahertz-Bereich läuft, liefert Integer-Zählerarithmetik ausreichende Auflösung. Um Rundungsmehrdeutigkeiten zu vermeiden, nutzen wir die round-Funktion aus dem ieee.math_real-Paket, die immer von Null weg rundet.
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.math_real.round;
Im Folgenden die Entity-Deklaration für das Servo-Modul:
entity servo is generic ( clk_hz : real; pulse_hz : real; -- PWM-Pulsfrequenz min_pulse_us : real; -- Mikrosekunden bei Minimalposition max_pulse_us : real; -- Mikrosekunden bei Maximalposition step_count : positive -- Anzahl diskreter Positionen ); port ( clk : in std_logic; rst : in std_logic; position : in integer range 0 to step_count - 1; pwm : out std_logic ); end servo;
Der position-Eingang steuert den Servo von Minimum (0) bis Maximum (step_count-1). Der pwm-Ausgang verbindet sich mit dem Signal-Draht des Servos (meist gelb oder weiß). Die meisten FPGAs geben 3,3-V-Logik aus, daher ist ein Level-Shifter erforderlich, um den 5-V-Eingang des Servos anzupassen.
Der deklarative Bereich
Wir beginnen mit einer Hilfsfunktion, die eine Zeit in Mikrosekunden in die nächstgelegene ganzzahlige Anzahl Takzyklen umrechnet:
function cycles_per_us (us_count : real) return integer is begin return integer(round(clk_hz / 1.0e6 * us_count)); end function;
Mit dieser Funktion berechnen wir die Kern-Timing-Konstanten:
constant min_count : integer := cycles_per_us(min_pulse_us); constant max_count : integer := cycles_per_us(max_pulse_us); constant min_max_range_us : real := max_pulse_us - min_pulse_us; constant step_us : real := min_max_range_us / real(step_count - 1); constant cycles_per_step : positive := cycles_per_us(step_us);
Als Nächstes richten wir den PWM-Zähler ein, der mit der gewünschten Pulsfrequenz läuft:
constant counter_max : integer := integer(round(clk_hz / pulse_hz)) - 1; signal counter : integer range 0 to counter_max; signal duty_cycle : integer range 0 to max_count;
Takzyklen zählen
Der folgende Prozess implementiert den freilaufenden Zähler:
COUNTER_PROC : process(clk) begin if rising_edge(clk) then if rst = '1' then counter <= 0; else if counter < counter_max then counter <= counter + 1; else counter <= 0; end if; end if; end if; end process;
PWM-Ausgabe-Prozess
Der PWM-Ausgang wird für so viele Zählerzyklen high getrieben, wie der aktuelle duty_cycle-Wert vorgibt:
PWM_PROC : process(clk) begin if rising_edge(clk) then if rst = '1' then pwm <= '0'; else pwm <= '0'; if counter < duty_cycle then pwm <= '1'; end if; end if; end if; end process;
Duty Cycle berechnen
Bei Reset nimmt duty_cycle standardmäßig die Minimalpulsbreite an. Ansonsten wird er aus dem position-Eingang berechnet:
DUTY_CYCLE_PROC : process(clk) begin if rising_edge(clk) then if rst = '1' then duty_cycle <= min_count; else duty_cycle <= position * cycles_per_step + min_count; end if; end if; end process;
Obwohl cycles_per_step auf das nächste Integer gerundet wird, ist der resultierende Fehler vernachlässigbar, da der FPGA-Takt um Größenordnungen schneller ist als die 50-Hz-Pulsrate.
Der Servo-Testbench
Zur Verifikation des Designs haben wir einen einfachen Testbench gebaut, der manuell durch alle Positions-Werte schreitet und das Wellenform aufzeichnet. Ein 1-MHz-Testtakt beschleunigt die Simulation bei realistischem Timing.
Simulationskonstanten
constant clk_hz : real := 1.0e6; constant clk_period : time := 1 sec / clk_hz; constant pulse_hz : real := 50.0; constant pulse_period : time := 1 sec / pulse_hz; constant min_pulse_us : real := 1000.0; constant max_pulse_us : real := 2000.0; constant step_count : positive := 5;
DUT-Signale
signal clk : std_logic := '1'; signal rst : std_logic := '1'; signal position : integer range 0 to step_count - 1; signal pwm : std_logic;
Takterzeugung:
clk <= not clk after clk_period / 2;
DUT-Instanziierung
DUT : entity work.servo(rtl) generic map ( clk_hz => clk_hz, pulse_hz => pulse_hz, min_pulse_us => min_pulse_us, max_pulse_us => max_pulse_us, step_count => step_count ) port map ( clk => clk, rst => rst, position => position, pwm => pwm );
Testbench-Sequencer
Der Sequencer setzt das DUT zurück, iteriert dann durch alle Positions-Werte und wartet eine Pulperiode zwischen den Änderungen. Nach Abschluss der Sequenz endet die Simulation.
SEQUENCER : process begin wait for 10 * clk_period; rst <= '0'; wait for pulse_period; for i in 0 to step_count - 1 loop position <= i; wait for pulse_period; end loop; report "Simulation done. Check waveform."; finish; end process;
Servo-Simulations-Wellenform
Der folgende Snapshot zeigt die Testbench-Wellenform. Der PWM-Ausgang ist nur für die frühesten Zählerzyklen high, passend zur erwarteten Pulsbreite für jeden Positions-Wert.

Laden Sie die vollständigen Projektdateien herunter, um die Simulation nachzuvollziehen.
Beispiel-FPGA-Implementierung
Als Nächstes steuern wir einen echten TowerPro SG90 Servo von einem Lattice iCEstick Board. Der Servo benötigt 5-V-Logikpegel, daher nutzen wir einen Level-Shifter zwischen FPGA-Pin und Servo-Signaldraht.
Top-Modul Entity
entity top is port ( clk : in std_logic; rst_n : in std_logic; -- Pull-up pwm : out std_logic ); end top;
Signale und Konstanten
Wir haben die Servo-Pulsbreiten experimentell kalibriert: 0,5 ms bis 2,5 ms ergeben einen vollen 180°-Sweep. Die folgenden Konstanten passen zum iCEstick-Takt und den Servo-Parametern.
constant clk_hz : real := 12.0e6; -- iCEstick-Takt constant pulse_hz : real := 50.0; constant min_pulse_us : real := 500.0; -- 0,5 ms constant max_pulse_us : real := 2500.0; -- 2,5 ms constant step_bits : positive := 8; -- 0 bis 255 constant step_count : positive := 2**step_bits;
Der freilaufende Zähler ist 25 Bit breit, was bei 12 MHz Takt eine Wrap-Zeit von ca. 2,8 Sekunden ergibt.
constant cnt_bits : integer := 25; signal cnt : unsigned(cnt_bits - 1 downto 0);
Module verbinden:
signal rst : std_logic; signal position : integer range 0 to step_count - 1; signal rom_addr : unsigned(step_bits - 1 downto 0); signal rom_data : unsigned(step_bits - 1 downto 0);
Servo-Modul-Instanziierung
SERVO : entity work.servo(rtl) generic map ( clk_hz => clk_hz, pulse_hz => pulse_hz, min_pulse_us => min_pulse_us, max_pulse_us => max_pulse_us, step_count => step_count ) port map ( clk => clk, rst => rst, position => position, pwm => pwm );
Selbst-Wrap-Zähler-Instanziierung
COUNTER : entity work.counter(rtl) generic map ( counter_bits => cnt_bits ) port map ( clk => clk, rst => rst, count_enable => '1', counter => cnt );
Sine-ROM-Instanziierung
SINE_ROM : entity work.sine_rom(rtl) generic map ( data_bits => step_bits, addr_bits => step_bits ) port map ( clk => clk, addr => rom_addr, data => rom_data );
Concurrent Assignments verdrahten Zähler, ROM und Servo zusammen:
position <= to_integer(rom_data); rom_addr <= cnt(cnt'left downto cnt'left - step_bits + 1);
Test auf dem Lattice iCEstick
Die gesamte Schaltung ist auf einem Breadboard aufgebaut: Der 3,3-V-Ausgang des FPGA wird für den Servo auf 5 V gelevel-shiftet. Das PWM-Signal verbindet sich mit Pin 119 am iCEstick. Bei Stromversorgung oszilliert der Servo sanft zwischen 0° und 180°, mit kurzer Pause an den Extremen. Ein Video des PWM-Wellenforms am Oszilloskop demonstriert das korrekte Timing.

Abschließende Gedanken
Es gibt viele Wege, einen PWM-Servo-Controller in VHDL zu implementieren, aber die Nutzung von Integer-Zählern hält die synthetisierte Logik simpel – nur Zähler, Register und Multiplexer. Die häufigste Falle ist stiller Überlauf in 32-Bit-Arithmetik; prüfen Sie daher stets, dass Zwischenergebnisse für Ihre gewählten Parameter im Bereich bleiben.
Beachten Sie, dass dieser PWM-Stil auf RC-Servos zugeschnitten ist. Für Anwendungen, bei denen der Duty Cycle wichtiger ist als die Frequenz (z. B. analoge Leistungssteuerung), ist eine andere PWM-Strategie vorzuziehen.
Mehr über analoge Leistungssteuerung mit PWM hier:
PWM-Controller in VHDL erstellen
Bereit, es selbst auszuprobieren? Laden Sie das ZIP-Archiv mit dem vollständigen VHDL-Code, ModelSim-Testbench, iCEcube2-Projekt und Programmierkonfiguration herunter, indem Sie Ihre E-Mail-Adresse unten eingeben.
Lassen Sie mich Ihre Gedanken in den Kommentaren wissen!
VHDL
- PWM-Leistungssteller: Pulsweitenmodulierter Lampentreiber bauen
- PWM-Controller in VHDL implementieren: Design, Simulation & FPGA-Demo
- FPGA Block RAM aus Textdateien mit VHDLs TEXTIO-Bibliothek initialisieren
- Echtzeit-Temperaturdaten vom ppDAQC Pi‑Plate zu InitialState streamen – Headless Raspberry Pi
- Heimtemperatur mit Raspberry Pi Dashboard überwachen
- Schritt-für-Schritt-Anleitung: PLC-Daten mit IIoT extrahieren für Echtzeit-Einblicke
- In-Cabin-KI mit Trusted Execution Environment auf sicherem FPGA SoC absichern
- Können Ihre Servo-Controller repariert werden? Experten-Einblicke & kosteneffektive Lösungen
- Osai-Prima Electro Servo Drive 97942050N: „C Axis drive not ok“ Fehler behoben
- 25-kHz-4-Pin-PWM-Lüftersteuerung mit Arduino Uno – Bau, Code & Test