Generación de tren de pulsos de frecuencia variable en un FPGA

Estoy trabajando en generar un tren de pulsos para controlar un motor que acepta un tren de pulsos como entrada. Cada pulso corresponde a un incremento de movimiento preestablecido; Puedo configurar un pulso igual a 1/1000 grados (o lo que sea), y si le envío 20 pulsos, el motor se moverá 20/1000 grados.

El software que realiza todo el trabajo pesado y determina a dónde debe ordenarse que vaya el motor en un momento dado está programado en LabVIEW. Luego, este programa envía comandos de posición y velocidad (como números enteros de 32 bits) a un FPGA, que me gustaría usar para generar una serie de pulsos para decirle al motor qué tan lejos y qué tan rápido debe moverse. Tengo un generador de pulsos simple que solo emite la cantidad requerida de pulsos a la velocidad del reloj de la FPGA (vea el diagrama a continuación). ¿Cómo puedo controlar la velocidad de estos pulsos en mi FPGA?

Estoy usando un Altera FPGA programado en Quartus II v9.0.

esquemático

simular este circuito : esquema creado con CircuitLab

Tenga en cuenta el terminal inversor a = b?en el comparador. El FPGA luego generará los valores de pulsey signle dirá a mi motor qué tan lejos debe girar y en qué dirección. Las entradas al FPGA son el número entero de pulsos que queremos generar, ref[31..00]y un indicador de escritura booleano, writeF. Múltiples motores son controlados por un programa, de ahí la necesidad de especificar cuándo los datos en el bus ref[31..00]son para un motor en particular. El bit más significativo del valor de referencia controlará la dirección del movimiento, por lo que err31se utiliza como entrada al updownterminal.

Como puede ver, el contador cuenta la cantidad de pulsos generados, usándolos pulsecomo su entrada de reloj, pero pulsesolo se genera a la velocidad de reloj de la FPGA. Dada una entrada adicional a mi FPGA para controlar la frecuencia del pulso, ¿puedo hacer que la frecuencia del pulso sea variable?

EDITAR: cambié mi circuito para que el reloj del sistema ingrese a la clockentrada de mi contador, y mi pulsesalida se use como la señal de habilitación del reloj ( clock_en) para este contador. Anteriormente, tenía mi pulsesalida conectada directamente a mi clockentrada, lo que es potencialmente malo. Publicaré mis hallazgos aquí cuando haya implementado las sugerencias.

Solución de contador variable VHDL

Estoy tratando de implementar el enfoque de David Kessner usando VHDL. Básicamente, estoy creando un contador que puede incrementarse en números distintos de 1, y estoy usando el rollover de este contador para determinar cuándo debo generar un pulso. El código se ve así hasta ahora:

--****************************************************************************
-- Load required libraries
--****************************************************************************
library ieee;
    use ieee.std_logic_1164.all;
    use ieee.numeric_std.all;

--****************************************************************************
-- Define the inputs, outputs, and parameters
--****************************************************************************
entity var_count is

    generic(N: integer :=32);               -- for generic counter size
    port(
            inc_i       : in    std_logic_vector(N-1 downto 0);
            load_i      : in    std_logic;
            clk_i       : in    std_logic;
            clear_i     : in    std_logic;
            clk_en_i    : in    std_logic;
            count_en_i  : in    std_logic;
            msb_o       : out   std_logic
        );

end var_count;

--****************************************************************************
-- Define the behavior of the counter
--****************************************************************************
architecture behavior of var_count is

    -- Define our count variable. No need to initialize in VHDL.
    signal count : unsigned(N-1 downto 0) := to_unsigned(0, N);
    signal incr  : unsigned(N-1 downto 0) := to_unsigned(0, N);

begin   
    -- Define our clock process
    clk_proc : process(clk_i, clear_i, load_i)
    begin
        -- Asynchronous clear
        if clear_i = '1' then
            count <= to_unsigned(0, N);
        end if;

        -- Asynchronous load
        if load_i = '1' then
            incr <= unsigned(inc_i);
        end if;

        -- Define processes synch'd with clock.
        if rising_edge(clk_i) and clk_en_i = '1' then
            if count_en_i = '1' then            -- increment the counter
                -- count <= count + unsigned(inc_i);
                count <= count + incr;
            end if;
        end if;     
    end process clk_proc;

    -- Output the MSB for the sake of generating a nice easy square wave.
    msb_o <= count(count'left);

end behavior;

Tengo la intención de generar el MSB directamente, o tomar el MSB de este contador ( msb_o(k)), pasarlo a través de un flip flop DQ de un solo bit para que también tenga msb_o(k-1), y generar un pulso cada vez que mi contador variable se da la vuelta al ejecutar :

PULSE = ~msb_o(k) * msb_o(k-1)

donde ~denota lógico NOTy *denota lógico AND. Este es el primer programa VHDL que he escrito, y lo escribí principalmente usando this , this y this . ¿Alguien tiene alguna recomendación sobre cómo podría mejorar mi código? Desafortunadamente, todavía no obtengo pulsos de mi FPGA.

EDITAR: Se actualizó el código VHDL a la implementación actual (2013-08-12). También agregando este libro gratuito a la lista de referencias.

EDIT 2: actualicé mi código a la versión de trabajo (final).

Respuestas (4)

Lo que quiere hacer se llama "Oscilador" controlado numéricamente, o NCO. Funciona así...

Cree un contador que pueda incrementarse en valores distintos de 1. Las entradas a este contador son el reloj maestro y un valor para contar (din). Para cada flanco de reloj, cuenta <= cuenta + din. El número de bits en din es el mismo que el número de bits en el contador. El valor de conteo real se puede usar para muchas cosas útiles, pero lo que quiere hacer es muy simple.

Desea detectar cada vez que el contador da la vuelta y enviar un pulso a su motor cuando eso suceda. Haga esto tomando el bit más significativo del contador y pasándolo por un solo flip-flop para retrasarlo un reloj. Ahora tiene dos señales que llamaré MSB y MSB_Previous. Sabrá si el contador se ha dado la vuelta porque MSB=0 y MSB_Prev=1. Cuando esa condición sea verdadera, envíe un pulso al motor.

Para establecer la frecuencia del pulso, la fórmula es esta: pulse_rate = main_clk_freq * inc_value/2^n_bits

Donde inc_value es el valor por el que se incrementa el contador y n_bits es el número de bits en el contador.

Una cosa importante a tener en cuenta es que agregar bits al contador no cambia el rango de la frecuencia de salida, que siempre es de 0 Hz a la mitad de main_clk_freq. Pero sí cambia la precisión con la que puede generar la frecuencia deseada. Es muy probable que no necesite 32 bits para este contador, y que tal vez solo 10 a 16 bits sean suficientes.

Este método de generar pulsos es bueno porque es súper fácil, la lógica es pequeña y rápida, y a menudo puede generar frecuencias con mayor precisión y flexibilidad que el tipo de diseño de contador+comparador que tiene en su pregunta.

La razón por la que la lógica es más pequeña no es solo porque puede funcionar con un contador más pequeño, sino que no tiene que comparar la salida completa del contador. Solo necesitas la parte superior. Además, comparar dos números grandes en un FPGA generalmente requiere muchas LUT. Comparar dos números de 32 bits requeriría 21 LUT de 4 entradas y 3 niveles lógicos, mientras que el diseño NCO requiere 1 LUT, 2 flip-flops y solo 1 nivel lógico. (Estoy ignorando el contador, ya que es básicamente el mismo para ambos diseños). El enfoque NCO es mucho más pequeño, mucho más rápido, mucho más simple y produce mejores resultados.

Actualización: un enfoque alternativo para hacer el detector de vuelcos es simplemente enviar el MSB del contador al motor. Si hace esto, la señal que va al motor siempre será un ciclo de trabajo 50/50. Elegir el mejor enfoque depende de qué tipo de pulso necesita su motor.

Actualización: aquí hay un fragmento de código VHDL para hacer el NCO.

signal count :std_logic_vector (15 downto 0) := (others=>'0);
signal inc   :std_logic_vector (15 downto 0) := (others=>'0);
signal pulse :std_logic := '0';

. . .

process (clk)
begin
  if rising_edge(clk) then
    count <= count + inc;
  end if;
end process;

pulse <= count(count'high);
Esto tiene mucho sentido y definitivamente suena más fácil que el enfoque del destornillador de 42 piezas que he estado tratando de implementar. Veré si puedo poner esto en marcha y publicarlo aquí con lo que encuentre. ¡Gracias! Y gracias por el nivel de detalle que diste en tu respuesta.
Dejando al descubierto una función integrada que puede contar en incrementos distintos de uno, que no creo que tenga en Quartus II v9.0, ¿cómo haría para construir uno de estos en código FPGA? No estoy muy familiarizado con el código de línea real que se usa para trabajar con FPGA (¿VHDL o lo que sea?).
@engineero, necesitas aprender VHDL o Verilog. De lo contrario, siempre se sentirá frustrado al desarrollar FPGA con esquemas. Escribir el código que describí anteriormente lleva, literalmente, 3 minutos. 5 si tienes cuidado. Pero para la entrada esquemática, use un sumador de n bits y un d-flip-flop de n bits.
¡Gracias! He escrito un código VHDL para hacer esto (creo) pero no parece que se revierta automáticamente. Cuando emito el MSB, simplemente sube y permanece alto. Puse un caso en el código (actualizándolo justo después de enviar esto) para que se reinicie cuando el conteo llegue a 2^N - inc, pero obtengo una especie de frecuencia aleatoria y ondas cuadradas del ciclo de trabajo, lo que no tiene sentido para mí. ¡Cualquier otra sugerencia sería muy apreciada!
@Engineero No tengo idea de por qué su código no se reinvierte automáticamente. En realidad, debe escribir el código específicamente para que NO se desplace. Actualizo mi respuesta para mostrar un código de ejemplo (¡mira, es súper simple!).
Bien, ahora aprendí a codificar mis componentes en VHDL y estoy configurando un banco de pruebas para verificar que todos funcionen correctamente por separado (ya terminé con esa parte) y juntos (aún trabajando en ello). Parece que sus sugerencias han logrado que la contraparte variable del circuito funcione perfectamente. ¡Muchas gracias por tu ayuda!

Supongo que este es aproximadamente el comportamiento funcional que desea implementar:

procedure generate_pulse_train( signal write : in std_logic; 
                                signal pulse : out std_logic;
                                constant t_pulse : time; 
                                signal n_pulses : in natural) is
begin
     wait until write = '1';

     for n in 0 to n_pulses-1 loop
         pulse <= '1';
         wait for t_pulse/2;
         pulse <= '0';
         wait for t_pulse/2;
     end loop;
end procedure;

A menos que tenga requisitos muy estrictos para la temporización del reloj de pulso, recomendaría una máquina de estado simple para generar el tren de pulso en lugar de activar el reloj. Para alterar la frecuencia del reloj durante el tiempo de ejecución, tendría que usar un PLL configurable, pero si la frecuencia del reloj de pulso es igual o menor que el reloj del sistema, puede usar eso para generar el reloj de pulso.

Un ejemplo de una máquina de estado de este tipo podría ser, por ejemplo:

pr_fsm : process(clk) is
begin
if rising_edge(clk) then
   case state is
       when IDLE =>
           scaler <= pulse_period/2 ;
           pulse_counter <= n_pulses;
           pulse  <= '0';

           if writeF = '1' then
               state <= ACTIVE;
           end if; 

       when ACTIVE =>
           if scaler = 0 then
               scaler <= pulse_length;

               pulse <= not pulse;

               if pulse_counter = 0 then
                   state <= IDLE;
               else
                   pulse_counter <= pulse_counter - 1;
               end if;

           else
               scaler <= scaler - 1;
           end if;
   end case;

end if;
end process pr_fsm;
Gracias. No estoy familiarizado con las máquinas de estado en FPGA, aunque he programado máquinas de estado en otros idiomas. ¿Podría dar más detalles sobre cómo usaría una máquina de estado para hacer esto? Además, para su código, ¿t_pulse y n_pulses serían simplemente números enteros (número de ciclos de reloj y número de pulsos respectivamente)?
Gracias por la respuesta detallada. El único problema es que necesito poder ajustar la frecuencia del pulso sobre la marcha, lo que puedo hacer usando un contador con incremento variable y generando el MSB del conteo. ¡Realmente aprecio tu ayuda!

Has bloqueado tu reloj, lo cual es algo muy malo a menos que sepas lo que estás haciendo. La misma señal de reloj debe ir a todas las entradas de reloj de sus circuitos.

Debe usar su señal de pulso como un reloj habilitado para su contador; de esa manera, todo está sincronizado y su contador solo cuenta cuando el pulso es alto.

Su generador de impulsos puede funcionar a otras velocidades, por debajo de la velocidad del reloj principal, si tiene otro contador que genera un solo impulso de reloj cada vez que se da la vuelta. Al aumentar el recuento de terminales de este contador, obtiene más tiempo entre pulsos y movimientos más lentos de su motor.

Ooo, eso suena como algo que podría hacer. Y ya actualicé el pulso que genero para ir a mi clock enablepin después de abordar un problema similar con la lógica de conteo del codificador en mi placa. Actualizaré mi esquema en consecuencia e intentaré usar un contador para controlar el período de mi pulso. ¡Gracias!

Hay varias formas de producir una frecuencia que sea un submúltiplo ajustable (posiblemente fraccionario) de un reloj de entrada. Un enfoque popular para permitir cualquier fracción de 1/2 ^ N a 1-(1/2 ^ N) es tener un registro de N bits programable para seleccionar la frecuencia deseada y un registro de N bits llamado acumulador de fase. Cada ciclo de reloj, agregue el valor de frecuencia al acumulador de fase y emita un pulso si hay un acarreo. Si los valores son 16 bits y el valor de la frecuencia es 573, habrá 573 pulsos de salida más o menos espaciados por igual cada 65 536 pulsos de entrada. Los cambios en la frecuencia solicitada afectarán "suavemente" la salida.

luego, cada vez que el contador se reinicia, la salida habrá enviado el número apropiado de pulsos. Tenga en cuenta que al usar este enfoque, la salida tendrá más fluctuaciones que con el enfoque del acumulador de fase, pero la cantidad de circuitos necesarios para cada salida puede reducirse. Tenga en cuenta también que los cambios en la frecuencia programada deben sincronizarse con los tiempos en los que se ajusta el temporizador, o de lo contrario pueden causar interrupciones en la salida.

Otro enfoque más, que es algo así como un híbrido entre los dos primeros, es tener un contador diseñado para que cierta entrada lo haga "bloquearse" durante un ciclo, y conectar un circuito similar al segundo anterior a ese circuito de bloqueo. . Si uno tiene un contador de 8 bits, el valor programado en el registro de frecuencia hará que se detenga entre una y 255 veces cada 256 veces que avance el contador, lo que hará que la tasa de vuelta del contador sea ajustable de 256 a 511. Si uno usa un multiplexor de 8 vías, por lo que puede tener una relación de división de K/2^N donde K es 256-511 y N es cero a siete. Tenga en cuenta que los "toques" de orden inferior del contador tendrán mucha fluctuación, pero los de orden superior serán mucho más limpios.