Este proyecto implementa un subconjunto mínimo de la especificación de extensión vectorial de RISC-V (RVV 1.0) como un co-procesador acoplado a un procesador escalar RISC-V host. El objetivo es una implementación en hardware funcionalmente correcta que cubra las operaciones vectoriales aritméticas y de acceso a memoria.
En este archivo se puede ver un resumen de las partes importantes de la especificación RVV para este proyecto.
Para nuestra implementación los siguientes parámetros seran fijos y no se podran modificar en tiempo de ejecución.
| Parámetro | Valor | Descripción |
|---|---|---|
| VLEN | 128 bits | Ancho de cada registro vectorial |
| SEW | 32 bits | Ancho de elemento estándar — solo se soportan elementos de 32 bits |
| LMUL | 1 | Agrupamiento de registros — cada instrucción opera sobre exactamente un registro |
| VLMAX | 4 | Número máximo de elementos por instrucción (VLEN / SEW) |
| Registros vectoriales | 32 | v0 – v31, cada uno de 128 bits |
Dado que SEW y LMUL son fijos, no existe configuración de vtype en tiempo de ejecución. Todas las instrucciones operan implícitamente sobre 4 elementos de 32 bits cada uno.
La razón principal de esta elección de parámetros fijos es que el procesador es de 32 bits.
La imagen anterior muestra cómo un registro vectorial de 128 bits aloja 4 elementos independientes de 32 bits.
- Aritmética vectorial-vectorial (ADD, SUB, AND, OR, XOR, SLL, SRL, SRA, SLT, SLTU)
- Aritmética vectorial-escalar (modo VX): el valor escalar se replica en los 4 carriles
- Acceso vectorial a memoria: cargas y almacenamientos en modalidad unit-stride, strided e indexed
- Carga y almacenamiento del registro de máscara (VLM/VSM) sobre v0
Las siguientes características de la especificación RVV 1.0 no están implementadas:
- Registros CSR: vtype, vl, vstart, vxrm, vxsat y vcsr no existen en el diseño. No hay configuración de tipo ni de longitud de vector en tiempo de ejecución.
- vl variable: La implementación siempre procesa los 4 elementos (VLMAX). No existe ejecución de vector parcial.
- Enmascaramiento por elemento: La semántica de elementos activos/inactivos (vma) no se aplica. El flag de operación de máscara solo habilita o deshabilita el acceso a DCache para las instrucciones VLM/VSM; no controla la ejecución condicional en operaciones aritméticas.
- Instrucciones de segmento: Las variantes vlseg/vsseg no están implementadas.
- Cargas fault-only-first: vle<eew>ff.v no está implementado.
- Carga/almacenamiento de registro completo: vl1r/vs1r y sus variantes no están implementados.
- SEW < 32 o SEW = 64: Solo se soportan elementos de 32 bits.
- LMUL ≠ 1: El agrupamiento de registros está fijo en LMUL=1.
Esta extensión vectorial funciona como co-procesador junto al procesador escalar RISC-V host. El procesador host es responsable del fetch de instrucciones, la decodificación y la lectura de registros escalares. Cuando el decodificador identifica una instrucción vectorial, envía señales de control pre-decodificadas y valores de operandos escalares directamente a la extensión vectorial a través de la interfaz de ve_top — la extensión vectorial no posee decodificador de instrucciones propio.
El decodificador del host provee dos caminos de entrada independientes a la extensión vectorial:
- Camino ALU: campos de instrucción (funct7, funct3, rs1, rs2, rd), una bandera que indica modo vectorial-escalar (
i_is_vx) y el valor del operando escalar. - Camino LSU: banderas de operación de memoria decodificadas (carga, almacenamiento, strided, indexed, operación de máscara), la dirección base (valor de rs1 del banco de registros escalar) y el stride (valor de rs2).
La extensión vectorial aserta o_stall de regreso al decodificador del host cuando no puede aceptar una nueva instrucción — ya sea por un conflicto en el bus de la caché de datos o por un hazard RAW. Ambas causas se describen en la Sección 2.4.
La extensión vectorial utiliza un pipeline de 4 etapas. Una instrucción recorre las cuatro etapas antes de que su resultado sea confirmado en el banco de registros vectoriales.
| Etapa | Módulo | Responsabilidad |
|---|---|---|
| Issue | issue |
Lee operandos del VRF, captura campos de la instrucción en registros de pipeline |
| Execute | execute |
Calcula el resultado ALU; emite el primer acceso a DCache (elementos 0–1) para LSU |
| MEM | mem |
Emite el segundo acceso a DCache (elementos 2–3) para LSU; ensambla el resultado de 128 bits en cargas |
| Writeback | writeback |
Escribe el resultado de vuelta al VRF (omitido en almacenamientos) |
| Módulo | Archivo | Función |
|---|---|---|
ve_integrated |
rtl_ve/ve_integrated.v | Sistema completo: procesador host + extensión vectorial + memorias |
ve_top |
rtl_ve/ve_top.v | Módulo raíz de la extensión vectorial: instancia todos los submódulos, lógica de stall y mux de DCache |
vregisters |
rtl_ve/vregfile/vregisters.v | Banco de registros vectoriales de 32 × 128 bits |
alu |
rtl_ve/alu/alu.v | Carril aritmético/lógico individual de 32 bits |
alu_array |
rtl_ve/alu/alu_array.v | 4 carriles ALU en paralelo operando sobre un vector de 128 bits |
vlsu |
rtl_ve/lsu/vlsu.v | Generador combinacional de direcciones y señales de control para acceso a memoria |
issue |
rtl_ve/pipeline/issue.v | Etapa 1 del pipeline |
execute |
rtl_ve/pipeline/execute.v | Etapa 2 del pipeline |
mem |
rtl_ve/pipeline/mem.v | Etapa 3 del pipeline |
writeback |
rtl_ve/pipeline/writeback.v | Etapa 4 del pipeline |
hazard_unit |
rtl_ve/pipeline/hazard_unit.v | Detección de hazards RAW entre etapas del pipeline vectorial |
dcache |
risc-v_RV32I/memory/DCache.v | Caché de datos de doble puerto (128 palabras de 32 bits) |
El pipeline vectorial puede detenerse por dos razones, y la combinación de ambas se reporta al host mediante o_stall:
- Conflicto de DCache: una instrucción LSU vectorial ocupa el bus de DCache durante dos etapas consecutivas del pipeline (Execute y MEM), por lo que dos instrucciones LSU consecutivas generarían un conflicto. La condición es:
dcache_stall = s1_is_lsu AND s2_is_lsu
Cuando se aserta:
- La etapa Issue mantiene sus registros sin cambios.
- La etapa Execute inserta una burbuja en MEM (fuerza
o_valid = 0,o_is_lsu = 0). - La etapa MEM continúa normalmente con la instrucción que ya está en vuelo.
- Hazard RAW: la instrucción entrante necesita leer un registro vectorial que una instrucción todavía en vuelo aún no ha escrito. En este caso Issue le niega la entrada al pipeline insertando burbujas hasta que el dato esté disponible. La detección se describe en detalle en el capítulo de control de hazards.
En ambos casos o_stall se aserta alto hacia el decodificador del host para que no emita una nueva instrucción ese ciclo.
La DCache tiene dos puertos (A y B). Tanto Execute como MEM generan señales hacia la DCache, pero el mecanismo de stall garantiza que nunca estén ambas activas para LSU al mismo tiempo. Un mux combinacional en ve_top selecciona cuál etapa maneja el bus:
sel_mem = s2_is_lsu AND NOT s2_is_mask_op
Cuando sel_mem = 1, las señales ACCESS_23 de MEM se enrutan hacia la DCache. Cuando sel_mem = 0, se usan las señales ACCESS_01 de Execute. Las operaciones de máscara quedan fuera de esta selección (ACCESS_23 se deshabilita para VLM/VSM ya que solo se accede al elemento 0).
El banco de registros vectoriales almacena el estado de los 32 registros vectoriales v0–v31. Es el punto de partida y destino de toda operación vectorial: las instrucciones leen sus operandos de aquí al inicio del pipeline y escriben su resultado de vuelta al final.
Para el empaquetado de los elementos dentro de cada registro de 128 bits, ver spec_overview.md — Sección 3.
El VRF implementa un arreglo de 32 registros de 128 bits cada uno. Todas las lecturas son combinacionales — los datos están disponibles el mismo ciclo en que se presenta la dirección. Las escrituras son síncronas.
En reset, todos los registros se inicializan a cero.
El VRF expone cuatro puertos de lectura independientes para permitir que una sola instrucción acceda a todos los operandos que necesita simultáneamente, sin necesidad de ciclos adicionales.
| Puerto | Dirección | Dato | Uso |
|---|---|---|---|
| A | addr_a |
data_a |
Primer operando de la ALU (vs1) |
| B | addr_b |
data_b |
Segundo operando de la ALU (vs2) |
| C | addr_c |
data_c |
Dato a escribir en memoria en almacenamientos (vs3) |
| D | addr_d |
data_d |
Offsets de dirección en modo indexed (vs2 offsets) |
Los puertos C y D son necesarios por el modo indexed de la VLSU: en ese modo vs2 cumple el rol de registro de offsets (puerto D), y el dato que se almacena en memoria viene del campo rd de la instrucción reinterpretado como vs3 (puerto C). La especificación los trata como operandos distintos, por eso requieren puertos separados. Para más detalle ver spec_overview.md — Sección 4.3.
El VRF tiene un único puerto de escritura síncrono:
| Señal | Descripción |
|---|---|
we |
Habilitación de escritura |
addr_w |
Registro destino (0–31) |
data_in |
Dato a escribir (128 bits) |
La etapa Writeback es la única que conduce este puerto. Solo escribe cuando la instrucción es válida y no es un almacenamiento — los stores escriben en memoria, no en el VRF.
El registro v0 no tiene tratamiento especial en el hardware del VRF: puede leerse y escribirse a través de los mismos puertos que cualquier otro registro. Su rol especial como registro de máscara es una convención de la especificación que se para usar las instrucciones VLM y VSM, las cuales apuntan siempre a v0 como destino o fuente.
La ALU vectorial ejecuta operaciones aritméticas y lógicas elemento a elemento sobre vectores de 128 bits. Está compuesta por cuatro carriles de 32 bits que operan en paralelo, produciendo los cuatro resultados en un único ciclo combinacional.
El módulo alu_array instancia cuatro unidades alu idénticas de 32 bits. Cada carril recibe el slice de 32 bits correspondiente de los operandos de entrada y produce su resultado de 32 bits de forma independiente. La misma operación (alu_op) se aplica a los cuatro carriles simultáneamente.
alu_array (128 bits)
├── carril 0: in_a[31:0] op in_b[31:0] → out[31:0]
├── carril 1: in_a[63:32] op in_b[63:32] → out[63:32]
├── carril 2: in_a[95:64] op in_b[95:64] → out[95:64]
└── carril 3: in_a[127:96] op in_b[127:96] → out[127:96]
La operación a ejecutar se codifica en 4 bits formados por {funct7[5], funct3}, tomados directamente de la instrucción RISC-V. Este encoding es el mismo que usa la ALU escalar del procesador host para instrucciones de tipo R.
alu_op |
funct7[5] |
funct3 |
Operación | Descripción |
|---|---|---|---|---|
4'b0000 |
0 | 000 | ADD | Suma |
4'b1000 |
1 | 000 | SUB | Resta |
4'b0001 |
0 | 001 | SLL | Desplazamiento lógico a la izquierda |
4'b0010 |
0 | 010 | SLT | Comparación con signo (resultado 0 o 1) |
4'b0011 |
0 | 011 | SLTU | Comparación sin signo (resultado 0 o 1) |
4'b0100 |
0 | 100 | XOR | XOR bit a bit |
4'b0101 |
0 | 101 | SRL | Desplazamiento lógico a la derecha |
4'b1101 |
1 | 101 | SRA | Desplazamiento aritmético a la derecha |
4'b0110 |
0 | 110 | OR | OR bit a bit |
4'b0111 |
0 | 111 | AND | AND bit a bit |
Para los desplazamientos (SLL, SRL, SRA) solo se usan los 5 bits menos significativos del operando in_b, siguiendo la misma convención que la instrucción escalar equivalente.
Las operaciones aritméticas soportan dos formas de obtener sus operandos, distinguidas por el sufijo del mnemónico (.vv o .vx):
Modo VV — Vectorial-Vectorial. Es el caso base: ambos operandos provienen de registros vectoriales. vs1 se lee por el puerto A del VRF y vs2 por el puerto B, y cada carril de la ALU opera sobre el par de elementos correspondiente:
resultado[n] = vs1[n] op vs2[n] // para n = 0, 1, 2, 3
Modo VX — Vectorial-Escalar. El segundo operando no proviene de un registro vectorial sino de un registro escalar del procesador host. Para que la alu_array pueda operar sin modificaciones, la etapa Issue replica el valor escalar en los cuatro slots de 32 bits del operando vs2 antes de capturarlo en el registro de pipeline:
vs2_replicado = { escalar, escalar, escalar, escalar } // 4 × 32 bits
resultado[n] = vs1[n] op escalar // para n = 0, 1, 2, 3
La selección entre ambos modos ocurre únicamente en Issue. Desde la perspectiva de Execute y de la alu_array, una instrucción VX es indistinguible de una VV — ambas llegan con dos operandos vectoriales de 128 bits.
La VLSU es el módulo encargado de traducir una instrucción de memoria vectorial en accesos individuales a la DCache. Es un módulo puramente combinacional y recibe los parámetros de la operación (dirección base, stride, offsets, modo de acceso), luego genera las direcciones y señales de control para los puertos de la DCache.
Los modos de direccionamiento que implementa están descritos en spec_overview.md — Sección 4.1.
La VLSU calcula siempre las direcciones de los 4 elementos en paralelo. La fórmula depende del modo de acceso:
| Modo | Dirección del elemento n |
|---|---|
| Unit-stride | base + n × 4 |
| Strided | base + n × stride |
| Indexed | base + offset[n] |
En modo strided el salto proviene del registro escalar rs2 y puede ser cualquier valor, incluso negativo o cero. En modo indexed los 4 offsets provienen del registro vectorial vs2, leído por el puerto D del VRF.
Un vector completo requiere 4 accesos a memoria, pero la DCache solo tiene 2 puertos. La solución es dividir la operación en dos fases de 2 elementos cada una:
| Fase | Elementos | Puerto A | Puerto B | Etapa del pipeline |
|---|---|---|---|---|
ACCESS_01 |
0 y 1 | elemento 0 | elemento 1 | Execute |
ACCESS_23 |
2 y 3 | elemento 2 | elemento 3 | MEM |
La fase no es una decisión interna de la VLSU: llega como entrada desde el pipeline. La instancia de la VLSU en Execute siempre opera en fase ACCESS_01, y la instancia en MEM siempre en fase ACCESS_23. Así, una instrucción de memoria completa sus 4 accesos en 2 ciclos consecutivos conforme avanza por el pipeline.
Con las 4 direcciones ya calculadas, la fase determina cuáles se envían a los puertos de la DCache. Junto con cada dirección se generan las señales de habilitación (read_en o write_en según sea carga o almacenamiento) y, en almacenamientos, el slice de 32 bits correspondiente del dato vs3.
Cuando la entrada de habilitación general está desactivada — por ejemplo durante un stall — ambos puertos quedan inactivos.
Las instrucciones de máscara son un caso especial: solo transfieren 1 byte (los 4 bits de máscara caben en el byte 0 de v0, ver spec_overview.md — Sección 3). Para esto la VLSU:
- Solo accede al elemento 0 durante
ACCESS_01; el puerto B queda desactivado. - Reduce el byte-enable del puerto A a
4'b0001— solo el byte 0 de la palabra. - La fase
ACCESS_23se omite por completo: la instancia de MEM no habilita ningún acceso.
Como los 4 elementos llegan en dos ciclos distintos, el resultado de 128 bits se construye por partes:
- En Execute, los datos de los elementos 0 y 1 (fase
ACCESS_01) se capturan en un registro intermedio de 64 bits. - En MEM, los datos de los elementos 2 y 3 (fase
ACCESS_23) se concatenan con ese registro para formar los 128 bits completos.
Para VLM solo se conserva el byte 0 del primer acceso; el resto del resultado se rellena con ceros.
Este capítulo describe en detalle cada etapa del pipeline de la extensión vectorial: qué submódulos instancia, qué información captura en su registro de salida y cómo se comporta ante un stall. Los submódulos funcionales (VRF, ALU, VLSU) ya fueron descritos en los capítulos 3–5.
Entre cada par de etapas existe un registro de pipeline que captura toda la información de la instrucción y las señales de estos siguen la siguiente convención de nombres:
| Prefijo | Frontera | Contenido principal |
|---|---|---|
s1_* |
Issue → Execute | Operandos leídos del VRF, operación ALU, banderas LSU |
s2_* |
Execute → MEM | Resultado ALU, banderas LSU, datos de ACCESS_01 |
s3_* |
MEM → Writeback | Resultado final (ALU o carga ensamblada), registro destino |
Cada registro lleva además una bandera valid que indica si contiene una instrucción real o una burbuja, y una bandera is_lsu que distingue el camino de memoria del camino aritmético.
Issue es la puerta de entrada del pipeline. Sus responsabilidades:
- Lectura del VRF (combinacional): con los campos de la instrucción conduce las direcciones de los 4 puertos de lectura —
rs1al puerto A,rs2a los puertos B y D, yrdal puerto C. Los datos quedan disponibles el mismo ciclo. - Modo VX en instrucciones aritméticas: si la instrucción es vectorial-escalar, replica el valor escalar en los 4 slots del operando
vs2(ver Sección 4.3). - Captura en el registro de pipeline: en el flanco de reloj, todos los campos de la instrucción y los operandos leídos se capturan en las señales
s1_*.
Issue lee siempre los 4 puertos del VRF sin importar el tipo de instrucción; las etapas siguientes simplemente ignoran los datos que no necesitan.
Execute contiene los dos submódulos de ejecución, que operan en paralelo sobre la instrucción que entra:
alu_array: calcula el resultado aritmético de los 4 carriles a partir des1_vs1_datays1_vs2_data. El resultado se captura ens2_result.vlsuen faseACCESS_01: si la instrucción es de memoria, genera los accesos a DCache de los elementos 0 y 1. En cargas, los datos devueltos por la DCache (que responde de forma combinacional) se capturan en el registros2_asm_lode 64 bits para su ensamblado posterior.
Solo uno de los dos caminos produce información útil por instrucción, la bandera is_lsu determina cuál. La ALU computa siempre, sin importar el tipo de instrucción; el VLSU también calcula sus direcciones siempre, pero sus habilitaciones hacia la DCache solo se activan cuando la instrucción es LSU y no hay stall.
MEM completa la mitad restante de los accesos a memoria:
vlsuen faseACCESS_23: genera los accesos de los elementos 2 y 3. Se habilita solo si la instrucción es LSU válida y no es operación de máscara.- Ensamblado del resultado de carga: concatena los datos de la DCache de este ciclo (elementos 2–3) con
s2_asm_lo(elementos 0–1) formando los 128 bits completos. Para VLM el resultado es{120'b0, byte_0}. - Selección del resultado: el registro
s3_resultcaptura el vector ensamblado si la instrucción es una carga, os2_result(resultado ALU) en caso contrario.
Writeback es puramente combinacional — no tiene registro de salida propio, conduce directamente el puerto de escritura del VRF:
we = s3_valid AND NOT s3_is_store
addr_w = s3_rd
data_in = s3_result
Los almacenamientos llegan hasta esta etapa con su bandera is_store activa, lo que suprime la escritura: su efecto ya ocurrió en las etapas Execute y MEM al escribir en la DCache.
El pipeline tiene dos fuentes de stall independientes, cuya combinación (OR) se reporta al host como o_stall:
| Fuente | Condición | Efecto en Issue | Efecto en Execute |
|---|---|---|---|
| Conflicto de DCache | s1_is_lsu AND s2_is_lsu |
Congela su registro de salida (la instrucción LSU espera) | Inserta burbuja en MEM |
| Hazard RAW | Instrucción entrante lee un registro aún no escrito | Inserta burbuja (la instrucción no entra al pipeline) | Avanza normalmente |
La diferencia clave: en el conflicto de DCache la instrucción ya está dentro del pipeline y debe esperar en su lugar, mientras que en el hazard RAW la instrucción todavía no ha entrado y simplemente se le niega la entrada hasta que el productor termine. La detección del hazard RAW la realiza el módulo hazard_unit, que compara los registros fuente de la instrucción entrante (rs1, rs2, y rd en almacenamientos) contra el registro destino de las instrucciones en vuelo en las tres etapas siguientes.
El módulo ve_integrated es el nivel más alto de la jerarquía: conecta el pipeline escalar RV32I de 5 etapas (Fetch → Decode → Execute → MEM → Writeback) con la extensión vectorial (ve_top), compartiendo un único decodificador, un banco de registros escalar y la caché de datos.
El sistema tiene completo tiene un solo decodificador (Modified_DecodeUnit), que pertenece al pipeline escalar pero fue extendido para reconocer también las instrucciones vectoriales. Cuando decodifica una instrucción vectorial, emite las señales pre-decodificadas hacia ve_top (los caminos ALU y LSU descritos en la Sección 2.1) y la instrucción no produce ningún efecto en el pipeline escalar.
Además de los campos de la instrucción, el decodificador captura en ese momento los valores escalares que la instrucción vectorial necesita, leyéndolos del banco de registros escalar:
| Valor | Registro fuente | Uso en la extensión vectorial |
|---|---|---|
vec_scalar |
rs1 |
Operando escalar en modo VX |
vec_base_addr |
rs1 |
Dirección base para cargas/almacenamientos |
vec_stride |
rs2 |
Salto entre elementos en modo strided |
La DCache tiene dos puertos, repartidos así entre los dos pipelines:
- Puerto B: exclusivo de la extensión vectorial.
- Puerto A: compartido — un mux combinacional decide quién lo controla:
vec_port_a_active = vext_read_en OR vext_write_en
Cuando la extensión vectorial tiene un acceso activo, controla el puerto A; en caso contrario lo controla la etapa MEM del pipeline escalar. No se necesita arbitraje más sofisticado porque el stall vectorial (descrito en el Capítulo 8) garantiza que el pipeline escalar está congelado mientras una instrucción vectorial accede a memoria, por esto nunca hay competencia real por el puerto.
El sistema expone una interfaz mínima para los testbenches:
- Durante el reset (
rst = 1), el testbench escribe el programa en la ICache mediante las señalesi_imem_wen,i_imem_addr(indexada por palabra) ei_imem_data. - Al liberar el reset, la ICache pasa a modo solo-lectura y el pipeline comienza a ejecutar desde
INITIAL_PC. - Por convención, los programas usan el registro
x20como sentinel de finalización:x20 = 0mientras el programa corre, y la última instrucción ejecutaaddi x20, x0, 1. El testbench monitorea este registro para detectar la terminación y medir ciclos.
Un hazard RAW (Read After Write) ocurre cuando una instrucción necesita leer un registro que una instrucción anterior — todavía dentro del pipeline — aún no ha terminado de escribir. Si no se hace nada, la instrucción leería el valor viejo:
add x1, x2, x3 # escribe x1 (el resultado llega al RF varios ciclos después)
add x4, x1, x5 # lee x1 — ¿valor nuevo o viejo?
Cada uno de los dos pipelines del sistema resuelve este problema con una estrategia distinta.
El pipeline escalar resuelve los hazards RAW sin detener el pipeline: en lugar de esperar a que el dato llegue al banco de registros, lo toma directamente de la etapa donde ya está calculado. Un mux en la etapa Decode selecciona, para cada operando, el valor más reciente disponible entre cuatro fuentes:
| Prioridad | Ruta | Fuente del dato |
|---|---|---|
| 1 | EX→EX | Resultado de la ALU en Execute (instrucción a 1 de distancia) |
| 2 | MEM→EX | Resultado en la etapa MEM — de la ALU, o del dato leído de la DCache con extensión de signo |
| 3 | WB→EX | Dato que Writeback está por escribir al RF (a 3 de distancia) |
| 4 | RF | Lectura normal del banco de registros (sin dependencia) |
Existe un único caso que el forwarding no puede resolver: el hazard load-use, cuando la instrucción inmediatamente siguiente a una carga usa el registro cargado. El dato aún no ha salido de la DCache cuando el consumidor llega a Execute. Para este caso se inserta automáticamente un ciclo de stall: Fetch y Decode se congelan, Execute recibe una burbuja, y al ciclo siguiente la ruta MEM→EX ya puede entregar el dato.
El pipeline vectorial usa la estrategia opuesta: detección y espera, sin caminos de forwarding. El módulo hazard_unit compara los registros fuente de la instrucción que intenta entrar al pipeline contra el registro destino de las instrucciones en vuelo en las tres etapas siguientes:
raw_stall = entrante.valid AND (
(s1 escribe vd == rs1/rs2/rd_entrante) OR
(s2 escribe vd == rs1/rs2/rd_entrante) OR
(s3 escribe vd == rs1/rs2/rd_entrante) )
Los almacenamientos en vuelo se excluyen de la comparación (no escriben al VRF), pero el campo rd de un almacenamiento entrante sí se compara — porque en un store rd es en realidad vs3, el registro que se va a leer.
Mientras raw_stall esté activo, la etapa Issue inserta burbujas: la instrucción entrante no ingresa al pipeline mientras la instrucción productora avanza normalmente hasta escribir el VRF. En el peor caso (dependencia con la instrucción inmediatamente anterior) la espera es de 3 ciclos.
Ahora bien, detener el pipeline vectorial no es suficiente: como el host es quien emite las instrucciones, también hay que detenerlo. Para esto la extensión vectorial reporta su stall al host, donde se combina con el stall load-use del propio pipeline escalar — si cualquiera de los dos está activo, las etapas Fetch y Decode del host se congelan: el PC no avanza y la instrucción que estaba en decode se queda esperando. Como el decode congelado sigue presentando la instrucción vectorial en la entrada de la extensión, esta no se pierde: en cuanto el stall baja, entra al pipeline vectorial y el host continúa con la instrucción siguiente.
La verificación del proyecto sigue una metodología jerárquica con pruebas dirigidas, organizada en cuatro niveles según qué parte del sistema se ejercita: primero los módulos individuales en de manera independiente, luego cada uno de los dos pipelines por separado (el escalar del host y el vectorial), y finalmente el sistema integrado donde ambos conviven. Todos los testbenches usan casos de prueba definidos manualmente con verificación automática — cada caso compara el resultado obtenido contra el esperado y acumula contadores de PASS/FAIL que se reportan al final de la simulación.
Las herramientas utilizadas son Icarus Verilog (iverilog/vvp) para compilación y simulación, y GTKWave para inspección de formas de onda. Cada testbench genera un archivo .vcd con las señales de la simulación.
Nivel 1 — Módulos individuales: Cada bloque funcional se prueba de manera aislada, aplicando estímulos directamente sobre sus puertos: tb_alu (todas las operaciones de la ALU), tb_vregfile (escritura síncrona, lectura dual, escritura inhibida, reset) y tb_vlsu_integration (direcciones, enables y rutas de datos para todos los modos de acceso, en ambas fases).
Nivel 2 — Pipeline escalar del host: Programas puramente escalares (lw/add/sw) donde la extensión vectorial nunca se activa. Verifican el pipeline escalar — en particular sus caminos de forwarding — y producen las referencias escalares de rendimiento: tb_scalar_perf (N=4, sin aprovechar forwarding, NOPs manuales), tb_scalar_fwd (N=4, optimizado con 0 NOPs en el camino crítico), tb_scalar_n8 (N=8) y tb_vector_add_scalar (N=16).
Nivel 3 — Extensión vectorial: tb_ve_top prueba el pipeline vectorial completo aislado del pipeline escalar: el testbench hace el papel del host, simulando el banco de registros escalar y la DCache, e ingresando instrucciones vectoriales codificadas.
Nivel 4 — Sistema integrado: Programas ensamblados reales ejecutados sobre ve_integrated, ejercitando la interacción completa entre ambos pipelines — decodificador compartido, vec_stall congelando el host, y la DCache compartida: tb_ve_integrated (verificación funcional con programas mixtos), tb_vector_perf (rendimiento, N=4) y tb_vector_n8 (rendimiento, N=8, con instrucciones vectoriales consecutivas que ejercitan el stall de DCache y la hazard_unit).
Los testbenches de rendimiento de los niveles 2 y 4 forman una familia comparativa: todos resuelven el mismo problema — la suma elemento a elemento de dos arreglos, C[i] = A[i] + B[i] — con los mismos datos, midiendo los ciclos de reloj hasta que el sentinel x20 indica la finalización del programa. Esto permite comparar directamente el pipeline escalar sin forwarding, el escalar con forwarding y el vectorial, para distintos tamaños de problema (N = 4, 8, 16).
Los resultados de estas mediciones se presentan en el Capítulo 10.
El Makefile del proyecto define un target por testbench. Cada target compila el RTL necesario, ejecuta la simulación y reporta el archivo de ondas generado:
make tb_alu # verificación de la ALU
make tb_ve_integrated # verificación del sistema completo
make tb_vector_perf # medición de rendimiento vectorial
make all # ejecuta todos los testbenchesPara inspeccionar las formas de onda de una simulación:
gtkwave tb_ve_integrated.vcdLas mediciones se obtuvieron ejecutando el benchmark descrito en la Sección 9.2 — la suma elemento a elemento C[i] = A[i] + B[i] — sobre las tres configuraciones del sistema, contando los ciclos de reloj desde el inicio del programa hasta que el sentinel indica la finalización.
| Configuración | N = 4 | N = 8 |
|---|---|---|
| Escalar sin forwarding (baseline) | 59 | 75* |
| Escalar con forwarding | 23 | 39 |
| Vectorial SIMD (VLEN=128, 4 carriles) | 57 | 60 |
* Valor estimado mediante un modelo lineal; las demás celdas son mediciones directas de simulación RTL.
Para pocos elementos el resultado muestra varias cosas: en N=4 el pipeline vectorial (57 ciclos) apenas supera al baseline (59 ciclos, ×1.04) y pierde claramente contra el escalar con forwarding (23 ciclos). La razón es el costo fijo de la interfaz escalar→vectorial: cada instrucción vectorial congela el pipeline del host mientras se ejecuta, y el programa necesita instrucciones escalares de preparación (direcciones base) entre las instrucciones vectoriales. Para 4 elementos, ese costo fijo domina sobre el beneficio del paralelismo.
En N=8 la tendencia empieza a invertirse: el vectorial solo agrega 3 ciclos (de 57 a 60) mientras el escalar con forwarding agrega 16 (de 23 a 39). El auemnto de rendimiento contra el baseline sube a ×1.25.
Ajustando un modelo lineal a las mediciones se obtienen las pendientes de crecimiento:
| Configuración | Costo incremental |
|---|---|
| Escalar sin forwarding | +4 ciclos por elemento |
| Escalar con forwarding | +4 ciclos por elemento |
| Vectorial SIMD | +0.75 ciclos por elemento |
Las dos configuraciones escalares crecen a la misma tasa — el forwarding reduce el costo fijo pero no cambia la naturaleza secuencial del procesamiento: cada elemento requiere su propia carga, suma y almacenamiento. El vectorial, en cambio, procesa 4 elementos por instrucción, por lo que su pendiente es unas 5 veces más pequeña.
Observando los modelo, las rectas del vectorial y del escalar con forwarding se cruzan en N ≈ 14 elementos (~65 ciclos): a partir de ese tamaño de problema, la extensión vectorial supera incluso a la versión escalar optimizada, y la ventaja crece indefinidamente con N — en N=32 el vectorial ya es ×1.7 veces más rápido.
De esta implementacion y pruebas se puede concluir que la extensión vectorial no muestra un mejor rendimiento para operaciones aisladas sobre una pequeña cantidad de elementos, donde el costo fijo de la interfaz domina, pero se vuelve la opción claramente superior conforme crece el volumen de datos — que es precisamente el caso de uso para el que las extensiones vectoriales existen.







