fpga-memorias-block-ram-bram-registros-distribuidos

Memorias en FPGA: block RAM, RAM distribuida y registros

  • 6 min

Las memorias internas de una FPGA son recursos para almacenar datos mediante registros, LUT configurables o bloques RAM dedicados.

De momento, cuando necesitábamos guardar un dato (como un contador o un estado), usábamos un Registro compuesto por Flip-Flops. Es rápido, fácil y está disponible en todas partes.

Pero, ¿qué pasa si queremos guardar una imagen para mostrarla en un monitor? ¿O un buffer de audio de 5 segundos? ¿O el código de un procesador softcore?

Si intentas crear un array gigante de registros reg [7:0] imagen [0:10000], tu herramienta de síntesis se echará a reír (o a llorar). Te quedarás sin Flip-Flops en un instante y el ruteado será imposible.

Para almacenamiento masivo, las FPGAs tienen un as bajo la manga: las Memorias Internas. Hoy vamos a ver las dos formas principales de guardar datos masivos: Distributed RAM y Block RAM (BRAM).

Registros: memoria con flip-flops

Ya los conocemos. Están dispersos por todo el chip, junto a cada LUT.

  • Capacidad: 1 bit por unidad.
  • Velocidad: Extremadamente rápida.
  • Acceso: Paralelo total (puedes leer 1000 registros a la vez si tienes cables suficientes).
  • Coste: Altísimo. Usar Flip-Flops para guardar arrays de datos es un desperdicio de silicio.

Regla: Solo usa Flip-Flops para variables de control, máquinas de estados o pipelines. Nunca para guardar tablas de datos o buffers.

RAM distribuida (LUT RAM)

Aquí empezamos a ser listos. ¿Recordáis qué era una LUT (Look-Up Table)? Dijimos que era una pequeña memoria que guardaba la tabla de verdad de una función lógica.

Normalmente, esa memoria es de solo lectura (ROM) durante el funcionamiento del circuito. Pero en muchas FPGAs (como las de Xilinx o Lattice), podemos configurar las LUTs para que sean escribibles dinámicamente.

Esto convierte a las LUTs, que normalmente hacen lógica, en pequeñas memorias RAM. A esto lo llamamos Distributed RAM.

  • Uso ideal: Pequeños arrays (ej: un banco de 16 o 32 registros, pequeñas FIFOs).
  • Ventaja: Están “distribuidas” por el chip, así que puedes ponerlas justo al lado de la lógica que las necesita. Permiten lectura asíncrona (combinacional).
  • Desventaja: Si intentas hacer una memoria grande, consumirás toda la lógica de la FPGA y no te quedará espacio para procesar nada.

Block RAM (BRAM)

Cuando necesitamos guardar KiloBytes o MegaBytes de información, recurrimos a los “pesos pesados”.

Las Block RAM (BRAM) son bloques de memoria SRAM dedicados que están incrustados físicamente en el silicio de la FPGA. Son islas de alta densidad rodeadas por el mar de lógica programable.

Dependiendo del fabricante, estos bloques tienen tamaños fijos (por ejemplo, en Lattice iCE40 son bloques de 4 Kbits; en Xilinx Series 7 son de 36 Kbits). El sintetizador puede unir varios bloques para formar la memoria del tamaño que tú necesites.

Características de la BRAM

  1. Lectura normalmente síncrona: En la mayoría de familias, la BRAM se infiere con una lectura registrada: presentas la dirección y obtienes el dato tras un flanco. Revisa la primitiva de tu dispositivo porque los modos disponibles varían.
  2. Alta Densidad: Puedes guardar muchísimos datos ocupando muy poco espacio en el chip.
  3. Dual Port (Doble Puerto): Muchas BRAMs tienen dos puertos de acceso independientes (A y B).
  • Puedes estar escribiendo un dato por el Puerto A (desde un sensor).
  • Y leyendo otro dato distinto por el Puerto B (hacia un monitor VGA).
  • Según la primitiva, ambos puertos pueden funcionar a la vez e incluso utilizar relojes distintos.

Comparación de tecnologías

CaracterísticaRegistros (FF)Distributed RAM (LUT)Block RAM (BRAM)
Recurso BaseFlip-FlopsLUTs (Lógica)Bloques Dedicados
CapacidadMuy Baja (Bits)Baja (Cientos de Bits)Alta (KiloBits / MegaBits)
LecturaAsíncrona / SíncronaAsíncrona posibleEstrictamente Síncrona
Coste de ÁreaMuy AltoMedioMuy Bajo (Eficiente)
Uso TípicoEstados, Flags, ContadoresRegister Files, FIFOs pequeñasBuffers de Video, Audio, RAM de CPU

Inferencia de memoria en Verilog

¿Cómo le decimos a la herramienta “quiero usar una BRAM”? Podríamos instanciar la primitiva específica del fabricante (ej: SB_RAM40_4K en Lattice), pero eso haría que nuestro código no funcionara en Xilinx.

Lo mejor es usar Inferencia. Escribimos un array en Verilog genérico, y la herramienta es lo suficientemente lista para decir: “Mmm, este array es muy grande, mejor lo meto en una BRAM”.

Código para inferir una BRAM de un puerto

module memoria_ram #(
    parameter DATOS_WIDTH = 8,   // 8 bits por dato (un byte)
    parameter ADDR_WIDTH  = 10   // 10 bits de dirección (2^10 = 1024 posiciones)
)(
    input wire clk,
    input wire write_enable,
    input wire [ADDR_WIDTH-1:0] addr, // Dirección (0 a 1023)
    input wire [DATOS_WIDTH-1:0] data_in,
    output reg [DATOS_WIDTH-1:0] data_out
);

    // 1. Declaración del Array (La Memoria)
    // "Un array de 1024 huecos, donde cada hueco tiene 8 bits"
    reg [DATOS_WIDTH-1:0] memoria [0:(2**ADDR_WIDTH)-1];

    always @(posedge clk) begin
        // Escritura (Si está habilitada)
        if (write_enable) begin
            memoria[addr] <= data_in;
        end
        
        // Lectura síncrona (necesaria para inferir BRAM)
    // Si pusiéramos esto fuera del always, podría inferirse RAM distribuida
        data_out <= memoria[addr];
    end

endmodule
Copied!

Fíjate en que la lectura data_out <= memoria[addr] está dentro del bloque always @(posedge clk). Esto implica un ciclo de latencia y coincide con la plantilla de inferencia habitual. Consulta la guía de síntesis de tu herramienta para conocer las plantillas exactas y el comportamiento de lectura durante una escritura.

Inicialización ($readmemh)

Una gran ventaja de las FPGAs es que podemos inicializar estas memorias con datos pre-grabados al programar el chip. Esto es fundamental si estamos haciendo una ROM (Memoria de Solo Lectura) que contenga:

  • Una tabla de senos/cosenos para cálculos.
  • Los caracteres (font) para un generador de texto VGA.
  • El programa compilado (firmware) de un procesador RISC-V.

Verilog tiene funciones del sistema para esto: $readmemh (hexadecimal) y $readmemb (binario).

    // ... dentro del módulo de memoria ...

    initial begin
        // Carga el archivo "mi_firmware.hex" en el array 'memoria'
        // El archivo debe estar en la carpeta del proyecto
        $readmemh("mi_firmware.hex", memoria);
    end
Copied!

El archivo mi_firmware.hex sería un simple archivo de texto con los valores:

FF
A0
00
1B
...
Copied!