Manufaturação industrial
Internet das coisas industrial | Materiais industriais | Manutenção e reparo de equipamentos | Programação industrial |
home  MfgRobots >> Manufaturação industrial >  >> Industrial programming >> VHDL

Servo controlador RC usando PWM de um pino FPGA


Servos de modelo controlados por rádio (RC) são atuadores minúsculos normalmente usados ​​em modelos de aviões, carros e barcos amadores. Eles permitem que o operador controle o veículo remotamente por meio de um link de rádio. Como os modelos RC existem há muito tempo, a interface padrão de fato é a modulação por largura de pulso (PWM), em vez de um esquema digital.

Felizmente, é fácil implementar o PWM com o tempo preciso que um FPGA pode exercer em seus pinos de saída. Neste artigo, criaremos um servo controlador genérico que funcionará para qualquer servo RC que use PWM.


Como funciona o controle PWM para um servo RC


Eu já abordei o PWM em um post anterior, mas não podemos usar esse módulo para controlar um servo RC. O problema é que o servo RC não espera que os pulsos PWM cheguem com tanta frequência. Ele não se importa com o ciclo de trabalho completo, apenas com a duração do período de alta.



A ilustração acima mostra como funciona a temporização do sinal PWM.

O intervalo ideal entre os pulsos é de 20 ms, embora sua duração seja menos importante. Os 20 ms se traduzem em uma frequência PWM de 50 Hz. Isso significa que o servo recebe um novo comando de posição a cada 20 ms.

Quando um pulso chega ao servo RC, ele amostra a duração do período alto. O tempo é crucial porque esse intervalo se traduz diretamente em uma posição angular no servo. A maioria dos servos espera ver uma largura de pulso variando entre 1 e 2 ms, mas não há uma regra definida.

O servocontrolador VHDL


Vamos criar um módulo controlador de servo VHDL genérico que você pode configurar para funcionar com qualquer servo RC usando PWM. Para fazer isso, precisamos realizar alguns cálculos com base no valor das entradas genéricas.

As frequências PWM usadas por servos RC são lentas em comparação com as frequências de comutação de megahertz de um FPGA. A contagem de inteiros de ciclos de clock fornece precisão suficiente do comprimento do pulso PWM. No entanto, haverá um pequeno erro de arredondamento, a menos que a frequência do relógio corresponda perfeitamente ao período do pulso.

Faremos os cálculos usando real (ponto flutuante), mas, eventualmente, temos que converter os resultados em números inteiros. Ao contrário da maioria das linguagens de programação, o VHDL arredonda flutua para o inteiro mais próximo, mas o comportamento para meios números (0,5, 1,5 etc.) é indefinido. O simulador ou ferramenta de síntese pode optar por arredondar de qualquer maneira.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.round;

Para garantir a consistência entre as plataformas, usaremos a rodada função do math_real library, que sempre arredonda a partir de 0. O código acima mostra as importações em nosso módulo VHDL com o math_real biblioteca destacada.

Se você precisar do código completo para este projeto, você pode baixá-lo digitando seu endereço de e-mail no formulário abaixo. Em poucos minutos você receberá um arquivo Zip com o código VHDL, o projeto ModelSim e o projeto Lattice iCEcube2 para a placa iCEstick FPGA.




Entidade do módulo servo com genéricos


Usando constantes genéricas, podemos criar um módulo que funcionará para qualquer servo RC habilitado para PWM. O código abaixo mostra a entidade do servo módulo.

A primeira constante é a freqüência de clock do FPGA dada como um tipo real, enquanto pulse_hz especifica com que frequência a saída PWM deve ser pulsada e as duas constantes a seguir definem a largura do pulso em microssegundos nas posições mínima e máxima. A constante genérica final define quantos passos existem entre a posição mínima e máxima, incluindo os pontos finais.
entity servo is
  generic (
    clk_hz : real;
    pulse_hz : real; -- PWM pulse frequency
    min_pulse_us : real; -- uS pulse width at min position
    max_pulse_us : real; -- uS pulse width at max position
    step_count : positive -- Number of steps from min to max
  );
  port (
    clk : in std_logic;
    rst : in std_logic;
    position : in integer range 0 to step_count - 1;
    pwm : out std_logic
  );
end servo;

Além do clock e do reset, a declaração da porta consiste em uma única entrada e um único sinal de saída.

A posição sinal é a entrada de controle para o módulo servo. Se definirmos como zero, o módulo produzirá min_pulse_us pulsos PWM longos de microssegundos. Quando posição estiver no valor mais alto, produzirá max_pulse_us pulsos longos.

O pwm saída é a interface para o servo RC externo. Ele deve passar por um pino FPGA e se conectar à entrada “Sinal” no servo, geralmente o fio amarelo ou branco. Observe que você provavelmente precisará usar um conversor de nível. A maioria dos FPGAs usa nível lógico de 3,3 V, enquanto a maioria dos servos RC roda em 5 V.

A região declarativa


No topo da região declarativa do servo módulo, estou declarando uma função que usaremos para calcular algumas constantes. Os cycles_per_us A função, mostrada abaixo, retorna o número mais próximo de ciclos de clock que precisamos contar para medir us_count microssegundos.
function cycles_per_us (us_count : real) return integer is
begin
  return integer(round(clk_hz / 1.0e6 * us_count));
end function;

Diretamente abaixo da função, declaramos as constantes auxiliares, que usaremos para fazer o sincronismo do PWM de saída de acordo com os genéricos.

Primeiro, traduzimos os valores mínimo e máximo de microssegundos para o número absoluto de ciclos de clock:min_count e max_count . Em seguida, calculamos o intervalo em microssegundos entre os dois, do qual derivamos step_us , a diferença de duração entre cada passo de posição linear. Finalmente, convertemos o microssegundo real valor para um número fixo de períodos de relógio:cycles_per_step .
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, declaramos o contador PWM. Este sinal inteiro é um contador de execução livre que envolve pulse_hz vezes a cada segundo. É assim que alcançamos a frequência PWM dada nos genéricos. O código abaixo mostra como calculamos o número de ciclos de clock que temos que contar e como usamos a constante para declarar o intervalo do inteiro.
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;

Por fim, declaramos uma cópia do contador chamado duty_cycle . Este sinal determinará a duração do período alto na saída PWM.

Contando ciclos de clock


O código abaixo mostra o processo que implementa o contador de execução livre.
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;

Ao contrário de assinado e não assinado tipos que são self-wrapping, precisamos atribuir explicitamente zero quando o contador atingir o valor máximo. Porque já temos o valor máximo definido no counter_max constante, é fácil de conseguir com uma construção If-Else.

Processo de saída PWM


Para determinar se a saída PWM deve ser um valor alto ou baixo, comparamos o contador e duty_cycle sinais. Se o contador for menor que o ciclo de trabalho, a saída será um valor alto. Assim, o valor do duty_cycle sinal controla a duração do pulso PWM.
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;

Cálculo do ciclo de trabalho


O ciclo de trabalho nunca deve ser menor que min_count ciclos de clock porque esse é o valor que corresponde ao min_pulse_us entrada genérica. Portanto, usamos min_count como o valor de redefinição para o duty_cycle sinal, como mostrado abaixo.
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;

Quando o módulo não está em reset, calculamos o ciclo de trabalho em função da posição de entrada. O cycles_per_step constante é uma aproximação, arredondada para o inteiro mais próximo. Portanto, o erro nesta constante pode ser de até 0,5. Quando multiplicamos com a posição comandada, o erro aumentará. No entanto, com o clock do FPGA sendo muito mais rápido que a frequência PWM, não será perceptível.

A bancada de teste do servo


Para testar o servo-módulo RC, criei um testbench de verificação manual que nos permitirá observar o comportamento do servo-módulo na forma de onda. Se você tiver o ModelSim instalado em seu computador, você pode baixar o projeto de simulação de exemplo digitando seu endereço de e-mail no formulário abaixo.




Constantes de simulação


Para acelerar o tempo de simulação, especificaremos uma frequência de clock baixa de 1 MHz no testbench. Também defini a contagem de passos apenas 5, o que deve ser suficiente para vermos o dispositivo em teste (DUT) em ação.

O código abaixo mostra todas as constantes de simulação definidas no testbench.
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


Os sinais declarados no testbench correspondem às entradas e saídas do DUT. Como podemos ver no código abaixo, dei o clk e primeiro sinaliza um valor inicial de '1'. Isso significa que o relógio iniciará na posição alta e que o módulo será reinicializado inicialmente.
signal clk : std_logic := '1';
signal rst : std_logic := '1';
signal position : integer range 0 to step_count - 1;
signal pwm : std_logic;

Para gerar o sinal de clock no testbench, uso o processo regular de uma linha mostrado abaixo.
clk <= not clk after clk_period / 2;

Instanciação do DUT


Abaixo da linha de estímulo do relógio, procedemos à instanciação do DUT. Atribuímos as constantes do testbench aos genéricos com nomes correspondentes. Além disso, mapeamos os sinais de porta do DUT para sinais locais no testbench.
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 de bancada de teste


Para fornecer estímulos para o DUT, usamos o processo sequenciador mostrado abaixo. Primeiro, redefinimos o DUT. Em seguida, usamos um loop For para iterar sobre todas as posições de entrada possíveis (5 no nosso caso). Por fim, imprimimos uma mensagem no console do simulador e encerramos o testbench chamando o VHDL-2008 finish procedimento.
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 de simulação de servo


A forma de onda abaixo mostra parte da forma de onda que o testbench produz no ModelSim. Podemos ver que o testbench está alterando periodicamente a entrada de posição e que o DUT está respondendo produzindo pulsos PWM. Observe que a saída PWM é alta apenas nos valores mais baixos do contador. Esse é o trabalho do nosso processo PWM_PROC.



Se você baixar os arquivos do projeto, poderá reproduzir a simulação seguindo as instruções encontradas no arquivo Zip.

Exemplo de implementação de FPGA


A próxima coisa que quero é implementar o projeto em um FPGA e deixá-lo controlar um servo RC da vida real, o TowerPro SG90. Usaremos a placa de desenvolvimento Lattice iCEstick FPGA para isso. É a mesma placa que estou usando no meu curso de VHDL para iniciantes e no meu curso de FPGA avançado.

Se você possui o Lattice iCEstick, você pode baixar o projeto iCEcube2 usando o formulário abaixo.





No entanto, o servo módulo não pode agir sozinho. Precisamos ter alguns módulos de suporte para isso funcionar em um FPGA. Pelo menos, precisamos de algo para alterar a posição de entrada e também devemos ter um módulo de reset.

Para tornar o movimento do servo mais interessante, vou usar o módulo Sine ROM que abordei em um artigo anterior. Juntamente com o módulo Counter do artigo mencionado anteriormente, a Sine ROM gerará um padrão de movimento suave de um lado para o outro.

Leia sobre o módulo Sine ROM aqui:
Como criar um efeito de LED de respiração usando uma onda senoidal armazenada no bloco de RAM


O fluxograma de dados abaixo mostra os submódulos e como eles estão conectados.


Entidade do módulo principal


A entidade do módulo superior consiste nas entradas de clock e reset e na saída PWM, que controla o servo RC. Encaminhei o pwm sinal para o pino 119 no FPGA Lattice iCE40 HX1K. Esse é o pino mais à esquerda no rack de cabeçalho mais à esquerda. O relógio vem do oscilador on-board do iCEstick, e eu conectei o primeiro sinal para um pino configurado com um resistor pull-up interno.
entity top is
  port (
    clk : in std_logic;
    rst_n : in std_logic; -- Pullup
    pwm : out std_logic
  );
end top; 

Sinais e constantes


Na região declarativa do módulo superior, defini constantes que correspondem ao Lattice iCEstick e ao meu servo TowerPro SG90.

Através da experimentação, descobri que 0,5 ms a 2,5 ms me davam os 180 graus de movimento que eu queria. Várias fontes na internet sugerem outros valores, mas esses são os que funcionaram para mim. Não tenho certeza se estou usando um servo legítimo TowerPro SG90, pode ser uma falsificação.

Se for esse o caso, não foi intencional, pois comprei de um vendedor na Internet, mas pode explicar os diferentes valores do período de pulso. Verifiquei as durações com meu osciloscópio. Eles são o que está escrito no código mostrado abaixo.
constant clk_hz : real := 12.0e6; -- Lattice iCEstick clock
constant pulse_hz : real := 50.0;
constant min_pulse_us : real := 500.0; -- TowerPro SG90 values
constant max_pulse_us : real := 2500.0; -- TowerPro SG90 values
constant step_bits : positive := 8; -- 0 to 255
constant step_count : positive := 2**step_bits;

Eu configurei o cnt sinal para o contador de execução livre ter 25 bits de largura, o que significa que ele será encapsulado em aproximadamente 2,8 segundos quando executado no clock de 12 MHz do iCEstick.
constant cnt_bits : integer := 25;
signal cnt : unsigned(cnt_bits - 1 downto 0);

Por fim, declaramos os sinais que conectarão os módulos de nível superior de acordo com o fluxograma de dados que apresentei anteriormente. Mostrarei como os sinais abaixo interagem mais adiante neste artigo.
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


A instanciação do módulo servo é semelhante a como fizemos no testbench:constante para genérico e sinal local para sinal de porta.
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 de contador de encapsulamento automático


Eu usei o módulo de contador de auto-empacotamento em artigos anteriores. É um contador de execução livre que conta até counter_bits , e então vai para zero novamente. Não há muito a dizer sobre isso, mas se você quiser inspecioná-lo, você pode baixar o projeto de exemplo.
COUNTER : entity work.counter(rtl)
generic map (
  counter_bits => cnt_bits
)
port map (
  clk => clk,
  rst => rst,
  count_enable => '1',
  counter => cnt
);

Instanciação de ROM senoidal


Expliquei o módulo Sine ROM em detalhes em um artigo anterior. Para resumir, ele traduz um valor numérico linear para uma onda senoidal completa com a mesma amplitude mínima/máxima. A entrada é o addr sinal, e os valores de seno aparecem nos dados resultado.
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
);

Usaremos as atribuições simultâneas mostradas abaixo para conectar o módulo Counter, o módulo Sine ROM e o módulo Servo.
position <= to_integer(rom_data);
rom_addr <= cnt(cnt'left downto cnt'left - step_bits + 1);

A entrada de posição do módulo Servo é uma cópia da saída Sine ROM, mas temos que converter o valor sem sinal para um inteiro porque eles são de tipos diferentes. Para a entrada do endereço ROM, usamos os bits superiores do contador de execução livre. Ao fazer isso, o ciclo de movimento da onda senoidal será concluído quando o cnt quebra de sinal, após 2,8 segundos.

Teste no Lattice iCEstick


Eu conectei todo o circuito em uma placa de ensaio, conforme mostrado no esboço abaixo. Como o FPGA usa 3,3 V enquanto o servo opera em 5 V, usei uma fonte de alimentação externa de 5 V e um shifter de nível de breadboard. Sem considerar o conversor de nível, a saída PWM do pino FPGA vai diretamente para o fio “Sinal” no servo TowerPro SG90.



Depois de apertar o botão liga/desliga, o servo deve se mover para frente e para trás em um movimento suave de 180 graus, parando levemente nas posições extremas. O vídeo abaixo mostra minha configuração com o sinal PWM visualizado no osciloscópio.


Considerações finais


Como sempre, há muitas maneiras de implementar um módulo VHDL. Mas eu prefiro a abordagem descrita neste artigo, usando tipos inteiros como contadores. Todos os cálculos pesados ​​acontecem em tempo de compilação, e a lógica resultante é apenas contadores, registradores e multiplexadores.

O perigo mais significativo ao lidar com inteiros de 32 bits em VHDL é que eles transbordam silenciosamente nos cálculos. Você deve verificar se nenhuma subexpressão transbordará para nenhum valor no intervalo de entrada esperado. Nosso módulo servo funcionará para qualquer frequência de clock realista e configurações de servo.

Observe que esse tipo de PWM não é adequado para a maioria das aplicações que não sejam servos RC. Para controle de potência analógico, o ciclo de trabalho é mais importante que a frequência de comutação.

Leia sobre controle de energia analógico usando PWM aqui:
Como criar um controlador PWM em VHDL


Se você quiser experimentar os exemplos por conta própria, pode começar rapidamente baixando o arquivo Zip que preparei para você. Digite seu endereço de e-mail no formulário abaixo e você receberá tudo o que precisa para começar em poucos minutos! O pacote contém o código VHDL completo, o projeto ModelSim com um script de execução, o projeto Lattice iCEcube2 e o arquivo de configuração do programador Lattice Diamond.





Deixe-me saber o que você pensa na seção de comentários!

VHDL

  1. Controlador de energia PWM
  2. Como criar um controlador PWM em VHDL
  3. Como inicializar a RAM do arquivo usando TEXTIO
  4. Transmissão de dados do sensor de uma placa ppDAQC Pi usando InitialState
  5. Monitore a temperatura de sua casa usando o Raspberry Pi
  6. Passo a passo:Como obter dados de um PLC usando IIoT?
  7. Um exemplo de proteção de IA na cabine usando TEE em um SoC FPGA seguro
  8. Seus servo controladores podem ser reparados?
  9. Vida real da fábrica:drive do eixo C não está ok Erro no servo drive
  10. Controle de ventilador PWM de 4 pinos de 25 kHz com Arduino Uno