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
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:
- ANCHO_RAM:16
- PROFUNDIDAD_RAM:256
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ón → Generales → Gené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
- Cómo crear una lista de cadenas en VHDL
- Cómo crear un banco de pruebas controlado por Tcl para un módulo de bloqueo de código VHDL
- Cómo detener la simulación en un banco de pruebas VHDL
- Cómo crear un controlador PWM en VHDL
- Cómo generar números aleatorios en VHDL
- Cómo crear un banco de pruebas de autocomprobación
- Cómo crear una lista enlazada en VHDL
- Cómo usar un procedimiento en un proceso en VHDL
- Cómo usar una función impura en VHDL
- Cómo usar una función en VHDL
- Cómo crear una máquina de estados finitos en VHDL