From 1b85c3e2161348b1942b1a9996df1548b2b4627d Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 9 Sep 2024 12:47:19 -0700 Subject: [PATCH] lib/Strobe: rewrite, using better math and some sad type hacking The numeric types vs numeric value thing sucks, but there's a mild workaround where you just recurse through numeric types until you find one that matches the value you wanted. It's icky, but it ensures registers are exactly the correct width instead of relying on later synthesis to find and execute the width reduction. --- lib/Strobe.bsv | 142 +++++++++++++++++++++++++++++++++++--------- lib/Strobe_Test.bsv | 38 ++++++++---- 2 files changed, 139 insertions(+), 41 deletions(-) diff --git a/lib/Strobe.bsv b/lib/Strobe.bsv index e7c4a92..66bc082 100644 --- a/lib/Strobe.bsv +++ b/lib/Strobe.bsv @@ -2,6 +2,7 @@ package Strobe; import Real::*; import Printf::*; +import DReg::*; // A Strobe provides a synchronization signal to other modules, when // an event happens at a cadence other than the module clock. @@ -9,49 +10,134 @@ import Printf::*; interface Strobe; method Bool _read(); // reset resets the strobe cycle, starting with a strobe on the - // cycle following reset. + // cycle following the reset. method Action reset(); endinterface +module mkStrobeRaw(UInt#(sz) increment, UInt#(sz) max, Strobe ifc); + Reg#(UInt#(sz)) cnt <- mkReg(0); + PulseWire want_reset <- mkPulseWire(); + Reg#(Bool) strobe_out <- mkDReg(False); + + (* no_implicit_conditions, fire_when_enabled *) + rule increment; + if (want_reset) begin + cnt <= 0; + strobe_out <= True; + end + else begin + UInt#(TAdd#(sz, 1)) new_cnt = extend(cnt)+extend(increment); + if (new_cnt >= extend(max)) begin + new_cnt = new_cnt - extend(max); + strobe_out <= True; + end + cnt <= truncate(new_cnt); + end + endrule + + method Bool _read = strobe_out._read; + method Action reset(); + want_reset.send(); + endmethod +endmodule + +module mkStrobeRawOld(UInt#(sz) increment, UInt#(sz) max, Strobe ifc); + Reg#(UInt#(sz)) cnt[2] <- mkCReg(2, 0); + Reg#(Bool) pulse <- mkDReg(False); + PulseWire pw <- mkPulseWireOR(); + + (* no_implicit_conditions, fire_when_enabled *) + rule increment; + UInt#(TAdd#(sz, 1)) new_cnt = extend(cnt[0])+extend(increment); + if (new_cnt >= extend(max)) begin + cnt[0] <= truncate(new_cnt - extend(max)); + pulse <= True; + end + else + cnt[0] <= truncate(new_cnt); + endrule + + (* no_implicit_conditions, fire_when_enabled *) + rule strobe (pulse); + pw.send(); + endrule + + method _read = pw._read; + method Action reset(); + cnt[1] <= increment; + pw.send(); + endmethod +endmodule + +module mkStrobeRawRec(UInt#(n) hack, Integer increment, Integer max, Strobe ifc); + let register_width = ceil(log2(fromInteger(max))); + if (valueOf(n) < register_width) begin + UInt#(TAdd#(n, 1)) hack2 = 0; + let _ret <- mkStrobeRawRec(hack2, increment, max); + return _ret; + end + else begin + UInt#(n) hack2 = fromInteger(increment); + let _ret <- mkStrobeRaw(hack2, fromInteger(max)); + return _ret; + end +endmodule + // mkStrobe returns a Strobe that triggers at the given // target_frequency, assuming mkStrobe is being clocked at the given // higher clock_frequency. module mkStrobe(Integer clock_frequency, Integer target_frequency, Strobe ifc); if (target_frequency > clock_frequency) error("mkStrobe target_frequency must be less than clock_frequency"); - let strobe_every = round(fromInteger(clock_frequency)/fromInteger(target_frequency)); - // Because we're using integer counters to divide frequencies, - // unless the clock and target frequencies divide cleanly we'll end - // up with a small amount of error. + // This is how many main clock ticks there are per strobe at the + // target frequency. For all but the simplest cases, this will + // include fractional clock cycles due to the two frequencies not + // dividing evenly. + Real target_ratio = fromInteger(clock_frequency)/fromInteger(target_frequency); + + // At this point we could round the target ratio and instantiate a + // counter for that amount, but the resulting strobe frequency can + // be quite drastically off target (empirically I've seen up to 5% + // without trying). // - // Strobes like this tend to be used for relatively short - // operations before some other synchronization event happens - // (e.g. sending one byte on UART), so we can allow a small amount - // of frequency error. For now, the target frequency error is fixed - // at <=0.1%. - Real actual_frequency = fromInteger(clock_frequency)/fromInteger(strobe_every); - Real frequency_error_pct = abs(fromInteger(target_frequency)-actual_frequency) / fromInteger(target_frequency) * 100; - if (frequency_error_pct > 0.1) - error(sprintf("mkStrobe actual frequency is %0f, %0f%% error vs. requested %0d. Your clock_frequency and target_frequency are probably too near each other.", actual_frequency, frequency_error_pct, target_frequency)); + // We can improve on this by counting in larger increments modulo + // some maximum, and strobing on every overflow. This will make the + // strobe period vary slightly by the main clock's reckoning, + // jittering around the "true" strobe time as error accumulates and + // is then subtracted back out. + Real target_error = 1/1000; // 0.1% - Reg#(UInt#(32)) cnt[2] <- mkCReg(2, 0); + Integer incr; + Integer max; + Real actual_error = 1; + for (incr=1; incr<=32 && actual_error > target_error; incr=incr+1) begin + Real mul_ratio = fromInteger(incr)*target_ratio; + max = round(mul_ratio); + actual_error = abs(mul_ratio - fromInteger(max))/fromInteger(max); + end - (* no_implicit_conditions, fire_when_enabled *) - rule increment; - if (cnt[0] == fromInteger(strobe_every-1)) - cnt[0] <= 0; - else - cnt[0] <= cnt[0]+1; - endrule + // Exited the loop, so either we found a good counter width to hit + // the requested error, or we maxed out on a 64-bit counter. + if (actual_error > target_error) + error(sprintf("cannot construct a strobe running at %0dHz from a main clock of %0dHz without exceeding the target frequency error of %0d%%", target_frequency, clock_frequency, target_error*100)); - method Bool _read(); - return cnt[0] == 0; - endmethod + // The loop over-incremented by 1 after finding an acceptable error amount. + incr = incr-1; - method Action reset(); - cnt[1] <= 0; - endmethod + // Debug messages, uncomment if you're wondering what the logic + // above decided for your main clock and target strobe rate. + Bool show_debug = True; + if (show_debug) begin + Real actual_ratio = fromInteger(max)/fromInteger(incr); + Real actual_freq_low = fromInteger(clock_frequency)/fromInteger(ceil(actual_ratio)); + Real actual_freq_high = fromInteger(clock_frequency)/fromInteger(floor(actual_ratio)); + Real actual_freq_avg = fromInteger(clock_frequency)/actual_ratio; + messageM(sprintf("Strobe at %0dHz given clock at %0dHz:\n count by %0d mod %0d\n strobing every %d to %d cycles\n effective strobe frequency %0.2fHz to %0.2fHz\n avg frequency %0.2fHz (%0.2f%% error)", target_frequency, clock_frequency, incr, max, floor(actual_ratio), ceil(actual_ratio), actual_freq_low, actual_freq_high, actual_freq_avg, actual_error*100)); + end + + let _ret <- mkStrobeRawRec(UInt#(1) ' (0), incr, max); + return _ret; endmodule endpackage diff --git a/lib/Strobe_Test.bsv b/lib/Strobe_Test.bsv index a1cd63d..d1e9373 100644 --- a/lib/Strobe_Test.bsv +++ b/lib/Strobe_Test.bsv @@ -11,10 +11,12 @@ module mkTB(); let cycles <- mkCycleCounter(); // For this test, we assume we're clocked at 25MHz, and want a - // 115_200bps strobe for a serial port. That translates to a strobe - // every 217 cycles. - let dut <- mkStrobe(25_000_000, 115_200); - let want_pulse_every = 217; + // 16x115_200bps strobe for a serial port. That translates to a + // strobe that happens every 13 or 14 cycles, depending on the + // amount of accumulated error from the imprecise frequency + // division. + let dut <- mkStrobe(25_000_000, 115_200*16); + let want_pulse_every = 14; function Action check_dut(Bool want); return action @@ -24,29 +26,39 @@ module mkTB(); endaction; endfunction - function Stmt check_one_cycle(); + function Stmt check_one_cycle(Integer cycle_len); return seq action + $display("%0d: cycle start", cycles.all); check_dut(True); cycles.reset(); endaction - while (cycles < want_pulse_every) + while (cycles < fromInteger(cycle_len)) check_dut(False); endseq; endfunction - runTest(2000, + runTest(500, mkTest("Strobe", seq dut.reset(); - repeat(3) check_one_cycle(); + check_one_cycle(14); + check_one_cycle(14); + check_one_cycle(13); + check_one_cycle(14); + check_one_cycle(13); + check_one_cycle(14); + check_one_cycle(13); + check_one_cycle(14); + check_one_cycle(14); + check_one_cycle(13); + check_one_cycle(14); + check_one_cycle(13); // Reset should actually reset repeat(10) noAction; - par - check_dut(False); - dut.reset(); - endpar - repeat(3) check_one_cycle(); + dut.reset(); + check_one_cycle(14); + check_one_cycle(14); endseq)); endmodule