Projetando um Controlador de Servo RC Baseado em FPGA com PWM Preciso
Servos de radiocontrole (RC) são o coração de aeronaves, carros e barcos de hobby. Eles traduzem um comando de rádio remoto em uma posição angular precisa. Como sistemas RC existem há décadas, a interface padrão da indústria é modulação por largura de pulso (PWM) em vez de um protocolo puramente digital. Com o tempo determinístico que um field-programmable gate array (FPGA) oferece, implementar PWM de alta precisão é simples. Este artigo percorre um controlador de servo VHDL genérico que funciona com qualquer servo RC usando PWM, incluindo um testbench de simulação e uma implementação real em uma placa FPGA Lattice iCEstick.
Como o Controle PWM para um Servo RC Funciona
Embora os fundamentos de PWM sejam abordados em outro lugar, servos RC exigem uma abordagem ligeiramente diferente. O servo ignora o ciclo de trabalho completo; ele amostra apenas a duração do período alto de cada pulso. O intervalo padrão entre pulsos é 20 ms, o que corresponde a uma frequência de 50 Hz. Dentro dessa janela de 20 ms, o servo lê um pulso cuja largura tipicamente varia de 1 ms (posição mínima) a 2 ms (posição máxima), embora os valores exatos variem por fabricante.

A figura ilustra a relação de tempo: um período de 20 ms se repete, e o tempo alto de cada pulso mapeia diretamente para a posição angular do servo.
O Controlador de Servo VHDL
Construiremos um módulo VHDL genérico que aceita parâmetros genéricos para frequência de clock, frequência de pulso PWM, larguras mínima e máxima de pulso (em microssegundos) e o número de posições discretas entre os extremos. Como o clock do FPGA roda na faixa de megahertz, aritmética de contador inteiro nos dá resolução suficiente. Para evitar ambiguidades de arredondamento, usaremos a função round do pacote ieee.math_real, que sempre arredonda para longe de zero.
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.math_real.round;
Abaixo está a declaração da entidade para o módulo servo:
entity servo is generic ( clk_hz : real; pulse_hz : real; -- PWM pulse frequency min_pulse_us : real; -- microseconds at minimum position max_pulse_us : real; -- microseconds at maximum position step_count : positive -- Number of discrete positions ); port ( clk : in std_logic; rst : in std_logic; position : in integer range 0 to step_count - 1; pwm : out std_logic ); end servo;
A entrada position aciona o servo do mínimo (0) ao máximo (step_count-1). A saída pwm conecta ao fio de sinal do servo (geralmente amarelo ou branco). A maioria dos FPGAs gera lógica de 3.3 V, então um conversor de nível é necessário para corresponder à entrada de 5 V do servo.
A Região Declarativa
Começamos definindo uma função auxiliar que converte um tempo em microssegundos para o número inteiro mais próximo de ciclos de clock:
function cycles_per_us (us_count : real) return integer is begin return integer(round(clk_hz / 1.0e6 * us_count)); end function;
Usando essa função, calculamos as constantes de tempo principais:
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);
Em seguida, configuramos o contador PWM que roda na frequência de pulso desejada:
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;
Contando Ciclos de Clock
O processo a seguir implementa o contador free-running:
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;
Processo de Saída PWM
A saída PWM é mantida alta por tantos ciclos de contador quanto o valor atual de duty_cycle ditar:
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;
Calculando o Ciclo de Trabalho
Quando em reset, duty_cycle assume como padrão a largura mínima de pulso. Caso contrário, é calculado a partir da entrada position:
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;
Embora cycles_per_step seja arredondado para o inteiro mais próximo, o erro resultante é insignificante porque o clock do FPGA é ordens de magnitude mais rápido que a taxa de pulso de 50 Hz.
O Testbench do Servo
Para verificar o projeto, construímos um testbench simples que percorre manualmente todos os valores de posição e captura a forma de onda. Um clock de teste de 1 MHz acelera a simulação enquanto ainda reflete tempos realistas.
Constantes de Simulação
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;
Sinais do DUT
signal clk : std_logic := '1'; signal rst : std_logic := '1'; signal position : integer range 0 to step_count - 1; signal pwm : std_logic;
Geração de clock:
clk <= not clk after clk_period / 2;
Instanciação do DUT
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 );
Sequenciador do Testbench
O sequenciador reseta o DUT, depois itera por todos os valores de posição, esperando um período de pulso entre mudanças. Após completar a sequência, a simulação termina.
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;
Forma de Onda da Simulação do Servo
O snapshot a seguir mostra a forma de onda do testbench. A saída PWM fica alta apenas pelos primeiros ciclos de contador, correspondendo à largura de pulso esperada para cada valor de posição.

Baixe os arquivos completos do projeto para reproduzir a simulação.
Implementação de Exemplo em FPGA
Em seguida, acionamos um servo TowerPro SG90 real a partir de uma placa Lattice iCEstick. O servo requer nível lógico de 5 V, então usamos um conversor de nível entre o pino do FPGA e o fio de sinal do servo.
Entidade do Módulo Topo
entity top is port ( clk : in std_logic; rst_n : in std_logic; -- Pull-up pwm : out std_logic ); end top;
Sinais e Constantes
Calibramos as larguras de pulso do servo experimentalmente: 0.5 ms a 2.5 ms resulta em uma varredura completa de 180°. As constantes a seguir correspondem ao clock do iCEstick e parâmetros do servo.
constant clk_hz : real := 12.0e6; -- iCEstick clock 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 to 255 constant step_count : positive := 2**step_bits;
O contador free-running tem 25 bits de largura, dando um tempo de wrap de aproximadamente 2.8 segundos no clock de 12 MHz.
constant cnt_bits : integer := 25; signal cnt : unsigned(cnt_bits - 1 downto 0);
Conectando os módulos:
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);
Instanciação do Módulo Servo
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 );
Instanciação do Contador Auto-Wrapping
COUNTER : entity work.counter(rtl) generic map ( counter_bits => cnt_bits ) port map ( clk => clk, rst => rst, count_enable => '1', counter => cnt );
Instanciação da ROM de Seno
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 );
Atribuições concorrentes conectam o contador, ROM e servo:
position <= to_integer(rom_data); rom_addr <= cnt(cnt'left downto cnt'left - step_bits + 1);
Teste na Lattice iCEstick
O circuito completo é montado em protoboard: a saída de 3.3 V do FPGA é convertida para 5 V para o servo. O sinal PWM conecta ao pino 119 no iCEstick. Quando alimentado, o servo oscila suavemente entre 0° e 180°, pausando brevemente nos extremos. Um vídeo da forma de onda PWM no osciloscópio demonstra o tempo correto.

Considerações Finais
Existem muitas maneiras de implementar um controlador de servo PWM em VHDL, mas usar contadores inteiros mantém a lógica sintetizada simples — apenas contadores, registradores e multiplexadores. A armadilha mais comum é overflow silencioso em aritmética de 32 bits, então sempre verifique se cálculos intermediários permanecem dentro dos limites para seus parâmetros escolhidos.
Note que este estilo PWM é adaptado para servos RC. Para aplicações onde o ciclo de trabalho importa mais que a frequência (ex: controle de potência analógico), uma estratégia PWM diferente é preferível.
Leia sobre controle de potência analógico usando PWM aqui:
Como criar um controlador PWM em VHDL
Pronto para testar você mesmo? Baixe o arquivo ZIP contendo o código VHDL completo, testbench ModelSim, projeto iCEcube2 e configuração do programador inserindo seu endereço de e-mail abaixo.
Deixe sua opinião nos comentários!
VHDL
- Controlador de Potência PWM: Construa um Driver de Lâmpada com Modulação de Largura de Pulso
- Implementando um Controlador PWM em VHDL: Projeto, Simulação e Demo em FPGA
- Inicializando Block RAM de FPGA a partir de Arquivos de Texto Usando a Biblioteca TEXTIO do VHDL
- Transmissão de Dados de Temperatura em Tempo Real do ppDAQC Pi-Plate para InitialState com Raspberry Pi Headless
- Monitore a Temperatura da Sua Casa com Dashboard no Raspberry Pi
- Guia Passo a Passo: Extraindo Dados de CLP com IIoT para Insights em Tempo Real
- Protegendo IA Embarcada com Ambiente de Execução Confiável em SoC FPGA Seguro
- Seus Controladores Servo Podem Ser Reparados? Insights de Especialistas e Soluções Econômicas
- Resolvendo Erro 'C Axis Drive Not OK' no Servo Drive Osai-Prima Electro 97942050N
- Controle de Ventoinha PWM 4 Pinos 25 kHz com Arduino Uno – Montagem, Código e Teste