Xilinx XST no inferirá RAM de bloque

Tengo problemas para que el diseño de la computadora de mi FPGA 80 encaje en una placa Papilio Duo, que es una Spartan 6 - xcs6slx9. El problema se deriva de que la RAM se infiere como distribuida en lugar de en bloque.

Versión corta: estoy usando una entidad genérica para inferir los bloques de RAM (ver más abajo) y encuentro que para cualquier cosa hasta un ancho de dirección de 11 parece distribuirse, un ancho de dirección de 12 o más XST está feliz de ponerlo en bloques. Probé atributos para marcarlo como bloque, pero parece que no funciona.

Solución actual: ampliar el ancho de la dirección de una instancia, poniendo a cero el bit de dirección alta... ahora el diseño encaja.

Versión larga :

El diseño requiere tres módulos ram de puerto dual de 2048x8 bits. Un puerto necesita acceso de lectura/escritura (acceso a la CPU), el otro requiere solo lectura (controlador de video). Los puertos son asíncronos y se ejecutan en diferentes dominios de reloj.

Originalmente usé este módulo: RamDualPort para esto.

entity RamDualPort is
    generic
    (
        ADDR_WIDTH : integer;
        DATA_WIDTH : integer := 8
    );
    port
    (
        -- Port A
        clock_a : in std_logic;
        clken_a : in std_logic;
        addr_a : in std_logic_vector(ADDR_WIDTH-1 downto 0);
        din_a : in std_logic_vector(DATA_WIDTH-1 downto 0);
        dout_a : out std_logic_vector(DATA_WIDTH-1 downto 0);
        wr_a : in std_logic;

        -- Port B
        clock_b : in std_logic;
        addr_b : in std_logic_vector(ADDR_WIDTH-1 downto 0);
        dout_b : out std_logic_vector(DATA_WIDTH-1 downto 0)
    );
end RamDualPort;

architecture behavior of RamDualPort is 
    constant MEM_DEPTH : integer := 2**ADDR_WIDTH;
    type mem_type is array(0 to MEM_DEPTH-1) of std_logic_vector(DATA_WIDTH-1 downto 0);
    shared variable ram : mem_type;
begin

    process (clock_a)
    begin
        if rising_edge(clock_a) then

            if clken_a='1' then
                if wr_a = '1' then
                    ram(to_integer(unsigned(addr_a))) := din_a;
                end if;

                dout_a <= ram(to_integer(unsigned(addr_a)));
            end if;

        end if;
    end process;

    process (clock_b)
    begin
        if rising_edge(clock_b) then

            dout_b <= ram(to_integer(unsigned(addr_b)));

        end if;
    end process;

end;

Un par de problemas con esto: 1) dependiendo del ancho de la dirección, algunos se infieren como distribuidos (el problema principal por el que pregunto), pero también 2) aquellos que se infirieron para bloquear RAMS se implementaron como lectura primero que para los relojes asíncronos tienen problemas en los Spartan 6.

La única forma que pude encontrar para solucionar el problema de lectura primero fue hacer que ambos puertos leyeran/escribieran con un nuevo módulo "RamTrueDualPort" de la siguiente manera:

entity RamTrueDualPort is
    generic
    (
        ADDR_WIDTH : integer;
        DATA_WIDTH : integer := 8
    );
    port
    (
        -- Port A
        clock_a : in std_logic;
        clken_a : in std_logic;
        addr_a : in std_logic_vector(ADDR_WIDTH-1 downto 0);
        din_a : in std_logic_vector(DATA_WIDTH-1 downto 0);
        dout_a : out std_logic_vector(DATA_WIDTH-1 downto 0);
        wr_a : in std_logic;

        -- Port B
        clock_b : in std_logic;
        clken_b : in std_logic;
        addr_b : in std_logic_vector(ADDR_WIDTH-1 downto 0);
        din_b : in std_logic_vector(DATA_WIDTH-1 downto 0);
        dout_b : out std_logic_vector(DATA_WIDTH-1 downto 0);
        wr_b : in std_logic
    );
end RamTrueDualPort;

architecture behavior of RamTrueDualPort is 
    constant MEM_DEPTH : integer := 2**ADDR_WIDTH;
    type mem_type is array(0 to MEM_DEPTH-1) of std_logic_vector(DATA_WIDTH-1 downto 0);
    shared variable ram : mem_type;
begin

    process (clock_a)
    begin
        if rising_edge(clock_a) then

            if clken_a='1' then

                if wr_a = '1' then
                    ram(to_integer(unsigned(addr_a))) := din_a;
                end if;

                dout_a <= ram(to_integer(unsigned(addr_a)));

            end if;

        end if;
    end process;

    process (clock_b)
    begin
        if rising_edge(clock_b) then

            if clken_b='1' then

                if wr_b = '1' then
                    ram(to_integer(unsigned(addr_b))) := din_b;
                end if;

                dout_b <= ram(to_integer(unsigned(addr_b)));

            end if;

        end if;
    end process;

end;

Eso solucionó el problema de lectura primero y esos carneros que iban a bloquear el carnero ahora se implementan como escritura primero (NB: en realidad no me importa leer primero/escribir primero, excepto el problema de lectura primero de carnero corrupto de Spartan 6 ).

Ahora el problema es obtener las instancias más pequeñas de 2k (addrWidth 11) en el RAM de bloque. Como mencioné, probé atributos pero aún insiste en ponerlo en RAM distribuida. No pude encontrar ninguna documentación sobre ram_style para variables (a diferencia de las señales), pero adiviné esto: (Tenga en cuenta el bit ram:variable)

constant MEM_DEPTH : integer := 2**ADDR_WIDTH;
type mem_type is array(0 to MEM_DEPTH-1) of std_logic_vector(DATA_WIDTH-1 downto 0);
shared variable ram : mem_type;
ATTRIBUTE ram_extract: string;
ATTRIBUTE ram_extract OF ram:variable is "yes";
ATTRIBUTE ram_style: string;
ATTRIBUTE ram_style OF ram:variable is "block";

Ahora XST escupe esto, lo que sugiere que se entiende la sintaxis del atributo: (Nótese la mención de ram_extracty ram_style)

Synthesizing Unit <RamTrueDualPort_1>.
    Related source file is "C:/Documents and Settings/Brad/Projects/fpgabee/Hardware/FPGABeeCore/RamTrueDualPort.vhd".
        ADDR_WIDTH = 12
        DATA_WIDTH = 8
    Set property "ram_extract = yes" for signal <ram>.
    Set property "ram_style = block" for signal <ram>.
    Found 4096x8-bit dual-port RAM <Mram_ram> for signal <ram>.
    Found 8-bit register for signal <dout_b>.
    Found 8-bit register for signal <dout_a>.
    Summary:
    inferred   1 RAM(s).
    inferred  16 D-type flip-flop(s).
    inferred   1 Multiplexer(s).
Unit <RamTrueDualPort_1> synthesized.

Synthesizing Unit <RamTrueDualPort_2>.
    Related source file is "C:/Documents and Settings/Brad/Projects/fpgabee/Hardware/FPGABeeCore/RamTrueDualPort.vhd".
        ADDR_WIDTH = 11
        DATA_WIDTH = 8
    Set property "ram_extract = yes" for signal <ram>.
    Set property "ram_style = block" for signal <ram>.
    Found 2048x8-bit dual-port RAM <Mram_ram> for signal <ram>.
    Found 8-bit register for signal <dout_b>.
    Found 8-bit register for signal <dout_a>.
    Summary:
    inferred   1 RAM(s).
    inferred  16 D-type flip-flop(s).
    inferred   2 Multiplexer(s).
Unit <RamTrueDualPort_2> synthesized.

Sin embargo, los bloques de 2k aún terminan distribuidos:

2048x8-bit dual-port distributed RAM                  : 2
4096x8-bit dual-port block RAM                        : 1

Si elimino la línea de dirección redundante (es decir, la devuelvo a addrWidth=11), las tres instancias terminan distribuidas y el diseño ya no encaja:

2048x8-bit dual-port distributed RAM                  : 3

¿Qué hacer? Realmente no quiero volver a Coregen para esto.

PD: Soy un aficionado en esto, ¡sé amable!.

Puede probar nuestra implementación de RAM PoC.mem.ocram.tdp_wf . Esa es una verdadera RAM de doble puerto con escritura primero. Ha sido sintetizado y probado con diferentes herramientas y tableros. Otra implementación reside en la misma carpeta. Los archivos están llenos de comentarios :).
Gracias @Paebbels. Lo revisaré, aunque a primera vista parece que el tuyo usa un reloj compartido para cada puerto. Necesito relojes separados. (También estoy interesado en entender por qué XST no usará bram para esto)
Hmmm, sí, la ram TDP normal tiene doble reloj, la variación WF no; porque los sincronizadores provocan un mayor retardo.

Respuestas (3)

Si sabe exactamente con qué quiere terminar, no es necesario que Xst intente inferirlo a partir de un modelo de comportamiento.

Puede crear una instancia de un objeto RAM de bloque directamente en el código HDL. Los detalles sobre la sintaxis adecuada y las opciones involucradas se pueden encontrar en Xilinx UG615: Spartan-6 Libraries Guide for HDL Designs , alrededor de la página 274 ("RAMB16BWER"). También puede utilizar la macro BRAM_TDP_MACRO, que se explica en la página 20 del mismo documento.

Deberá estar familiarizado con el funcionamiento del elemento de RAM de bloque Spartan-6. La información sobre esto está disponible en Xilinx UG383: Spartan-6 FPGA Block RAM Resources .

(Tenga en cuenta que el bloque de RAM tiene anchos estándar de 9, 18 o 36 bits; probablemente querrá usarlo en modo de 9 bits e ignorar el bit extra. Está ahí para diseños que necesitan bits de paridad).

Gracias @duskwuff. Estaba al tanto de otros enfoques como este, pero quería entender si había una razón para lo que estoy viendo. Pensé que tal vez tenía la sintaxis de atributos un poco mal o algo más trivial. Gracias de nuevo, esos enlaces serán muy útiles.

En primer lugar, los estilos de codificación admitidos están documentados en la guía del usuario de síntesis. Para Xilinx ISE, esta sería la guía del usuario de XST . A partir de la página 200 se explica lo que se admite. Lamentablemente, usa la conv_integerfunción anterior, pero ya está usando los numeric_stdequivalentes más nuevos, y estos funcionarán de la misma manera con XST. Si puede encontrar una versión anterior de este documento, contendrá muchos más ejemplos. También hay ejemplos en ISE; vaya a Edición > Plantillas de idioma > VHDL > Construcciones de síntesis > Ejemplos de codificación > RAM. Estos incluyen un ejemplo específico de 'Escritura primero' en Bloque de RAM > Puerto dual > Puertos asimétricos.

Ahora a tus problemas:

Dependiendo del ancho de la dirección, algunos se infieren como distribuidos

Esto es deliberado. En la mayoría de los casos, no es eficiente usar un bloque de RAM cuando el ancho de la dirección es pequeño. Qué tan pequeño es 'pequeño' dependerá de la familia de dispositivos en uso, pero en general, si uno de los bits de datos se puede implementar utilizando el recurso LUT y cabe en un bloque lógico combinatorio (CLB), entonces una RAM distribuida también funcionará. como lo tendría un bloque de RAM.

Para su dispositivo en particular, mire UG384, que tiene una sección sobre 'RAM distribuida', que contiene una tabla que enumera qué tamaños de RAM se pueden implementar de manera eficiente en un solo CLB. Eso no quiere decir que las memorias más grandes no puedan usar RAM distribuida, solo que una más grande que esta no funcionará a una velocidad de reloj tan alta como lo haría un bloque de RAM.

Además de esto, la RAM distribuida no puede implementar una memoria de doble puerto 'verdadera', por lo que si tiene más de un puerto, y se usan varias direcciones de lectura por separado, o se usan varias habilitaciones de escritura por separado, XST debe inferir RAM de bloque. Normalmente, solo utilizará la interfaz de escritura de un puerto y la interfaz de lectura de otro, por lo que, en mi experiencia, esto rara vez es un factor.

Normalmente, no debería preocuparse por si una memoria termina implementándose utilizando RAM distribuida o en bloque. Piense en ello como una ventaja; debido a que ha inferido la RAM en lugar de instanciado un CoreGen primitivo o usado, las herramientas pueden elegir cómo implementarlo en función de los recursos disponibles en el dispositivo, las restricciones que ha configurado, la configuración de síntesis, etc.

Parece extraño que si su diseño no encaja, no está utilizando las RAM de bloque disponibles. No creo que estés haciendo nada malo. Si realmente desea forzar un bloque de RAM, puede intentar hacer clic derecho en 'Sintetizar', elegir 'Propiedades del proceso', luego en 'Opciones HDL', cambiar 'Estilo RAM' a 'Bloquear'. Tenga en cuenta que en la ayuda, la descripción de 'Auto' es:

XST determina la mejor implementación para cada macro.

En general, desconfiaría de cambiar esto sin una buena comprensión de exactamente por qué el diseño será mejor al forzar el bloque de RAM. Esta opción también podría ser muy derrochadora si tienes muchos pequeños recuerdos.

También puede intentar establecer el 'Objetivo de optimización' de la síntesis en 'Área'. Esto podría cambiar el umbral para cuando se infiere un bloque de RAM sobre una RAM distribuida.

Por último, asegúrese de estar utilizando la última versión de la herramienta (14.7).

Hola @scary_jeff, gracias por la respuesta detallada. La mayor parte de lo que has dicho lo conozco y tiene mucho sentido para mí. Normalmente no me preocupo por dónde terminan los carneros, es solo que no encajaba y necesitaba investigar por qué. No cambiaría las opciones globales/de proyecto para el estilo ram, eso parece un poco extremo. Es frustrante que los atributos que se supone que controlan esto no parecen funcionar. Sí, ejecutando las últimas herramientas (ISE WebPack 14.7). Gracias de nuevo.
@BradRobinson He descubierto antes que el ram_styleatributo solo funcionaba para forzar la RAM distribuida, y no al revés. ¡Siento tu frustración!
¡Bien! Bueno, al menos no solo yo entonces. :)

Entonces, un seguimiento rápido... como parte del cambio a coregen block ram para esto, envolví mi RamTrueDualPort existente dentro de otro módulo que efectivamente acaba de pasar. (La intención era usar genéricos para cambiar la implementación subyacente real).

Resulta que no era necesario: simplemente envolver el módulo original dentro de otro fue suficiente para que XST comenzara a inferir RAM de bloque para los bloques más pequeños.

Imagínate...