// 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 + 2x51-dash separators + info lines + ESC[0m localparam [8:0] BANNER_LEN = 9'd411; 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" + ESC[0m + \r\n (36 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'h1B; 8'd31: err_char = "["; 8'd32: err_char = "0"; 8'd33: err_char = "m"; 8'd34: err_char = 8'h0D; 8'd35: err_char = 8'h0A; default: err_char = 8'h00; endcase endfunction // OK layout: ESC[32m + "OK" + ESC[0m + \r\n (13 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'h1B; 8'd8: ok_char = "["; 8'd9: ok_char = "0"; 8'd10: ok_char = "m"; 8'd11: ok_char = 8'h0D; 8'd12: ok_char = 8'h0A; default: ok_char = 8'h00; endcase endfunction // Banner layout (411 bytes, indices 0-410): // 0-11: ESC[2J ESC[H ESC[33m // 12-62: '-'x51 63:\r 64:\n // 65-72: "ARRIVE\r\n" // 73-74: \r\n (blank) // 75-112: "PARC LA FAYETTE, 6 RUE ISAAC NEWTON,\r\n" // 113-128: "25000 BESANCON\r\n" // 129-130: \r\n (blank) // 131-163: "Project: LVDS Protocol Analyser\r\n" // 164-216: "Board: DS90CF386 Interface (CI NU TEST LVDS LVCMOS)\r\n" // 217-239: "S/N: 1000052088 Rev A\r\n" // 240-241: \r\n (blank) // 242-261: "Author: David Rice\r\n" // 262-296: "Role: Senior Electronics Engineer\r\n" // 297-326: "Email: david.rice@arrive.com\r\n" // 327-328: \r\n (blank) // 329-353: "Software Rev: " + 9-char version field + "\r\n" // version chars at indices 343-351 (bump_version.py updates these) // 354-404: '-'x51 405:\r 406:\n // 407-410: ESC[0m function [7:0] banner_char(input [8:0] i); if ((i >= 9'd12 && i <= 9'd62) || (i >= 9'd354 && i <= 9'd404)) 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 \r\n 9'd63: banner_char = 8'h0D; 9'd64: banner_char = 8'h0A; // "ARRIVE\r\n" 9'd65: banner_char = "A"; 9'd66: banner_char = "R"; 9'd67: banner_char = "R"; 9'd68: banner_char = "I"; 9'd69: banner_char = "V"; 9'd70: banner_char = "E"; 9'd71: banner_char = 8'h0D; 9'd72: banner_char = 8'h0A; // blank line 9'd73: banner_char = 8'h0D; 9'd74: banner_char = 8'h0A; // "PARC LA FAYETTE, 6 RUE ISAAC NEWTON,\r\n" 9'd75: banner_char = "P"; 9'd76: banner_char = "A"; 9'd77: banner_char = "R"; 9'd78: banner_char = "C"; 9'd79: banner_char = " "; 9'd80: banner_char = "L"; 9'd81: banner_char = "A"; 9'd82: banner_char = " "; 9'd83: banner_char = "F"; 9'd84: banner_char = "A"; 9'd85: banner_char = "Y"; 9'd86: banner_char = "E"; 9'd87: banner_char = "T"; 9'd88: banner_char = "T"; 9'd89: banner_char = "E"; 9'd90: banner_char = ","; 9'd91: banner_char = " "; 9'd92: banner_char = "6"; 9'd93: banner_char = " "; 9'd94: banner_char = "R"; 9'd95: banner_char = "U"; 9'd96: banner_char = "E"; 9'd97: banner_char = " "; 9'd98: banner_char = "I"; 9'd99: banner_char = "S"; 9'd100: banner_char = "A"; 9'd101: banner_char = "A"; 9'd102: banner_char = "C"; 9'd103: banner_char = " "; 9'd104: banner_char = "N"; 9'd105: banner_char = "E"; 9'd106: banner_char = "W"; 9'd107: banner_char = "T"; 9'd108: banner_char = "O"; 9'd109: banner_char = "N"; 9'd110: banner_char = ","; 9'd111: banner_char = 8'h0D; 9'd112: banner_char = 8'h0A; // "25000 BESANCON\r\n" 9'd113: banner_char = "2"; 9'd114: banner_char = "5"; 9'd115: banner_char = "0"; 9'd116: banner_char = "0"; 9'd117: banner_char = "0"; 9'd118: banner_char = " "; 9'd119: banner_char = "B"; 9'd120: banner_char = "E"; 9'd121: banner_char = "S"; 9'd122: banner_char = "A"; 9'd123: banner_char = "N"; 9'd124: banner_char = "C"; 9'd125: banner_char = "O"; 9'd126: banner_char = "N"; 9'd127: banner_char = 8'h0D; 9'd128: banner_char = 8'h0A; // blank line 9'd129: banner_char = 8'h0D; 9'd130: banner_char = 8'h0A; // "Project: LVDS Protocol Analyser\r\n" 9'd131: banner_char = "P"; 9'd132: banner_char = "r"; 9'd133: banner_char = "o"; 9'd134: banner_char = "j"; 9'd135: banner_char = "e"; 9'd136: banner_char = "c"; 9'd137: banner_char = "t"; 9'd138: banner_char = ":"; 9'd139: banner_char = " "; 9'd140: banner_char = "L"; 9'd141: banner_char = "V"; 9'd142: banner_char = "D"; 9'd143: banner_char = "S"; 9'd144: banner_char = " "; 9'd145: banner_char = "P"; 9'd146: banner_char = "r"; 9'd147: banner_char = "o"; 9'd148: banner_char = "t"; 9'd149: banner_char = "o"; 9'd150: banner_char = "c"; 9'd151: banner_char = "o"; 9'd152: banner_char = "l"; 9'd153: banner_char = " "; 9'd154: banner_char = "A"; 9'd155: banner_char = "n"; 9'd156: banner_char = "a"; 9'd157: banner_char = "l"; 9'd158: banner_char = "y"; 9'd159: banner_char = "s"; 9'd160: banner_char = "e"; 9'd161: banner_char = "r"; 9'd162: banner_char = 8'h0D; 9'd163: banner_char = 8'h0A; // "Board: DS90CF386 Interface (CI NU TEST LVDS LVCMOS)\r\n" 9'd164: banner_char = "B"; 9'd165: banner_char = "o"; 9'd166: banner_char = "a"; 9'd167: banner_char = "r"; 9'd168: banner_char = "d"; 9'd169: banner_char = ":"; 9'd170: banner_char = " "; 9'd171: banner_char = "D"; 9'd172: banner_char = "S"; 9'd173: banner_char = "9"; 9'd174: banner_char = "0"; 9'd175: banner_char = "C"; 9'd176: banner_char = "F"; 9'd177: banner_char = "3"; 9'd178: banner_char = "8"; 9'd179: banner_char = "6"; 9'd180: banner_char = " "; 9'd181: banner_char = "I"; 9'd182: banner_char = "n"; 9'd183: banner_char = "t"; 9'd184: banner_char = "e"; 9'd185: banner_char = "r"; 9'd186: banner_char = "f"; 9'd187: banner_char = "a"; 9'd188: banner_char = "c"; 9'd189: banner_char = "e"; 9'd190: banner_char = " "; 9'd191: banner_char = "("; 9'd192: banner_char = "C"; 9'd193: banner_char = "I"; 9'd194: banner_char = " "; 9'd195: banner_char = "N"; 9'd196: banner_char = "U"; 9'd197: banner_char = " "; 9'd198: banner_char = "T"; 9'd199: banner_char = "E"; 9'd200: banner_char = "S"; 9'd201: banner_char = "T"; 9'd202: banner_char = " "; 9'd203: banner_char = "L"; 9'd204: banner_char = "V"; 9'd205: banner_char = "D"; 9'd206: banner_char = "S"; 9'd207: banner_char = " "; 9'd208: banner_char = "L"; 9'd209: banner_char = "V"; 9'd210: banner_char = "C"; 9'd211: banner_char = "M"; 9'd212: banner_char = "O"; 9'd213: banner_char = "S"; 9'd214: banner_char = ")"; 9'd215: banner_char = 8'h0D; 9'd216: banner_char = 8'h0A; // "S/N: 1000052088 Rev A\r\n" 9'd217: banner_char = "S"; 9'd218: banner_char = "/"; 9'd219: banner_char = "N"; 9'd220: banner_char = ":"; 9'd221: banner_char = " "; 9'd222: banner_char = "1"; 9'd223: banner_char = "0"; 9'd224: banner_char = "0"; 9'd225: banner_char = "0"; 9'd226: banner_char = "0"; 9'd227: banner_char = "5"; 9'd228: banner_char = "2"; 9'd229: banner_char = "0"; 9'd230: banner_char = "8"; 9'd231: banner_char = "8"; 9'd232: banner_char = " "; 9'd233: banner_char = "R"; 9'd234: banner_char = "e"; 9'd235: banner_char = "v"; 9'd236: banner_char = " "; 9'd237: banner_char = "A"; 9'd238: banner_char = 8'h0D; 9'd239: banner_char = 8'h0A; // blank line 9'd240: banner_char = 8'h0D; 9'd241: banner_char = 8'h0A; // "Author: David Rice\r\n" 9'd242: banner_char = "A"; 9'd243: banner_char = "u"; 9'd244: banner_char = "t"; 9'd245: banner_char = "h"; 9'd246: banner_char = "o"; 9'd247: banner_char = "r"; 9'd248: banner_char = ":"; 9'd249: banner_char = " "; 9'd250: banner_char = "D"; 9'd251: banner_char = "a"; 9'd252: banner_char = "v"; 9'd253: banner_char = "i"; 9'd254: banner_char = "d"; 9'd255: banner_char = " "; 9'd256: banner_char = "R"; 9'd257: banner_char = "i"; 9'd258: banner_char = "c"; 9'd259: banner_char = "e"; 9'd260: banner_char = 8'h0D; 9'd261: banner_char = 8'h0A; // "Role: Senior Electronics Engineer\r\n" 9'd262: banner_char = "R"; 9'd263: banner_char = "o"; 9'd264: banner_char = "l"; 9'd265: banner_char = "e"; 9'd266: banner_char = ":"; 9'd267: banner_char = " "; 9'd268: banner_char = "S"; 9'd269: banner_char = "e"; 9'd270: banner_char = "n"; 9'd271: banner_char = "i"; 9'd272: banner_char = "o"; 9'd273: banner_char = "r"; 9'd274: banner_char = " "; 9'd275: banner_char = "E"; 9'd276: banner_char = "l"; 9'd277: banner_char = "e"; 9'd278: banner_char = "c"; 9'd279: banner_char = "t"; 9'd280: banner_char = "r"; 9'd281: banner_char = "o"; 9'd282: banner_char = "n"; 9'd283: banner_char = "i"; 9'd284: banner_char = "c"; 9'd285: banner_char = "s"; 9'd286: banner_char = " "; 9'd287: banner_char = "E"; 9'd288: banner_char = "n"; 9'd289: banner_char = "g"; 9'd290: banner_char = "i"; 9'd291: banner_char = "n"; 9'd292: banner_char = "e"; 9'd293: banner_char = "e"; 9'd294: banner_char = "r"; 9'd295: banner_char = 8'h0D; 9'd296: banner_char = 8'h0A; // "Email: david.rice@arrive.com\r\n" 9'd297: banner_char = "E"; 9'd298: banner_char = "m"; 9'd299: banner_char = "a"; 9'd300: banner_char = "i"; 9'd301: banner_char = "l"; 9'd302: banner_char = ":"; 9'd303: banner_char = " "; 9'd304: banner_char = "d"; 9'd305: banner_char = "a"; 9'd306: banner_char = "v"; 9'd307: banner_char = "i"; 9'd308: banner_char = "d"; 9'd309: banner_char = "."; 9'd310: banner_char = "r"; 9'd311: banner_char = "i"; 9'd312: banner_char = "c"; 9'd313: banner_char = "e"; 9'd314: banner_char = "@"; 9'd315: banner_char = "a"; 9'd316: banner_char = "r"; 9'd317: banner_char = "r"; 9'd318: banner_char = "i"; 9'd319: banner_char = "v"; 9'd320: banner_char = "e"; 9'd321: banner_char = "."; 9'd322: banner_char = "c"; 9'd323: banner_char = "o"; 9'd324: banner_char = "m"; 9'd325: banner_char = 8'h0D; 9'd326: banner_char = 8'h0A; // blank line 9'd327: banner_char = 8'h0D; 9'd328: banner_char = 8'h0A; // "Software Rev: " + 9-char version field (indices 343-351) + "\r\n" 9'd329: banner_char = "S"; 9'd330: banner_char = "o"; 9'd331: banner_char = "f"; 9'd332: banner_char = "t"; 9'd333: banner_char = "w"; 9'd334: banner_char = "a"; 9'd335: banner_char = "r"; 9'd336: banner_char = "e"; 9'd337: banner_char = " "; 9'd338: banner_char = "R"; 9'd339: banner_char = "e"; 9'd340: banner_char = "v"; 9'd341: banner_char = ":"; 9'd342: banner_char = " "; 9'd343: banner_char = "0"; 9'd344: banner_char = "."; 9'd345: banner_char = "1"; 9'd346: banner_char = "."; 9'd347: banner_char = "0"; 9'd348: banner_char = " "; 9'd349: banner_char = " "; 9'd350: banner_char = " "; 9'd351: banner_char = " "; 9'd352: banner_char = 8'h0D; 9'd353: banner_char = 8'h0A; // second separator \r\n 9'd405: banner_char = 8'h0D; 9'd406: banner_char = 8'h0A; // ESC[0m 9'd407: banner_char = 8'h1B; 9'd408: banner_char = "["; 9'd409: banner_char = "0"; 9'd410: 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'd36 : 9'd13; 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