Manufactura industrial
Internet industrial de las cosas | Materiales industriales | Mantenimiento y reparación de equipos | Programación industrial |
home  MfgRobots >> Manufactura industrial >  >> Industrial programming >> VHDL

Servocontrolador RC usando PWM desde un pin FPGA

Los servos modelo controlados por radio (RC) son actuadores diminutos que se utilizan normalmente en modelos de aviones, automóviles y barcos para aficionados. Permiten al operador controlar el vehículo a través de un enlace de radio de forma remota. Debido a que los modelos RC existen desde hace mucho tiempo, la interfaz estándar de facto es la modulación de ancho de pulso (PWM), en lugar de un esquema digital.

Afortunadamente, es fácil implementar PWM con la sincronización precisa que un FPGA puede ejercer en sus pines de salida. En este artículo, crearemos un servocontrolador genérico que funcionará con cualquier servocontrol remoto que use PWM.

Cómo funciona el control PWM para un servo RC

Ya he cubierto PWM en una publicación de blog anterior, pero no podemos usar ese módulo para controlar un servo RC. El problema es que el servo RC no espera que los pulsos PWM lleguen con tanta frecuencia. No le importa el ciclo de trabajo completo, solo la duración del período alto.

La ilustración anterior muestra cómo funciona la temporización de la señal PWM.

El intervalo ideal entre pulsos es de 20 ms, aunque su duración es de menor importancia. Los 20 ms se traducen en una frecuencia PWM de 50 Hz. Esto significa que el servo recibe un nuevo comando de posición cada 20 ms.

Cuando llega un pulso al servo RC, muestrea la duración del período alto. La sincronización es crucial porque este intervalo se traduce directamente en una posición angular en el servo. La mayoría de los servos esperan ver un ancho de pulso que varía entre 1 y 2 ms, pero no hay una regla establecida.

El servocontrolador VHDL

Crearemos un módulo de controlador de servo VHDL genérico que puede configurar para que funcione con cualquier servo RC usando PWM. Para hacer eso, necesitamos realizar algunos cálculos basados ​​en el valor de las entradas genéricas.

Las frecuencias PWM utilizadas por los servos RC son lentas en comparación con las frecuencias de conmutación de megahercios de un FPGA. El conteo de enteros de los ciclos de reloj da suficiente precisión de la longitud del pulso PWM. Sin embargo, habrá un pequeño error de redondeo a menos que la frecuencia del reloj coincida perfectamente con el período del pulso.

Realizaremos los cálculos usando real (punto flotante) números, pero eventualmente, tenemos que convertir los resultados a números enteros. A diferencia de la mayoría de los lenguajes de programación, los redondeos de VHDL flotan al entero más cercano, pero el comportamiento de los medios números (0.5, 1.5, etc.) no está definido. El simulador o la herramienta de síntesis pueden optar por redondear de cualquier forma.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.round;

Para garantizar la coherencia entre plataformas, utilizaremos la redonda función de math_real biblioteca, que siempre se redondea desde 0. El código anterior muestra las importaciones en nuestro módulo VHDL con el math_real biblioteca resaltada.

Si necesita el código completo para este proyecto, puede descargarlo ingresando su dirección de correo electrónico en el formulario a continuación. En cuestión de minutos, recibirá un archivo Zip con el código VHDL, el proyecto ModelSim y el proyecto Lattice iCEcube2 para la placa iCEstick FPGA.

Entidad de servomódulo con genéricos

Mediante el uso de constantes genéricas, podemos crear un módulo que funcione para cualquier servo RC habilitado para PWM. El siguiente código muestra la entidad del módulo servo.

La primera constante es la frecuencia de reloj de la FPGA dada como un tipo real, mientras que pulse_hz especifica con qué frecuencia se pulsará la salida PWM, y las siguientes dos constantes establecen el ancho de pulso en microsegundos en las posiciones mínima y máxima. La constante genérica final define cuántos pasos hay entre la posición mínima y máxima, incluidos los puntos finales.

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;

Además del reloj y el reinicio, la declaración del puerto consta de una sola entrada y una sola señal de salida.

La posición La señal es la entrada de control al módulo servo. Si lo ponemos a cero, el módulo producirá min_pulse_us Pulsos PWM de microsegundos de duración. Cuando posición está en el valor más alto, producirá max_pulse_us pulsos largos.

El pwm La salida es la interfaz para el servo RC externo. Debe pasar por un pin FPGA y conectarse a la entrada de "Señal" en el servo, generalmente el cable amarillo o blanco. Tenga en cuenta que probablemente necesitará usar un convertidor de nivel. La mayoría de los FPGA utilizan un nivel lógico de 3,3 V, mientras que la mayoría de los servos RC funcionan con 5 V.

La región declarativa

En la parte superior de la región declarativa del módulo servo, declaro una función que usaremos para calcular algunas constantes. Los cycles_per_us La función, que se muestra a continuación, devuelve el número más cercano de ciclos de reloj que necesitamos contar para medir us_count microsegundos.

function cycles_per_us (us_count : real) return integer is
begin
  return integer(round(clk_hz / 1.0e6 * us_count));
end function;

Directamente debajo de la función, declaramos las constantes auxiliares, que usaremos para sincronizar el PWM de salida de acuerdo con los genéricos.

Primero, traducimos los valores mínimo y máximo de microsegundos a un número absoluto de ciclos de reloj:min_count y cantidad_máxima . Luego, calculamos el rango en microsegundos entre los dos, del cual derivamos step_us , la diferencia de duración entre cada paso de posición lineal. Finalmente, convertimos el microsegundo real valor a un número fijo de períodos de reloj: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);

A continuación, declaramos el contador PWM. Esta señal de número entero es un contador de ejecución libre que envuelve pulse_hz veces cada segundo. Así es como logramos la frecuencia PWM dada en los genéricos. El siguiente código muestra cómo calculamos el número de ciclos de reloj que tenemos que contar y cómo usamos la constante para declarar el rango del entero.

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;

Finalmente, declaramos una copia del contador llamado duty_cycle . Esta señal determinará la duración del período alto en la salida PWM.

Contar ciclos de reloj

El siguiente código muestra el proceso que implementa el contador de ejecución libre.

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;

A diferencia de firmado y sin firmar tipos que se envuelven automáticamente, necesitamos asignar cero explícitamente cuando el contador alcanza el valor máximo. Porque ya tenemos el valor máximo definido en el counter_max constante, es fácil de lograr con una construcción If-Else.

Proceso de salida PWM

Para determinar si la salida PWM debe tener un valor alto o bajo, comparamos el contador y duty_cycle señales Si el contador es menor que el ciclo de trabajo, la salida es un valor alto. Por lo tanto, el valor del duty_cycle La señal controla la duración del 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 del ciclo de trabajo

El ciclo de trabajo nunca debe ser inferior a min_count ciclos de reloj porque ese es el valor que corresponde al min_pulse_us entrada genérica. Por lo tanto, usamos min_count como el valor de reinicio para el duty_cycle señal, como se muestra a continuación.

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;

Cuando el módulo no está en reinicio, calculamos el ciclo de trabajo en función de la posición de entrada. Los ciclos_por_paso constante es una aproximación, redondeada al entero más cercano. Por lo tanto, el error en esta constante puede ser de hasta 0,5. Cuando multiplicamos con la posición ordenada, el error aumentará. Sin embargo, dado que el reloj FPGA es mucho más rápido que la frecuencia PWM, no se notará.

El banco de pruebas de servos

Para probar el servomódulo RC, he creado un banco de pruebas de verificación manual que nos permitirá observar el comportamiento del servomódulo en la forma de onda. Si tiene instalado ModelSim en su computadora, puede descargar el proyecto de simulación de ejemplo ingresando su dirección de correo electrónico en el formulario a continuación.

Constantes de simulación

Para acelerar el tiempo de simulación, especificaremos una frecuencia de reloj baja de 1 MHz en el banco de pruebas. También configuré el recuento de pasos en solo 5, lo que debería ser suficiente para que podamos ver el dispositivo bajo prueba (DUT) en acción.

El siguiente código muestra todas las constantes de simulación definidas en el banco de pruebas.

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;

Señales bajo prueba

Las señales declaradas en el banco de pruebas coinciden con las entradas y salidas del dispositivo bajo prueba. Como podemos ver en el código de abajo, he dado el clk y primero señala un valor inicial de '1'. Esto significa que el reloj comenzará en la posición alta y que el módulo estará reiniciado 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 generar la señal del reloj en el banco de pruebas, utilizo el proceso regular de una sola línea que se muestra a continuación.

clk <= not clk after clk_period / 2;

Instanciación de DUT

Debajo de la línea de estímulo del reloj, procedemos a instanciar el DUT. Asignamos las constantes del banco de pruebas a los genéricos con nombres coincidentes. Además, asignamos las señales del puerto del DUT a las señales locales en el banco de pruebas.

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
);

Secuenciador de banco de pruebas

Para proporcionar estímulos para el DUT, usamos el proceso de secuenciador que se muestra a continuación. Primero, reiniciamos el DUT. Luego, usamos un ciclo For para iterar sobre todas las posiciones de entrada posibles (5 en nuestro caso). Finalmente, imprimimos un mensaje en la consola del simulador y finalizamos el banco de pruebas llamando al VHDL-2008 finish procedimiento.

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 simulación de servo

La siguiente forma de onda muestra parte de la forma de onda que produce el banco de pruebas en ModelSim. Podemos ver que el banco de pruebas cambia periódicamente la entrada de posición y que el DUT responde produciendo pulsos PWM. Tenga en cuenta que la salida PWM es alta solo en los valores de contador más bajos. Ese es el trabajo de nuestro proceso PWM_PROC.

Si descarga los archivos del proyecto, debería poder reproducir la simulación siguiendo las instrucciones que se encuentran en el archivo Zip.

Ejemplo de implementación de FPGA

Lo siguiente que quiero es implementar el diseño en un FPGA y dejar que controle un servo RC de la vida real, el TowerPro SG90. Usaremos la placa de desarrollo Lattice iCEstick FPGA para eso. Es la misma placa que uso en mi curso de VHDL para principiantes y en mi curso de FPGA avanzado.

Si tiene el Lattice iCEstick, puede descargar el proyecto iCEcube2 utilizando el siguiente formulario.

Sin embargo, el módulo servo no puede actuar solo. Necesitamos tener algunos módulos de soporte para que esto funcione en un FPGA. Al menos, necesitamos algo para cambiar la posición de entrada y también deberíamos tener un módulo de reinicio.

Para hacer que el movimiento del servo sea más interesante, usaré el módulo Sine ROM que cubrí en un artículo anterior. Junto con el módulo Counter del artículo mencionado anteriormente, Sine ROM generará un patrón de movimiento suave de lado a lado.

Lea sobre el módulo Sine ROM aquí:
Cómo crear un efecto LED de respiración usando una onda sinusoidal almacenada en un bloque de RAM

El siguiente diagrama de flujo de datos muestra los submódulos y cómo están conectados.

Entidad del módulo superior

La entidad del módulo superior consiste en las entradas de reloj y reinicio y la salida PWM, que controla el servo RC. He enrutado el pwm señal al pin 119 en el Lattice iCE40 HX1K FPGA. Ese es el pin más a la izquierda en el bastidor de encabezado más a la izquierda. El reloj proviene del oscilador integrado del iCEstick y conecté el primero señal a un pin configurado con una resistencia pull-up interna.

entity top is
  port (
    clk : in std_logic;
    rst_n : in std_logic; -- Pullup
    pwm : out std_logic
  );
end top; 

Señales y constantes

En la región declarativa del módulo superior, definí constantes que coinciden con Lattice iCEstick y mi servo TowerPro SG90.

A través de la experimentación, descubrí que de 0,5 ms a 2,5 ms me daban los 180 grados de movimiento que quería. Varias fuentes en Internet sugieren otros valores, pero estos son los que me funcionaron. No estoy completamente seguro de estar usando un servo TowerPro SG90 legítimo, podría ser una falsificación.

Si ese es el caso, no fue intencional ya que lo compré a un vendedor de Internet, pero podría explicar los diferentes valores del período de pulso. He verificado las duraciones con mi osciloscopio. Son lo que está escrito en el código que se muestra a continuación.

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;

He configurado el cnt señal para que el contador de ejecución libre tenga 25 bits de ancho, lo que significa que se ajustará en aproximadamente 2,8 segundos cuando se ejecute en el reloj de 12 MHz del iCEstick.

constant cnt_bits : integer := 25;
signal cnt : unsigned(cnt_bits - 1 downto 0);

Finalmente, declaramos las señales que conectarán los módulos de nivel superior de acuerdo con el diagrama de flujo de datos que presenté anteriormente. Mostraré cómo interactúan las señales a continuación más adelante en este artículo.

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);

Instanciación de módulo servo

La creación de instancias del servomódulo es similar a cómo lo hicimos en el banco de pruebas:de constante a genérica y de señal local a señal de puerto.

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
);

Instanciación de contador autoenvolvente

He usado el módulo de contador autoenvolvente en artículos anteriores. Es un contador de ejecución libre que cuenta hasta counter_bits , y luego vuelve a cero. No hay mucho que decir al respecto, pero si desea inspeccionarlo, puede descargar el proyecto de ejemplo.

COUNTER : entity work.counter(rtl)
generic map (
  counter_bits => cnt_bits
)
port map (
  clk => clk,
  rst => rst,
  count_enable => '1',
  counter => cnt
);

Instanciación de ROM sinusoidal

He explicado el módulo Sine ROM en detalle en un artículo anterior. En pocas palabras, traduce un valor de número lineal a una onda sinusoidal completa con la misma amplitud mínima/máxima. La entrada es la dirección señal, y los valores del seno aparecen en los datos salida.

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 las asignaciones simultáneas que se muestran a continuación para conectar el módulo contador, el módulo ROM sinusoidal y el módulo servo.

position <= to_integer(rom_data);
rom_addr <= cnt(cnt'left downto cnt'left - step_bits + 1);

La entrada de posición del módulo Servo es una copia de la salida Sine ROM, pero tenemos que convertir el valor sin signo en un número entero porque son de diferentes tipos. Para la entrada de la dirección de la ROM, usamos los bits superiores del contador de ejecución libre. Al hacer esto, el ciclo de movimiento de onda sinusoidal se completará cuando el cnt la señal termina, después de 2,8 segundos.

Pruebas en el Lattice iCEstick

Conecté todo el circuito en una placa de prueba, como se muestra en el esquema a continuación. Debido a que el FPGA usa 3.3 V mientras que el servo funciona a 5 V, he usado una fuente de alimentación externa de 5 V y un cambiador de nivel que se puede probar. Sin considerar el convertidor de nivel, la salida PWM del pin FPGA va directamente al cable de "Señal" en el servo TowerPro SG90.

Después de accionar el interruptor de encendido, el servo debe moverse hacia adelante y hacia atrás en un movimiento suave de 180 grados, deteniéndose levemente en las posiciones extremas. El siguiente video muestra mi configuración con la señal PWM visualizada en el osciloscopio.

Reflexiones finales

Como siempre, hay muchas formas de implementar un módulo VHDL. Pero prefiero el enfoque descrito en este artículo, usando tipos enteros como contadores. Todos los cálculos pesados ​​ocurren en tiempo de compilación y la lógica resultante es solo contadores, registros y multiplexores.

El peligro más significativo cuando se trata de números enteros de 32 bits en VHDL es que se desbordan silenciosamente en los cálculos. Debe verificar que ninguna subexpresión se desborde para ningún valor en el rango de entrada esperado. Nuestro módulo servo funcionará para cualquier frecuencia de reloj realista y configuración de servo.

Tenga en cuenta que este tipo de PWM no es adecuado para la mayoría de las aplicaciones que no sean servos RC. Para el control de potencia analógico, el ciclo de trabajo es más importante que la frecuencia de conmutación.

Lea sobre el control de potencia analógico usando PWM aquí:
Cómo crear un controlador PWM en VHDL

Si desea probar los ejemplos por su cuenta, puede comenzar rápidamente descargando el archivo Zip que he preparado para usted. ¡Ingrese su dirección de correo electrónico en el siguiente formulario y recibirá todo lo que necesita para comenzar en minutos! El paquete contiene el código VHDL completo, el proyecto ModelSim con un script de ejecución, el proyecto Lattice iCEcube2 y el archivo de configuración del programador Lattice Diamond.

¡Déjame saber lo que piensas en la sección de comentarios!


VHDL

  1. Controlador de potencia PWM
  2. Cómo crear un controlador PWM en VHDL
  3. Cómo inicializar RAM desde un archivo usando TEXTIO
  4. Transmisión de datos del sensor desde una placa ppDAQC Pi utilizando InitialState
  5. Controle la temperatura de su hogar usando su Raspberry Pi
  6. Paso a paso:¿Cómo obtener datos de un PLC usando IIoT?
  7. Un ejemplo de protección de la IA en cabina mediante TEE en un SoC FPGA seguro
  8. Alternativas al uso de clavijas en los pies de la máquina
  9. ¿Se pueden reparar sus servocontroladores?
  10. La vida real de la planta:el variador del eje C no funciona Error en el servodrive
  11. Control de ventilador PWM de 4 pines y 25 kHz con Arduino Uno