Implementación de una FIFO AXI en RAM de bloques usando handshake Ready/Valid
Cuando me encontré por primera vez con la interfaz AXI al diseñar un módulo, me sorprendió su enfoque de control de flujo simplificado. A diferencia de protocolos tradicionales que dependen de pares busy/valid, full/valid o empty/valid, AXI usa solo dos señales: ready y valid. Lo que comenzó como frustración rápidamente se convirtió en fascinación.
AXI integra el control de flujo directamente en su handshake, eliminando la necesidad de líneas de estado adicionales. Aunque las reglas básicas son simples, se requiere atención cuidadosa al mapear el protocolo a hardware FPGA. En este tutorial, recorro una implementación VHDL de una FIFO AXI que funciona en RAM de bloques.
AXI resuelve el problema del retraso de un ciclo
Prevenir sobre-lectura y sobrescritura es un desafío común cuando dos módulos sincronizados por reloj intercambian flujos de datos. El núcleo del problema es que cada módulo solo puede observar la salida del otro un ciclo de reloj después de que ocurre.
El diagrama anterior ilustra un módulo secuencial escribiendo en una FIFO que usa el esquema de señalización clásico write enable/full. El escritor afirma wr_en para meter datos en la FIFO, mientras que la FIFO levanta full cuando no hay más espacio. El problema es que full se afirma exactamente en el flanco ascendente del reloj, por lo que el escritor ya no puede reaccionar a él en el mismo ciclo. La solución es proporcionar una señal de anticipación como almost_empty, dando a la lógica upstream una ventaja.
El handshake ready/valid
Las interfaces AXI stream resuelven este dilema de temporización con un handshake de dos señales: ready y valid. El receptor controla ready; un valor alto indica que está preparado para aceptar un nuevo dato. El emisor controla valid; un valor alto significa que el dato en el bus es estable y puede muestrearse. Una transferencia ocurre solo cuando ambas señales están altas en el mismo flanco ascendente.
La forma de onda demuestra la transferencia de un solo dato. El dato se muestrea en el flanco ascendente del reloj, igual que en diseños síncronos convencionales.
Implementación
Hay varias formas de construir una FIFO AXI en VHDL. Aunque un registro de desplazamiento funcionaría, un buffer anillado respaldado por RAM de bloques es la opción más eficiente y escalable. A continuación, presento un diseño modular que separa la lógica en procesos dedicados para claridad y amigabilidad con el sintetizador.
La entidad
La entidad define dos parámetros genéricos – ram_width y ram_depth – que establecen la anchura de palabra y la profundidad de la FIFO. La capacidad es efectivamente ram_depth - 1, porque un slot se reserva para diferenciar una FIFO llena de una vacía.
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;
La interfaz sigue convenciones AXI: el lado de entrada usa in_ready y in_valid, mientras que el lado de salida refleja las señales con direcciones invertidas. Esta simetría permite encadenar múltiples FIFOs sin problemas.
Declaraciones de señales
Primero, definimos un tipo RAM que coincida con la anchura y profundidad genéricas, luego instanciamos el array:
-- La FIFO está llena cuando la RAM contiene ram_depth - 1 elementos type ram_type is array (0 to ram_depth - 1) of std_logic_vector(in_data'range); signal ram : ram_type;
Luego, declaramos un subtipo de índice que puede representar cualquier dirección en la RAM, y cuatro señales principales: head, tail, count, y una copia retrasada count_p1:
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;
También mantenemos versiones internas de las salidas ready y valid para evitar leer señales out dentro de la arquitectura:
signal in_ready_i : std_logic; signal out_valid_i : std_logic;
Una señal auxiliar rastrea cuándo ocurre una lectura y escritura simultánea en el ciclo anterior, lo cual es crítico para establecer correctamente out_valid cuando la FIFO está casi vacía:
signal read_while_write_p1 : std_logic;
Subprogramas
La función next_index determina la siguiente dirección basada en si un handshake de lectura o escritura acaba de completarse. Se envuelve al final del array.
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;
El procedimiento index_proc encapsula la lógica síncrona que actualiza ya sea el puntero head o tail. Esto evita código duplicado y mantiene la arquitectura ordenada.
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;
Copiar señales internas a la salida
in_ready <= in_ready_i; out_valid <= out_valid_i;
Actualizar head y tail
Instanciamos el procedimiento auxiliar dos veces – una para el puntero de escritura (head) y otra para el puntero de lectura (tail):
PROC_HEAD : index_proc(clk, rst, head, in_ready_i, in_valid); PROC_TAIL : index_proc(clk, rst, tail, out_ready, out_valid_i);
Inferir RAM de bloques
La RAM de bloques en la mayoría de FPGAs es síncrona. Para que el sintetizador infiera una primitiva RAM, mantenemos las operaciones de lectura y escritura dentro de un proceso con reloj y evitamos señales de enable explícitas:
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;
La escritura ocurre incondicionalmente en el ciclo cuando in_valid está alto, y el dato de lectura se actualiza cada ciclo para que el consumidor pueda observar una palabra válida cuando la FIFO contenga datos.
Contar el número de elementos en la FIFO
La señal count refleja la ocupación actual. Cuando head está por delante de tail, el conteo es simplemente la diferencia; si head ha dado la vuelta, sumamos la profundidad total:
PROC_COUNT : process(head, tail) begin if head < tail then count <= head - tail + ram_depth; else count <= head - tail; end if; end process;
La copia retrasada count_p1 se necesita para detectar cuándo la FIFO estaba vacía en el ciclo anterior, lo que influye en out_valid:
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;
Actualizar la salida ready
Al escritor se le debe indicar cuándo la FIFO puede aceptar más datos. La señal ready está alta cuando el conteo es menor que ram_depth - 1:
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
Cuando tanto una lectura como una escritura ocurren en el mismo ciclo, la FIFO no cambia su ocupación pero el estado interno debe reflejar que un valor estará disponible solo después de dos ciclos cuando la FIFO está casi vacía. Por tanto, capturamos este evento en read_while_write_p1:
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 la salida valid
La señal out_valid indica a la lógica downstream cuándo out_data contiene una palabra usable. El proceso combinacional siguiente la pone alta por defecto pero la limpia en dos casos esquina:
PROC_OUT_VALID : process(count, count_p1, read_while_write_p1) begin out_valid_i <= '1'; -- Si la FIFO está vacía o lo estaba en el ciclo anterior if count = 0 or count_p1 = 0 then out_valid_i <= '0'; end if; -- Si ocurrió lectura/escritura simultánea cuando la FIFO tenía solo un elemento if count = 1 and read_while_write_p1 = '1' then out_valid_i <= '0'; end if; end process;
La primera condición garantiza que out_valid permanezca baja durante el retardo de propagación tras la primera escritura. La segunda protege contra una situación donde la FIFO parece permanecer llena un ciclo pero en realidad no tiene datos para emitir.
Síntesis en Vivado
Para sintetizar este diseño en Xilinx Vivado, proporcione valores concretos para los genéricos. En el diálogo Settings (General → Generics/Parameters), elija una profundidad que coincida con una primitiva RAMB36E1 para el dispositivo Zynq objetivo. Tras la implementación, la utilización de recursos es modesta – una RAM de bloques más unos pocos LUTs y FFs.


AXI es más que ready/valid
AXI (Advanced eXtensible Interface) es un componente de la arquitectura de bus AMBA de ARM y ofrece un conjunto de características rico más allá del simple handshake ready/valid. Para comprensión más profunda, considere los siguientes recursos:
- Wikipedia: AXI
- Introducción ARM AXI
- Introducción Xilinx AXI
- Especificación AXI4
VHDL
- Internet Industrial: Revolucionando la gestión de activos mediante conectividad, analítica y experiencia de usuario
- Mejores prácticas de rastreo de activos: Desbloqueando el valor de tus datos de activos
- Cadenas de suministro basadas en datos: Impulsando la logística del mañana
- Garantizando datos confiables en la cadena de suministro: Una estrategia probada
- Cómo la IA resuelve los desafíos de datos "sucios" en las cadenas de suministro
- La revolución cloud: cómo el cloud computing transforma TI, seguridad y carreras
- Desbloqueando datos IoT: Cómo la gestión de reglas de negocio impulsa valor empresarial
- Preparando sus operaciones de fabricación para la IA con IoT
- Mejorando Insight en Internet de las Cosas: Aprovechando Visualización de Datos y Bases de Datos Gráficas
- Maximizar el IoT en Restaurantes: Impulsar la Eficiencia, Reducir Desperdicios y Encantar a los Clientes
- Inicialización de RAM de bloques FPGA desde archivos de texto usando la biblioteca TEXTIO de VHDL