// top.v
//
// LVDS FlatLink monitor top level.
//
// Inputs:
//   clk_50mhz - onboard oscillator
//   rx_clk    - DS90CF386 RxCLKOUT (~72 MHz pixel clock)
//   de        - DS90CF386 RxOUT24
//   vsync     - DS90CF386 RxOUT25 (currently unused, brought in for future use)
//   hsync     - DS90CF386 RxOUT26 (currently unused, brought in for future use)
//   rst_n_pin - optional external reset (tie high if not used)
//
// Output:
//   uart_tx_pin - to CH340 RX
//
// Frame reports:
//   - On a clean frame, sends "OK\n" once every HEARTBEAT_FRAMES frames.
//   - On an anomalous frame, immediately sends
//        "ERR lines=NNNN width=NNNN\n"
//     where NNNN are zero-padded 4-digit decimals.

module top (
    input  wire clk_50mhz,
    input  wire rx_clk,
    input  wire de,
    input  wire vsync,
    input  wire hsync,
    input  wire rst_n_pin,
    output wire uart_tx_pin
);

    // unused for now — keep ports alive so pin assignments stick
    wire _unused = &{1'b0, vsync, hsync, 1'b0};

    localparam integer HEARTBEAT_FRAMES = 60;

    // ----------------------------------------------------------------
    // Clocking: UART domain runs directly off the 50 MHz oscillator.
    // ----------------------------------------------------------------
    wire clk_uart    = clk_50mhz;
    wire pll_locked  = 1'b1;

    // ----------------------------------------------------------------
    // Reset synchronisers
    // ----------------------------------------------------------------
    reg [2:0] rst_sync_uart;
    reg [2:0] rst_sync_pix;

    always @(posedge clk_uart or negedge rst_n_pin) begin
        if (!rst_n_pin) rst_sync_uart <= 3'b000;
        else            rst_sync_uart <= {rst_sync_uart[1:0], pll_locked};
    end
    wire rst_n_uart = rst_sync_uart[2];

    always @(posedge rx_clk or negedge rst_n_pin) begin
        if (!rst_n_pin) rst_sync_pix <= 3'b000;
        else            rst_sync_pix <= {rst_sync_pix[1:0], 1'b1};
    end
    wire rst_n_pix = rst_sync_pix[2];

    // ----------------------------------------------------------------
    // DE monitor in pixel-clock domain
    // ----------------------------------------------------------------
    wire        frame_done_pix;
    wire [15:0] lines_pix;
    wire [15:0] width_pix;
    wire        anomaly_pix;

    de_monitor #(
        .EXPECTED_LINES (16'd800),
        .EXPECTED_WIDTH (16'd1280),
        .FRAME_GAP_PIX  (16'd2048)
    ) u_mon (
        .pix_clk    (rx_clk),
        .rst_n      (rst_n_pix),
        .de         (de),
        .frame_done (frame_done_pix),
        .lines_o    (lines_pix),
        .width_o    (width_pix),
        .anomaly_o  (anomaly_pix)
    );

    // ----------------------------------------------------------------
    // Pixel-domain: latch report fields + heartbeat, toggle req
    // ----------------------------------------------------------------
    reg [15:0] lines_lat;
    reg [15:0] width_lat;
    reg        anomaly_lat;
    reg        heartbeat_lat;
    reg        req_tog;
    reg [7:0]  hb_count;  // up to 255 — plenty for HEARTBEAT_FRAMES=60

    always @(posedge rx_clk or negedge rst_n_pix) begin
        if (!rst_n_pix) begin
            lines_lat     <= 16'd0;
            width_lat     <= 16'd0;
            anomaly_lat   <= 1'b0;
            heartbeat_lat <= 1'b0;
            req_tog       <= 1'b0;
            hb_count      <= 8'd0;
        end else if (frame_done_pix) begin
            lines_lat   <= lines_pix;
            width_lat   <= width_pix;
            anomaly_lat <= anomaly_pix;

            if (hb_count == HEARTBEAT_FRAMES - 1) begin
                hb_count      <= 8'd0;
                heartbeat_lat <= 1'b1;
            end else begin
                hb_count      <= hb_count + 8'd1;
                heartbeat_lat <= 1'b0;
            end
            req_tog <= ~req_tog;
        end
    end

    // ----------------------------------------------------------------
    // CDC: sync req toggle into UART domain, edge-detect
    // ----------------------------------------------------------------
    reg [2:0] req_sync;
    always @(posedge clk_uart or negedge rst_n_uart) begin
        if (!rst_n_uart) req_sync <= 3'b000;
        else             req_sync <= {req_sync[1:0], req_tog};
    end
    wire req_edge = req_sync[2] ^ req_sync[1];

    // ----------------------------------------------------------------
    // UART-domain: decimal conversion + report FSM
    // ----------------------------------------------------------------
    reg [15:0] lines_u;
    reg [15:0] width_u;
    reg        anomaly_u;
    reg        heartbeat_u;

    // BCD digit registers — computed in F_CONVERT before transmission
    reg [3:0] L0_r, L1_r, L2_r, L3_r;
    reg [3:0] W0_r, W1_r, W2_r, W3_r;
    reg [1:0] conv_step;
    reg [15:0] l_rem, w_rem;

    // Sample latched fields when req_edge fires. Data is stable in
    // pix domain until the next frame_done (~16 ms), far longer than we
    // need for the handful of µs of UART setup.
    always @(posedge clk_uart or negedge rst_n_uart) begin
        if (!rst_n_uart) begin
            lines_u     <= 16'd0;
            width_u     <= 16'd0;
            anomaly_u   <= 1'b0;
            heartbeat_u <= 1'b0;
        end else if (req_edge) begin
            lines_u     <= lines_lat;
            width_u     <= width_lat;
            anomaly_u   <= anomaly_lat;
            heartbeat_u <= heartbeat_lat;
        end
    end

    // FSM that emits the message byte-by-byte.
    localparam [2:0] F_IDLE    = 3'd0,
                     F_CONVERT = 3'd1,
                     F_LOAD    = 3'd2,
                     F_WAIT    = 3'd3,
                     F_BANNER  = 3'd4;

    // Banner: ESC[2J ESC[H ESC[33m + 2 separator lines + 6 info lines + ESC[0m
    localparam [8:0] BANNER_LEN = 9'd288;

    reg [2:0]  fstate;
    reg [8:0]  idx;       // byte index within message
    reg [8:0]  msg_len;
    reg        is_banner;
    reg        is_err_msg;
    reg        tx_start;
    reg [7:0]  tx_byte;
    wire       tx_busy;

    // ERR layout: ESC[31m + "ERR lines=LLLL width=WWWW\n" + ESC[0m  (35 bytes)
    function [7:0] err_char(input [7:0] i);
        case (i)
            8'd0:  err_char = 8'h1B;
            8'd1:  err_char = "[";
            8'd2:  err_char = "3";
            8'd3:  err_char = "1";
            8'd4:  err_char = "m";
            8'd5:  err_char = "E";
            8'd6:  err_char = "R";
            8'd7:  err_char = "R";
            8'd8:  err_char = " ";
            8'd9:  err_char = "l";
            8'd10: err_char = "i";
            8'd11: err_char = "n";
            8'd12: err_char = "e";
            8'd13: err_char = "s";
            8'd14: err_char = "=";
            8'd15: err_char = "0" + L0_r;
            8'd16: err_char = "0" + L1_r;
            8'd17: err_char = "0" + L2_r;
            8'd18: err_char = "0" + L3_r;
            8'd19: err_char = " ";
            8'd20: err_char = "w";
            8'd21: err_char = "i";
            8'd22: err_char = "d";
            8'd23: err_char = "t";
            8'd24: err_char = "h";
            8'd25: err_char = "=";
            8'd26: err_char = "0" + W0_r;
            8'd27: err_char = "0" + W1_r;
            8'd28: err_char = "0" + W2_r;
            8'd29: err_char = "0" + W3_r;
            8'd30: err_char = 8'h0A;
            8'd31: err_char = 8'h1B;
            8'd32: err_char = "[";
            8'd33: err_char = "0";
            8'd34: err_char = "m";
            default: err_char = 8'h00;
        endcase
    endfunction

    // OK layout: ESC[32m + "OK\n" + ESC[0m  (12 bytes)
    function [7:0] ok_char(input [7:0] i);
        case (i)
            8'd0:  ok_char = 8'h1B;
            8'd1:  ok_char = "[";
            8'd2:  ok_char = "3";
            8'd3:  ok_char = "2";
            8'd4:  ok_char = "m";
            8'd5:  ok_char = "O";
            8'd6:  ok_char = "K";
            8'd7:  ok_char = 8'h0A;
            8'd8:  ok_char = 8'h1B;
            8'd9:  ok_char = "[";
            8'd10: ok_char = "0";
            8'd11: ok_char = "m";
            default: ok_char = 8'h00;
        endcase
    endfunction

    // Banner: ESC[2J ESC[H ESC[33m + separator + info lines + separator + ESC[0m
    // Indices 12-55 and 239-282 are '-' (handled by range check).
    // Total: 288 bytes (indices 0-287).
    function [7:0] banner_char(input [8:0] i);
        if ((i >= 9'd12 && i <= 9'd55) || (i >= 9'd239 && i <= 9'd282))
            banner_char = "-";
        else begin
            case (i)
                // ESC[2J
                9'd0:   banner_char = 8'h1B;
                9'd1:   banner_char = "[";
                9'd2:   banner_char = "2";
                9'd3:   banner_char = "J";
                // ESC[H
                9'd4:   banner_char = 8'h1B;
                9'd5:   banner_char = "[";
                9'd6:   banner_char = "H";
                // ESC[33m
                9'd7:   banner_char = 8'h1B;
                9'd8:   banner_char = "[";
                9'd9:   banner_char = "3";
                9'd10:  banner_char = "3";
                9'd11:  banner_char = "m";
                // first separator \n
                9'd56:  banner_char = 8'h0A;
                // "Arrive LVDS Protocol Analyser\n"
                9'd57:  banner_char = "A";
                9'd58:  banner_char = "r";
                9'd59:  banner_char = "r";
                9'd60:  banner_char = "i";
                9'd61:  banner_char = "v";
                9'd62:  banner_char = "e";
                9'd63:  banner_char = " ";
                9'd64:  banner_char = "L";
                9'd65:  banner_char = "V";
                9'd66:  banner_char = "D";
                9'd67:  banner_char = "S";
                9'd68:  banner_char = " ";
                9'd69:  banner_char = "P";
                9'd70:  banner_char = "r";
                9'd71:  banner_char = "o";
                9'd72:  banner_char = "t";
                9'd73:  banner_char = "o";
                9'd74:  banner_char = "c";
                9'd75:  banner_char = "o";
                9'd76:  banner_char = "l";
                9'd77:  banner_char = " ";
                9'd78:  banner_char = "A";
                9'd79:  banner_char = "n";
                9'd80:  banner_char = "a";
                9'd81:  banner_char = "l";
                9'd82:  banner_char = "y";
                9'd83:  banner_char = "s";
                9'd84:  banner_char = "e";
                9'd85:  banner_char = "r";
                9'd86:  banner_char = 8'h0A;
                // "Board: DS90CF386 Interface  S/N: 1000052088 Rev A\n"
                9'd87:  banner_char = "B";
                9'd88:  banner_char = "o";
                9'd89:  banner_char = "a";
                9'd90:  banner_char = "r";
                9'd91:  banner_char = "d";
                9'd92:  banner_char = ":";
                9'd93:  banner_char = " ";
                9'd94:  banner_char = "D";
                9'd95:  banner_char = "S";
                9'd96:  banner_char = "9";
                9'd97:  banner_char = "0";
                9'd98:  banner_char = "C";
                9'd99:  banner_char = "F";
                9'd100: banner_char = "3";
                9'd101: banner_char = "8";
                9'd102: banner_char = "6";
                9'd103: banner_char = " ";
                9'd104: banner_char = "I";
                9'd105: banner_char = "n";
                9'd106: banner_char = "t";
                9'd107: banner_char = "e";
                9'd108: banner_char = "r";
                9'd109: banner_char = "f";
                9'd110: banner_char = "a";
                9'd111: banner_char = "c";
                9'd112: banner_char = "e";
                9'd113: banner_char = " ";
                9'd114: banner_char = " ";
                9'd115: banner_char = "S";
                9'd116: banner_char = "/";
                9'd117: banner_char = "N";
                9'd118: banner_char = ":";
                9'd119: banner_char = " ";
                9'd120: banner_char = "1";
                9'd121: banner_char = "0";
                9'd122: banner_char = "0";
                9'd123: banner_char = "0";
                9'd124: banner_char = "0";
                9'd125: banner_char = "5";
                9'd126: banner_char = "2";
                9'd127: banner_char = "0";
                9'd128: banner_char = "8";
                9'd129: banner_char = "8";
                9'd130: banner_char = " ";
                9'd131: banner_char = "R";
                9'd132: banner_char = "e";
                9'd133: banner_char = "v";
                9'd134: banner_char = " ";
                9'd135: banner_char = "A";
                9'd136: banner_char = 8'h0A;
                // "Author: David Rice\n"
                9'd137: banner_char = "A";
                9'd138: banner_char = "u";
                9'd139: banner_char = "t";
                9'd140: banner_char = "h";
                9'd141: banner_char = "o";
                9'd142: banner_char = "r";
                9'd143: banner_char = ":";
                9'd144: banner_char = " ";
                9'd145: banner_char = "D";
                9'd146: banner_char = "a";
                9'd147: banner_char = "v";
                9'd148: banner_char = "i";
                9'd149: banner_char = "d";
                9'd150: banner_char = " ";
                9'd151: banner_char = "R";
                9'd152: banner_char = "i";
                9'd153: banner_char = "c";
                9'd154: banner_char = "e";
                9'd155: banner_char = 8'h0A;
                // "Role: Senior Electronics Engineer\n"
                9'd156: banner_char = "R";
                9'd157: banner_char = "o";
                9'd158: banner_char = "l";
                9'd159: banner_char = "e";
                9'd160: banner_char = ":";
                9'd161: banner_char = " ";
                9'd162: banner_char = "S";
                9'd163: banner_char = "e";
                9'd164: banner_char = "n";
                9'd165: banner_char = "i";
                9'd166: banner_char = "o";
                9'd167: banner_char = "r";
                9'd168: banner_char = " ";
                9'd169: banner_char = "E";
                9'd170: banner_char = "l";
                9'd171: banner_char = "e";
                9'd172: banner_char = "c";
                9'd173: banner_char = "t";
                9'd174: banner_char = "r";
                9'd175: banner_char = "o";
                9'd176: banner_char = "n";
                9'd177: banner_char = "i";
                9'd178: banner_char = "c";
                9'd179: banner_char = "s";
                9'd180: banner_char = " ";
                9'd181: banner_char = "E";
                9'd182: banner_char = "n";
                9'd183: banner_char = "g";
                9'd184: banner_char = "i";
                9'd185: banner_char = "n";
                9'd186: banner_char = "e";
                9'd187: banner_char = "e";
                9'd188: banner_char = "r";
                9'd189: banner_char = 8'h0A;
                // "Email: david.rice@arrive.com\n"
                9'd190: banner_char = "E";
                9'd191: banner_char = "m";
                9'd192: banner_char = "a";
                9'd193: banner_char = "i";
                9'd194: banner_char = "l";
                9'd195: banner_char = ":";
                9'd196: banner_char = " ";
                9'd197: banner_char = "d";
                9'd198: banner_char = "a";
                9'd199: banner_char = "v";
                9'd200: banner_char = "i";
                9'd201: banner_char = "d";
                9'd202: banner_char = ".";
                9'd203: banner_char = "r";
                9'd204: banner_char = "i";
                9'd205: banner_char = "c";
                9'd206: banner_char = "e";
                9'd207: banner_char = "@";
                9'd208: banner_char = "a";
                9'd209: banner_char = "r";
                9'd210: banner_char = "r";
                9'd211: banner_char = "i";
                9'd212: banner_char = "v";
                9'd213: banner_char = "e";
                9'd214: banner_char = ".";
                9'd215: banner_char = "c";
                9'd216: banner_char = "o";
                9'd217: banner_char = "m";
                9'd218: banner_char = 8'h0A;
                // "Software Rev: 1.0.0\n"
                9'd219: banner_char = "S";
                9'd220: banner_char = "o";
                9'd221: banner_char = "f";
                9'd222: banner_char = "t";
                9'd223: banner_char = "w";
                9'd224: banner_char = "a";
                9'd225: banner_char = "r";
                9'd226: banner_char = "e";
                9'd227: banner_char = " ";
                9'd228: banner_char = "R";
                9'd229: banner_char = "e";
                9'd230: banner_char = "v";
                9'd231: banner_char = ":";
                9'd232: banner_char = " ";
                9'd233: banner_char = "1";
                9'd234: banner_char = ".";
                9'd235: banner_char = "0";
                9'd236: banner_char = ".";
                9'd237: banner_char = "0";
                9'd238: banner_char = 8'h0A;
                // second separator \n
                9'd283: banner_char = 8'h0A;
                // ESC[0m
                9'd284: banner_char = 8'h1B;
                9'd285: banner_char = "[";
                9'd286: banner_char = "0";
                9'd287: banner_char = "m";
                default: banner_char = 8'h00;
            endcase
        end
    endfunction

    // Trigger: one cycle after req_edge (so lines_u/width_u are updated).
    reg req_edge_q;
    always @(posedge clk_uart or negedge rst_n_uart) begin
        if (!rst_n_uart) req_edge_q <= 1'b0;
        else             req_edge_q <= req_edge;
    end

    always @(posedge clk_uart or negedge rst_n_uart) begin
        if (!rst_n_uart) begin
            fstate     <= F_BANNER;
            idx        <= 9'd0;
            msg_len    <= BANNER_LEN;
            is_banner  <= 1'b1;
            is_err_msg <= 1'b0;
            tx_start   <= 1'b0;
            tx_byte    <= 8'd0;
            conv_step  <= 2'd0;
            l_rem      <= 16'd0;
            w_rem      <= 16'd0;
            L0_r <= 4'd0; L1_r <= 4'd0; L2_r <= 4'd0; L3_r <= 4'd0;
            W0_r <= 4'd0; W1_r <= 4'd0; W2_r <= 4'd0; W3_r <= 4'd0;
        end else begin
            tx_start <= 1'b0;
            case (fstate)
                F_BANNER: begin
                    // idx/msg_len/is_banner already set in reset block; kick off load
                    fstate <= F_LOAD;
                end
                F_IDLE: begin
                    is_banner <= 1'b0;
                    if (req_edge_q && (anomaly_u || heartbeat_u)) begin
                        idx        <= 9'd0;
                        is_err_msg <= anomaly_u;
                        msg_len    <= anomaly_u ? 9'd35 : 9'd12;
                        l_rem      <= lines_u;
                        w_rem      <= width_u;
                        conv_step  <= 2'd0;
                        fstate     <= F_CONVERT;
                    end
                end
                F_CONVERT: begin
                    conv_step <= conv_step + 2'd1;
                    case (conv_step)
                        2'd0: begin // thousands
                            if      (l_rem >= 16'd9000) begin L0_r <= 4'd9; l_rem <= l_rem - 16'd9000; end
                            else if (l_rem >= 16'd8000) begin L0_r <= 4'd8; l_rem <= l_rem - 16'd8000; end
                            else if (l_rem >= 16'd7000) begin L0_r <= 4'd7; l_rem <= l_rem - 16'd7000; end
                            else if (l_rem >= 16'd6000) begin L0_r <= 4'd6; l_rem <= l_rem - 16'd6000; end
                            else if (l_rem >= 16'd5000) begin L0_r <= 4'd5; l_rem <= l_rem - 16'd5000; end
                            else if (l_rem >= 16'd4000) begin L0_r <= 4'd4; l_rem <= l_rem - 16'd4000; end
                            else if (l_rem >= 16'd3000) begin L0_r <= 4'd3; l_rem <= l_rem - 16'd3000; end
                            else if (l_rem >= 16'd2000) begin L0_r <= 4'd2; l_rem <= l_rem - 16'd2000; end
                            else if (l_rem >= 16'd1000) begin L0_r <= 4'd1; l_rem <= l_rem - 16'd1000; end
                            else                              L0_r <= 4'd0;
                            if      (w_rem >= 16'd9000) begin W0_r <= 4'd9; w_rem <= w_rem - 16'd9000; end
                            else if (w_rem >= 16'd8000) begin W0_r <= 4'd8; w_rem <= w_rem - 16'd8000; end
                            else if (w_rem >= 16'd7000) begin W0_r <= 4'd7; w_rem <= w_rem - 16'd7000; end
                            else if (w_rem >= 16'd6000) begin W0_r <= 4'd6; w_rem <= w_rem - 16'd6000; end
                            else if (w_rem >= 16'd5000) begin W0_r <= 4'd5; w_rem <= w_rem - 16'd5000; end
                            else if (w_rem >= 16'd4000) begin W0_r <= 4'd4; w_rem <= w_rem - 16'd4000; end
                            else if (w_rem >= 16'd3000) begin W0_r <= 4'd3; w_rem <= w_rem - 16'd3000; end
                            else if (w_rem >= 16'd2000) begin W0_r <= 4'd2; w_rem <= w_rem - 16'd2000; end
                            else if (w_rem >= 16'd1000) begin W0_r <= 4'd1; w_rem <= w_rem - 16'd1000; end
                            else                              W0_r <= 4'd0;
                        end
                        2'd1: begin // hundreds
                            if      (l_rem >= 16'd900) begin L1_r <= 4'd9; l_rem <= l_rem - 16'd900; end
                            else if (l_rem >= 16'd800) begin L1_r <= 4'd8; l_rem <= l_rem - 16'd800; end
                            else if (l_rem >= 16'd700) begin L1_r <= 4'd7; l_rem <= l_rem - 16'd700; end
                            else if (l_rem >= 16'd600) begin L1_r <= 4'd6; l_rem <= l_rem - 16'd600; end
                            else if (l_rem >= 16'd500) begin L1_r <= 4'd5; l_rem <= l_rem - 16'd500; end
                            else if (l_rem >= 16'd400) begin L1_r <= 4'd4; l_rem <= l_rem - 16'd400; end
                            else if (l_rem >= 16'd300) begin L1_r <= 4'd3; l_rem <= l_rem - 16'd300; end
                            else if (l_rem >= 16'd200) begin L1_r <= 4'd2; l_rem <= l_rem - 16'd200; end
                            else if (l_rem >= 16'd100) begin L1_r <= 4'd1; l_rem <= l_rem - 16'd100; end
                            else                             L1_r <= 4'd0;
                            if      (w_rem >= 16'd900) begin W1_r <= 4'd9; w_rem <= w_rem - 16'd900; end
                            else if (w_rem >= 16'd800) begin W1_r <= 4'd8; w_rem <= w_rem - 16'd800; end
                            else if (w_rem >= 16'd700) begin W1_r <= 4'd7; w_rem <= w_rem - 16'd700; end
                            else if (w_rem >= 16'd600) begin W1_r <= 4'd6; w_rem <= w_rem - 16'd600; end
                            else if (w_rem >= 16'd500) begin W1_r <= 4'd5; w_rem <= w_rem - 16'd500; end
                            else if (w_rem >= 16'd400) begin W1_r <= 4'd4; w_rem <= w_rem - 16'd400; end
                            else if (w_rem >= 16'd300) begin W1_r <= 4'd3; w_rem <= w_rem - 16'd300; end
                            else if (w_rem >= 16'd200) begin W1_r <= 4'd2; w_rem <= w_rem - 16'd200; end
                            else if (w_rem >= 16'd100) begin W1_r <= 4'd1; w_rem <= w_rem - 16'd100; end
                            else                             W1_r <= 4'd0;
                        end
                        2'd2: begin // tens
                            if      (l_rem >= 16'd90) begin L2_r <= 4'd9; l_rem <= l_rem - 16'd90; end
                            else if (l_rem >= 16'd80) begin L2_r <= 4'd8; l_rem <= l_rem - 16'd80; end
                            else if (l_rem >= 16'd70) begin L2_r <= 4'd7; l_rem <= l_rem - 16'd70; end
                            else if (l_rem >= 16'd60) begin L2_r <= 4'd6; l_rem <= l_rem - 16'd60; end
                            else if (l_rem >= 16'd50) begin L2_r <= 4'd5; l_rem <= l_rem - 16'd50; end
                            else if (l_rem >= 16'd40) begin L2_r <= 4'd4; l_rem <= l_rem - 16'd40; end
                            else if (l_rem >= 16'd30) begin L2_r <= 4'd3; l_rem <= l_rem - 16'd30; end
                            else if (l_rem >= 16'd20) begin L2_r <= 4'd2; l_rem <= l_rem - 16'd20; end
                            else if (l_rem >= 16'd10) begin L2_r <= 4'd1; l_rem <= l_rem - 16'd10; end
                            else                           L2_r <= 4'd0;
                            if      (w_rem >= 16'd90) begin W2_r <= 4'd9; w_rem <= w_rem - 16'd90; end
                            else if (w_rem >= 16'd80) begin W2_r <= 4'd8; w_rem <= w_rem - 16'd80; end
                            else if (w_rem >= 16'd70) begin W2_r <= 4'd7; w_rem <= w_rem - 16'd70; end
                            else if (w_rem >= 16'd60) begin W2_r <= 4'd6; w_rem <= w_rem - 16'd60; end
                            else if (w_rem >= 16'd50) begin W2_r <= 4'd5; w_rem <= w_rem - 16'd50; end
                            else if (w_rem >= 16'd40) begin W2_r <= 4'd4; w_rem <= w_rem - 16'd40; end
                            else if (w_rem >= 16'd30) begin W2_r <= 4'd3; w_rem <= w_rem - 16'd30; end
                            else if (w_rem >= 16'd20) begin W2_r <= 4'd2; w_rem <= w_rem - 16'd20; end
                            else if (w_rem >= 16'd10) begin W2_r <= 4'd1; w_rem <= w_rem - 16'd10; end
                            else                           W2_r <= 4'd0;
                        end
                        2'd3: begin // units — remainder is 0-9 by construction
                            L3_r   <= l_rem[3:0];
                            W3_r   <= w_rem[3:0];
                            fstate <= F_LOAD;
                        end
                    endcase
                end
                F_LOAD: begin
                    if (!tx_busy) begin
                        tx_byte  <= is_banner  ? banner_char(idx) :
                                    is_err_msg ? err_char(idx) : ok_char(idx);
                        tx_start <= 1'b1;
                        fstate   <= F_WAIT;
                    end
                end
                F_WAIT: begin
                    if (tx_busy == 1'b0 && tx_start == 1'b0) begin
                        // byte fully sent (uart drops busy at stop-bit end)
                        if (idx == msg_len - 9'd1) begin
                            fstate <= F_IDLE;
                        end else begin
                            idx    <= idx + 9'd1;
                            fstate <= F_LOAD;
                        end
                    end
                end
                default: fstate <= F_IDLE;
            endcase
        end
    end

    uart_tx #(
        .CLK_HZ (50_000_000),
        .BAUD   (115_200)
    ) u_uart (
        .clk    (clk_uart),
        .rst_n  (rst_n_uart),
        .start  (tx_start),
        .data   (tx_byte),
        .tx     (uart_tx_pin),
        .busy   (tx_busy)
    );

endmodule
