module axi_dmac_burst_memory #(
  parameter DATA_WIDTH_SRC = 64,
  parameter DATA_WIDTH_DEST = 64,
  parameter ID_WIDTH = 3,
  parameter MAX_BYTES_PER_BURST = 128,
  parameter ASYNC_CLK = 1,
  parameter BYTES_PER_BEAT_WIDTH_SRC = $clog2(DATA_WIDTH_SRC/8),
  parameter BYTES_PER_BURST_WIDTH = $clog2(MAX_BYTES_PER_BURST),
  parameter DMA_LENGTH_ALIGN = 3,
  parameter ENABLE_DIAGNOSTICS_IF = 0,
  parameter ALLOW_ASYM_MEM = 0
) (
  input src_clk,
  input src_reset,

  input src_data_valid,
  input [DATA_WIDTH_SRC-1:0] src_data,
  input src_data_last,
  input [BYTES_PER_BEAT_WIDTH_SRC-1:0] src_data_valid_bytes,
  input src_data_partial_burst,

  output [ID_WIDTH-1:0] src_data_request_id,

  input dest_clk,
  input dest_reset,

  output dest_data_valid,
  input dest_data_ready,
  output [DATA_WIDTH_DEST-1:0] dest_data,
  output dest_data_last,
  output [DATA_WIDTH_DEST/8-1:0] dest_data_strb,

  output [BYTES_PER_BURST_WIDTH-1:0] dest_burst_info_length,
  output dest_burst_info_partial,
  output [ID_WIDTH-1:0] dest_burst_info_id,
  output reg dest_burst_info_write = 1'b0,

  output [ID_WIDTH-1:0] dest_request_id,
  input [ID_WIDTH-1:0] dest_data_request_id,
  output [ID_WIDTH-1:0] dest_data_response_id,

  // Diagnostics interface
  output  [7:0] dest_diag_level_bursts
);

localparam DATA_WIDTH_MEM = DATA_WIDTH_SRC > DATA_WIDTH_DEST ?
  DATA_WIDTH_SRC : DATA_WIDTH_DEST;
localparam MEM_RATIO = DATA_WIDTH_SRC > DATA_WIDTH_DEST ?
  DATA_WIDTH_SRC / DATA_WIDTH_DEST : DATA_WIDTH_DEST / DATA_WIDTH_SRC;

/* A burst can have up to 256 beats */
localparam BURST_LEN = MAX_BYTES_PER_BURST / (DATA_WIDTH_MEM / 8);
localparam BURST_LEN_WIDTH = BURST_LEN > 128 ? 8 :
  BURST_LEN > 64 ? 7 :
  BURST_LEN > 32 ? 6 :
  BURST_LEN > 16 ? 5 :
  BURST_LEN > 8 ? 4 :
  BURST_LEN > 4 ? 3 :
  BURST_LEN > 2 ? 2 : 1;

localparam AUX_FIFO_SIZE = 2**(ID_WIDTH-1);

localparam MEM_RATIO_WIDTH =
    (ALLOW_ASYM_MEM == 0 || MEM_RATIO == 1) ? 0 :
    MEM_RATIO == 2 ? 1 :
    MEM_RATIO == 4 ? 2 : 3;

localparam BURST_LEN_WIDTH_SRC = BURST_LEN_WIDTH +
  (DATA_WIDTH_SRC < DATA_WIDTH_MEM ? MEM_RATIO_WIDTH : 0);
localparam BURST_LEN_WIDTH_DEST = BURST_LEN_WIDTH +
  (DATA_WIDTH_DEST < DATA_WIDTH_MEM ? MEM_RATIO_WIDTH : 0);
localparam DATA_WIDTH_MEM_SRC = DATA_WIDTH_MEM >>
  (DATA_WIDTH_SRC < DATA_WIDTH_MEM ? MEM_RATIO_WIDTH : 0);
localparam DATA_WIDTH_MEM_DEST = DATA_WIDTH_MEM >>
  (DATA_WIDTH_DEST < DATA_WIDTH_MEM ? MEM_RATIO_WIDTH : 0);

localparam ADDRESS_WIDTH_SRC = BURST_LEN_WIDTH_SRC + ID_WIDTH - 1;
localparam ADDRESS_WIDTH_DEST = BURST_LEN_WIDTH_DEST + ID_WIDTH - 1;

localparam BYTES_PER_BEAT_WIDTH_MEM_SRC = BYTES_PER_BURST_WIDTH - BURST_LEN_WIDTH_SRC;
localparam BYTES_PER_BEAT_WIDTH_DEST = BYTES_PER_BURST_WIDTH - BURST_LEN_WIDTH_DEST;

/*
 * The burst memory is separated into 2**(ID_WIDTH-1) segments. Each segment can
 * hold up to BURST_LEN beats. The addresses that are used to access the memory
 * are split into two parts. The MSBs index the segment and the LSBs index a
 * beat in a specific segment.
 *
 * src_id and dest_id are used to index the segment of the burst memory on the
 * write and read side respectively. The IDs are 1 bit wider than the address of
 * the burst memory. So we can't use them directly as an index into the burst
 * memory.  Since the IDs are gray counted we also can't just leave out the MSB
 * like with a binary counter. But XOR-ing the two MSBs of a gray counter gives
 * us a gray counter of 1 bit less. Use this to generate the segment index.
 * These addresses are captured in the src_id_reduced and dest_id_reduced
 * signals.
 *
 * src_beat_counter and dest_beat_counter are used to index the beat on the
 * write and read side respectively. They will be incremented for each beat that
 * is written/read. Note that the beat counters are not reset to 0 on the last
 * beat of a burst. This means the first beat of a burst might not be stored at
 * offset 0 in the segment memory. But this is OK since the beat counter
 * increments modulo the segment size and both the write and read side agree on
 * the order.
 */

reg [ID_WIDTH-1:0] src_id_next;
reg [ID_WIDTH-1:0] src_id = 'h0;
reg src_id_reduced_msb = 1'b0;
reg [BURST_LEN_WIDTH_SRC-1:0] src_beat_counter = 'h00;

reg [ID_WIDTH-1:0] dest_id_next = 'h0;
reg dest_id_reduced_msb_next = 1'b0;
reg dest_id_reduced_msb = 1'b0;
reg [ID_WIDTH-1:0] dest_id = 'h0;
reg [BURST_LEN_WIDTH_DEST-1:0] dest_beat_counter = 'h00;
wire [BURST_LEN_WIDTH_DEST-1:0] dest_burst_len;
reg dest_valid = 1'b0;
reg dest_mem_data_valid = 1'b0;
reg dest_mem_data_last = 1'b0;
reg [DATA_WIDTH_MEM_DEST/8-1:0] dest_mem_data_strb = {DATA_WIDTH_MEM_DEST/8{1'b1}};

reg [BYTES_PER_BURST_WIDTH+1-1-DMA_LENGTH_ALIGN:0] burst_len_mem[0:AUX_FIFO_SIZE-1];

wire [BYTES_PER_BURST_WIDTH+1-1:0] src_burst_len_data;
reg [BYTES_PER_BURST_WIDTH+1-1:0] dest_burst_len_data = {DMA_LENGTH_ALIGN{1'b1}};

wire src_beat;
wire src_last_beat;
wire [ID_WIDTH-1:0] src_dest_id;
wire [ADDRESS_WIDTH_SRC-1:0] src_waddr;
wire [ID_WIDTH-2:0] src_id_reduced;
wire src_mem_data_valid;
wire src_mem_data_last;
wire [DATA_WIDTH_MEM_SRC-1:0] src_mem_data;
wire [BYTES_PER_BEAT_WIDTH_MEM_SRC-1:0] src_mem_data_valid_bytes;
wire src_mem_data_partial_burst;

wire dest_beat;
wire dest_last_beat;
wire dest_last;
wire [ID_WIDTH-1:0] dest_src_id;
wire [ADDRESS_WIDTH_DEST-1:0] dest_raddr;
wire [ID_WIDTH-2:0] dest_id_reduced_next;
wire [ID_WIDTH-1:0] dest_id_next_inc;
wire [ID_WIDTH-2:0] dest_id_reduced;
wire dest_burst_valid;
wire dest_burst_ready;
wire dest_ready;
wire [DATA_WIDTH_MEM_DEST-1:0] dest_mem_data;
wire dest_mem_data_ready;

`include "inc_id.vh"

generate if (ID_WIDTH >= 3) begin
  assign src_id_reduced = {src_id_reduced_msb,src_id[ID_WIDTH-3:0]};
  assign dest_id_reduced_next = {dest_id_reduced_msb_next,dest_id_next[ID_WIDTH-3:0]};
  assign dest_id_reduced = {dest_id_reduced_msb,dest_id[ID_WIDTH-3:0]};
end else begin
  assign src_id_reduced = src_id_reduced_msb;
  assign dest_id_reduced_next = dest_id_reduced_msb_next;
  assign dest_id_reduced = dest_id_reduced_msb;
end endgenerate

assign src_beat = src_mem_data_valid;
assign src_last_beat = src_beat & src_mem_data_last;
assign src_waddr = {src_id_reduced,src_beat_counter};

assign src_data_request_id = src_dest_id;

always @(*) begin
  if (src_last_beat == 1'b1) begin
    src_id_next <= inc_id(src_id);
  end else begin
    src_id_next <= src_id;
  end
end

always @(posedge src_clk) begin
  if (src_reset == 1'b1) begin
    src_id <= 'h00;
    src_id_reduced_msb <= 1'b0;
  end else begin
    src_id <= src_id_next;
    src_id_reduced_msb <= ^src_id_next[ID_WIDTH-1-:2];
  end
end

always @(posedge src_clk) begin
  if (src_reset == 1'b1 || src_last_beat == 1'b1) begin
    src_beat_counter <= 'h00;
  end else if (src_beat == 1'b1) begin
    src_beat_counter <= src_beat_counter + 1'b1;
  end
end

always @(posedge src_clk) begin
  if (src_last_beat == 1'b1) begin
    burst_len_mem[src_id_reduced] <= src_burst_len_data[BYTES_PER_BURST_WIDTH:DMA_LENGTH_ALIGN];
  end
end

assign dest_ready = ~dest_mem_data_valid | dest_mem_data_ready;
assign dest_last = dest_beat_counter == dest_burst_len;

assign dest_beat = dest_valid & dest_ready;
assign dest_last_beat = dest_last & dest_beat;
assign dest_raddr = {dest_id_reduced,dest_beat_counter};

assign dest_burst_valid = dest_data_request_id != dest_id_next;
assign dest_burst_ready = ~dest_valid | dest_last_beat;

/*
 * The data valid signal for the destination side is asserted if there are one
 * or more pending bursts. It is de-asserted if there are no more pending burst
 * and it is the last beat of the current burst
 */
always @(posedge dest_clk) begin
  if (dest_reset == 1'b1) begin
    dest_valid <= 1'b0;
  end else if (dest_burst_valid == 1'b1) begin
    dest_valid <= 1'b1;
  end else if (dest_last_beat == 1'b1) begin
    dest_valid <= 1'b0;
  end
end

/*
 * The output register of the memory creates a extra clock cycle of latency on
 * the data path. We need to handle this more the handshaking signals. If data
 * is available in the memory it will be available one clock cycle later in the
 * output register.
 */
always @(posedge dest_clk) begin
  if (dest_reset == 1'b1) begin
    dest_mem_data_valid <= 1'b0;
  end else if (dest_valid == 1'b1) begin
    dest_mem_data_valid <= 1'b1;
  end else if (dest_mem_data_ready == 1'b1) begin
    dest_mem_data_valid <= 1'b0;
  end
end

/*
 * This clears dest_data_last after the last beat. Strictly speaking this is not
 * necessary if this followed AXI handshaking rules since dest_data_last would
 * be qualified by dest_data_valid and it is OK to retain the previous value of
 * dest_data_last when dest_data_valid is not asserted. But clearing the signal
 * here doesn't cost much and can simplify some of the more congested
 * combinatorical logic further up the pipeline since we can assume that
 * fifo_last == 1'b1 implies fifo_valid == 1'b1.
 */
always @(posedge dest_clk) begin
  if (dest_reset == 1'b1) begin
    dest_mem_data_last <= 1'b0;
  end else if (dest_beat == 1'b1) begin
    dest_mem_data_last <= dest_last;
  end else if (dest_mem_data_ready == 1'b1) begin
    dest_mem_data_last <= 1'b0;
  end
end

always @(posedge dest_clk) begin
  if (dest_beat == 1'b1) begin
    if (dest_last == 1'b1) begin
      dest_mem_data_strb <= {DATA_WIDTH_MEM_DEST/8{1'b1}} >> ~dest_burst_len_data[BYTES_PER_BEAT_WIDTH_DEST-1:0];
    end else begin
      dest_mem_data_strb <= {DATA_WIDTH_MEM_DEST/8{1'b1}};
    end
  end
end

assign dest_id_next_inc = inc_id(dest_id_next);

always @(posedge dest_clk) begin
  if (dest_reset == 1'b1) begin
    dest_id_next <= 'h00;
    dest_id_reduced_msb_next <= 1'b0;
  end else if (dest_burst_valid == 1'b1 && dest_burst_ready == 1'b1) begin
    dest_id_next <= dest_id_next_inc;
    dest_id_reduced_msb_next <= ^dest_id_next_inc[ID_WIDTH-1-:2];
  end
end

always @(posedge dest_clk) begin
  if (dest_burst_valid == 1'b1 && dest_burst_ready == 1'b1) begin
    dest_burst_len_data[BYTES_PER_BURST_WIDTH:DMA_LENGTH_ALIGN] <= burst_len_mem[dest_id_reduced_next];
  end
end

always @(posedge dest_clk) begin
  if (dest_burst_ready == 1'b1) begin
    dest_id <= dest_id_next;
    dest_id_reduced_msb <= dest_id_reduced_msb_next;
  end
end

always @(posedge dest_clk) begin
  if (dest_reset == 1'b1 || dest_last_beat == 1'b1) begin
    dest_beat_counter <= 'h00;
  end else if (dest_beat == 1'b1) begin
    dest_beat_counter <= dest_beat_counter + 1'b1;
  end
end

assign dest_burst_info_length = dest_burst_len_data[BYTES_PER_BURST_WIDTH-1:0];
assign dest_burst_info_partial = dest_burst_len_data[BYTES_PER_BURST_WIDTH];
assign dest_burst_info_id = dest_id;

always @(posedge dest_clk) begin
  dest_burst_info_write <= (dest_burst_valid == 1'b1 && dest_burst_ready == 1'b1);
end

assign dest_burst_len = dest_burst_len_data[BYTES_PER_BURST_WIDTH-1 -: BURST_LEN_WIDTH_DEST];

axi_dmac_resize_src #(
  .DATA_WIDTH_SRC (DATA_WIDTH_SRC),
  .BYTES_PER_BEAT_WIDTH_SRC (BYTES_PER_BEAT_WIDTH_SRC),
  .DATA_WIDTH_MEM (DATA_WIDTH_MEM_SRC),
  .BYTES_PER_BEAT_WIDTH_MEM (BYTES_PER_BEAT_WIDTH_MEM_SRC)
) i_resize_src (
  .clk (src_clk),
  .reset (src_reset),

  .src_data_valid (src_data_valid),
  .src_data (src_data),
  .src_data_last (src_data_last),
  .src_data_valid_bytes (src_data_valid_bytes),
  .src_data_partial_burst (src_data_partial_burst),

  .mem_data_valid (src_mem_data_valid),
  .mem_data (src_mem_data),
  .mem_data_last (src_mem_data_last),
  .mem_data_valid_bytes (src_mem_data_valid_bytes),
  .mem_data_partial_burst (src_mem_data_partial_burst)
);

assign src_burst_len_data = {src_mem_data_partial_burst,
                             src_beat_counter,
                             src_mem_data_valid_bytes};

ad_mem_asym #(
  .A_ADDRESS_WIDTH (ADDRESS_WIDTH_SRC),
  .A_DATA_WIDTH (DATA_WIDTH_MEM_SRC),
  .B_ADDRESS_WIDTH (ADDRESS_WIDTH_DEST),
  .B_DATA_WIDTH (DATA_WIDTH_MEM_DEST)
) i_mem (
  .clka (src_clk),
  .wea (src_beat),
  .addra (src_waddr),
  .dina (src_mem_data),

  .clkb (dest_clk),
  .reb (dest_beat),
  .addrb (dest_raddr),
  .doutb (dest_mem_data)
);

axi_dmac_resize_dest #(
  .DATA_WIDTH_DEST (DATA_WIDTH_DEST),
  .DATA_WIDTH_MEM (DATA_WIDTH_MEM_DEST)
) i_resize_dest (
  .clk (dest_clk),
  .reset (dest_reset),

  .mem_data_valid (dest_mem_data_valid),
  .mem_data_ready (dest_mem_data_ready),
  .mem_data (dest_mem_data),
  .mem_data_last (dest_mem_data_last),
  .mem_data_strb (dest_mem_data_strb),

  .dest_data_valid (dest_data_valid),
  .dest_data_ready (dest_data_ready),
  .dest_data (dest_data),
  .dest_data_last (dest_data_last),
  .dest_data_strb (dest_data_strb)
);

sync_bits #(
  .NUM_OF_BITS (ID_WIDTH),
  .ASYNC_CLK (ASYNC_CLK)
) i_dest_sync_id (
  .in_bits (src_id),
  .out_clk (dest_clk),
  .out_resetn (1'b1),
  .out_bits (dest_src_id)
);

sync_bits #(
  .NUM_OF_BITS (ID_WIDTH),
  .ASYNC_CLK (ASYNC_CLK)
) i_src_sync_id (
  .in_bits (dest_id),
  .out_clk (src_clk),
  .out_resetn (1'b1),
  .out_bits (src_dest_id)
);

assign dest_request_id = dest_src_id;
assign dest_data_response_id = dest_id;

generate if (ENABLE_DIAGNOSTICS_IF == 1) begin

  reg [ID_WIDTH-1:0] _dest_diag_level_bursts = 'h0;

  // calculate buffer fullness in bursts
  always @(posedge dest_clk) begin
    if (dest_reset == 1'b1) begin
      _dest_diag_level_bursts <= 'h0;
    end else begin
      _dest_diag_level_bursts <= g2b(dest_src_id) - g2b(dest_id);
    end
  end
  assign dest_diag_level_bursts = {{{8-ID_WIDTH}{1'b0}},_dest_diag_level_bursts};

end else begin
  assign dest_diag_level_bursts = 'h0;
end
endgenerate

endmodule