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

Domine la verificación aleatoria restringida con OSVVM

La verificación aleatoria restringida es una estrategia de testbench que se basa en generar transacciones pseudoaleatorias para el device under test (DUT). El objetivo es alcanzar cobertura funcional de un número de eventos predefinidos a través de interacción aleatoria con el DUT.

Open Source VHDL Verification Methodology (OSVVM) es una biblioteca VHDL gratuita que incluye varios paquetes convenientes para crear testbenches de verificación aleatoria restringida. Estamos particularmente interesados en RandomPkg y CoveragePck, que usaremos en este artículo. Recomiendo visitar la página GitHub de OSVVM para aprender más sobre las características de esta biblioteca.

El device under test

Voy a sumergirme directamente en un ejemplo para explicar mejor cómo un testbench de verificación aleatoria restringida difiere del testbench clásico, que usa pruebas dirigidas. Creamos un buffer anillo FIFO en el artículo anterior de este blog, pero no creamos un testbench auto-comprobable para verificar la corrección del módulo.

Vamos a crear un testbench adecuado para el buffer anillo FIFO que use verificación aleatoria restringida.

 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; 

La entidad del módulo buffer anillo se muestra en el código anterior. Vamos a tratar el DUT como una caja negra, lo que significa que no asumiremos ningún conocimiento de cómo está implementado el DUT. Después de todo, este artículo trata sobre el testbench, no sobre el buffer anillo FIFO.

Instanciaremos el DUT en el testbench usando el método de instanciación de entidad. La instanciación es trivial, así que omitiré el código por ahora, pero puede descargarse más adelante en este artículo.

Los genéricos del DUT se mapearán a los siguientes valores:

Estrategia del testbench

Repasemos la estrategia de prueba antes de empezar a implementar nada. La imagen siguiente muestra el concepto principal del testbench que vamos a crear.

Domine la verificación aleatoria restringida con OSVVM

Realizaremos transacciones de escritura aleatorias en el lado de entrada del DUT. Los datos de entrada se establecerán a un valor aleatorio en cada ciclo de reloj, y los estrobos en la entrada de habilitación de escritura tendrán duración aleatoria.

De manera similar, realizaremos lecturas aleatorias. Asertiremos la señal de habilitación de lectura en ráfagas que duran un número aleatorio de ciclos de reloj.

Habrá un modelo conductual en paralelo con el DUT. Este es un FIFO implementado de forma diferente al buffer anillo usado en el DUT, pero que todavía tiene la misma interfaz. A diferencia del DUT, el modelo conductual no tiene que ser sintetizable. Esto nos da la libertad de usar características avanzadas de programación VHDL para crearlo.

Compararemos la salida del DUT con la salida del modelo conductual en un proceso separado. Este proceso será el único responsable de hacer esta comparación en cada ciclo de reloj usando sentencias assert. Si las dos implementaciones de FIFO se comportan de forma diferente en algún momento, un fallo de aserción causará que la simulación termine con error.

Finalmente, recopilaremos datos de cobertura funcional observando las transacciones que van hacia y vienen del DUT. Un punto de cobertura funcional podría significar, por ejemplo, una lectura y escritura simultáneas, o que la FIFO se llene al menos una vez. Monitorearemos estos eventos en nuestro proceso secuenciador principal del testbench. La simulación se detendrá cuando todos los eventos de cobertura funcional que estamos monitoreando hayan ocurrido.

Importación de la biblioteca OSVVM

La biblioteca OSVVM puede usarse con cualquier simulador que soporte VHDL-2008. Puede que ya esté incluida con las bibliotecas por defecto que vienen con su simulador. Está incluida en ModelSim PE Student Edition, que puede descargarse gratis de Mentor Graphics.

ModelSim viene con una versión antigua de OSVVM, pero está bien, tiene todo lo que necesitamos. Podemos simplemente importar los paquetes random y coverage así:

 library osvvm; use osvvm.RandomPkg.all; use osvvm.CoveragePkg.all; 

La última versión de la biblioteca OSVVM siempre puede descargarse de la página GitHub. Hágalo si su simulador no la incluye, o si quiere usar las últimas características de la biblioteca.

Declaración de variables OSVVM

La biblioteca OSVVM contiene paquetes con tipos protegidos. Las variables creadas de estos tipos tendrían ámbito limitado al proceso en el que se definieron. Por tanto, las declararemos como variables shared en la región declarativa de la arquitectura del testbench, como se muestra en el código siguiente.

 -- OSVVM variables shared variable rv : RandomPType; shared variable bin1, bin2, bin3, bin4, bin5, bin6 : CovPType; 

La variable rv de tipo RandomPType es para generar valores aleatorios. Solo necesitamos una de estas, porque podemos usar el mismo objeto en cada proceso donde necesitemos generar valores aleatorios. La última línea de código declara seis variables de tipo CovPType.

Declaramos seis variables de cobertura porque vamos a tener seis objetivos de cobertura, nos referiremos a estos objetos como «bins». Las variables shared tienen que ser inicializadas antes de poder usarlas para recopilar datos de cobertura. Lo hacemos llamando al procedimiento AddBins en cada uno de los bins CovPType.

 -- Set up coverage bins bin1.AddBins("Write while empty", ONE_BIN); bin2.AddBins("Read while full", ONE_BIN); bin3.AddBins("Read and write while almost empty", ONE_BIN); bin4.AddBins("Read and write while almost full", ONE_BIN); bin5.AddBins("Read without write when almost empty", ONE_BIN); bin6.AddBins("Write without read when almost full", ONE_BIN); 

Suministramos una descripción en cadena del bin de cobertura como primer parámetro al procedimiento AddBins. Esta cadena reaparecerá al final de la simulación cuando imprimamos las estadísticas de cada bin de cobertura. Como puede ver en las descripciones de texto, vamos a usar los bins para comprobar si se han producido algunos casos esquina muy específicos.

AddBins es un procedimiento sobrecargado que puede usarse para crear varios scoreboards dentro de las variables bin. Sin embargo, tendremos solo un scoreboard asociado con cada bin. Por tanto, suministraremos la constante de conveniencia ONE_BIN como parámetro al procedimiento AddBins. Esto inicializará las variables CovPType con un bin cada una. Los scoreboards representados por los bins se consideran cubiertos cuando los eventos que monitorizan han ocurrido al menos una vez.

Generación de entrada aleatoria

Empecemos creando el proceso que genera datos de entrada para el DUT. El buffer anillo FIFO está diseñado para ignorar intentos de sobrescritura y sobre-lectura. Por tanto, podemos simplemente escribir datos aleatorios en ráfagas de duración aleatoria. No tenemos que pensar si el DUT está realmente listo para absorber los datos o no.

 PROC_WRITE : process begin wr_en <= rv.RandSlv(1)(1) and not rst; for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop wr_data <= rv.RandSlv(RAM_WIDTH); wait until rising_edge(clk); end loop; end process; 

La única consideración que toma este proceso es que el DUT no está en reset. Aleatoriamente habilitamos o deshabilitamos la señal de habilitación de escritura en la primera línea de este proceso, pero solo se habilitará si rst es '0'.

El posterior bucle for escribirá datos aleatorios en el DUT por un número aleatorio de ciclos de reloj, incluso si la señal de habilitación no está activa. Podemos hacer esto porque se supone que el DUT ignora el puerto wr_data a menos que la señal wr_en sea '1'. Después del bucle for, el programa volverá al inicio del proceso, desencadenando otra transacción de escritura aleatoria.

Realización de lecturas aleatorias

El proceso que lee datos del DUT es similar al proceso de escritura. Podemos activar aleatoriamente la señal rd_en en cualquier momento porque el DUT está diseñado para ignorar intentos de lectura cuando está vacío. Los datos que aparecen en el puerto rd_data no se comprueban realmente. Este proceso solo controla la señal de habilitación de lectura.

 PROC_READ : process begin rd_en <= rv.RandSlv(1)(1) and not rst; for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop wait until rising_edge(clk); end loop; end process; 

Verificación conductual

Construiremos un modelo conductual del DUT dentro de nuestro testbench para verificar su comportamiento. Esta es una estrategia de testbench bien conocida. Primero, alimentamos el modelo conductual simultáneamente con la misma entrada que el DUT. Luego, podemos comparar la salida de ambos para verificar si el DUT tiene el comportamiento correcto.

Domine la verificación aleatoria restringida con OSVVM

La imagen anterior muestra la disposición básica de tal testbench. El modelo conductual funciona en paralelo con el DUT. Lo usamos como plano para comprobar las salidas del DUT.

El FIFO del testbench

Usaremos una lista enlazada para implementar el modelo conductual. Las listas enlazadas no pueden sintetizarse, pero son perfectas para testbenches. Puede recordar el artículo Cómo crear una Lista Enlazada en VHDL si es lector habitual de este blog. Usaremos el código de ese artículo para implementar el modelo conductual para el buffer anillo FIFO.

 package DataStructures is type LinkedList is protected procedure Push(constant Data : in integer); impure function Pop return integer; impure function IsEmpty return boolean; end protected; end package DataStructures; 

La declaración de paquete para la Lista Enlazada FIFO se muestra en el código anterior. Es un tipo protegido que tiene tres funciones: Push, Pop e IsEmpty. Estas se usan para añadir y eliminar elementos de la FIFO, así como para comprobar si quedan cero elementos en ella.

 -- Testbench FIFO that emulates the DUT shared variable fifo : LinkedList; 

Los tipos protegidos son construcciones tipo clase en VHDL. Crearemos un objeto de la lista enlazada declarando una variable shared en la región declarativa del testbench, como se muestra en el código anterior.

El modelo conductual

Para emular completamente el comportamiento del buffer anillo FIFO, declaramos dos nuevas señales que reflejan las señales de salida del DUT. La primera señal contiene el dato de salida del modelo conductual, y la segunda es la señal valid asociada.

 -- Testbench FIFO signals signal fifo_out : integer; signal fifo_out_valid : std_logic := '0'; 

El código anterior muestra la declaración de las dos señales de salida del modelo conductual. No necesitamos señales de entrada dedicadas para el modelo conductual, porque son las mismas que las conectadas al DUT. Usamos señales para emular la salida del DUT porque nos permite recopilar datos de cobertura fácilmente, como veremos más adelante en este artículo.

 PROC_BEHAVIORAL_MODEL : process begin wait until rising_edge(clk) and rst = '0'; -- Emular una escritura if wr_en = '1' and full = '0' then fifo.Push(to_integer(unsigned(wr_data))); report "Push " & integer'image(to_integer(unsigned(wr_data))); end if; -- Emular una lectura if rd_en = '1' and empty = '0' then fifo_out <= fifo.Pop; fifo_out_valid <= '1'; else fifo_out_valid <= '0'; end if; end process; 

El proceso que implementa el modelo conductual del buffer anillo FIFO se muestra en el código anterior. Este proceso se disparará en cada flanco ascendente del reloj, si la señal de reset no está activa.

El modelo conductual empuja un nuevo valor al FIFO del testbench siempre que la señal wr_en está asertada mientras la señal full es '0'. De manera similar, la lógica de lectura en el proceso del modelo conductual funciona escuchando las señales rd_en y empty. Esta última es controlada por el DUT, pero la verificaremos en el siguiente proceso que crearemos.

Comprobación de salidas

Toda la lógica responsable de comprobar las salidas del DUT se reúne en un proceso llamado «PROC_VERIFY». Usamos sentencias assert para comprobar que las salidas del DUT coinciden con las del modelo conductual. También comprobamos que el DUT y el modelo conductual coinciden en cuándo la FIFO está vacía.

El código para el proceso de verificación se muestra a continuación.

 PROC_VERIFY : process begin wait until rising_edge(clk) and rst = '0'; -- Comprobar que DUT y TB FIFO reportan empty simultáneamente assert (empty = '1' and fifo.IsEmpty) or (empty = '0' and not fifo.IsEmpty) report "empty=" & std_logic'image(empty) & " while fifo.IsEmpty=" & boolean'image(fifo.IsEmpty) severity failure; -- Comprobar que las señales valid coinciden assert rd_valid = fifo_out_valid report "rd_valid=" & std_logic'image(rd_valid) & " while fifo_out_valid=" & std_logic'image(fifo_out_valid) severity failure; -- Comprobar que la salida del DUT coincide con el TB FIFO if rd_valid then assert fifo_out = to_integer(unsigned(rd_data)) report "rd_data=" & integer'image(to_integer(unsigned(rd_data))) & " while fifo_out=" & integer'image(fifo_out) severity failure; report "Pop " & integer'image(fifo_out); end if; end process; 

El proceso se dispara por el flanco ascendente del reloj, como vemos en la primera línea de código. El DUT es un proceso con reloj, y la lógica downstream conectada al DUT también se espera que sea síncrona a la señal de reloj. Por tanto, tiene sentido comprobar las salidas en el flanco ascendente del reloj.

El segundo bloque de código comprueba que la señal empty del DUT se aserte solo cuando el FIFO del testbench está vacío. El DUT y el modelo conductual ambos tienen que estar de acuerdo en que la FIFO está vacía o no para que esta prueba pase.

Luego sigue una comparación de las señales valid de datos de lectura. El DUT y el modelo conductual deberían emitir datos simultáneamente, de lo contrario hay algo mal.

Finalmente, comprobamos que el dato de salida del DUT coincide con el siguiente elemento que sacamos del FIFO del testbench. Esto, por supuesto, solo ocurre si la señal rd_valid está asertada, significando que la señal rd_data puede muestrearse.

Recopilación de datos de cobertura

Para controlar el flujo principal del testbench, crearemos un proceso secuenciador. Este proceso inicializará los bins de cobertura, ejecutará las pruebas y detendrá el testbench cuando se hayan cumplido todos los objetivos de cobertura. El código siguiente muestra el proceso completo «PROC_SEQUENCER».

 PROC_SEQUENCER : process begin -- Set up coverage bins bin1.AddBins("Write while empty", ONE_BIN); bin2.AddBins("Read while full", ONE_BIN); bin3.AddBins("Read and write while almost empty", ONE_BIN); bin4.AddBins("Read and write while almost full", ONE_BIN); bin5.AddBins("Read without write when almost empty", ONE_BIN); bin6.AddBins("Write without read when almost full", ONE_BIN); wait until rising_edge(clk); wait until rising_edge(clk); rst <= '0'; wait until rising_edge(clk); loop wait until rising_edge(clk); -- Collect coverage data bin1.ICover(to_integer(wr_en = '1' and empty = '1')); bin2.ICover(to_integer(rd_en = '1' and full = '1')); bin3.ICover(to_integer(rd_en = '1' and wr_en = '1' and empty = '0' and empty_next = '1')); bin4.ICover(to_integer(rd_en = '1' and wr_en = '1' and full = '0' and full_next = '1')); bin5.ICover(to_integer(rd_en = '1' and wr_en = '0' and empty = '0' and empty_next = '1')); bin6.ICover(to_integer(rd_en = '0' and wr_en = '1' and full = '0' and full_next = '1')); -- Stop the test when all coverage goals have been met exit when bin1.IsCovered and bin2.IsCovered and bin3.IsCovered and bin4.IsCovered and bin5.IsCovered and bin6.IsCovered; end loop; report("Coverage goals met"); -- Make sure that the DUT is empty before terminating the test wr_en <= force '0'; rd_en <= force '1'; loop wait until rising_edge(clk); exit when empty = '1'; end loop; -- Print coverage data bin1.WriteBin; bin2.WriteBin; bin3.WriteBin; bin4.WriteBin; bin5.WriteBin; bin6.WriteBin; finish; end process; 

Primero, inicializamos los objetos bin de cobertura llamando al procedimiento AddBins en ellos, como ya discutimos anteriormente en este artículo. Luego, tras liberar el reset, pasamos a recopilar datos de cobertura. En cada flanco ascendente del reloj, el código dentro del constructo loop se ejecutará.

El primer bloque de código dentro del loop es para recopilar datos de cobertura. Podemos llamar al procedimiento ICover en el bin para registrar un acierto en el punto de cobertura que representa. Si suministramos el parámetro entero 0, la llamada no tendrá efecto. Si usamos el parámetro entero 1, contará como un acierto.

Hay solo un «bin» dentro de cada objeto bin de cobertura, porque los inicializamos usando la constante ONE_BIN. Este único bin puede alcanzarse llamando a ICover(1). Podemos registrar un acierto o fallo en el punto de cobertura convirtiendo nuestras expresiones booleanas a los enteros 1 o 0 usando la función to_integer.

Tras recopilar los datos de cobertura, comprobamos si se han cumplido todos los objetivos de cobertura llamando a la función IsCovered en todos los bins. Luego, salimos del bucle si se han cumplido todos los objetivos de cobertura.

Nos aseguraremos de que el DUT esté vacío antes de terminar la prueba. Para lograr esto, tomamos control de los procesos escritor y lector forzando la señal wr_en a '0' y la señal rd_en a '1'.

Finalmente, imprimimos estadísticas de cuántas veces se ha alcanzado cada objetivo de cobertura llamando a la función WriteBin en cada uno de los bins de cobertura. La palabra clave finish al final del proceso hará que el simulador detenga la simulación.

Ejecución del testbench

Puede descargar el proyecto completo de ModelSim con todos los archivos VHDL usando el formulario a continuación.

Después de cargar el proyecto ejecutando el do-file incluido en el Zip, podemos ejecutar el testbench simplemente escribiendo «runtb» en la consola de ModelSim. El tiempo de ejecución del testbench será aleatorio porque los objetivos de cobertura se alcanzan aleatoriamente. Sin embargo, los resultados de la prueba son reproducibles porque en realidad se usa una secuencia pseudoaleatoria.

No inicializamos una semilla en nuestro código, lo que significa que se usará el valor de semilla por defecto para el generador pseudoaleatorio. Es posible establecer una semilla diferente llamando al procedimiento InitSeed en el objeto RandomPType, esto producirá una secuencia aleatoria diferente.

Salida de consola

La salida impresa en la consola de ModelSim tras dar el comando «runtb» se muestra a continuación. Habrá muchos pushes y pops aleatorios hacia y desde el FIFO del testbench mientras la simulación se ejecuta.

VSIM 2> runtb # ** Warning: Design size of 15929 statements or 2 leaf instances exceeds # ModelSim PE Student Edition recommended capacity. # Expect performance to be quite adversely affected. # ** Note: Push 34910 # Time: 790 ns Iteration: 0 Instance: /ring_buffer_tb ... # ** Note: Pop 37937 # Time: 83100 ns Iteration: 0 Instance: /ring_buffer_tb # ** Note: Pop 13898 # Time: 83110 ns Iteration: 0 Instance: /ring_buffer_tb # %% WriteBin: # %% Write while empty Bin:(1) Count = 2 AtLeast = 1 # # %% WriteBin: # %% Read while full Bin:(1) Count = 3 AtLeast = 1 # # %% WriteBin: # %% Read and write while almost empty Bin:(1) Count = 106 AtLeast = 1 # # %% WriteBin: # %% Read and write while almost full Bin:(1) Count = 1 AtLeast = 1 # # %% WriteBin: # %% Read without write when almost empty Bin:(1) Count = 1 AtLeast = 1 # # %% WriteBin: # %% Write without read when almost full Bin:(1) Count = 3 AtLeast = 1 # # Break in Process PROC_SEQUENCER at C:/crv/ring_buffer_tb.vhd line 127 

Las estadísticas de todos los bins de cobertura se imprimen cuando se han cumplido todos los objetivos. Algunos bins solo se han alcanzado una vez, mientras que uno se ha alcanzado 106 veces. Pero al final, cada bin se ha alcanzado al menos una vez. Así, podemos saber que todos los eventos para los que definimos bins de cobertura han sido probados y verificados.

La forma de onda

Examinemos la forma de onda para hacernos una idea de lo que ha hecho el testbench. La imagen siguiente muestra la forma de onda con la señal fill_count representada como valor analógico. La FIFO está llena cuando la traza de esta señal está en la parte superior, y vacía cuando está en la inferior.

Domine la verificación aleatoria restringida con OSVVM

Como vemos en la forma de onda, el buffer anillo se llena y vacía aleatoriamente. Sin embargo, no programamos explícitamente estas subidas y bajadas en el nivel de llenado. No obstante, vemos un patrón orgánico de interacción con el DUT.

Más sobre verificación aleatoria restringida

La verificación aleatoria restringida es una buena estrategia de testbench cuando el vector de prueba tiene demasiadas permutaciones para que una prueba exhaustiva sea práctica. Las interacciones aleatorias exhiben un comportamiento más natural que una prueba de casos esquina dirigida, sin sacrificar precisión.

Podemos estar seguros de que todos los casos esquina se han cumplido, siempre que hayamos configurado correctamente la recopilación de datos de cobertura. La ventaja añadida es que las pruebas aleatorias son más propensas a exponer debilidades en el DUT que no está específicamente probando. Mientras conozca todos los casos esquina, puede crear pruebas dirigidas para ellos. Pero los casos esquina se pasan por alto fácilmente, y ese es cuando podría beneficiarse de la metodología de verificación aleatoria restringida.

Este artículo solo ha arañado la superficie de lo que puede hacer con verificación aleatoria restringida. Recomiendo leer la documentación en la página GitHub de OSVVM para profundizar en el tema.

También recomiendo el curso Advanced VHDL Testbenches and Verification de SynthWorks, con el que no estoy afiliado. Sin embargo, asistí a la versión de 5 días de este curso físico. El curso es impartido por Jim Lewis, presidente del VHDL Analysis and Standardization Group (VASG). En general, una gran inversión para cualquier empresa que quiera llevar sus testbenches VHDL al siguiente nivel.


VHDL

  1. Los Desafíos Impredecibles del Mantenimiento: Perspectivas del Experto Jason Afara
  2. Sistema de control de acceso integrado con QR, RFID y verificación de temperatura
  3. Diseño PCB alta velocidad: Procesamiento de señales diferenciales y verificación por simulación
  4. Torno de bancada inclinada Hurco TM8: diseño compacto, control de precisión y herramientas versátiles
  5. Programa de Verificación de Rendimiento de Compresores Rotativos CAGI: Garantizando una Operación Confiable y Eficiente
  6. Cadence presenta soluciones de emulación y prototipado de alto rendimiento para verificación de SoC de miles de millones de puertas
  7. Siemens impulsa el sistema Veloce con cuatro nuevos productos para verificación asistida por hardware sin fisuras
  8. Iniciativa VE-VIDES presenta marco de diseño y verificación confiable para seguridad IoT
  9. Synopsys presenta suite completa de IP HBM3 para sistemas multi-die, aumentando ancho de banda a 921 GB/s
  10. Generar números aleatorios en Java: Guía práctica con Random y Math.random
  11. Java 8 Streams: Enfoque Declarativo para Procesamiento de Datos