FPGA-gebaseerde RC Servo-controller ontwerpen met nauwkeurige PWM
Radio-gecontroleerde (RC) servo's zijn het hart van hobby-vliegtuigen, auto's en boten. Ze vertalen een radio-commando naar een nauwkeurige hoekpositie. Omdat RC-systemen al decennialang bestaan, is de industriestandaard-interface pulsbreedtemodulatie (PWM) in plaats van een zuiver digitaal protocol. Met de deterministische timing die een field-programmable gate array (FPGA) biedt, is het implementeren van hoge-precisie PWM eenvoudig. Dit artikel loopt door een generieke VHDL servo-controller die met elke RC servo met PWM werkt, inclusief een simulatie-testbench en een real-world implementatie op een Lattice iCEstick FPGA-board.
Hoe PWM-controle voor een RC servo werkt
Terwijl PWM-fundamenten elders behandeld worden, vereisen RC servo's een iets andere benadering. De servo negeert de volledige duty cycle; het sampleert alleen de duur van de hoge periode van elke puls. Het standaardinterval tussen pulsen is 20 ms, wat overeenkomt met een frequentie van 50 Hz. Binnen dat 20 ms venster leest de servo een puls wiens breedte typisch varieert van 1 ms (minimale positie) tot 2 ms (maximale positie), hoewel de exacte waarden per fabrikant verschillen.

De figuur illustreert de timing-relatie: een 20 ms periode herhaalt zich, en de hoge tijd van elke puls wijst direct de hoekpositie van de servo aan.
De VHDL Servo Controller
We bouwen een generieke VHDL-module die generieke parameters accepteert voor klokfrequentie, PWM-pulsfrequentie, minimum- en maximumpulsbreedtes (in microseconden), en het aantal discrete posities tussen de extreme waarden. Omdat de FPGA-klok in de megahertz-zone draait, geeft integer-telleraritmetiek ons voldoende resolutie. Om afrondingsambiguïteiten te vermijden, gebruiken we de round functie uit het ieee.math_real pakket, die altijd afrondt weg van nul.
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.math_real.round;
Hieronder is de entiteitsdeclaratie voor de servo-module:
entity servo is generic ( clk_hz : real; pulse_hz : real; -- PWM-pulsfrequentie min_pulse_us : real; -- microseconden bij minimale positie max_pulse_us : real; -- microseconden bij maximale positie step_count : positive -- Aantal discrete posities ); port ( clk : in std_logic; rst : in std_logic; position : in integer range 0 to step_count - 1; pwm : out std_logic ); end servo;
De position invoer stuurt de servo van zijn minimum (0) naar maximum (step_count‑1) instelling. De pwm uitgang verbindt met de servo's signaaldraad (meestal geel of wit). Meeste FPGA's leveren 3.3 V logica, dus een level shifter is vereist om de servo's 5 V invoer aan te sturen.
Het Declaratieve Gebied
We beginnen met het definiëren van een helper-functie die een tijd in microseconden omzet naar het dichtstbijzijnde gehele aantal klokcycli:
function cycles_per_us (us_count : real) return integer is begin return integer(round(clk_hz / 1.0e6 * us_count)); end function;
Met die functie berekenen we de kern-timing-constanten:
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);
Vervolgens zetten we de PWM-teller op die draait op de gewenste puls-frequentie:
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;
Klokcycli tellen
Het volgende proces implementeert de vrijlopende teller:
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 Uitgang Proces
De PWM-uitgang wordt hoog gedreven voor zoveel tellercycli als de huidige duty_cycle waarde dicteert:
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;
De Duty Cycle Berekenen
Bij reset gaat duty_cycle standaard naar de minimale pulsbreedte. Anders wordt het berekend uit de position invoer:
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;
Hoewel cycles_per_step afgerond is naar het dichtstbijzijnde integer, is de resulterende fout verwaarloosbaar omdat de FPGA-klok orders van grootte sneller is dan de 50 Hz puls-frequentie.
De Servo Testbench
Om het ontwerp te verifiëren, bouwden we een eenvoudige testbench die handmatig door alle positie-waarden stapt en de waveform vastlegt. Een 1 MHz testklok versnelt de simulatie terwijl realistische timing behouden blijft.
Simulatie Constanten
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 Signalen
signal clk : std_logic := '1'; signal rst : std_logic := '1'; signal position : integer range 0 to step_count - 1; signal pwm : std_logic;
Klokgeneratie:
clk <= not clk after clk_period / 2;
DUT Instantiatie
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
De sequencer reset de DUT, itereert vervolgens door alle positie-waarden, wacht één pulsperiode tussen wijzigingen. Na afloop van de sequentie eindigt de simulatie.
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 Simulatie Waveform
De volgende snapshot toont de testbench-waveform. De PWM-uitgang is hoog alleen voor de vroegste tellercycli, wat overeenkomt met de verwachte pulsbreedte voor elke positie-waarde.

Download de volledige projectbestanden om de simulatie te reproduceren.
Voorbeeld FPGA Implementatie
Vervolgens sturen we een echte TowerPro SG90 servo aan vanaf een Lattice iCEstick board. De servo vereist een 5 V logica-niveau, dus we gebruiken een level shifter tussen de FPGA-pin en de servo's signaaldraad.
Top Module Entiteit
entity top is port ( clk : in std_logic; rst_n : in std_logic; -- Pull-up pwm : out std_logic ); end top;
Signalen en Constanten
We kalibreerden de servo-pulsbreedtes experimenteel: 0.5 ms tot 2.5 ms levert een volledige 180° sweep op. De volgende constanten matchen de iCEstick-klok en servo-parameters.
constant clk_hz : real := 12.0e6; -- iCEstick-klok 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 tot 255 constant step_count : positive := 2**step_bits;
De vrijlopende teller is 25 bits breed, wat een wraptijd van ruwweg 2.8 seconden geeft op de 12 MHz klok.
constant cnt_bits : integer := 25; signal cnt : unsigned(cnt_bits - 1 downto 0);
Modules koppelen:
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 Module Instantiatie
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 );
Self-Wrapping Counter Instantiatie
COUNTER : entity work.counter(rtl) generic map ( counter_bits => cnt_bits ) port map ( clk => clk, rst => rst, count_enable => '1', counter => cnt );
Sine ROM Instantiatie
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 draaien de teller, ROM en servo samen:
position <= to_integer(rom_data); rom_addr <= cnt(cnt'left downto cnt'left - step_bits + 1);
Testen op de Lattice iCEstick
Het gehele circuit is op een breadboard gemonteerd: de FPGA's 3.3 V uitgang wordt ge-level-shift naar 5 V voor de servo. Het PWM-signaal verbindt met pin 119 op de iCEstick. Bij inschakelen oscilleert de servo soepel tussen 0° en 180°, met een korte pauze aan de uitersten. Een video van de PWM-waveform op een oscilloscoop demonstreert de correcte timing.

Eindgedachten
Er zijn veel manieren om een PWM-servocontroller in VHDL te implementeren, maar het gebruik van integer-tellers houdt de gesynthetiseerde logica simpel — alleen tellers, registers en multiplexers. De meest voorkomende valkuil is stille overflow in 32-bit aritmetiek, dus verifieer altijd dat tussentijdse berekeningen binnen grenzen blijven voor je gekozen parameters.
Let op dat deze PWM-stijl gespecialiseerd is voor RC servo's. Voor toepassingen waar de duty cycle belangrijker is dan de frequentie (bijv. analoge stroomsturing), is een andere PWM-strategie te prefereren.
Lees over analoge stroomsturing met PWM hier:
Hoe een PWM-controller in VHDL te maken
Klaar om het zelf te proberen? Download het ZIP-archief met de complete VHDL-code, ModelSim-testbench, iCEcube2-project en programmeerconfiguratie door je e-mailadres hieronder in te voeren.
Laat me weten wat je denkt in de reacties!
VHDL
- Waarom Doppen Niet De Beste Keuze Zijn Voor Machine Voet Positionering – Alternatieven en Praktische Oplossingen
- Kunnen Uw Servo Controllers Gerepareerd Worden? Expert Inzichten & Kosten-Effectieve Oplossingen
- Oplossen van de 'C-as Drive Niet OK' Fout op Osai-Prima Electro Servo Drive 97942050N
- Real-time Temperatuurdata Streamen van een ppDAQC Pi-Plate naar InitialState met een Headless Raspberry Pi
- Je huis temperatuur monitoren met een Raspberry Pi-dashboard
- 25 kHz 4-pins PWM-ventilatorbesturing met Arduino Uno – Bouw, Code & Test
- PWM Vermogenscontroller: Bouw een Pulsbreedte Gemoduleerde Lampstuur
- In‑Cabin AI Beveiligen met een Trusted Execution Environment op een Veilige FPGA SoC
- Stapsgewijze handleiding: PLC-data extraheren met IIoT voor realtime inzichten
- Een PWM-controller implementeren in VHDL: Ontwerp, Simulatie en FPGA-demo
- FPGA Block RAM initialiseren vanuit tekstbestanden met VHDL's TEXTIO-bibliotheek