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

Cómo crear un FIFO de búfer de anillo en VHDL

Los búferes circulares son construcciones populares para crear colas en lenguajes de programación secuencial, pero también se pueden implementar en hardware. En este artículo, crearemos un búfer de anillo en VHDL para implementar un FIFO en RAM de bloque.

Hay muchas decisiones de diseño que tendrá que tomar al implementar un FIFO. ¿Qué tipo de interfaz necesitas? ¿Está limitado por los recursos? ¿Debería ser resistente a la sobrelectura y la sobrescritura? ¿La latencia es aceptable? Esas son algunas de las preguntas que surgen en mi mente cuando me piden que cree un FIFO.

Existen muchas implementaciones FIFO gratuitas en línea, así como generadores FIFO como Xilinx LogiCORE. Pero aún así, muchos ingenieros prefieren implementar sus propios FIFO. Porque aunque todos realizan las mismas tareas básicas de cola y de cola, pueden ser muy diferentes cuando se tienen en cuenta los detalles.

Cómo funciona un búfer circular

Un búfer de anillo es una implementación FIFO que utiliza memoria contigua para almacenar los datos almacenados en el búfer con un mínimo de barajado de datos. Los elementos nuevos permanecen en la misma ubicación de memoria desde el momento de la escritura hasta que se leen y se eliminan del FIFO.

Se utilizan dos contadores para realizar un seguimiento de la ubicación y la cantidad de elementos en el FIFO. Estos contadores se refieren a un desplazamiento desde el inicio del espacio de memoria donde se almacenan los datos. En VHDL, será un índice de una celda de matriz. En el resto de este artículo, nos referiremos a estos contadores como punteros .

Estos dos punteros son la cabeza y cola punteros La cabeza siempre apunta a la ranura de memoria que contendrá los siguientes datos escritos, mientras que la cola se refiere al siguiente elemento que se leerá del FIFO. Hay otras variantes, pero esta es la que vamos a utilizar.

Estado vacío

Si la cabeza y la cola apuntan al mismo elemento, significa que el FIFO está vacío. La imagen de arriba muestra un ejemplo de FIFO con ocho ranuras. Tanto el puntero de la cabeza como el de la cola apuntan al elemento 0, lo que indica que el FIFO está vacío. Este es el estado inicial del búfer circular.

Tenga en cuenta que el FIFO aún estaría vacío si ambos punteros estuvieran en otro índice, por ejemplo, 3. Para cada escritura, el puntero principal se mueve un lugar hacia adelante. El puntero de cola se incrementa cada vez que el usuario de FIFO lee un elemento.

Cuando cualquiera de los punteros está en el índice más alto, la siguiente escritura o lectura hará que el puntero vuelva al índice más bajo. Esta es la belleza del búfer de anillo, los datos no se mueven, solo lo hacen los punteros.

La cabeza lleva a la cola

La imagen de arriba muestra el mismo búfer de anillo después de cinco escrituras. El puntero de la cola todavía está en la ranura número 0, pero el puntero de la cabeza se ha movido a la ranura número 5. Las ranuras que contienen datos están coloreadas en azul claro en la ilustración. El puntero de la cola estará en el elemento más antiguo, mientras que la cabeza apuntará al siguiente espacio libre.

Cuando la cabeza tiene un índice más alto que la cola, podemos calcular el número de elementos en el búfer anular restando la cola de la cabeza. En la imagen de arriba, eso arroja un recuento de cinco elementos.

La cola lleva a la cabeza

Restar la cabeza de la cola solo funciona si la cabeza lleva la cola. En la imagen de arriba, la cabeza está en el índice 2 mientras que la cola está en el índice 5. Por lo tanto, si realizamos este simple cálculo, obtenemos 2 – 5 =-3, lo cual no tiene sentido.

La solución es compensar la cabeza con el número total de ranuras en el FIFO, 8 en este caso. El cálculo ahora arroja (2 + 8) – 5 =5, que es la respuesta correcta.

La cola siempre estará persiguiendo a la cabeza, así es como funciona un búfer circular. La mitad de las veces la cola tendrá un índice más alto que la cabeza. Los datos se almacenan entre los dos, como lo indica el color azul claro en la imagen de arriba.

Estado completo

Un búfer de anillo completo tendrá la cola apuntando al índice directamente después de la cabeza. Una consecuencia de este esquema es que nunca podemos usar todas las ranuras para almacenar datos, tiene que haber al menos una ranura libre. La imagen de arriba muestra una situación en la que el búfer circular está lleno. La ranura abierta, pero inutilizable, es de color amarillo.

También se podría utilizar una señal dedicada de vacío/lleno para indicar que el búfer circular está lleno. Esto permitiría que todas las ranuras de memoria almacenen datos, pero requiere una lógica adicional en forma de registros y tablas de búsqueda (LUT). Por lo tanto, vamos a utilizar mantener uno abierto esquema para nuestra implementación del búfer de anillo FIFO, ya que esto solo desperdicia RAM de bloque más barata.

La implementación FIFO del búfer de anillo

La forma en que defina las señales de interfaz hacia y desde su FIFO limitará la cantidad de posibles implementaciones de su búfer circular. En nuestro ejemplo, vamos a utilizar una variación de la clásica interfaz de lectura/escritura habilitada y vacía/llena/válida.

Habrá un datos de escritura bus en el lado de entrada que transporta los datos que se enviarán al FIFO. También habrá una habilitación de escritura señal, que cuando se afirma, hará que el FIFO muestree los datos de entrada.

El lado de salida tendrá un datos de lectura y una lectura válida señal controlada por el FIFO. También tendrá una habilitación de lectura señal controlada por el usuario intermedio de la FIFO.

El vacío y lleno Las señales de control son parte de la interfaz FIFO clásica, también las usaremos. Están controlados por el FIFO y su propósito es comunicar el estado del FIFO al lector y al escritor.

Contrapresión

El problema de esperar hasta que FIFO esté vacío o lleno antes de actuar es que la lógica de la interfaz no tendrá tiempo de reaccionar. La lógica secuencial funciona de ciclo de reloj a ciclo de reloj, los flancos ascendentes del reloj separan efectivamente los eventos en su diseño en intervalos de tiempo.

Una solución es incluir casi vacío y casi lleno señales que preceden a las señales originales en un ciclo de reloj. Esto le da tiempo a la lógica externa para reaccionar, incluso cuando lee o escribe continuamente.

En nuestra implementación, las señales anteriores se llamarán empty_next y full_next , simplemente porque prefiero poner un postfijo en lugar de un prefijo en los nombres.

La entidad

La siguiente imagen muestra la entidad de nuestro búfer de anillo FIFO. Además de las señales de entrada y salida en el puerto, tiene dos constantes genéricas. El RAM_WIDTH generic define el número de bits en las palabras de entrada y salida, el número de bits que contendrá cada ranura de memoria.

El RAM_DEPTH generic define el número de ranuras que se reservarán para el búfer de anillo. Debido a que una ranura está reservada para indicar que el búfer circular está lleno, la capacidad del FIFO será RAM_DEPTH – 1. El RAM_DEPTH La constante debe coincidir con la profundidad de RAM en el FPGA de destino. La RAM no utilizada dentro de una primitiva de RAM de bloque se desperdiciará, no se puede compartir con otra lógica en la FPGA.

entity ring_buffer is
  generic (
    RAM_WIDTH : natural;
    RAM_DEPTH : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- Write port
    wr_en : in std_logic;
    wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Read port
    rd_en : in std_logic;
    rd_valid : out std_logic;
    rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Flags
    empty : out std_logic;
    empty_next : out std_logic;
    full : out std_logic;
    full_next : out std_logic;

    -- The number of elements in the FIFO
    fill_count : out integer range RAM_DEPTH - 1 downto 0
  );
end ring_buffer;

Además del reloj y el restablecimiento, la declaración del puerto incluirá los puertos clásicos de datos/habilitación de lectura y escritura. Estos son utilizados por los módulos ascendentes y descendentes para enviar nuevos datos al FIFO y para extraer el elemento más antiguo.

El rd_valid la señal es afirmada por el FIFO cuando el rd_data El puerto contiene datos válidos. Este evento se retrasa un ciclo de reloj después de un pulso en el rd_en señal. Hablaremos más sobre por qué tiene que ser así al final de este artículo.

Luego vienen las banderas de vacío/lleno establecidas por FIFO. El empty_next la señal se afirmará cuando queden 1 o 0 elementos, mientras que empty solo está activo cuando hay 0 elementos en el FIFO. Del mismo modo, el full_next señal indicará que hay espacio para 1 o 0 elementos más, mientras que full solo afirma cuando FIFO no puede acomodar otro elemento de datos.

Finalmente, hay un fill_count producción. Este es un número entero que reflejará el número de elementos almacenados actualmente en el FIFO. He incluido esta señal de salida simplemente porque la usaremos internamente en el módulo. Desglosarlo a través de la entidad es esencialmente gratuito, y el usuario puede optar por dejar esta señal desconectada al instanciar este módulo.

La región declarativa

En la región declarativa del archivo VHDL, declararemos un tipo personalizado, un subtipo, una cantidad de señales y un procedimiento para uso interno en el módulo del búfer circular.

  type ram_type is array (0 to RAM_DEPTH - 1) of
    std_logic_vector(wr_data'range);
  signal ram : ram_type;

  subtype index_type is integer range ram_type'range;
  signal head : index_type;
  signal tail : index_type;

  signal empty_i : std_logic;
  signal full_i : std_logic;
  signal fill_count_i : integer range RAM_DEPTH - 1 downto 0;

  -- Increment and wrap
  procedure incr(signal index : inout index_type) is
  begin
    if index = index_type'high then
      index <= index_type'low;
    else
      index <= index + 1;
    end if;
  end procedure;

Primero, declaramos un nuevo tipo para modelar nuestra RAM. El ram_type type es una matriz de vectores, dimensionada por las entradas genéricas. El nuevo tipo se usa en la siguiente línea para declarar el ram señal que mantendrá los datos en el búfer circular.

En el siguiente bloque de código, declaramos index_type , un subtipo de entero. Su rango se regirá indirectamente por el RAM_DEPTH genérico. Debajo de la declaración del subtipo, estamos usando el tipo de índice para declarar dos nuevas señales, los punteros de cabeza y cola.

Luego sigue un bloque de declaraciones de señales que son copias internas de señales de entidad. Tienen los mismos nombres base que las señales de entidad, pero tienen el sufijo _i para indicar que son para uso interno. Usamos este enfoque porque se considera de mal estilo usar inout mode en señales de entidad, aunque esto tendría el mismo efecto.

Finalmente, declaramos un procedimiento llamado incr que toma un index_type señal como parámetro. Este subprograma se usará para incrementar los punteros de cabeza y cola, y los ajustará de nuevo a 0 cuando estén en el valor más alto. La cabeza y la cola son subtipos de enteros, que normalmente no admiten el comportamiento de ajuste. Usaremos el procedimiento para sortear este problema.

Declaraciones concurrentes

En la parte superior de la arquitectura, declaramos nuestras declaraciones concurrentes. Prefiero recopilar estas asignaciones de señales de una sola línea antes de los procesos normales porque se pasan por alto fácilmente. Una sentencia concurrente es en realidad una forma de proceso, puede leer más sobre sentencias concurrentes aquí:

Cómo crear una declaración concurrente en VHDL

  -- Copy internal signals to output
  empty <= empty_i;
  full <= full_i;
  fill_count <= fill_count_i;

  -- Set the flags
  empty_i <= '1' when fill_count_i = 0 else '0';
  empty_next <= '1' when fill_count_i <= 1 else '0';
  full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0';
  full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0';

En el primer bloque de asignaciones concurrentes, estamos copiando las versiones internas de las señales de entidad a la salida. Estas líneas garantizarán que las señales de la entidad sigan las versiones internas exactamente al mismo tiempo, pero con un ciclo delta de retraso en la simulación.

El segundo y último bloque de declaraciones concurrentes es donde asignamos las banderas de salida, señalando el estado lleno/vacío del búfer circular. Estamos basando los cálculos en el RAM_DEPTH genérico y en el fill_count señal. La profundidad de la RAM es una constante que no cambiará. Por lo tanto, las banderas cambiarán solo como resultado de un conteo de llenado actualizado.

Actualizando el puntero de la cabeza

La función básica del puntero principal es incrementar cada vez que la señal de habilitación de escritura se activa desde el exterior de este módulo. Estamos haciendo esto pasando el head señal al incr mencionado anteriormente procedimiento.

  PROC_HEAD : process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        head <= 0;
      else

        if wr_en = '1' and full_i = '0' then
          incr(head);
        end if;

      end if;
    end if;
  end process;

Nuestro código contiene un and full_i = '0' adicional declaración para proteger contra sobrescrituras. Esta lógica se puede omitir si está seguro de que la fuente de datos nunca intentará escribir en el FIFO mientras esté lleno. Sin esta protección, una sobrescritura hará que el búfer circular vuelva a estar vacío.

Si el puntero de la cabeza se incrementa mientras el búfer circular está lleno, la cabeza apuntará al mismo elemento que la cola. Por lo tanto, el módulo "olvidará" los datos contenidos y el relleno FIFO parece estar vacío.

Evaluando el full_i señal antes de incrementar el puntero de la cabeza, solo olvidará el valor sobrescrito. Creo que esta solución es mejor. Pero de cualquier manera, si alguna vez se sobrescriben, es indicativo de un mal funcionamiento fuera de este módulo.

Actualizando el puntero de la cola

El puntero de la cola se incrementa de manera similar al puntero de la cabeza, pero el read_en la entrada se utiliza como disparador. Al igual que con las sobrescrituras, estamos protegiendo contra lecturas excesivas al incluir and empty_i = '0' en la expresión booleana.

  PROC_TAIL : process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        tail <= 0;
        rd_valid <= '0';
      else
        rd_valid <= '0';

        if rd_en = '1' and empty_i = '0' then
          incr(tail);
          rd_valid <= '1';
        end if;

      end if;
    end if;
  end process;

Además, estamos pulsando el rd_valid señal en cada lectura válida. Los datos leídos siempre son válidos en el ciclo de reloj después de rd_en se afirmó, si el FIFO no estaba vacío. Con este conocimiento, no hay realmente ninguna necesidad de esta señal, pero la incluiremos por conveniencia. El rd_valid la señal se optimizará en la síntesis si se deja desconectada cuando se instancia el módulo.

Inferir RAM de bloque

Para hacer que la herramienta de síntesis infiera el bloque de RAM, tenemos que declarar los puertos de lectura y escritura en un proceso síncrono sin reinicio. Leeremos y escribiremos en la RAM en cada ciclo de reloj y dejaremos que las señales de control manejen el uso de estos datos.

  PROC_RAM : process(clk)
  begin
    if rising_edge(clk) then
      ram(head) <= wr_data;
      rd_data <= ram(tail);
    end if;
  end process;

Este proceso no sabe cuándo ocurrirá la próxima escritura, pero no necesita saberlo. En cambio, solo estamos escribiendo continuamente. Cuando el head la señal se incrementa como resultado de una escritura, comenzamos a escribir en la siguiente ranura. Esto bloqueará efectivamente el valor que se escribió.

Actualizando el conteo de llenado

El fill_count La señal se utiliza para generar las señales llena y vacía, que a su vez se utilizan para evitar la sobrescritura y la sobrelectura del FIFO. El contador de llenado se actualiza mediante un proceso combinacional que es sensible al puntero de cabeza y cola, pero esas señales solo se actualizan en el flanco ascendente del reloj. Por lo tanto, el conteo de llenado también cambiará inmediatamente después del borde del reloj.

  PROC_COUNT : process(head, tail)
  begin
    if head < tail then
      fill_count_i <= head - tail + RAM_DEPTH;
    else
      fill_count_i <= head - tail;
    end if;
  end process;

El conteo de llenado se calcula simplemente restando la cola de la cabeza. Si el índice de la cola es mayor que el de la cabeza, hay que sumar el valor de RAM_DEPTH constante para obtener el número correcto de elementos que se encuentran actualmente en el búfer circular.

El código VHDL completo para el búfer de anillo FIFO

library ieee;
use ieee.std_logic_1164.all;

entity ring_buffer is
  generic (
    RAM_WIDTH : natural;
    RAM_DEPTH : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- Write port
    wr_en : in std_logic;
    wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Read port
    rd_en : in std_logic;
    rd_valid : out std_logic;
    rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Flags
    empty : out std_logic;
    empty_next : out std_logic;
    full : out std_logic;
    full_next : out std_logic;

    -- The number of elements in the FIFO
    fill_count : out integer range RAM_DEPTH - 1 downto 0
  );
end ring_buffer;

architecture rtl of ring_buffer is

  type ram_type is array (0 to RAM_DEPTH - 1) of
    std_logic_vector(wr_data'range);
  signal ram : ram_type;

  subtype index_type is integer range ram_type'range;
  signal head : index_type;
  signal tail : index_type;

  signal empty_i : std_logic;
  signal full_i : std_logic;
  signal fill_count_i : integer range RAM_DEPTH - 1 downto 0;

  -- Increment and wrap
  procedure incr(signal index : inout index_type) is
  begin
    if index = index_type'high then
      index <= index_type'low;
    else
      index <= index + 1;
    end if;
  end procedure;

begin

  -- Copy internal signals to output
  empty <= empty_i;
  full <= full_i;
  fill_count <= fill_count_i;

  -- Set the flags
  empty_i <= '1' when fill_count_i = 0 else '0';
  empty_next <= '1' when fill_count_i <= 1 else '0';
  full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0';
  full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0';

  -- Update the head pointer in write
  PROC_HEAD : process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        head <= 0;
      else

        if wr_en = '1' and full_i = '0' then
          incr(head);
        end if;

      end if;
    end if;
  end process;

  -- Update the tail pointer on read and pulse valid
  PROC_TAIL : process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        tail <= 0;
        rd_valid <= '0';
      else
        rd_valid <= '0';

        if rd_en = '1' and empty_i = '0' then
          incr(tail);
          rd_valid <= '1';
        end if;

      end if;
    end if;
  end process;

  -- Write to and read from the RAM
  PROC_RAM : process(clk)
  begin
    if rising_edge(clk) then
      ram(head) <= wr_data;
      rd_data <= ram(tail);
    end if;
  end process;

  -- Update the fill count
  PROC_COUNT : process(head, tail)
  begin
    if head < tail then
      fill_count_i <= head - tail + RAM_DEPTH;
    else
      fill_count_i <= head - tail;
    end if;
  end process;

end architecture;

El código anterior muestra el código completo para el FIFO del búfer circular. Puede completar el formulario a continuación para recibir los archivos del proyecto ModelSim y el banco de pruebas al instante.

El banco de pruebas

El FIFO se instancia en un banco de pruebas simple para demostrar cómo funciona. Puede descargar el código fuente del banco de pruebas junto con el proyecto ModelSim mediante el siguiente formulario.

Las entradas genéricas se han establecido en los siguientes valores:

El banco de pruebas primero reinicia el FIFO. Cuando se libera el reinicio, el banco de pruebas escribe valores secuenciales (1-255) en el FIFO hasta que está lleno. Finalmente, el FIFO se vacía antes de que se complete la prueba.

Podemos ver la forma de onda para la ejecución completa del banco de pruebas en la imagen a continuación. El fill_count la señal se muestra como un valor analógico en la forma de onda para ilustrar mejor el nivel de llenado del FIFO.

La cabeza, la cola y el recuento de relleno son 0 al comienzo de la simulación. En el punto donde el full se afirma la señal, la cabeza tiene el valor 255, y también el fill_count señal. El conteo de llenado solo sube a 255 a pesar de que tenemos una profundidad de RAM de 256. Eso es porque estamos usando mantener uno abierto método para distinguir entre lleno y vacío, como discutimos anteriormente en este artículo.

En el punto de inflexión en el que dejamos de escribir en el FIFO y comenzamos a leer de él, el valor principal se congela mientras que la cola y el conteo de llenado comienzan a disminuir. Finalmente, al final de la simulación cuando el FIFO está vacío, tanto la cabeza como la cola tienen el valor 255 mientras que el conteo de llenado es 0.

Este banco de pruebas no debe considerarse adecuado para otra cosa que no sean fines de demostración. No tiene ningún comportamiento o lógica de autocomprobación para verificar que la salida del FIFO sea correcta en absoluto.

Usaremos este módulo en el artículo de la próxima semana cuando nos sumerjamos en el tema de la verificación aleatoria restringida . Esta es una estrategia de prueba diferente de las pruebas dirigidas más comúnmente utilizadas. En resumen, el banco de pruebas realizará interacciones aleatorias con el DUT (dispositivo bajo prueba), y el comportamiento del DUT debe verificarse mediante un proceso de banco de pruebas separado. Finalmente, cuando se han producido una serie de eventos predefinidos, la prueba está completa.

Haga clic aquí para leer la publicación de seguimiento del blog:
Verificación aleatoria restringida

Sintetizar en Vivado

Sinteticé el búfer de anillo en Xilinx Vivado porque es la herramienta de implementación de FPGA más popular. Sin embargo, debería funcionar en todas las arquitecturas FPGA que tienen RAM de bloque de puerto dual.

Tenemos que asignar algunos valores a las entradas genéricas para poder implementar el búfer de anillo como un módulo independiente. Esto se hace en Vivado usando la ConfiguraciónGeneralesGenéricos/Parámetros menú, como se muestra en la imagen de abajo.

El valor para el RAM_WIDTH se establece en 16, que es lo mismo que en la simulación. Pero he configurado el RAM_DEPTH a 2048 porque esta es la profundidad máxima de la primitiva RAMB36E1 en la arquitectura Xilinx Zynq que he elegido. Podríamos haber seleccionado un valor más bajo, pero aun así habría usado la misma cantidad de RAM de bloque. Un valor más alto habría resultado en el uso de más de un bloque de RAM.

La siguiente imagen muestra el uso de recursos posterior a la implementación, según lo informado por Vivado. De hecho, nuestro búfer de anillo ha consumido un bloque de RAM y un puñado de LUT y flip-flops.

Abandonar la señal válida

Puede que se pregunte si el ciclo de un reloj se retrasa entre el rd_en y el rd_valid la señal realmente es necesaria. Después de todo, los datos ya están presentes en rd_data cuando afirmamos el rd_en señal. ¿No podemos simplemente usar este valor y dejar que el búfer de anillo salte al siguiente elemento en el próximo ciclo de reloj mientras leemos del FIFO?

Estrictamente hablando, no necesitamos el valid señal. Incluí esta señal solo por conveniencia. La parte crucial es que tenemos que esperar hasta el ciclo del reloj después de afirmar el rd_en señal, de lo contrario, la RAM no tendrá tiempo de reaccionar.

Los bloques de RAM en los FPGA son componentes totalmente síncronos, necesitan un borde de reloj tanto para leer como para escribir datos. El reloj de lectura y escritura no tiene que provenir de la misma fuente de reloj, pero tiene que haber flancos de reloj. Además, no puede haber lógica entre la salida de la RAM y el siguiente registro (flip-flops). Esto se debe a que el registro que se usa para cronometrar la salida de RAM está dentro de la primitiva de RAM de bloque.

La imagen de arriba muestra un diagrama de tiempo de cómo un valor se propaga desde el wr_data entrada en nuestro búfer de anillo, a través de la RAM, y finalmente aparece en el rd_data producción. Debido a que cada señal se muestrea en el flanco ascendente del reloj, se necesitan tres ciclos de reloj desde que comenzamos a activar el puerto de escritura antes de que aparezca en el puerto de lectura. Y pasa un ciclo de reloj adicional antes de que el módulo receptor pueda utilizar estos datos.

Reduciendo la latencia

Hay formas de mitigar este problema, pero tiene el costo de recursos adicionales utilizados en el FPGA. Probemos un experimento para reducir el retraso de un ciclo de reloj desde el puerto de lectura de nuestro búfer circular. En el fragmento de código a continuación, hemos cambiado el rd_data salida de un proceso síncrono a un proceso combinacional que es sensible al ram y tail señal.

  PROC_READ : process(ram, tail)
   begin
     rd_data <= ram(tail);
   end process;

Desafortunadamente, este código no se puede asignar para bloquear la RAM porque puede haber una lógica combinatoria entre la salida de la RAM y el primer registro descendente en el rd_data señal.

La siguiente imagen muestra el uso de recursos según lo informado por Vivado. El bloque RAM ha sido reemplazado por LUTRAM; una forma de RAM distribuida implementada en LUT. El uso de LUT se ha disparado de 37 LUT a 947. Las tablas de búsqueda y los flip-flops son más caros que la RAM en bloque, esa es la razón principal por la que tenemos RAM en bloque en primer lugar.

Hay muchas formas de implementar un FIFO de búfer de anillo en RAM de bloque. Es posible que pueda ahorrar el ciclo de reloj adicional utilizando otro diseño, pero tendrá un costo en forma de lógica de soporte adicional. Para la mayoría de las aplicaciones, el búfer de anillo presentado en este artículo será suficiente.

Actualización:
Cómo crear un FIFO de búfer de anillo en RAM de bloque usando el protocolo de enlace válido/listo para AXI

En la próxima publicación del blog, crearemos un mejor banco de pruebas para el módulo de búfer de anillo mediante el uso de verificación aleatoria restringida .

Haga clic aquí para leer la publicación de seguimiento del blog:
Verificación aleatoria restringida


VHDL

  1. Cómo crear una lista de cadenas en VHDL
  2. Cómo crear un banco de pruebas controlado por Tcl para un módulo de bloqueo de código VHDL
  3. Cómo detener la simulación en un banco de pruebas VHDL
  4. Cómo crear un controlador PWM en VHDL
  5. Cómo generar números aleatorios en VHDL
  6. Cómo crear un banco de pruebas de autocomprobación
  7. Cómo crear una lista enlazada en VHDL
  8. Cómo usar un procedimiento en un proceso en VHDL
  9. Cómo usar una función impura en VHDL
  10. Cómo usar una función en VHDL
  11. Cómo crear una máquina de estados finitos en VHDL