/**********************************************************
 * 
 * USB Video packet sender
 * (c) Bigscreen, Inc. 2024
 * Created by: David Miller
 *
 * Sends USB Video Class (UVC) data packets containing 
 * video frame data.
 * Interfaces with a frame buffer to pull captured video
 * data and forward it to USB.
 *
 * This "2" version includes a last input, allows the buffer
 * to send small chunks of a video frame rather than always
 * needing the entire frame at once.
 * The "last_buffer_of_frame" input is high when the buffer
 * contains the final data for this video frame.
 *
 *********************************************************/

 `timescale 1ns/1ps

module video_packet_sender
#(
    parameter VIDEO_STREAM_ENDPOINT = 4'h1,
    parameter MAX_PACKET_LEN = 512,
    parameter MAX_PAYLOAD_SIZE = 512 // note that payload size is defined as header + data
)
(
    input clk, 
    input areset, // async active high reset

    // USB Device controller interface
    input txact, // transmission active, holds high for the duration of a packet
    input txpop, // tells us to prepare the next byte
    input pktfin, // end of this tx packet
    output [7:0] txdat, // data to write over USB
//    output txcork, // assert high to indicate no data is ready
    output txcork, // for isochronous must never NAK. Instead reply with zero length packet
    output [11:0] txdat_len, // how many bytes are we writing?

    // Video buffer interface
    input [7:0] video_buffer_data, // byte data from the video buffer
    input [15:0] video_buffer_length, // length in bytes of this frame's buffer. 
                                     // Should be held same value through entire frame readout.
    input last_buffer_of_frame,     // true when the data in the buffer is the last one for this frame
    input buffer_ready, // true when there is valid data to send
    output [15:0] buffer_address, // not sure if 16 bits is required, but it will be truncated anyway
    output buffer_complete // set true when reading the last byte (or after), reset when buffer_ready de-asserted

);

localparam HEADER_SIZE = 2;

wire empty; // true when we are all out of data to send.
reg [11:0] packet_byte_count; // read pointer for this packet, resets to zero on each packet
reg [15:0] bytes_remaining; // counter for the buffer read address, how far in the frame we have sent
reg [15:0] buf_ptr; // output address to read from the frame buffer

reg uvc_eof; // end of frame, sent in the payload header
reg uvc_frame_id; // frame id (toggles 0 or 1), sent in the payload header


assign buffer_address = buf_ptr;

reg [1:0] vps_state;
localparam      VPS_STATE_IDLE = 'd0,
                VPS_STATE_SENDING = 'd1,
                VPS_STATE_FINISHED = 'd2;

always @(posedge clk or posedge areset) begin
    if(areset) begin
        vps_state <= VPS_STATE_IDLE;
    end else begin
        case(vps_state)
        VPS_STATE_IDLE: begin
            if(buffer_ready && (~txact))
                // only start sending if we're not currently in a transaction
                // mostly just covering a possible edge case.
                vps_state <= VPS_STATE_SENDING;
        end
        VPS_STATE_SENDING: begin
            if(empty)
                vps_state <= VPS_STATE_FINISHED;
        end
        VPS_STATE_FINISHED: begin
            if(buffer_ready == 1'b0)
                vps_state <= VPS_STATE_IDLE;
        end
        endcase
    end
end

assign buffer_complete = (vps_state == VPS_STATE_FINISHED);

// start a packet on buffer_ready
always @(posedge clk or posedge areset) begin
    if(areset) begin
        uvc_eof <= 1'b0;
        uvc_frame_id <= 1'b0;
        bytes_remaining <= 16'b0;
    end else begin
        if(buffer_ready && (vps_state == VPS_STATE_IDLE) && (~txact)) begin
            // latch in the frame length when we go from IDLE to SENDING
            bytes_remaining <= video_buffer_length;
        end 
        if(pktfin) begin  // end of this packet / payload
            if(bytes_remaining > (MAX_PACKET_LEN - HEADER_SIZE)) begin
                // there are more packets to send
                bytes_remaining <= bytes_remaining - (MAX_PACKET_LEN - HEADER_SIZE);
            end else begin
                // this was the last packet for this buffer
                bytes_remaining <= 16'd0;
                if(last_buffer_of_frame) begin
                    // toggle the frame id for our next frame to send
                    uvc_frame_id <= ~uvc_frame_id;
                end
            end
        end
        if((bytes_remaining <= (MAX_PACKET_LEN - HEADER_SIZE)) && last_buffer_of_frame)
            uvc_eof <= 1'b1;
        else
            uvc_eof <= 1'b0;
    end
end

// data selection - either from the header or real frame data
reg [7:0] my_txdat;
assign txdat = my_txdat;


// Reading from the buffer...
// There's a single clock delay from the buffer pointer changing
// to the valid data being available on "video_buffer_data"
// 
// Since there's no guarantee that data will be removed continuously 
// (that is, TXPOP might not be continuously asserted) we can't just
// keep the buffer read pointer one space ahead
// There will be times when we have to increment the buffer pointer before
// txpop goes high...but that's impossible to know. 
// Or you could always increment whenever you take data into the TXDAT output.
// But then the "video_buffer_data" will be at the wrong position. It will be 1 
// spot behind where it needs to be.
//
// So instead we need a pipeline register. It will always contain the previous
// ram data entry, one slot before whatever is currently on the
// "video_buffer_data" input.

reg [7:0] txdat_r; // pipeline register.
reg [1:0] pipeline_state;
reg buf_increment; // used to load the pipeline
// if buffer pointer incremented in the previous cycle,
// we need to save the ram value to the pipeline
// because the ram value will change in the next cycle.
reg first_pop_after_idle; // true when this is the first txpop
// after any cycle(s) when txpop was low

localparam  PIPELINE_IDLE = 2'b00,
            PIPELINE_WAIT = 2'b01,
            PIPELINE_BYTE_IN_RAM = 2'b10,
            PIPELINE_BYTE_IN_PIPELINE = 2'b11;

always @(posedge clk or posedge areset) begin
    if(areset) begin
        my_txdat <= 8'd0;
        txdat_r <= 8'd0;
        buf_ptr <= 16'd0;
        packet_byte_count <= 12'd0;
        pipeline_state <= PIPELINE_IDLE;
        buf_increment <= 1'b0;
        first_pop_after_idle <= 1'b0;
    end else begin
        if(~txact) begin
            packet_byte_count <= 12'd0;
            if(vps_state == VPS_STATE_SENDING) begin
                case(pipeline_state)
                PIPELINE_IDLE: begin
                    // get the first byte in ram, no need to change pipeline yet
                    buf_ptr <= video_buffer_length - bytes_remaining; // first data byte of this packet / payload
                    pipeline_state <= PIPELINE_WAIT;
                end
                PIPELINE_WAIT: begin
                    // hold one cycle before shifting data to pipeline register
                    pipeline_state <= PIPELINE_BYTE_IN_RAM;
                end
                PIPELINE_BYTE_IN_RAM: begin
                    // get next byte in ram, first byte in pipeline
                    buf_ptr <= buf_ptr + 16'd1;
                    txdat_r <= video_buffer_data;
                    pipeline_state <= PIPELINE_BYTE_IN_PIPELINE;
                end
                PIPELINE_BYTE_IN_PIPELINE: begin
                    // we reset back to the idle state when packet is finished
                    if(pktfin) 
                    // this path is unlikely to happen, as packet-finished usually only
                    // goes high while txact is high
                        pipeline_state <= PIPELINE_IDLE;
                end
                
                // for any other case, no need to change anything
                // we're all set up. pipeline has Data_0, ram has Data_1
                endcase

                // first data byte is always the header size, queue this up right away
                my_txdat <= HEADER_SIZE;
            end
            else begin
                // if not in the SENDING state, pipeline should remain idle
                pipeline_state <= PIPELINE_IDLE;
            end

        end else begin
            pipeline_state <= PIPELINE_IDLE; // reset startup for the next
                                                // packet, whenever it will occur
            if(~empty) begin
                if(txpop) begin
                    packet_byte_count <= packet_byte_count + 12'd1;
                    if(packet_byte_count == 12'd0) begin
                        // next is header byte 1, frame ID and end-of-frame
                        my_txdat <= {6'd0, uvc_eof, uvc_frame_id};
                        // no need to change ram pointer or pipeline
                        // still waiting for the first data byte
                        // to be popped

                        // Note we don't change "first_pop_after_idle"
                        // here. This is a special case, as the next
                        // byte to send will be the 2nd header byte, not
                        // a data byte. We act as if txpop is still idle 
                        // with respect to data bytes.
                    end else begin
                        
                        buf_ptr <= buf_ptr + 16'd1;
                        buf_increment <= 1'b1;
                        first_pop_after_idle <= 1'b0;

                        if(first_pop_after_idle) begin
                            my_txdat <= txdat_r;
                        end else begin
                            my_txdat <= video_buffer_data;
                        end
                    end
                end else begin
                    // if we didn't pop this cycle, then the buffer was not
                    // incremented
                    // so don't change the pipeline reg next cycle.
                    buf_increment <= 1'b0;
                    first_pop_after_idle <= 1'b1;
                end

                if(buf_increment)
                    // need to save this value of ram data only if we incremented
                    // the buffer pointer in the PREVIOUS cycle
                    // Because next cycle the ram data will change.
                    txdat_r <= video_buffer_data;
            end
        end
    end
end



// packet length for this packet
assign txdat_len = empty ? 12'd0 : 
    (bytes_remaining > (MAX_PACKET_LEN - HEADER_SIZE)) ? (MAX_PACKET_LEN) : HEADER_SIZE + bytes_remaining[11:0];

// if we are empty or not
assign empty = (bytes_remaining == 12'd0);
assign txcork = 1'b0;


endmodule
