`timescale 1ns/1ps
module async_fifo #(
    parameter int DATA_WIDTH = 8,
    parameter int ADDR_WIDTH = 3
) (
    input  logic                  arst_n,
    input  logic                  wr_clk,
    input  logic                  wr_en,
    input  logic [DATA_WIDTH-1:0] wr_data,
    output logic                  wr_full,
    input  logic                  rd_clk,
    input  logic                  rd_en,
    output logic [DATA_WIDTH-1:0] rd_data,
    output logic                  rd_valid,
    output logic                  rd_empty
);
    localparam int DEPTH = 2 ** ADDR_WIDTH;
    localparam int PTR_WIDTH = ADDR_WIDTH + 1;
    localparam logic [PTR_WIDTH-1:0] FULL_MASK = PTR_WIDTH'(3) << (PTR_WIDTH - 2);

    logic [DATA_WIDTH-1:0] memory [0:DEPTH-1];
    logic [PTR_WIDTH-1:0] wr_binary, rd_binary, wr_gray, rd_gray;
    logic [PTR_WIDTH-1:0] wr_binary_next, rd_binary_next, wr_gray_next, rd_gray_next;
    (* async_reg = "true" *) logic wr_reset_meta, wr_rst_n;
    (* async_reg = "true" *) logic rd_reset_meta, rd_rst_n;
    (* async_reg = "true" *) logic [PTR_WIDTH-1:0] rd_gray_wr_meta, rd_gray_wr_sync;
    (* async_reg = "true" *) logic [PTR_WIDTH-1:0] wr_gray_rd_meta, wr_gray_rd_sync;
    logic wr_full_reg, rd_empty_reg, wr_take, rd_take;

    // The common reset flushes both domains. Each domain releases locally.
    always_ff @(posedge wr_clk or negedge arst_n) begin
        if (!arst_n) begin
            wr_reset_meta <= 1'b0;
            wr_rst_n <= 1'b0;
        end else begin
            wr_reset_meta <= 1'b1;
            wr_rst_n <= wr_reset_meta;
        end
    end

    always_ff @(posedge rd_clk or negedge arst_n) begin
        if (!arst_n) begin
            rd_reset_meta <= 1'b0;
            rd_rst_n <= 1'b0;
        end else begin
            rd_reset_meta <= 1'b1;
            rd_rst_n <= rd_reset_meta;
        end
    end
    assign wr_full = !wr_rst_n || wr_full_reg;
    assign rd_empty = !rd_rst_n || rd_empty_reg;
    assign wr_take = wr_en && !wr_full;
    assign rd_take = rd_en && !rd_empty;

    // Synchronize registered Gray pointers. Memory data crosses through
    // the dual-port storage and is read only after pointer synchronization.
    always_ff @(posedge wr_clk or negedge wr_rst_n) begin
        if (!wr_rst_n) begin
            rd_gray_wr_meta <= '0;
            rd_gray_wr_sync <= '0;
        end else begin
            rd_gray_wr_meta <= rd_gray;
            rd_gray_wr_sync <= rd_gray_wr_meta;
        end
    end

    always_ff @(posedge rd_clk or negedge rd_rst_n) begin
        if (!rd_rst_n) begin
            wr_gray_rd_meta <= '0;
            wr_gray_rd_sync <= '0;
        end else begin
            wr_gray_rd_meta <= wr_gray;
            wr_gray_rd_sync <= wr_gray_rd_meta;
        end
    end

    assign wr_binary_next = wr_binary + PTR_WIDTH'(wr_take);
    assign rd_binary_next = rd_binary + PTR_WIDTH'(rd_take);
    assign wr_gray_next = wr_binary_next ^ (wr_binary_next >> 1);
    assign rd_gray_next = rd_binary_next ^ (rd_binary_next >> 1);

    always_ff @(posedge wr_clk or negedge wr_rst_n) begin
        if (!wr_rst_n) begin
            wr_binary <= '0;
            wr_gray <= '0;
            wr_full_reg <= 1'b0;
        end else begin
            wr_binary <= wr_binary_next;
            wr_gray <= wr_gray_next;
            wr_full_reg <= wr_gray_next == (rd_gray_wr_sync ^ FULL_MASK);
        end
    end

    always_ff @(posedge rd_clk or negedge rd_rst_n) begin
        if (!rd_rst_n) begin
            rd_binary <= '0;
            rd_gray <= '0;
            rd_empty_reg <= 1'b1;
            rd_valid <= 1'b0;
        end else begin
            rd_binary <= rd_binary_next;
            rd_gray <= rd_gray_next;
            rd_empty_reg <= rd_gray_next == wr_gray_rd_sync;
            rd_valid <= rd_take;
        end
    end

    // No reset on the RAM or read data register. rd_valid qualifies rd_data.
    always_ff @(posedge wr_clk)
        if (wr_take) memory[wr_binary[ADDR_WIDTH-1:0]] <= wr_data;

    always_ff @(posedge rd_clk)
        if (rd_take) rd_data <= memory[rd_binary[ADDR_WIDTH-1:0]];
endmodule
