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.
This commit is contained in:
David Anderson 2024-09-09 12:47:19 -07:00
parent b527a62ab8
commit 1b85c3e216
2 changed files with 139 additions and 41 deletions

View File

@ -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

View File

@ -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();
check_one_cycle(14);
check_one_cycle(14);
endseq));
endmodule