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

Cómo hacer un AXI FIFO en RAM de bloque usando el apretón de manos listo/válido

Estaba un poco molesto por las peculiaridades de la interfaz AXI la primera vez que tuve que crear lógica para interconectar un módulo AXI. En lugar de las señales de control habituales ocupado/válido, completo/válido o vacío/válido, la interfaz AXI utiliza dos señales de control denominadas "listo" y "válido". Mi frustración pronto se transformó en asombro.

La interfaz AXI tiene control de flujo incorporado sin usar señales de control adicionales. Las reglas son bastante fáciles de entender, pero hay algunas trampas que se deben tener en cuenta al implementar la interfaz AXI en una FPGA. Este artículo le muestra cómo crear un AXI FIFO en VHDL.

AXI resuelve el problema del retraso de un ciclo

Evitar la sobrelectura y la sobrescritura es un problema común al crear interfaces de flujo de datos. El problema es que cuando dos módulos lógicos sincronizados se comunican, cada módulo solo podrá leer las salidas de su contraparte con un retraso de ciclo de reloj.

La imagen de arriba muestra el diagrama de tiempo de un módulo secuencial que escribe en un FIFO que usa habilitar escritura/completa esquema de señalización. Un módulo de interfaz escribe datos en el FIFO afirmando el wr_en señal. El FIFO afirmará el full señal cuando no hay espacio para otro elemento de datos, lo que hace que la fuente de datos deje de escribir.

Desafortunadamente, el módulo de interfaz no tiene forma de detenerse a tiempo siempre que use solo lógica sincronizada. El FIFO sube el full bandera exactamente en el flanco ascendente del reloj. Simultáneamente, el módulo de interfaz intenta escribir el siguiente elemento de datos. No puede muestrear y reaccionar al full señal antes de que sea demasiado tarde.

Una solución es incluir un almost_empty extra señal, hicimos esto en el tutorial Cómo crear un FIFO de búfer de anillo en VHDL. La señal adicional precede al empty señal, lo que le da al módulo de interfaz tiempo para reaccionar.

El apretón de manos listo/válido

El protocolo AXI implementa el control de flujo usando solo dos señales de control en cada dirección, una llamada ready y el otro valid . El ready la señal es controlada por el receptor, un '1' lógico valor en esta señal significa que el receptor está listo para aceptar un nuevo elemento de datos. El valid la señal, por otro lado, es controlada por el emisor. El remitente establecerá valid a '1' cuando los datos presentados en el bus de datos son válidos para el muestreo.

Aquí viene la parte importante: la transferencia de datos solo ocurre cuando ready y valid son '1' en el mismo ciclo de reloj. El receptor informa cuando está listo para aceptar datos, y el remitente simplemente envía los datos cuando tiene algo que transmitir. La transferencia ocurre cuando ambos están de acuerdo, cuando el remitente está listo para enviar y el receptor está listo para recibir.

La forma de onda anterior muestra una transacción de ejemplo de un elemento de datos. El muestreo se produce en el flanco ascendente del reloj, como suele ser el caso con la lógica sincronizada.

Implementación

Hay muchas formas de implementar un AXI FIFO en VHDL. Podría ser un registro de desplazamiento, pero usaremos una estructura de búfer de anillo porque es la forma más sencilla de crear un FIFO en RAM de bloque. Puede crearlo todo en un proceso gigante usando variables y señales, o puede dividir la funcionalidad en múltiples procesos.

Esta implementación utiliza procesos separados para la mayoría de las señales que deben actualizarse. Solo los procesos que necesitan ser sincrónicos son sensibles al reloj, los demás usan lógica combinacional.

La entidad

La declaración de entidad incluye un puerto genérico que se utiliza para configurar el ancho de las palabras de entrada y salida, así como el número de ranuras para reservar espacio en la RAM. La capacidad del FIFO es igual a la profundidad de RAM menos uno. Una ranura siempre se mantiene vacía para distinguir entre un FIFO lleno y uno vacío.

entity axi_fifo is
  generic (
    ram_width : natural;
    ram_depth : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- AXI input interface
    in_ready : out std_logic;
    in_valid : in std_logic;
    in_data : in std_logic_vector(ram_width - 1 downto 0);

    -- AXI output interface
    out_ready : in std_logic;
    out_valid : out std_logic;
    out_data : out std_logic_vector(ram_width - 1 downto 0)
  );
end axi_fifo; 

Las dos primeras señales en la declaración del puerto son las entradas de reloj y reinicio. Esta implementación utiliza un reinicio síncrono y es sensible al flanco ascendente del reloj.

Hay una interfaz de entrada de estilo AXI que usa las señales de control listas/válidas y una señal de datos de entrada de ancho genérico. Finalmente viene la interfaz de salida AXI con señales similares a las de entrada, solo que con direcciones invertidas. Las señales que pertenecen a la interfaz de entrada y salida tienen el prefijo in_ o out_ .

La salida de un AXI FIFO podría conectarse directamente a la entrada de otro, las interfaces encajan perfectamente entre sí. Aunque, una mejor solución que apilarlos sería aumentar el ram_depth genérico si desea un FIFO más grande.

Declaraciones de señales

Las dos primeras declaraciones en la región declarativa del archivo VHDL declaran el tipo de RAM y su señal. La RAM se dimensiona dinámicamente a partir de las entradas genéricas.

-- The FIFO is full when the RAM contains ram_depth - 1 elements
type ram_type is array (0 to ram_depth - 1)
  of std_logic_vector(in_data'range);
signal ram : ram_type;

El segundo bloque de código declara un nuevo subtipo entero y cuatro señales del mismo. El index_type está dimensionado para representar exactamente la profundidad de la RAM. El head La señal siempre indica la ranura de RAM que se utilizará en la siguiente operación de escritura. El tail la señal apunta a la ranura a la que se accederá en la siguiente operación de lectura. El valor del count la señal siempre es igual al número de elementos actualmente almacenados en el FIFO, y count_p1 es una copia de la misma señal retrasada por un ciclo de reloj.

-- Newest element at head, oldest element at tail
subtype index_type is natural range ram_type'range;
signal head : index_type;
signal tail : index_type;
signal count : index_type;
signal count_p1 : index_type;

Luego vienen dos señales llamadas in_ready_i y out_valid_i . Estas son simplemente copias de las salidas de la entidad in_ready y out_valid . El _i sufijo solo significa interno , es parte de mi estilo de codificación.

-- Internal versions of entity signals with mode "out"
signal in_ready_i : std_logic;
signal out_valid_i : std_logic;

Finalmente, declaramos una señal que se utilizará para indicar una lectura y escritura simultáneas. Explicaré su propósito más adelante en este artículo.

-- True the clock cycle after a simultaneous read and write
signal read_while_write_p1 : std_logic;

Subprogramas

Después de las señales, declaramos una función para incrementar nuestro index_type personalizado . El next_index función mira el read y valid parámetros para determinar si hay una transacción de lectura o lectura/escritura en curso. Si ese es el caso, el índice se incrementará o ajustará. De lo contrario, se devuelve el valor del índice sin cambios.

function next_index(
  index : index_type;
  ready : std_logic;
  valid : std_logic) return index_type is
begin
  if ready = '1' and valid = '1' then
    if index = index_type'high then
      return index_type'low;
    else
      return index + 1;
    end if;
  end if;

  return index;
end function;

Para salvarnos de la escritura repetitiva, creamos la lógica para actualizar el head y tail señales en un procedimiento, en lugar de dos procesos idénticos. El update_index El procedimiento toma el reloj y reinicia las señales, una señal de index_type , un ready señal y un valid señal como entradas.

procedure index_proc(
  signal clk : in std_logic;
  signal rst : in std_logic;
  signal index : inout index_type;
  signal ready : in std_logic;
  signal valid : in std_logic) is
begin
    if rising_edge(clk) then
      if rst = '1' then
        index <= index_type'low;
      else
        index <= next_index(index, ready, valid);
      end if;
    end if;
end procedure;

Este proceso totalmente síncrono utiliza el next_index función para actualizar el index señal cuando el módulo está fuera de reinicio. Cuando se reinicia, el index la señal se establecerá en el valor más bajo que pueda representar, que siempre es 0 debido a cómo index_type y ram_type se declara. Podríamos haber usado 0 como el valor de reinicio, pero hago todo lo posible para evitar la codificación.

Copia señales internas a la salida

Estas dos sentencias simultáneas copian las versiones internas de las señales de salida en las salidas reales. Necesitamos operar en copias internas porque VHDL no nos permite leer señales de entidad con el modo out dentro del módulo. Una alternativa hubiera sido declarar in_ready y out_valid con modo inout , pero la mayoría de los estándares de codificación de empresas restringen el uso de inout señales de entidad.

in_ready <= in_ready_i;
out_valid <= out_valid_i;

Actualiza la cabeza y la cola

Ya hemos discutido el index_proc procedimiento que se utiliza para actualizar el head y tail señales Al asignar las señales apropiadas a los parámetros de este subprograma, obtenemos el equivalente de dos procesos idénticos, uno para controlar la entrada FIFO y otro para la salida.

-- Update head index on write
PROC_HEAD : index_proc(clk, rst, head, in_ready_i, in_valid);

-- Update tail index on read
PROC_TAIL : index_proc(clk, rst, tail, out_ready, out_valid_i);

Dado que tanto el head y el tail se establecen en el mismo valor por la lógica de reinicio, el FIFO estará vacío inicialmente. Así es como funciona este búfer circular, cuando ambos apuntan al mismo índice significa que el FIFO está vacío.

Inferir RAM de bloque

En la mayoría de las arquitecturas FPGA, las primitivas de RAM de bloque son componentes totalmente síncronos. Esto significa que si queremos que la herramienta de síntesis infiera bloques de RAM de nuestro código VHDL, debemos colocar los puertos de lectura y escritura dentro de un proceso cronometrado. Además, no puede haber valores de reinicio asociados con el bloque de RAM.

PROC_RAM : process(clk)
begin
  if rising_edge(clk) then
    ram(head) <= in_data;
    out_data <= ram(next_index(tail, out_ready, out_valid_i));
  end if;
end process;

No hay habilitación de lectura o habilitar escritura aquí, eso sería demasiado lento para AXI. En cambio, estamos escribiendo continuamente en la ranura de RAM a la que apunta el head índice. Luego, cuando determinamos que se ha producido una transacción de escritura, simplemente avanzamos el head para bloquear el valor escrito.

Asimismo, out_data se actualiza en cada ciclo de reloj. El tail el puntero simplemente se mueve a la siguiente ranura cuando ocurre una lectura. Tenga en cuenta que el next_index La función se utiliza para calcular la dirección del puerto de lectura. Tenemos que hacer esto para asegurarnos de que la RAM reaccione lo suficientemente rápido después de una lectura y comience a generar el siguiente valor.

Cuenta el número de elementos en el FIFO

Contar el número de elementos en la RAM es simplemente cuestión de restar el head del tail . Si el head ha terminado, tenemos que compensarlo por el número total de ranuras en la RAM. Tenemos acceso a esta información a través del ram_depth constante de la entrada genérica.

PROC_COUNT : process(head, tail)
begin
  if head < tail then
    count <= head - tail + ram_depth;
  else
    count <= head - tail;
  end if;
end process;

También necesitamos realizar un seguimiento del valor anterior del count señal. El proceso a continuación crea una versión que se retrasa un ciclo de reloj. El _p1 postfix es una convención de nomenclatura para indicar esto.

PROC_COUNT_P1 : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      count_p1 <= 0;
    else
      count_p1 <= count;
    end if;
  end if;
end process;

Actualiza el listo salida

El in_ready la señal será '1' cuando este módulo esté listo para aceptar otro elemento de datos. Este debería ser el caso siempre que FIFO no esté lleno, y eso es exactamente lo que dice la lógica de este proceso.

PROC_IN_READY : process(count)
begin
  if count < ram_depth - 1 then
    in_ready_i <= '1';
  else
    in_ready_i <= '0';
  end if;
end process;

Detectar lectura y escritura simultánea

Debido a un caso de esquina que explicaré en la siguiente sección, necesitamos poder identificar operaciones simultáneas de lectura y escritura. Cada vez que haya transacciones válidas de lectura y escritura durante el mismo ciclo de reloj, este proceso establecerá el read_while_write_p1 señal a '1' en el siguiente ciclo de reloj.

PROC_READ_WHILE_WRITE_P1: process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      read_while_write_p1 <= '0';

    else
      read_while_write_p1 <= '0';
      if in_ready_i = '1' and in_valid = '1' and
        out_ready = '1' and out_valid_i = '1' then
        read_while_write_p1 <= '1';
      end if;
    end if;
  end if;
end process;

Actualizar el válido salida

El out_valid señal indica a los módulos aguas abajo que los datos presentados en out_data es válido y puede ser muestreado en cualquier momento. El out_data la señal proviene directamente de la salida de RAM. Implementando el out_valid la señal es un poco complicada debido a la demora adicional del ciclo de reloj entre la entrada y la salida de RAM del bloque.

La lógica se implementa en un proceso combinacional para que pueda reaccionar sin demora a la señal de entrada cambiante. La primera línea del proceso es un valor predeterminado que establece el out_valid señal a '1' . Este será el valor prevaleciente si no se activa ninguna de las dos declaraciones If posteriores.

PROC_OUT_VALID : process(count, count_p1, read_while_write_p1)
begin
  out_valid_i <= '1';

  -- If the RAM is empty or was empty in the prev cycle
  if count = 0 or count_p1 = 0 then
    out_valid_i <= '0';
  end if;

  -- If simultaneous read and write when almost empty
  if count = 1 and read_while_write_p1 = '1' then
    out_valid_i <= '0';
  end if;

end process;

La primera instrucción If verifica si el FIFO está vacío o estuvo vacío en el ciclo de reloj anterior. Obviamente, el FIFO está vacío cuando tiene 0 elementos, pero también debemos examinar el nivel de llenado del FIFO en el ciclo de reloj anterior.

Considere la siguiente forma de onda. Inicialmente, el FIFO está vacío, como lo indica el count la señal es 0 . Luego, se produce una escritura en el tercer ciclo de reloj. La ranura 0 de RAM se actualiza en el siguiente ciclo de reloj, pero se necesita un ciclo adicional antes de que los datos aparezcan en el out_data producción. El propósito del or count_p1 = 0 declaración es asegurarse de que out_valid permanece '0' (encerrado en un círculo rojo) mientras el valor se propaga a través de la RAM.

La última sentencia If protege contra otro caso de esquina. Acabamos de hablar sobre cómo manejar el caso especial de escritura en vacío al verificar los niveles de llenado FIFO actuales y anteriores. Pero, ¿qué sucede si y realizamos una lectura y escritura simultáneas cuando count ya es 1 ?

La siguiente forma de onda muestra tal situación. Inicialmente, hay un elemento de datos D0 presente en el FIFO. Ha estado allí por un tiempo, así que tanto count y count_p1 son 0 . Luego aparece una lectura y escritura simultáneas en el tercer ciclo de reloj. Un elemento sale del FIFO y entra uno nuevo, dejando los contadores sin cambios.

En el momento de leer y escribir, no hay ningún valor siguiente en la RAM listo para ser emitido, como lo habría sido si el nivel de llenado fuera superior a uno. Tenemos que esperar dos ciclos de reloj antes de que el valor de entrada aparezca en la salida. Sin ninguna información adicional, sería imposible detectar este caso de esquina y el valor de out_valid en el siguiente ciclo de reloj (marcado en rojo sólido) se establecería erróneamente en '1' .

Por eso necesitamos el read_while_write_p1 señal. Detecta que ha habido una lectura y escritura simultáneas, y podemos tener esto en cuenta configurando out_valid al '0' en ese ciclo de reloj.

Sintetizar en Vivado

Para implementar el diseño como un módulo independiente en Xilinx Vivado, primero debemos dar valores a las entradas genéricas. Esto se puede lograr en Vivado usando la ConfiguraciónGeneralesGenéricos/Parámetros menú, como se muestra en la imagen de abajo.

Los valores genéricos se eligieron para que coincidan con la primitiva RAMB36E1 en la arquitectura Xilinx Zynq, que es el dispositivo de destino. El uso de recursos posterior a la implementación se muestra en la siguiente imagen. El AXI FIFO utiliza un bloque de RAM y una pequeña cantidad de LUT y flip-flops.

AXI está más que listo/válido

AXI significa Interfaz extensible avanzada, es parte del estándar de arquitectura de bus de microcontrolador avanzado (AMBA) de ARM. El estándar AXI es mucho más que el apretón de manos leído/válido. Si quieres saber más sobre AXI, te recomiendo estos recursos para leer más:


VHDL

  1. La nube y cómo está cambiando el mundo de las TI
  2. Cómo aprovechar al máximo sus datos
  3. Cómo inicializar RAM desde un archivo usando TEXTIO
  4. Cómo prepararse para la IA utilizando IoT
  5. Cómo la Internet industrial está cambiando la gestión de activos
  6. Mejores prácticas de seguimiento de activos:cómo aprovechar al máximo los datos de activos que tanto le costó ganar
  7. ¿Cómo podemos obtener una mejor imagen del IoT?
  8. Cómo aprovechar al máximo IoT en el negocio de los restaurantes
  9. Cómo los datos están habilitando la cadena de suministro del futuro
  10. Cómo hacer que los datos de la cadena de suministro sean confiables
  11. Cómo la IA aborda el problema de los datos "sucios"