Files
2026-06-10 13:14:22 +02:00

806 lines
34 KiB
Verilog

// 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 (unused, kept for pin assignment)
// hsync - DS90CF386 RxOUT26 (unused, kept for pin assignment)
// cntl - DS90CF386 RxOUT27 (unused, kept for pin assignment)
// red[7:0] - DS90CF386 red channel
// green[7:0]- DS90CF386 green channel
// blue[7:0] - DS90CF386 blue channel
// 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\r\n" once every HEARTBEAT_FRAMES frames.
// - On an anomalous frame, immediately sends
// "ERR lines=NNNN width=NNNN R=RR G=GG B=BB\r\n"
// where NNNN are zero-padded 4-digit decimals and RR/GG/BB are
// 2-digit uppercase hex of the first pixel of the anomalous frame.
module top (
input wire clk_50mhz,
input wire rx_clk,
input wire de,
input wire vsync,
input wire hsync,
input wire cntl,
input wire [7:0] red,
input wire [7:0] green,
input wire [7:0] blue,
input wire rst_n_pin,
output wire uart_tx_pin
);
// keep undriven ports alive so pin assignments stick
wire _unused = &{1'b0, vsync, hsync, cntl, 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;
wire [7:0] first_r_pix;
wire [7:0] first_g_pix;
wire [7:0] first_b_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),
.red (red),
.green (green),
.blue (blue),
.frame_done (frame_done_pix),
.lines_o (lines_pix),
.width_o (width_pix),
.anomaly_o (anomaly_pix),
.first_r (first_r_pix),
.first_g (first_g_pix),
.first_b (first_b_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 [7:0] fr_lat;
reg [7:0] fg_lat;
reg [7:0] fb_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;
fr_lat <= 8'd0;
fg_lat <= 8'd0;
fb_lat <= 8'd0;
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;
fr_lat <= first_r_pix;
fg_lat <= first_g_pix;
fb_lat <= first_b_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;
reg [7:0] fr_u;
reg [7:0] fg_u;
reg [7:0] fb_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;
fr_u <= 8'd0;
fg_u <= 8'd0;
fb_u <= 8'd0;
end else if (req_edge) begin
lines_u <= lines_lat;
width_u <= width_lat;
anomaly_u <= anomaly_lat;
heartbeat_u <= heartbeat_lat;
fr_u <= fr_lat;
fg_u <= fg_lat;
fb_u <= fb_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;
// 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;
// Nibble (0-15) to uppercase ASCII hex digit
function [7:0] hex_char(input [3:0] n);
hex_char = (n <= 4'd9) ? ("0" + {4'h0, n}) : ("A" - 8'd10 + {4'h0, n});
endfunction
// ERR layout (51 bytes):
// ESC[31m(5) + "ERR lines="(10) + LLLL(4) + " width="(7) + WWWW(4)
// + " R="(3) + RR(2) + " G="(3) + GG(2) + " B="(3) + BB(2)
// + ESC[0m(4) + \r\n(2)
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'd31: err_char = "R";
8'd32: err_char = "=";
8'd33: err_char = hex_char(fr_u[7:4]);
8'd34: err_char = hex_char(fr_u[3:0]);
8'd35: err_char = " ";
8'd36: err_char = "G";
8'd37: err_char = "=";
8'd38: err_char = hex_char(fg_u[7:4]);
8'd39: err_char = hex_char(fg_u[3:0]);
8'd40: err_char = " ";
8'd41: err_char = "B";
8'd42: err_char = "=";
8'd43: err_char = hex_char(fb_u[7:4]);
8'd44: err_char = hex_char(fb_u[3:0]);
8'd45: err_char = 8'h1B;
8'd46: err_char = "[";
8'd47: err_char = "0";
8'd48: err_char = "m";
8'd49: err_char = 8'h0D;
8'd50: 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 = "6";
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_LOAD;
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_IDLE: begin
is_banner <= 1'b0;
if (req_edge_q && (anomaly_u || heartbeat_u) && !is_banner) begin
idx <= 9'd0;
is_err_msg <= anomaly_u;
msg_len <= anomaly_u ? 9'd51 : 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