Calculate Average
High-Precision FP16 Version
Key Design Principles
avg_calc_highprec.sv
Computes the IEEE-754 FP16 (half-precision) average of a window of values stored in a memory, centred on a user-supplied peak (max) address.
Interface notes
start_i: sustained LEVEL- Computation begins on the first rising edge;
- a new edge triggers a fresh computation after
done_ois acknowledged (start_igoes low).
mem_rd_data_valid_i: handshake signal from memory;- data is latched only when this is asserted.
- The module issues a one-cycle
mem_rd_en_opulse and waits inST_MEM_WAITuntil valid.
done_o: sustained LEVEL- asserted when outputs are stable,
- held until
start_ideasserts, then auto-clears.
window_valid_o: mirrors done_o (outputs stable).
Memory protocol (ST_MEM_READ → ST_MEM_WAIT)
The module issues a single-cycle mem_rd_en_o pulse and holds mem_rd_addr_o stable until mem_rd_data_valid_i arrives.
mem_rd_data_valid_i must be asserted for exactly one cycle per request (standard request-response / BRAM-with-output-register style).
NaN / ±Infinity handling
Detected by exp-field == 5'b11111. Such samples are skipped and flagged internally (nan_seen_r). The average is taken over valid samples only.
If every sample in the window is NaN/Inf the output is +0.0. Overflow capacity (default parameters, WINDOW_SIZE = 20)
max_FP16 ≈ 65504 → fixed-point ≈ 65504 × 2^24 ≈ 1.10 × 10^12
max_sum = 20 × 1.10×10^12 ≈ 2.20×10^13 << 2^48 ≈ 2.81×10^14 ✓
Latency (worst case, all samples valid, 1-cycle memory)
1 (CALC_WIN) + 2 · N_samples (READ+WAIT per sample)
+ 1 (DIV_INIT) + 48 (DIVIDING) + 1 (CONVERT) + 1 (DONE)
≈ 2 · WINDOW_SIZE + 53 clock cycles.
Code destructuring
Module declaration
module avg_calc_highprec #(
parameter int DATA_WIDTH = 16, // must be 16 (IEEE 754 FP16)
parameter int NUM_VALUES = 2600,
parameter int ADDR_WIDTH = $clog2(NUM_VALUES),
parameter int WINDOW_SIZE = 20 // maximum runtime window width
)(
input logic clk,
input logic rst_n,
// ── Control ──────────────────────────────────────────────────────────────
input logic start_i, // level: rising edge starts computation
// ── Window specification ──────────────────────────────────────────────────
input logic [ADDR_WIDTH-1:0] max_addr_i, // address of the peak (max) sample
input logic [ADDR_WIDTH-1:0] range_start_i, // first valid memory address
input logic [ADDR_WIDTH-1:0] range_end_i, // last valid memory address
input logic [ADDR_WIDTH-1:0] window_size_i, // runtime window width (<= WINDOW_SIZE)
// ── Memory read port (variable-latency, request-response) ─────────────────
output logic mem_rd_en_o, // one-cycle read-request pulse
output logic [ADDR_WIDTH-1:0] mem_rd_addr_o, // address (stable until valid)
input logic [DATA_WIDTH-1:0] mem_rd_data_i, // read data
input logic mem_rd_data_valid_i, // data valid handshake
// ── Results ───────────────────────────────────────────────────────────────
output logic [DATA_WIDTH-1:0] window_avg_o, // FP16 average
output logic [ADDR_WIDTH-1:0] window_start_o, // actual window start address
output logic [ADDR_WIDTH-1:0] window_end_o, // actual window end address
output logic window_valid_o, // level: outputs are stable
output logic done_o, // level: computation complete
output logic busy_o // level: computation in progress
);
Internal parameters and state encoding
// ===========================================================================
// Internal constants (not user-visible to keep the interface clean)
// ===========================================================================
localparam int FRAC_BITS = 24; // fractional bits in unsigned Q fixed-point
localparam int ACCUM_WIDTH = 48; // accumulator / divider register width
localparam int EXP_W = 5;
localparam int MANT_W = 10;
localparam int FP_BIAS = 15;
// Divider step counter: must hold values 0 ... ACCUM_WIDTH-1
localparam int STEP_W = $clog2(ACCUM_WIDTH) + 1; // 7 for ACCUM_WIDTH=48
// Sample counter: must hold 0 ... WINDOW_SIZE
localparam int CNT_W = $clog2(WINDOW_SIZE + 1); // 5 for WINDOW_SIZE=20
// ===========================================================================
// FSM state encoding
// ===========================================================================
typedef enum logic [3:0] {
ST_IDLE = 4'd0, // wait for start_i rising edge
ST_CALC_WIN = 4'd1, // register window boundaries (1 cycle)
ST_MEM_READ = 4'd2, // issue one-cycle mem_rd_en_o pulse
ST_MEM_WAIT = 4'd3, // stall until mem_rd_data_valid_i
ST_ACCUM = 4'd4, // convert FP16->fixed, accumulate; loop or proceed
ST_DIV_INIT = 4'd5, // latch dividend/divisor, clear quotient/remainder
ST_DIVIDING = 4'd6, // ACCUM_WIDTH-cycle restoring binary long division
ST_CONVERT = 4'd7, // fixed-point quotient -> FP16 output register
ST_DONE = 4'd8 // hold done_o until start_i deasserts
} state_e;
state_e state_r, nxt_state;
Rising-edge detector for start_i
// ===========================================================================
// Rising-edge detector for start_i (level -> internal pulse)
// ===========================================================================
logic start_q;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) start_q <= 1'b0;
else start_q <= start_i;
wire start_rise_w = start_i & ~start_q;
Input capture registers
// ===========================================================================
// Input capture registers
// Latched on start_rise_w so upstream can change inputs freely once busy.
// ===========================================================================
logic [ADDR_WIDTH-1:0] max_addr_r;
logic [ADDR_WIDTH-1:0] range_start_r;
logic [ADDR_WIDTH-1:0] range_end_r;
logic [ADDR_WIDTH-1:0] window_size_r;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
max_addr_r <= '0;
range_start_r <= '0;
range_end_r <= '0;
window_size_r <= ADDR_WIDTH'(1);
end else if (start_rise_w) begin
max_addr_r <= max_addr_i;
range_start_r <= range_start_i;
range_end_r <= range_end_i;
// Clamp window_size to [1, WINDOW_SIZE]:
// = 0 would cause divide-by-zero; > WINDOW_SIZE violates parameter.
if (window_size_i == '0)
window_size_r <= ADDR_WIDTH'(1);
else if (window_size_i > ADDR_WIDTH'(WINDOW_SIZE))
window_size_r <= ADDR_WIDTH'(WINDOW_SIZE);
else
window_size_r <= window_size_i;
end
end
Window-boundary combinational logic
// ===========================================================================
// Window-boundary combinational logic (uses registered inputs)
//
// Policy: place floor(window_size/2) samples BEFORE max_addr,
// remainder AFTER, then clamp to [range_start, range_end].
// This guarantees max_addr is always inside the resulting window.
// ===========================================================================
logic [ADDR_WIDTH-1:0] half_w;
logic [ADDR_WIDTH-1:0] calc_s_w; // computed window start
logic [ADDR_WIDTH:0] tent_e_w; // tentative end (extra bit detects wrap)
logic [ADDR_WIDTH-1:0] calc_e_w; // computed window end
always_comb begin
half_w = window_size_r >> 1;
// Left boundary: clamp to range_start on underflow
if (max_addr_r >= range_start_r + half_w)
calc_s_w = max_addr_r - half_w;
else
calc_s_w = range_start_r;
// Right boundary: clamp to range_end on overflow
tent_e_w = {1'b0, calc_s_w} + {1'b0, window_size_r} - 1;
if (tent_e_w[ADDR_WIDTH] || tent_e_w[ADDR_WIDTH-1:0] > range_end_r)
calc_e_w = range_end_r;
else
calc_e_w = tent_e_w[ADDR_WIDTH-1:0];
end
Function: FP16 -> unsigned Q(ACCUM_WIDTH).(FRAC_BITS) fixed-point
// ===========================================================================
// Function: FP16 -> unsigned Q(ACCUM_WIDTH).(FRAC_BITS) fixed-point
//
// Positive values only (sign bit always 0 per module specification).
// NaN / Infinity -> 0 (caller checks exp == 5'b11111 and sets nan_seen_r).
//
// For FRAC_BITS = 24 and normal numbers: shift = exp - 1 in [0, 29].
// ===========================================================================
function automatic [ACCUM_WIDTH-1:0] fp16_to_fix (
input logic [DATA_WIDTH-1:0] fp
);
logic [EXP_W-1:0] e;
logic [MANT_W-1:0] f;
logic [MANT_W:0] m; // 11-bit with implicit leading 1
logic [ACCUM_WIDTH-1:0] r;
int sh;
e = fp[14:10];
f = fp[ 9: 0];
r = '0;
if (e == {EXP_W{1'b1}}) begin
// NaN or +/-Infinity -> 0 (caller flags this)
r = '0;
end else if (e == '0) begin
// Zero or denormal: true_val = f * 2^(1-15-10) = f * 2^(-24)
// fixed = f * 2^(FRAC_BITS - 24); for FRAC_BITS=24 -> shift = 0
if (f != '0) begin
sh = FRAC_BITS - 24;
if (sh >= 0) r = ACCUM_WIDTH'(f) << sh;
else r = ACCUM_WIDTH'(f) >> (-sh);
end
end else begin
// Normal: true_val = (2^10 + f) * 2^(e - 25)
// fixed = (2^10 + f) * 2^(e + FRAC_BITS - 25)
// For FRAC_BITS=24: sh = e - 1, range [0, 29]
m = {1'b1, f};
sh = int'(e) + FRAC_BITS - 25;
if (sh >= 0) r = ACCUM_WIDTH'(m) << sh;
else r = ACCUM_WIDTH'(m) >> (-sh);
end
return r;
endfunction
Function: unsigned Q(ACCUM_WIDTH).(FRAC_BITS) fixed-point -> FP16
// ===========================================================================
// Function: unsigned Q(ACCUM_WIDTH).(FRAC_BITS) fixed-point -> FP16
//
// Round-to-nearest-half-up.
// Overflow -> +Infinity (exp all-ones, mantissa zero)
// Underflow -> +0.0
// ===========================================================================
function automatic [DATA_WIDTH-1:0] fix_to_fp16 (
input logic [ACCUM_WIDTH-1:0] val
);
int lead;
int exp_u, exp_b;
logic [ACCUM_WIDTH-1:0] aligned;
logic [MANT_W-1:0] mant;
logic rb;
logic [EXP_W-1:0] eout;
logic [DATA_WIDTH-1:0] res;
// Priority-encode MSB: iterate low->high; last write wins = highest set bit.
// Synthesises to a priority MUX tree (O(ACCUM_WIDTH) logic depth).
lead = -1;
for (int i = 0; i < ACCUM_WIDTH; i++)
if (val[i]) lead = i;
if (lead < 0) begin
res = '0; // exact zero
end else begin
exp_u = lead - FRAC_BITS;
exp_b = exp_u + FP_BIAS;
if (exp_b >= 31) begin
res = {1'b0, {EXP_W{1'b1}}, {MANT_W{1'b0}}}; // overflow -> +Inf
end else if (exp_b <= 0) begin
res = '0; // underflow -> +0.0
end else begin
eout = EXP_W'(unsigned'(exp_b));
// Shift so leading 1 sits at bit ACCUM_WIDTH-1
aligned = val << (ACCUM_WIDTH - 1 - lead);
// 10-bit mantissa = bits [ACCUM_WIDTH-2 : ACCUM_WIDTH-1-MANT_W]
mant = aligned[ACCUM_WIDTH-2 -: MANT_W];
// Round bit = first bit beyond stored mantissa
rb = aligned[ACCUM_WIDTH - 2 - MANT_W];
if (rb) begin
// Round up; handle mantissa carry-out into exponent
if (&mant) begin
mant = '0;
eout = eout + 1'b1;
if (eout == {EXP_W{1'b1}})
res = {1'b0, {EXP_W{1'b1}}, {MANT_W{1'b0}}}; // rounded to +Inf
else
res = {1'b0, eout, mant};
end else begin
res = {1'b0, eout, mant + 1'b1};
end
end else begin
res = {1'b0, eout, mant};
end
end
end
return res;
endfunction
Datapath registers
// ===========================================================================
// Datapath registers
// ===========================================================================
logic [ADDR_WIDTH-1:0] win_s_r; // registered window start
logic [ADDR_WIDTH-1:0] win_e_r; // registered window end
logic [ADDR_WIDTH-1:0] cur_addr_r; // current SRAM read pointer
logic [ACCUM_WIDTH-1:0] accum_r; // fixed-point running sum
logic [CNT_W-1:0] sample_cnt_r; // count of valid (non-NaN/Inf) samples
logic nan_seen_r; // at least one NaN/Inf was skipped
logic [ACCUM_WIDTH-1:0] dv_dvnd_r; // latched dividend (accumulator)
logic [CNT_W-1:0] dv_dvsr_r; // latched divisor (sample_cnt)
logic [ACCUM_WIDTH-1:0] dv_quot_r; // accumulating quotient (MSB-first)
logic [ACCUM_WIDTH-1:0] dv_rem_r; // partial remainder
logic [STEP_W-1:0] dv_step_r; // bit index: ACCUM_WIDTH-1 down to 0
Divider
// ===========================================================================
// Divider: combinational one-step signals (used inside ST_DIVIDING)
//
// Restoring binary long division, MSB-first:
// rem_shifted = { rem[N-2:0], dividend[step] }
// quotient_bit = (rem_shifted >= divisor)
// if quotient_bit: remainder <- rem_shifted - divisor
// else: remainder <- rem_shifted
// ===========================================================================
logic [ACCUM_WIDTH-1:0] div_rem_sh;
logic div_qbit;
always_comb begin
div_rem_sh = {dv_rem_r[ACCUM_WIDTH-2:0], dv_dvnd_r[dv_step_r]};
div_qbit = (div_rem_sh >= ACCUM_WIDTH'(dv_dvsr_r));
end
Next-state logic
// ===========================================================================
// Next-state logic (purely combinational)
// ===========================================================================
always_comb begin
nxt_state = state_r;
case (state_r)
ST_IDLE : nxt_state = start_rise_w ? ST_CALC_WIN : ST_IDLE;
ST_CALC_WIN: nxt_state = ST_MEM_READ;
ST_MEM_READ: nxt_state = ST_MEM_WAIT;
ST_MEM_WAIT: nxt_state = mem_rd_data_valid_i ? ST_ACCUM : ST_MEM_WAIT;
ST_ACCUM : nxt_state = (cur_addr_r == win_e_r) ? ST_DIV_INIT : ST_MEM_READ;
ST_DIV_INIT: nxt_state = ST_DIVIDING;
ST_DIVIDING: nxt_state = (dv_step_r == '0) ? ST_CONVERT : ST_DIVIDING;
ST_CONVERT : nxt_state = ST_DONE;
// Hold done_o / window_valid_o until start_i deasserts
ST_DONE : nxt_state = start_i ? ST_DONE : ST_IDLE;
default : nxt_state = ST_IDLE;
endcase
end
State register
// ===========================================================================
// State register
// ===========================================================================
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state_r <= ST_IDLE;
else state_r <= nxt_state;
Datapath
// ===========================================================================
// Datapath (sequential)
// ===========================================================================
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
win_s_r <= '0;
win_e_r <= '0;
cur_addr_r <= '0;
accum_r <= '0;
sample_cnt_r <= '0;
nan_seen_r <= 1'b0;
dv_dvnd_r <= '0;
dv_dvsr_r <= '0;
dv_quot_r <= '0;
dv_rem_r <= '0;
dv_step_r <= '0;
mem_rd_en_o <= 1'b0;
mem_rd_addr_o <= '0;
window_avg_o <= '0;
window_start_o <= '0;
window_end_o <= '0;
end else begin
mem_rd_en_o <= 1'b0; // default: de-assert; overridden in ST_MEM_READ only
case (state_r)
// --------------------------------------------------------------------
// IDLE
// Clear accumulation state on each start edge.
// --------------------------------------------------------------------
ST_IDLE: begin
if (start_rise_w) begin
accum_r <= '0;
sample_cnt_r <= '0;
nan_seen_r <= 1'b0;
end
end
// --------------------------------------------------------------------
// CALC_WIN
// Register window boundaries and initialise the read pointer.
// window_start_o / window_end_o are stable well before done_o fires.
// --------------------------------------------------------------------
ST_CALC_WIN: begin
win_s_r <= calc_s_w;
win_e_r <= calc_e_w;
cur_addr_r <= calc_s_w;
window_start_o <= calc_s_w;
window_end_o <= calc_e_w;
end
// --------------------------------------------------------------------
// MEM_READ
// Assert mem_rd_en_o for exactly one clock cycle.
// mem_rd_addr_o remains stable through ST_MEM_WAIT.
// --------------------------------------------------------------------
ST_MEM_READ: begin
mem_rd_en_o <= 1'b1;
mem_rd_addr_o <= cur_addr_r;
end
// --------------------------------------------------------------------
// MEM_WAIT
// No datapath action. Address held stable (cur_addr_r unchanged).
// Transition to ST_ACCUM is driven by mem_rd_data_valid_i in the
// next-state logic.
// --------------------------------------------------------------------
ST_MEM_WAIT: ;
// --------------------------------------------------------------------
// ACCUM
// mem_rd_data_i is valid this cycle (first cycle of valid).
// NaN / +/-Infinity: flag and skip (exclude from average).
// Valid sample: convert FP16 -> fixed-point, accumulate.
// --------------------------------------------------------------------
ST_ACCUM: begin
if (mem_rd_data_i[14:10] == {EXP_W{1'b1}}) begin
nan_seen_r <= 1'b1; // NaN or +/-Inf: exclude, record
end else begin
accum_r <= accum_r + fp16_to_fix(mem_rd_data_i);
sample_cnt_r <= sample_cnt_r + 1'b1;
end
// Advance read pointer for the next fetch (if more samples remain)
if (cur_addr_r != win_e_r)
cur_addr_r <= cur_addr_r + 1'b1;
end
// --------------------------------------------------------------------
// DIV_INIT
// Latch operands for the sequential divider.
// Division-by-zero guard: if all samples were NaN/Inf,
// sample_cnt_r == 0 -> substitute divisor = 1 so quotient = 0/1 = 0
// -> window_avg_o = +0.0 FP16.
// --------------------------------------------------------------------
ST_DIV_INIT: begin
dv_dvnd_r <= accum_r;
dv_dvsr_r <= (sample_cnt_r == '0) ? CNT_W'(1) : sample_cnt_r;
dv_quot_r <= '0;
dv_rem_r <= '0;
dv_step_r <= STEP_W'(ACCUM_WIDTH - 1); // begin at MSB of dividend
end
// --------------------------------------------------------------------
// DIVIDING
// One step of restoring binary long division per clock cycle.
// Quotient accumulates MSB-first via left-shift into the LSB.
// After ACCUM_WIDTH (=48) cycles:
// dv_quot_r = floor(accumulator / sample_cnt)
// --------------------------------------------------------------------
ST_DIVIDING: begin
dv_rem_r <= div_qbit
? (div_rem_sh - ACCUM_WIDTH'(dv_dvsr_r))
: div_rem_sh;
dv_quot_r <= {dv_quot_r[ACCUM_WIDTH-2:0], div_qbit};
dv_step_r <= dv_step_r - 1'b1;
end
// --------------------------------------------------------------------
// CONVERT
// Convert the fixed-point quotient to FP16 in one cycle.
// window_avg_o is registered HERE so it is fully stable when
// done_o asserts in the very first cycle of ST_DONE.
// --------------------------------------------------------------------
ST_CONVERT: begin
window_avg_o <= fix_to_fp16(dv_quot_r);
end
// --------------------------------------------------------------------
// DONE
// All output registers (window_avg_o, window_start_o, window_end_o)
// are already stable. No datapath action needed.
// done_o / window_valid_o are driven combinationally from state_r.
// FSM holds here until start_i deasserts.
// --------------------------------------------------------------------
ST_DONE: ;
default: ;
endcase
end
end
Combinational status outputs
// ===========================================================================
// Combinational status outputs (decoded directly from current state)
//
// busy_o : high while computation is active (not IDLE or DONE)
// done_o : sustained level, high during ST_DONE only
// window_valid_o: mirrors done_o; all output ports stable when asserted
//
// Output timing guarantee:
// window_avg_o registered in ST_CONVERT -> valid on entry to ST_DONE
// window_start_o registered in ST_CALC_WIN -> valid well before ST_DONE
// window_end_o registered in ST_CALC_WIN -> valid well before ST_DONE
// ===========================================================================
always_comb begin
case (state_r)
ST_IDLE, ST_DONE: busy_o = 1'b0;
default: busy_o = 1'b1;
endcase
end
assign done_o = (state_r == ST_DONE);
assign window_valid_o = (state_r == ST_DONE);
Simulation-only checks
// ===========================================================================
// Simulation-only checks (removed by synthesis translate_off/on guards)
// ===========================================================================
// synthesis translate_off
initial begin : param_check
if (DATA_WIDTH != 16)
$fatal(1, "[avg_calc_highprec] DATA_WIDTH must be 16 (FP16 only).");
if (ACCUM_WIDTH < FRAC_BITS + 11)
$fatal(1, "[avg_calc_highprec] ACCUM_WIDTH=%0d too narrow for FRAC_BITS=%0d.",
ACCUM_WIDTH, FRAC_BITS);
if (WINDOW_SIZE > (1 << CNT_W) - 1)
$fatal(1, "[avg_calc_highprec] WINDOW_SIZE=%0d overflows CNT_W=%0d.",
WINDOW_SIZE, CNT_W);
end
always_ff @(posedge clk) begin : protocol_checks
if (rst_n) begin
// Warn if start_i pulses while busy (rising edge ignored)
if ((state_r != ST_IDLE) && (state_r != ST_DONE) && start_rise_w)
$warning("[avg_calc_highprec] @%0t: start_i rising edge while busy — ignored.",
$time);
// Warn if memory valid arrives outside the expected window
if (mem_rd_data_valid_i &&
(state_r != ST_MEM_WAIT) && (state_r != ST_MEM_READ))
$warning("[avg_calc_highprec] @%0t: mem_rd_data_valid_i asserted unexpectedly.",
$time);
end
end
// synthesis translate_on
endmodule : avg_calc_highprec
code
// =============================================================================
// avg_calc_highprec.sv
//
// Computes the IEEE-754 FP16 (half-precision) average of a window of values
// stored in a memory, centred on a user-supplied peak (max) address.
//
// Interface notes
// ───────────────
// · start_i : sustained LEVEL — computation begins on the first
// rising edge; a new edge triggers a fresh computation
// after done_o is acknowledged (start_i goes low).
// · mem_rd_data_valid_i : handshake signal from memory; data is latched only
// when this is asserted. The module issues a one-cycle
// mem_rd_en_o pulse and waits in ST_MEM_WAIT until valid.
// · done_o : sustained LEVEL — asserted when outputs are stable,
// held until start_i deasserts, then auto-clears.
// · window_valid_o : mirrors done_o (outputs stable).
//
// Memory protocol (ST_MEM_READ → ST_MEM_WAIT)
// ─────────────────────────────────────────────
// The module issues a single-cycle mem_rd_en_o pulse and holds
// mem_rd_addr_o stable until mem_rd_data_valid_i arrives.
// mem_rd_data_valid_i must be asserted for exactly one cycle per request
// (standard request-response / BRAM-with-output-register style).
//
// NaN / ±Infinity handling
// ─────────────────────────
// Detected by exp-field == 5'b11111. Such samples are skipped and flagged
// internally (nan_seen_r). The average is taken over valid samples only.
// If every sample in the window is NaN/Inf the output is +0.0.
//
// Overflow capacity (default parameters, WINDOW_SIZE = 20)
// ──────────────────────────────────────────────────────────
// max_FP16 ≈ 65504 → fixed-point ≈ 65504 × 2^24 ≈ 1.10 × 10^12
// max_sum = 20 × 1.10×10^12 ≈ 2.20×10^13 << 2^48 ≈ 2.81×10^14 ✓
//
// Latency (worst case, all samples valid, 1-cycle memory)
// ─────────────────────────────────────────────────────────
// 1 (CALC_WIN) + 2·N_samples (READ+WAIT per sample)
// + 1 (DIV_INIT) + 48 (DIVIDING) + 1 (CONVERT) + 1 (DONE)
// ≈ 2·WINDOW_SIZE + 53 clock cycles.
// =============================================================================
module avg_calc_highprec #(
parameter int DATA_WIDTH = 16, // must be 16 (IEEE 754 FP16)
parameter int NUM_VALUES = 2600,
parameter int ADDR_WIDTH = $clog2(NUM_VALUES),
parameter int WINDOW_SIZE = 20 // maximum runtime window width
)(
input logic clk,
input logic rst_n,
// ── Control ──────────────────────────────────────────────────────────────
input logic start_i, // level: rising edge starts computation
// ── Window specification ──────────────────────────────────────────────────
input logic [ADDR_WIDTH-1:0] max_addr_i, // address of the peak (max) sample
input logic [ADDR_WIDTH-1:0] range_start_i, // first valid memory address
input logic [ADDR_WIDTH-1:0] range_end_i, // last valid memory address
input logic [ADDR_WIDTH-1:0] window_size_i, // runtime window width (<= WINDOW_SIZE)
// ── Memory read port (variable-latency, request-response) ─────────────────
output logic mem_rd_en_o, // one-cycle read-request pulse
output logic [ADDR_WIDTH-1:0] mem_rd_addr_o, // address (stable until valid)
input logic [DATA_WIDTH-1:0] mem_rd_data_i, // read data
input logic mem_rd_data_valid_i, // data valid handshake
// ── Results ───────────────────────────────────────────────────────────────
output logic [DATA_WIDTH-1:0] window_avg_o, // FP16 average
output logic [ADDR_WIDTH-1:0] window_start_o, // actual window start address
output logic [ADDR_WIDTH-1:0] window_end_o, // actual window end address
output logic window_valid_o, // level: outputs are stable
output logic done_o, // level: computation complete
output logic busy_o // level: computation in progress
);
// ===========================================================================
// Internal constants (not user-visible to keep the interface clean)
// ===========================================================================
localparam int FRAC_BITS = 24; // fractional bits in unsigned Q fixed-point
localparam int ACCUM_WIDTH = 48; // accumulator / divider register width
localparam int EXP_W = 5;
localparam int MANT_W = 10;
localparam int FP_BIAS = 15;
// Divider step counter: must hold values 0 ... ACCUM_WIDTH-1
localparam int STEP_W = $clog2(ACCUM_WIDTH) + 1; // 7 for ACCUM_WIDTH=48
// Sample counter: must hold 0 ... WINDOW_SIZE
localparam int CNT_W = $clog2(WINDOW_SIZE + 1); // 5 for WINDOW_SIZE=20
// ===========================================================================
// FSM state encoding
// ===========================================================================
typedef enum logic [3:0] {
ST_IDLE = 4'd0, // wait for start_i rising edge
ST_CALC_WIN = 4'd1, // register window boundaries (1 cycle)
ST_MEM_READ = 4'd2, // issue one-cycle mem_rd_en_o pulse
ST_MEM_WAIT = 4'd3, // stall until mem_rd_data_valid_i
ST_ACCUM = 4'd4, // convert FP16->fixed, accumulate; loop or proceed
ST_DIV_INIT = 4'd5, // latch dividend/divisor, clear quotient/remainder
ST_DIVIDING = 4'd6, // ACCUM_WIDTH-cycle restoring binary long division
ST_CONVERT = 4'd7, // fixed-point quotient -> FP16 output register
ST_DONE = 4'd8 // hold done_o until start_i deasserts
} state_e;
state_e state_r, nxt_state;
// ===========================================================================
// Rising-edge detector for start_i (level -> internal pulse)
// ===========================================================================
logic start_q;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) start_q <= 1'b0;
else start_q <= start_i;
wire start_rise_w = start_i & ~start_q;
// ===========================================================================
// Input capture registers
// Latched on start_rise_w so upstream can change inputs freely once busy.
// ===========================================================================
logic [ADDR_WIDTH-1:0] max_addr_r;
logic [ADDR_WIDTH-1:0] range_start_r;
logic [ADDR_WIDTH-1:0] range_end_r;
logic [ADDR_WIDTH-1:0] window_size_r;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
max_addr_r <= '0;
range_start_r <= '0;
range_end_r <= '0;
window_size_r <= ADDR_WIDTH'(1);
end else if (start_rise_w) begin
max_addr_r <= max_addr_i;
range_start_r <= range_start_i;
range_end_r <= range_end_i;
// Clamp window_size to [1, WINDOW_SIZE]:
// = 0 would cause divide-by-zero; > WINDOW_SIZE violates parameter.
if (window_size_i == '0)
window_size_r <= ADDR_WIDTH'(1);
else if (window_size_i > ADDR_WIDTH'(WINDOW_SIZE))
window_size_r <= ADDR_WIDTH'(WINDOW_SIZE);
else
window_size_r <= window_size_i;
end
end
// ===========================================================================
// Window-boundary combinational logic (uses registered inputs)
//
// Policy: place floor(window_size/2) samples BEFORE max_addr,
// remainder AFTER, then clamp to [range_start, range_end].
// This guarantees max_addr is always inside the resulting window.
// ===========================================================================
logic [ADDR_WIDTH-1:0] half_w;
logic [ADDR_WIDTH-1:0] calc_s_w; // computed window start
logic [ADDR_WIDTH:0] tent_e_w; // tentative end (extra bit detects wrap)
logic [ADDR_WIDTH-1:0] calc_e_w; // computed window end
always_comb begin
half_w = window_size_r >> 1;
// Left boundary: clamp to range_start on underflow
if (max_addr_r >= range_start_r + half_w)
calc_s_w = max_addr_r - half_w;
else
calc_s_w = range_start_r;
// Right boundary: clamp to range_end on overflow
tent_e_w = {1'b0, calc_s_w} + {1'b0, window_size_r} - 1;
if (tent_e_w[ADDR_WIDTH] || tent_e_w[ADDR_WIDTH-1:0] > range_end_r)
calc_e_w = range_end_r;
else
calc_e_w = tent_e_w[ADDR_WIDTH-1:0];
end
// ===========================================================================
// Function: FP16 -> unsigned Q(ACCUM_WIDTH).(FRAC_BITS) fixed-point
//
// Positive values only (sign bit always 0 per module specification).
// NaN / Infinity -> 0 (caller checks exp == 5'b11111 and sets nan_seen_r).
//
// For FRAC_BITS = 24 and normal numbers: shift = exp - 1 in [0, 29].
// ===========================================================================
function automatic [ACCUM_WIDTH-1:0] fp16_to_fix (
input logic [DATA_WIDTH-1:0] fp
);
logic [EXP_W-1:0] e;
logic [MANT_W-1:0] f;
logic [MANT_W:0] m; // 11-bit with implicit leading 1
logic [ACCUM_WIDTH-1:0] r;
int sh;
e = fp[14:10];
f = fp[ 9: 0];
r = '0;
if (e == {EXP_W{1'b1}}) begin
// NaN or +/-Infinity -> 0 (caller flags this)
r = '0;
end else if (e == '0) begin
// Zero or denormal: true_val = f * 2^(1-15-10) = f * 2^(-24)
// fixed = f * 2^(FRAC_BITS - 24); for FRAC_BITS=24 -> shift = 0
if (f != '0) begin
sh = FRAC_BITS - 24;
if (sh >= 0) r = ACCUM_WIDTH'(f) << sh;
else r = ACCUM_WIDTH'(f) >> (-sh);
end
end else begin
// Normal: true_val = (2^10 + f) * 2^(e - 25)
// fixed = (2^10 + f) * 2^(e + FRAC_BITS - 25)
// For FRAC_BITS=24: sh = e - 1, range [0, 29]
m = {1'b1, f};
sh = int'(e) + FRAC_BITS - 25;
if (sh >= 0) r = ACCUM_WIDTH'(m) << sh;
else r = ACCUM_WIDTH'(m) >> (-sh);
end
return r;
endfunction
// ===========================================================================
// Function: unsigned Q(ACCUM_WIDTH).(FRAC_BITS) fixed-point -> FP16
//
// Round-to-nearest-half-up.
// Overflow -> +Infinity (exp all-ones, mantissa zero)
// Underflow -> +0.0
// ===========================================================================
function automatic [DATA_WIDTH-1:0] fix_to_fp16 (
input logic [ACCUM_WIDTH-1:0] val
);
int lead;
int exp_u, exp_b;
logic [ACCUM_WIDTH-1:0] aligned;
logic [MANT_W-1:0] mant;
logic rb;
logic [EXP_W-1:0] eout;
logic [DATA_WIDTH-1:0] res;
// Priority-encode MSB: iterate low->high; last write wins = highest set bit.
// Synthesises to a priority MUX tree (O(ACCUM_WIDTH) logic depth).
lead = -1;
for (int i = 0; i < ACCUM_WIDTH; i++)
if (val[i]) lead = i;
if (lead < 0) begin
res = '0; // exact zero
end else begin
exp_u = lead - FRAC_BITS;
exp_b = exp_u + FP_BIAS;
if (exp_b >= 31) begin
res = {1'b0, {EXP_W{1'b1}}, {MANT_W{1'b0}}}; // overflow -> +Inf
end else if (exp_b <= 0) begin
res = '0; // underflow -> +0.0
end else begin
eout = EXP_W'(unsigned'(exp_b));
// Shift so leading 1 sits at bit ACCUM_WIDTH-1
aligned = val << (ACCUM_WIDTH - 1 - lead);
// 10-bit mantissa = bits [ACCUM_WIDTH-2 : ACCUM_WIDTH-1-MANT_W]
mant = aligned[ACCUM_WIDTH-2 -: MANT_W];
// Round bit = first bit beyond stored mantissa
rb = aligned[ACCUM_WIDTH - 2 - MANT_W];
if (rb) begin
// Round up; handle mantissa carry-out into exponent
if (&mant) begin
mant = '0;
eout = eout + 1'b1;
if (eout == {EXP_W{1'b1}})
res = {1'b0, {EXP_W{1'b1}}, {MANT_W{1'b0}}}; // rounded to +Inf
else
res = {1'b0, eout, mant};
end else begin
res = {1'b0, eout, mant + 1'b1};
end
end else begin
res = {1'b0, eout, mant};
end
end
end
return res;
endfunction
// ===========================================================================
// Datapath registers
// ===========================================================================
logic [ADDR_WIDTH-1:0] win_s_r; // registered window start
logic [ADDR_WIDTH-1:0] win_e_r; // registered window end
logic [ADDR_WIDTH-1:0] cur_addr_r; // current SRAM read pointer
logic [ACCUM_WIDTH-1:0] accum_r; // fixed-point running sum
logic [CNT_W-1:0] sample_cnt_r; // count of valid (non-NaN/Inf) samples
logic nan_seen_r; // at least one NaN/Inf was skipped
logic [ACCUM_WIDTH-1:0] dv_dvnd_r; // latched dividend (accumulator)
logic [CNT_W-1:0] dv_dvsr_r; // latched divisor (sample_cnt)
logic [ACCUM_WIDTH-1:0] dv_quot_r; // accumulating quotient (MSB-first)
logic [ACCUM_WIDTH-1:0] dv_rem_r; // partial remainder
logic [STEP_W-1:0] dv_step_r; // bit index: ACCUM_WIDTH-1 down to 0
// ===========================================================================
// Divider: combinational one-step signals (used inside ST_DIVIDING)
//
// Restoring binary long division, MSB-first:
// rem_shifted = { rem[N-2:0], dividend[step] }
// quotient_bit = (rem_shifted >= divisor)
// if quotient_bit: remainder <- rem_shifted - divisor
// else: remainder <- rem_shifted
// ===========================================================================
logic [ACCUM_WIDTH-1:0] div_rem_sh;
logic div_qbit;
always_comb begin
div_rem_sh = {dv_rem_r[ACCUM_WIDTH-2:0], dv_dvnd_r[dv_step_r]};
div_qbit = (div_rem_sh >= ACCUM_WIDTH'(dv_dvsr_r));
end
// ===========================================================================
// Next-state logic (purely combinational)
// ===========================================================================
always_comb begin
nxt_state = state_r;
case (state_r)
ST_IDLE : nxt_state = start_rise_w ? ST_CALC_WIN : ST_IDLE;
ST_CALC_WIN: nxt_state = ST_MEM_READ;
ST_MEM_READ: nxt_state = ST_MEM_WAIT;
ST_MEM_WAIT: nxt_state = mem_rd_data_valid_i ? ST_ACCUM : ST_MEM_WAIT;
ST_ACCUM : nxt_state = (cur_addr_r == win_e_r) ? ST_DIV_INIT : ST_MEM_READ;
ST_DIV_INIT: nxt_state = ST_DIVIDING;
ST_DIVIDING: nxt_state = (dv_step_r == '0) ? ST_CONVERT : ST_DIVIDING;
ST_CONVERT : nxt_state = ST_DONE;
// Hold done_o / window_valid_o until start_i deasserts
ST_DONE : nxt_state = start_i ? ST_DONE : ST_IDLE;
default : nxt_state = ST_IDLE;
endcase
end
// ===========================================================================
// State register
// ===========================================================================
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state_r <= ST_IDLE;
else state_r <= nxt_state;
// ===========================================================================
// Datapath (sequential)
// ===========================================================================
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
win_s_r <= '0;
win_e_r <= '0;
cur_addr_r <= '0;
accum_r <= '0;
sample_cnt_r <= '0;
nan_seen_r <= 1'b0;
dv_dvnd_r <= '0;
dv_dvsr_r <= '0;
dv_quot_r <= '0;
dv_rem_r <= '0;
dv_step_r <= '0;
mem_rd_en_o <= 1'b0;
mem_rd_addr_o <= '0;
window_avg_o <= '0;
window_start_o <= '0;
window_end_o <= '0;
end else begin
mem_rd_en_o <= 1'b0; // default: de-assert; overridden in ST_MEM_READ only
case (state_r)
// --------------------------------------------------------------------
// IDLE
// Clear accumulation state on each start edge.
// --------------------------------------------------------------------
ST_IDLE: begin
if (start_rise_w) begin
accum_r <= '0;
sample_cnt_r <= '0;
nan_seen_r <= 1'b0;
end
end
// --------------------------------------------------------------------
// CALC_WIN
// Register window boundaries and initialise the read pointer.
// window_start_o / window_end_o are stable well before done_o fires.
// --------------------------------------------------------------------
ST_CALC_WIN: begin
win_s_r <= calc_s_w;
win_e_r <= calc_e_w;
cur_addr_r <= calc_s_w;
window_start_o <= calc_s_w;
window_end_o <= calc_e_w;
end
// --------------------------------------------------------------------
// MEM_READ
// Assert mem_rd_en_o for exactly one clock cycle.
// mem_rd_addr_o remains stable through ST_MEM_WAIT.
// --------------------------------------------------------------------
ST_MEM_READ: begin
mem_rd_en_o <= 1'b1;
mem_rd_addr_o <= cur_addr_r;
end
// --------------------------------------------------------------------
// MEM_WAIT
// No datapath action. Address held stable (cur_addr_r unchanged).
// Transition to ST_ACCUM is driven by mem_rd_data_valid_i in the
// next-state logic.
// --------------------------------------------------------------------
ST_MEM_WAIT: ;
// --------------------------------------------------------------------
// ACCUM
// mem_rd_data_i is valid this cycle (first cycle of valid).
// NaN / +/-Infinity: flag and skip (exclude from average).
// Valid sample: convert FP16 -> fixed-point, accumulate.
// --------------------------------------------------------------------
ST_ACCUM: begin
if (mem_rd_data_i[14:10] == {EXP_W{1'b1}}) begin
nan_seen_r <= 1'b1; // NaN or +/-Inf: exclude, record
end else begin
accum_r <= accum_r + fp16_to_fix(mem_rd_data_i);
sample_cnt_r <= sample_cnt_r + 1'b1;
end
// Advance read pointer for the next fetch (if more samples remain)
if (cur_addr_r != win_e_r)
cur_addr_r <= cur_addr_r + 1'b1;
end
// --------------------------------------------------------------------
// DIV_INIT
// Latch operands for the sequential divider.
// Division-by-zero guard: if all samples were NaN/Inf,
// sample_cnt_r == 0 -> substitute divisor = 1 so quotient = 0/1 = 0
// -> window_avg_o = +0.0 FP16.
// --------------------------------------------------------------------
ST_DIV_INIT: begin
dv_dvnd_r <= accum_r;
dv_dvsr_r <= (sample_cnt_r == '0) ? CNT_W'(1) : sample_cnt_r;
dv_quot_r <= '0;
dv_rem_r <= '0;
dv_step_r <= STEP_W'(ACCUM_WIDTH - 1); // begin at MSB of dividend
end
// --------------------------------------------------------------------
// DIVIDING
// One step of restoring binary long division per clock cycle.
// Quotient accumulates MSB-first via left-shift into the LSB.
// After ACCUM_WIDTH (=48) cycles:
// dv_quot_r = floor(accumulator / sample_cnt)
// --------------------------------------------------------------------
ST_DIVIDING: begin
dv_rem_r <= div_qbit
? (div_rem_sh - ACCUM_WIDTH'(dv_dvsr_r))
: div_rem_sh;
dv_quot_r <= {dv_quot_r[ACCUM_WIDTH-2:0], div_qbit};
dv_step_r <= dv_step_r - 1'b1;
end
// --------------------------------------------------------------------
// CONVERT
// Convert the fixed-point quotient to FP16 in one cycle.
// window_avg_o is registered HERE so it is fully stable when
// done_o asserts in the very first cycle of ST_DONE.
// --------------------------------------------------------------------
ST_CONVERT: begin
window_avg_o <= fix_to_fp16(dv_quot_r);
end
// --------------------------------------------------------------------
// DONE
// All output registers (window_avg_o, window_start_o, window_end_o)
// are already stable. No datapath action needed.
// done_o / window_valid_o are driven combinationally from state_r.
// FSM holds here until start_i deasserts.
// --------------------------------------------------------------------
ST_DONE: ;
default: ;
endcase
end
end
// ===========================================================================
// Combinational status outputs (decoded directly from current state)
//
// busy_o : high while computation is active (not IDLE or DONE)
// done_o : sustained level, high during ST_DONE only
// window_valid_o: mirrors done_o; all output ports stable when asserted
//
// Output timing guarantee:
// window_avg_o registered in ST_CONVERT -> valid on entry to ST_DONE
// window_start_o registered in ST_CALC_WIN -> valid well before ST_DONE
// window_end_o registered in ST_CALC_WIN -> valid well before ST_DONE
// ===========================================================================
always_comb begin
case (state_r)
ST_IDLE, ST_DONE: busy_o = 1'b0;
default: busy_o = 1'b1;
endcase
end
assign done_o = (state_r == ST_DONE);
assign window_valid_o = (state_r == ST_DONE);
// ===========================================================================
// Simulation-only checks (removed by synthesis translate_off/on guards)
// ===========================================================================
// synthesis translate_off
initial begin : param_check
if (DATA_WIDTH != 16)
$fatal(1, "[avg_calc_highprec] DATA_WIDTH must be 16 (FP16 only).");
if (ACCUM_WIDTH < FRAC_BITS + 11)
$fatal(1, "[avg_calc_highprec] ACCUM_WIDTH=%0d too narrow for FRAC_BITS=%0d.",
ACCUM_WIDTH, FRAC_BITS);
if (WINDOW_SIZE > (1 << CNT_W) - 1)
$fatal(1, "[avg_calc_highprec] WINDOW_SIZE=%0d overflows CNT_W=%0d.",
WINDOW_SIZE, CNT_W);
end
always_ff @(posedge clk) begin : protocol_checks
if (rst_n) begin
// Warn if start_i pulses while busy (rising edge ignored)
if ((state_r != ST_IDLE) && (state_r != ST_DONE) && start_rise_w)
$warning("[avg_calc_highprec] @%0t: start_i rising edge while busy — ignored.",
$time);
// Warn if memory valid arrives outside the expected window
if (mem_rd_data_valid_i &&
(state_r != ST_MEM_WAIT) && (state_r != ST_MEM_READ))
$warning("[avg_calc_highprec] @%0t: mem_rd_data_valid_i asserted unexpectedly.",
$time);
end
end
// synthesis translate_on
endmodule : avg_calc_highprec