He creado esta entrada para que pensemos cómo podemos utilizar RCSG y covergroups en la verificación de un microprocesador.
Una forma que se me ocurre de hacerlo (y desde luego no es única) es la que os voy a describir. Es el momento adecuado porque empezamos esta semana a conocer el código máquina de los diferentes tipos de instrucciones del RISC-V que tenemos entre manos.
RCSG
Lo primero que pensé fue que la mejor manera de aleatorizar las instrucciones que realiza un microprocesador es aleatorizar el contenido txt que introducimos en la memoria ROM. Pero luego pensé que quizá fuera interesante modelizar la memoria ROM con dos estilos: Estilo sintetizable directo y estilo random.
- El primero utilizaría el clásico txt para inicializar el contenido y debería ser sintetizable por quartus sin problemas.
- El segundo generaría cada vez que el «address» cambiara un contenido gobernado por una clase RCSG, intentando claro está que si volviera a repetirse algún «address» el contenido aleatorio se repitiera.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
module rom_aleatoria (address,dout); parameter d_width = 32; parameter mem_depth = 1024; localparam a_width=$clog2(mem_depth); input [a_width-1:0] address; output logic [d_width-1:0] dout; logic [d_width-1:0] mem[mem_depth-1:0] ; `ifndef debug_aleatorio initial $readmemh("contenido_fijo.dat", mem); assign dout = mem[address]; `else logic [31:0] tipos_paquete[logic [9:0] ]; // tipo de paquete es 16 bits logic [31:0] temp; always @(address) if (!tipos_paquete.exists(address)) begin rom_aleatoria_tb.m1.get(temp); tipos_paquete[address]=temp; dout=tipos_paquete[address]; end else begin rom_aleatoria_tb.m1.get(temp); dout=tipos_paquete[address]; end `endif endmodule |
Puede verse estos dos estilos separados mediante una compilación condicional en la que elijamos o el comportamiento directo sintetizable o el comportamiento aleatorio.
Vamos a centrarnos en el comportamiento aleatorio (desde la línea 16 hasta la 26) que tiene más elementos nuevos.
- Cada vez que always se reanuda y ejecuta por un cambio de «address», obtenemos desde un «mailbox» denominado m1 un valor «random»
- Para ello una cierta infraestructura es necesaria, como por ejemplo la declaración de un mailbox que maneje mensajes de 32 bits, lo cual haremos desde el top de todo nuestro banco de pruebas y la escritura del mailbox que haremos desde la construcción «program» donde centralizaremos todos los elementos de test. Mas adelante veremos esas sintaxis. Lo que si que adelanto es que evidentemente el valor «random» lo conseguiré por el uso del método randomize que ya todos conocéis.
- Otra cosa que necesitamos es un pequeño diccionario (implementado con un array asociativo) que asocie una dirección con un valor aleatorio, de forma que si el microprocesador transitara de nuevo por la misma dirección, detectemos que ese «address» ya ha sido utilizado y reusemos el valor aleatorio generado la primera vez que ese address se utilizó (el valor procedente del mailbox se lee para vaciarlo, aunque no se usa).
Veamos como sería la clase donde generamos el valor aleatorio.
1 2 3 4 5 6 |
class RCSG_RISCV; rand logic [31:0] valor; constraint R_format {valor[6:0] == 7'b0110011;} constraint R_format_a {(valor[6:0] == 7'b0110011 && valor[14:12]!=3'b000 && valor[14:12]!=3'b101) -> valor[31:25]==7'b0000000 ;} constraint R_format_b {(valor[6:0] == 7'b0110011 && (valor[14:12]==3'b000) || valor[14:12]==3'b101) -> valor[31:25]==7'b0000000 || valor[31:25]==7'b0100000 ;} endclass |
He creado unos «constraints» para conseguir unas instrucciones de tipo R_format. la primera de esas constraints elige un tipo de instrucciones y deberá habilitarse o deshabilitarse según convenga, mientras que las dos siguientes «constraints» son derivadas de la anterior y pueden estar siempre habilitadas.
Ya solo nos quedaría ver cómo usamos dicha clase. Este uso lo vamos a centralizar en el test (realizado con un program).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
program estimulos (if_rom.testar testar_ports, if_rom.monitorizar monitorizar_ports); utilidades_verificacion::RCSG_RISCV generar_instrucciones; utilidades_verificacion::covergroups_RISCV monitorizar_instrucciones; logic [31:0] instruccion_random; initial begin generar_instrucciones=new; monitorizar_instrucciones=new(monitorizar_ports); # 50; $display("probando r_format" ); generar_instrucciones.R_format.constraint_mode(1); prueba_random_r_format(); $stop; $writememh("salida_random.txt", rom_aleatoria_tb.rom_aleatoria_dut.duv.tipos_paquete); $finish; end task prueba_random_r_format; begin generar_instrucciones.randomize(); rom_aleatoria_tb.m1.put(generar_instrucciones.valor); testar_ports.address='0; #0 monitorizar_instrucciones.instrucciones.sample(); while ( monitorizar_instrucciones.instrucciones.rformat.get_coverage()<100) begin # 100 ; generar_instrucciones.randomize(); rom_aleatoria_tb.m1.put(generar_instrucciones.valor); testar_ports.address= testar_ports.address +1; #0 monitorizar_instrucciones.instrucciones.sample(); end end endtask endprogram |
En este código se puede observar la generación del valor aleatorio y su escritura en el «mailbox» m1, cada vez que vamos a generar un nuevo address. Previamente hemos hecho la declaración (línea 3) y la creación (línea 10) del objeto que habíamos visto su definición en el código Clase RCSG.
En cuanto a la línea 19, podemos ver como escribir los contenidos aleatorios en un fichero txt para su posterior uso.
Cobertura funcional
Volvamos al código anterior y ya se vislumbra la declaración (línea 4) y creación (línea 11) del covergroup necesario para monitorizar las instrucciones. Solo nos faltaría visualizar la definición del covergroup que tenemos en la siguiente sección de código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class covergroups_RISCV; virtual if_rom.monitorizar monitor_port; covergroup instrucciones ; rformat : coverpoint ({monitor_port.dato[30],monitor_port.dato[14:12]}) iff (monitor_port.dato[6:0]==7'b0110011&&monitor_port.dato[31]==1'b0&&monitor_port.dato[29:25]==5'b0000) { bins add ={0}; bins sub={8}; bins sll={1}; bins slt={2}; bins sltu={3}; bins ixor={4}; bins slr={5}; bins sra={13}; bins ior={6}; bins iand={7}; illegal_bins imposibles_rformat={9,10,11,12,14,15}; } endgroup function new(virtual if_rom.monitorizar mpuertos); monitor_port=mpuertos; instrucciones=new; endfunction endclass |
Comentemos algunos detalles:
- El covergroup como era de esperar es inmediato y requiere del uso del método «sample» para determinar el momento de muestreo. Ver lineas 29 y 36 del código del program en donde utilizamos dicho método de muestreo.
- He puesto al coverpoint de una condición de enable (iff) que me permita habilitar únicamente el cubrimiento de esas combinaciones de código cuando estemos realmente en una instrucción r_format.
- Recurro a un interface virtual para poder obtener la monitorización del puerto de salida de la ROM. Ya lo habíamos hecho en otros ejemplos y he recurrido a una estructura muy parecida a la utilizada en el ejemplo final de la verificación del radicador
Estructura completa
Ya solo nos quedan los detalles. El primero es que las dos clases utilizadas las he puesto en un paquete con el nombre de utilidades_verificacion.
1 2 3 4 5 6 7 8 9 10 11 12 |
package utilidades_verificacion; typedef mailbox #(logic [31:0]) instruction_box; class RCSG_RISCV; .... endclass class covergroups_RISCV; ... endclass endpackage |
Para la monitorización de los puertos de salida ha sido esencial la definición de un interfaz que todo lo una. Un interfaz con los típicos tres modos que ya hemos utilizado en otros ejemplos y en el que destaca la ausencia de elementos de sincronización (clocking blocks) . Eso puede suponer un problema que puede ser subsanado con técnicas manuales de sincronización como es la utilización del cero delay visible en las líneas 29 y 36 del program. Esta técnica me garantiza que cuando monitorizo el puerto de salida en un instante en que ese puerto debe cambiar, el valor que monitorizo será el nuevo generado puesto que pospongo la lectura de muestreo al final de ese instante.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
interface if_rom; logic [9:0] address; logic [31:0] dato; modport monitorizar (input address, input dato); modport testar (output address, input dato); modport duv (input address, output dato); endinterface |
Por supuesto la utilización del interfaz me obliga a encapsular el duv en un livel jerárquico superior que utilice dicho interfaz
1 2 3 4 5 6 7 |
module top_duv( if_rom.duv bus); rom_aleatoria #(.d_width(32), .mem_depth(1024)) duv (.address(bus.address), .dout(bus.dato)); endmodule |
Ya solo nos queda el banco de pruebas (top de nuestra jerarquía) en donde destacará la declaración y construcción del mailbox que vamos a utilizar para intercomunicar los procesos de los estímulos (program) con los del duv (modelo de la memoria aleatorizada)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
module rom_aleatoria_tb (); // Parameters localparam d_width = 32; localparam mem_depth = 1024; localparam a_width=$clog2(mem_depth); // Ports reg [a_width-1:0] address; reg clk; logic [d_width-1:0] dato; utilidades_verificacion::instruction_box m1=new(); if_rom interfaz_rom (); top_duv rom_aleatoria_dut (.bus(interfaz_rom) ); estimulos rom_aleatoria_estimulos (.testar_ports(interfaz_rom), .monitorizar_ports(interfaz_rom)); endmodule |
Y para terminar un último detalle. Este comportamiento aleatorio de la ROM solo será efectivo si cuando compile con vlog defina la variable «debug_aleatorio». Si hago una compilación con las opciones por defecto actuará la ROM sintetizable y cargada desde un txt y por tanto sin ningún tipo de aleatorización. La forma de compilar con esa definición de variable es:
vlog +define+debug_aleatorio rom_aleatoria.sv
Veamos con el siguiente vídeo el comportamiento de esta simulación:
Para que podáis probar el proyecto adjunto el mismo en le siguiente laboratorio virtual:
[iframe width=»100%» height=»480″ src=»https://www.edaplayground.com/x/jztC»]