From d8d72f8a637b3f532f297f563be7957d7537bdf4 Mon Sep 17 00:00:00 2001 From: Max Korbel Date: Wed, 20 Nov 2024 08:53:03 -0800 Subject: [PATCH] Gated counter and toggle gate (#111) --- .github/workflows/general.yml | 3 + .gitignore | 1 + doc/README.md | 4 +- doc/components/summation.md | 8 + doc/components/toggle_gate.md | 16 + lib/rohd_hcl.dart | 1 + .../components/config_summation.dart | 56 +- lib/src/summation/counter.dart | 69 ++- lib/src/summation/gated_counter.dart | 529 ++++++++++++++++++ lib/src/summation/sum_interface.dart | 15 +- lib/src/summation/summation.dart | 1 + lib/src/summation/summation_base.dart | 36 +- lib/src/toggle_gate.dart | 84 +++ test/configurator_test.dart | 8 + test/serialization_test.dart | 2 - test/summation/counter_test.dart | 103 +++- test/summation/gated_counter_test.dart | 313 +++++++++++ test/summation/sum_test.dart | 177 +----- test/summation/summation_test_utils.dart | 249 +++++++++ test/toggle_gate_test.dart | 96 ++++ tool/gh_actions/check_tmp_test.sh | 20 + tool/run_checks.sh | 4 + 22 files changed, 1595 insertions(+), 200 deletions(-) create mode 100644 doc/components/toggle_gate.md create mode 100644 lib/src/summation/gated_counter.dart create mode 100644 lib/src/toggle_gate.dart create mode 100644 test/summation/gated_counter_test.dart create mode 100644 test/summation/summation_test_utils.dart create mode 100644 test/toggle_gate_test.dart create mode 100755 tool/gh_actions/check_tmp_test.sh diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index 93498ab74..1c8fd2b0e 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -62,6 +62,9 @@ jobs: - name: Generate HTML for examples run: tool/gh_actions/create_htmls.sh + + - name: Check temporary test files + run: tool/gh_actions/check_tmp_test.sh # https://github.com/devcontainers/ci/blob/main/docs/github-action.md - name: Build dev container and run tests in it diff --git a/.gitignore b/.gitignore index 26c5e66e6..40a909c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ tmp* confapp/.vscode/* *tracker.json *tracker.log +devtools_options.yaml *.sv # Exceptions diff --git a/doc/README.md b/doc/README.md index b3a342e33..fd6876234 100644 --- a/doc/README.md +++ b/doc/README.md @@ -65,6 +65,7 @@ Some in-development items will have opened issues, as well. Feel free to create - Counters - [Summation](./components/summation.md#sum) - [Binary counter](./components/summation.md#counter) + - [Gated counter](./components/summation.md#gated-counter) - Gray counter - Pseudorandom - LFSR @@ -73,8 +74,9 @@ Some in-development items will have opened issues, as well. Feel free to create - CRC - [Parity](./components/parity.md) - Interleaving -- Clocking +- Gating - [Clock gating](./components/clock_gating.md) + - [Toggle gating](./components/toggle_gate.md) - Data flow - Ready/Valid - Connect/Disconnect diff --git a/doc/components/summation.md b/doc/components/summation.md index bf13556d1..34e69525a 100644 --- a/doc/components/summation.md +++ b/doc/components/summation.md @@ -38,3 +38,11 @@ The `Counter` also has a `Counter.simple` constructor which is intended for very // A counter which increments by 1 each cycle up to 5, then rolls over. Counter.simple(clk: clk, reset: reset, maxValue: 5); ``` + +## Gated Counter + +The `GatedCounter` is a version of a `Counter` which contains a number of power-saving features including clock gating to save on flop power and enable gating to avoid unnecessary combinational toggles. + +The `GatedCounter` has a `clkGatePartitionIndex` which determines a dividing line for the counter to be clock gated such that flops at or above that index will be independently clock gated from the flops below that index. This is an effective method of saving extra power on many counters because the upper bits of the counter may change much less frequently than the lower bits (or vice versa). If the index is negative or greater than or equal to the width of the counter, then the whole counter will be clock gated in unison. + +The `gateToggles` flag will enable `ToggleGate` insertion on a per-interface basis to help reduce combinational toggles within the design when interfaces are not enabled. diff --git a/doc/components/toggle_gate.md b/doc/components/toggle_gate.md new file mode 100644 index 000000000..42b2a666b --- /dev/null +++ b/doc/components/toggle_gate.md @@ -0,0 +1,16 @@ +# Toggle Gate + +The `ToggleGate` component is intended to help save power by avoiding unnecessary toggles through combinational logic. It accomplishes this by flopping the previous value of data and muxing the previous value to the `gatedData` output if the `enable` is low. By default, the flops within the `ToggleGate` are also clock gated for extra power savings, but it can be controlled via a `ClockGateControlInterface`. + +As an example use case, if you have a large arithmetic unit but only care about the result when a `valid` bit is high, you could use a `ToggleGate` so that the inputs to that combinational logic do not change unless `valid` is high. + +```dart +final toggleGate = ToggleGate( + clk: clk, + reset: reset, + enable: arithmeticDataValid, + data: arithmeticData, +); + +BigArithmeticUnit(dataIn: toggleGate.gatedData); +``` diff --git a/lib/rohd_hcl.dart b/lib/rohd_hcl.dart index 5b95a5fb6..406acfde2 100644 --- a/lib/rohd_hcl.dart +++ b/lib/rohd_hcl.dart @@ -22,4 +22,5 @@ export 'src/serialization/serialization.dart'; export 'src/shift_register.dart'; export 'src/sort.dart'; export 'src/summation/summation.dart'; +export 'src/toggle_gate.dart'; export 'src/utils.dart'; diff --git a/lib/src/component_config/components/config_summation.dart b/lib/src/component_config/components/config_summation.dart index 0e2824e0a..534e25cdf 100644 --- a/lib/src/component_config/components/config_summation.dart +++ b/lib/src/component_config/components/config_summation.dart @@ -112,24 +112,56 @@ class CounterConfigurator extends SummationConfigurator { /// The reset value. final IntConfigKnob resetValueKnob = IntConfigKnob(value: 0); + /// Whether to instantiate a [GatedCounter]. + final ToggleConfigKnob clockGatingKnob = ToggleConfigKnob(value: false); + + /// The clock gating partition index. + final IntOptionalConfigKnob clockGatingPartitionIndexKnob = + IntOptionalConfigKnob(value: null); + + /// The gate toggles knob. + final ToggleConfigKnob gateTogglesKnob = ToggleConfigKnob(value: false); + @override Map> get knobs => { ...super.knobs, 'Reset Value': resetValueKnob, + 'Clock Gating': clockGatingKnob, + if (clockGatingKnob.value) ...{ + 'Clock Gating Partition Index': clockGatingPartitionIndexKnob, + 'Gate Toggles': gateTogglesKnob, + }, }; @override - Module createModule() => Counter( - sumInterfaceKnobs.knobs - .map((e) => e as SumInterfaceKnob) - .map((e) => SumInterface( - hasEnable: e.hasEnableKnob.value, - fixedAmount: - e.isFixedValueKnob.value ? e.fixedValueKnob.value : null, - width: e.widthKnob.value, - increments: e.incrementsKnob.value, - )) - .toList(), + Module createModule() { + final sumIntfs = sumInterfaceKnobs.knobs + .map((e) => e as SumInterfaceKnob) + .map((e) => SumInterface( + hasEnable: e.hasEnableKnob.value, + fixedAmount: + e.isFixedValueKnob.value ? e.fixedValueKnob.value : null, + width: e.widthKnob.value, + increments: e.incrementsKnob.value, + )) + .toList(); + + if (clockGatingKnob.value) { + return GatedCounter( + sumIntfs, + resetValue: resetValueKnob.value, + width: widthKnob.value, + minValue: minValueKnob.value, + maxValue: maxValueKnob.value, + saturates: saturatesKnob.value, + clk: Logic(), + reset: Logic(), + clkGatePartitionIndex: clockGatingPartitionIndexKnob.value, + gateToggles: gateTogglesKnob.value, + ); + } else { + return Counter( + sumIntfs, resetValue: resetValueKnob.value, width: widthKnob.value, minValue: minValueKnob.value, @@ -138,6 +170,8 @@ class CounterConfigurator extends SummationConfigurator { clk: Logic(), reset: Logic(), ); + } + } @override String get name => 'Counter'; diff --git a/lib/src/summation/counter.dart b/lib/src/summation/counter.dart index 6094f2fc4..785f892a5 100644 --- a/lib/src/summation/counter.dart +++ b/lib/src/summation/counter.dart @@ -7,6 +7,7 @@ // 2024 August 26 // Author: Max Korbel +import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; import 'package:rohd_hcl/src/summation/summation_base.dart'; @@ -17,6 +18,19 @@ class Counter extends SummationBase { /// The output value of the counter. Logic get count => output('count'); + /// The main clock signal. + @visibleForTesting + @protected + late final Logic clk; + + /// The reset signal. + @protected + late final Logic reset; + + /// The restart signal. + @protected + late final Logic? restart; + /// Creates a counter that increments according to the provided [interfaces]. /// /// The [width] can be either explicitly provided or inferred from other @@ -50,39 +64,54 @@ class Counter extends SummationBase { super.saturates, super.name = 'counter', }) : super(initialValue: resetValue) { - clk = addInput('clk', clk); - reset = addInput('reset', reset); + this.clk = addInput('clk', clk); + this.reset = addInput('reset', reset); if (restart != null) { - restart = addInput('restart', restart); + this.restart = addInput('restart', restart); + } else { + this.restart = null; } addOutput('count', width: width); - final sum = Sum( - interfaces, - initialValue: - restart != null ? mux(restart, initialValueLogic, count) : count, - maxValue: maxValueLogic, - minValue: minValueLogic, - width: width, - saturates: saturates, - ); + _buildLogic(); + } + + /// The internal [Sum] that is used to keep track of the count. + @protected + late final Sum summer = Sum( + interfaces, + initialValue: + restart != null ? mux(restart!, initialValueLogic, count) : count, + maxValue: maxValueLogic, + minValue: minValueLogic, + width: width, + saturates: saturates, + ); + /// Builds the internal logic for the counter. + void _buildLogic() { + buildFlops(); + + // need to flop these since value is flopped + overflowed <= flop(clk, summer.overflowed, reset: reset); + underflowed <= flop(clk, summer.underflowed, reset: reset); + + equalsMax <= count.eq(maxValueLogic); + equalsMin <= count.eq(minValueLogic); + } + + /// Builds the flops that store the [count]. + @protected + void buildFlops() { count <= flop( clk, - sum.sum, + summer.sum, reset: reset, resetValue: initialValueLogic, ); - - // need to flop these since value is flopped - overflowed <= flop(clk, sum.overflowed, reset: reset); - underflowed <= flop(clk, sum.underflowed, reset: reset); - - equalsMax <= count.eq(maxValueLogic); - equalsMin <= count.eq(minValueLogic); } /// A simplified constructor for [Counter] that accepts a single fixed amount diff --git a/lib/src/summation/gated_counter.dart b/lib/src/summation/gated_counter.dart new file mode 100644 index 000000000..f2a76d235 --- /dev/null +++ b/lib/src/summation/gated_counter.dart @@ -0,0 +1,529 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// gated_counter.dart +// A flexible counter implementation with clock and toggle gating. +// +// 2024 October +// Author: Max Korbel + +import 'dart:math'; + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; + +/// A version of a [Counter] which includes [ClockGate]ing and [ToggleGate]ing +/// for power savings. +class GatedCounter extends Counter { + /// If `true`, then the counter will gate the toggles of the interfaces when + /// they are not enabled. + final bool gateToggles; + + /// The [ClockGateControlInterface] to use for clock gating internally. + final ClockGateControlInterface? _clockGateControlInterface; + + @override + @protected + List get interfaces => _interfaces; + late final _interfaces = gateToggles + ? super.interfaces.map((e) { + final intf = SumInterface.clone(e); + + intf.enable?.gets(e.enable!); + + if (intf.hasEnable && intf.fixedAmount == null) { + // only need to ungate if enable is high + + intf.amount <= + (Logic(name: 'toggle_gated_amount', width: e.width) + ..gets(ToggleGate( + enable: e.enable!, + data: e.amount, + clk: clk, + reset: reset, + clockGateControlIntf: _clockGateControlInterface, + ).gatedData)); + } else if (intf.fixedAmount == null) { + intf.amount <= e.amount; + } + + return intf; + }).toList() + : super.interfaces; + + /// The index at which to partition the counter for clock gating. + /// + /// If less than 0 or greater than the [width], then the entire counter will + /// be gated together rather than partitioned. + late final int clkGatePartitionIndex; + + /// A provided [clkGatePartitionIndex], if one was provided. + final int? _providedClkGateParitionIndex; + + /// Constructs a [GatedCounter] in the same way as a [Counter], but with the + /// added ability to [gateToggles] of the interfaces when they are not enabled + /// and gate the clocks of the counter in a partitioned way. + /// + /// Clock gating is performed on the same cycle as the increment/decrement(s), + /// so the functionality when compared to the base [Counter] is identical with + /// no added latency to the [count]. + /// + /// The [clkGatePartitionIndex] is the index at which to partition the counter + /// for clock gating. If the [clkGatePartitionIndex] is less than 0 or greater + /// than the [width], then the entire counter will be gated together rather + /// than partitioned. If no [clkGatePartitionIndex] is provided, the counter + /// will attempt to infer a good partition index based on the interfaces + /// provided. + GatedCounter( + super.interfaces, { + required super.clk, + required super.reset, + super.restart, + super.resetValue, + super.maxValue, + super.minValue, + super.width, + super.saturates, + this.gateToggles = true, + ClockGateControlInterface? clockGateControlInterface, + int? clkGatePartitionIndex, + super.name, + }) : _providedClkGateParitionIndex = clkGatePartitionIndex, + _clockGateControlInterface = clockGateControlInterface == null + ? null + : ClockGateControlInterface.clone(clockGateControlInterface) { + _clockGateControlInterface?.pairConnectIO( + this, clockGateControlInterface!, PairRole.consumer); + } + + /// All [interfaces] which are incrementing. + late final _incrementingInterfaces = + interfaces.where((intf) => intf.increments); + + /// All [interfaces] which are decrementing. + late final _decrementingInterfaces = + interfaces.where((intf) => !intf.increments); + + /// High if the counter may overflow. + @visibleForTesting + late final Logic mayOverflow = Logic(name: 'mayOverflow') + ..gets(_calculateMayOverflow()); + + Logic _calculateMayOverflow() { + if (saturates) { + // if this is a saturating counter, we will never wrap-around + return Const(0); + } + + if (_incrementingInterfaces.isEmpty) { + // if we never increment, no chance of overflow + return Const(0); + } + + if (constantMaxValue == null) { + // for now, this is hard to handle, so just always maybe overflow if + // anything is incrementing + return _anyIncrements; + } + + Logic mayOverflow = Const(0); + + final maxValueBit = LogicValue.ofInferWidth(constantMaxValue).width - 1; + + final overflowDangerZoneStart = max( + maxValueBit - log2Ceil(_incrementingInterfaces.length), + 0, + ); + + final inOverflowDangerZone = Logic(name: 'inOverflowDangerZone') + ..gets(count.getRange(overflowDangerZoneStart).or()); + + mayOverflow |= inOverflowDangerZone & _anyIncrements; + + Logic anyIntfInIncrDangerZone = Const(0); + Logic anyIntfBigIncrement = Const(0); + + for (final intf in _incrementingInterfaces) { + // if we're in the danger zone, and interface is enabled, and the amount + // also reaches into the danger range + + if (intf.width <= overflowDangerZoneStart) { + // if it's too short, don't worry about it + continue; + } + + var intfInDangerZone = intf.amount + .getRange( + overflowDangerZoneStart, + min(intf.width, width), + ) + .or(); + + var intfBigIncrement = maxValueBit >= intf.width + ? Const(0) + : intf.amount.getRange(maxValueBit).or(); + + if (intf.hasEnable) { + intfInDangerZone &= intf.enable!; + intfBigIncrement &= intf.enable!; + } + + anyIntfInIncrDangerZone |= intfInDangerZone; + anyIntfBigIncrement |= intfBigIncrement; + } + + // if *any* interface is incrementing at all and upper-most bit(s) is 1, + // then we may overflow + Logic anyIntfIncrementing = Const(0); + for (final intf in _incrementingInterfaces) { + var intfIsIncrementing = intf.amount.or(); + if (intf.hasEnable) { + intfIsIncrementing &= intf.enable!; + } + + anyIntfIncrementing |= intfIsIncrementing; + } + final topMayOverflow = + anyIntfIncrementing & count.getRange(maxValueBit).or(); + + mayOverflow |= topMayOverflow; + + mayOverflow |= anyIntfInIncrDangerZone; + + mayOverflow |= anyIntfBigIncrement; + + return mayOverflow; + } + + /// High if the counter may underflow. + @visibleForTesting + late final Logic mayUnderflow = Logic(name: 'mayUnderflow') + ..gets(_calculateMayUnderflow()); + + Logic _calculateMayUnderflow() { + if (saturates) { + // if this is a saturating counter, we will never wrap-around + return Const(0); + } + + if (_decrementingInterfaces.isEmpty) { + // if we never decrement, no chance of underflow + return Const(0); + } + + if (constantMinValue == null) { + // for now, this is hard to handle, so just always maybe underflow if + // anything is decrementing + return _anyDecrements; + } + + Logic mayUnderflow = Const(0); + + final minValueBit = + max(0, LogicValue.ofInferWidth(constantMinValue).width - 1); + + // if we're close enough to the minimum value (as judged by upper bits being + // 0), and we are decrementing by a sufficiently large number (as judged by + // enough lower bits of decr interfaces), then we may underflow + + final dangerRange = log2Ceil(_decrementingInterfaces.length + 1); + final underflowDangerBit = minValueBit + dangerRange; + final inUnderflowDangerZone = Logic(name: 'inUnderflowDangerZone') + ..gets(underflowDangerBit >= count.width + ? Const(1) + : ~count.getRange(underflowDangerBit).or()); + + mayUnderflow |= inUnderflowDangerZone & _anyDecrements; + + Logic anyIntfInDangerZone = Const(0); + for (final intf in _decrementingInterfaces) { + if (intf.width <= underflowDangerBit) { + // if it's too short, don't worry about it + continue; + } + + var intfInDangerZone = intf.amount.getRange(underflowDangerBit).or(); + + if (intf.hasEnable) { + intfInDangerZone &= intf.enable!; + } + + anyIntfInDangerZone |= intfInDangerZone; + } + + return mayUnderflow | anyIntfInDangerZone; + } + + /// High if the counter [mayUnderflow] or [mayOverflow]. + late final _mayWrap = Logic(name: 'mayWrap') + ..gets(mayUnderflow | mayOverflow); + + /// Enable for the clock gate for the upper portion of the counter. + late final _lowerEnable = Logic(name: 'lowerEnable') + ..gets(_calculateLowerEnable()); + + Logic _calculateLowerEnable() { + Logic lowerEnable = Const(0); // default, not enabled + + // if any interface is enabled and has any 1's in the lower bits, enable + for (final intf in interfaces) { + var intfHasLowerBits = + intf.amount.getRange(0, min(clkGatePartitionIndex, intf.width)).or(); + + if (intf.hasEnable) { + intfHasLowerBits &= intf.enable!; + } + + lowerEnable |= intfHasLowerBits; + } + + lowerEnable |= _mayWrap; + + if (saturates) { + lowerEnable &= ~_stableSaturated; + } + + lowerEnable |= _unstableValue; + + // always enable during restart + if (restart != null) { + lowerEnable |= restart!; + } + + return lowerEnable; + } + + /// High if we're in a stable saturation + late final _stableSaturated = Logic(name: 'stableSaturated') + ..gets(saturates + ? (equalsMin & ~_anyIncrements) | (equalsMax & ~_anyDecrements) + : Const(0)); + + late final _anyDecrements = Logic(name: 'anyDecrements') + ..gets(_decrementingInterfaces.isEmpty + ? Const(0) + : _decrementingInterfaces + .map((intf) => intf.amount.or() & (intf.enable ?? Const(1))) + .toList() + .swizzle() + .or()); + + late final _anyIncrements = Logic(name: 'anyIncrements') + ..gets(_incrementingInterfaces.isEmpty + ? Const(0) + : _incrementingInterfaces + .map((intf) => intf.amount.or() & (intf.enable ?? Const(1))) + .toList() + .swizzle() + .or()); + + /// Enable for the clock gate for the upper portion of the counter. + late final _upperEnable = Logic(name: 'upperEnable') + ..gets(_calculateUpperEnable()); + + Logic _calculateUpperEnable() { + Logic upperEnable = Const(0); // default, not enabled + + // if any interface is enabled and has any 1's in the upper bits, enable + for (final intf in interfaces) { + if (clkGatePartitionIndex >= intf.width) { + // if the interface doesnt even reach the partition index, then skip + continue; + } + + var intfHasUpperBits = intf.amount.getRange(clkGatePartitionIndex).or(); + + if (intf.hasEnable) { + intfHasUpperBits &= intf.enable!; + } + + upperEnable |= intfHasUpperBits; + } + + // the first bit of the total count that's "dangerous" for enabling clock + final incrDangerZoneStart = max( + 0, + clkGatePartitionIndex - log2Ceil(_incrementingInterfaces.length + 1), + ); + + final currentCountInIncrDangerZone = + Logic(name: 'currentCountInIncrDangerZone') + ..gets(count + .getRange( + min(incrDangerZoneStart, width), + min(clkGatePartitionIndex, width), + ) + .or()); + + upperEnable |= currentCountInIncrDangerZone & _anyIncrements; + + Logic anyIntfInIncrDangerZone = Const(0); + // for increments... + for (final intf in _incrementingInterfaces) { + // if we're in the danger zone, and interface is enabled, and the amount + // also reaches into the danger range, then enable the upper gate + + if (intf.width <= incrDangerZoneStart) { + // if it's too short, don't worry about it + continue; + } + + var intfInDangerZone = intf.amount + .getRange( + incrDangerZoneStart, + min(clkGatePartitionIndex, intf.width), + ) + .or(); + + if (intf.hasEnable) { + intfInDangerZone &= intf.enable!; + } + + anyIntfInIncrDangerZone |= intfInDangerZone; + } + upperEnable |= anyIntfInIncrDangerZone; + + // for decrements... + + // if any decrement is "big enough" while the lower bits are "small enough', + // then we have to enable the upper region since it can roll-over + + // let's just draw the line half way for now? + final decrDangerZoneStart = clkGatePartitionIndex ~/ 2; + final currentCountInDecrDangerZone = + Logic(name: 'currentCountInDecrDangerZone') + ..gets(~count + .getRange( + min(width - 1, decrDangerZoneStart), + min(width, clkGatePartitionIndex), + ) + .or()); + + upperEnable |= currentCountInDecrDangerZone & _anyDecrements; + + Logic anyIntfEndangersDecr = Const(0); + + final decrDangerZoneStartIntf = max( + 0, decrDangerZoneStart - log2Ceil(_decrementingInterfaces.length + 1)); + for (final intf in _decrementingInterfaces) { + if (intf.width <= decrDangerZoneStartIntf) { + // if it's too short, don't worry about it + continue; + } + + var intfEndangersDecrZone = intf.amount + .getRange( + decrDangerZoneStartIntf, + min(intf.width, clkGatePartitionIndex), + ) + .or(); + + if (intf.hasEnable) { + intfEndangersDecrZone &= intf.enable!; + } + + anyIntfEndangersDecr |= intfEndangersDecrZone; + } + + upperEnable |= anyIntfEndangersDecr; + + upperEnable |= _mayWrap; + + if (saturates) { + upperEnable &= ~_stableSaturated; + } + + upperEnable |= _unstableValue; + + // always enable during restart + if (restart != null) { + upperEnable |= restart!; + } + + return upperEnable; + } + + /// The gated clock for the lower partition of the counter. + @visibleForTesting + @protected + late final Logic lowerGatedClock; + + /// The gated clock for the upper partition of the counter. + @visibleForTesting + @protected + late final Logic upperGatedClock; + + /// Whether the current value of the counter is not "stable" in that it's not + /// legal according to the minimum and maximum values. + /// + /// Covers the scenario where reset value is less than the minimum value or + /// greater than the maximum value. The first cycle after reset, we need to + /// ungate the count. + late final Logic _unstableValue = Logic(name: 'unstableValue') + ..gets( + (summer.underflowed & ~underflowed) | (summer.overflowed & ~overflowed), + ); + + /// Picks a partition index based on the interfaces provided. + int _pickPartitionIndex() => + // simple implementation is just cut it in half + width ~/ 2; + + @protected + @override + void buildFlops() { + clkGatePartitionIndex = + _providedClkGateParitionIndex ?? _pickPartitionIndex(); + + if (clkGatePartitionIndex >= width || clkGatePartitionIndex < 0) { + // just gate the whole thing together + final clkGate = ClockGate(clk, + enable: _lowerEnable | _upperEnable, + reset: reset, + controlIntf: _clockGateControlInterface); + + lowerGatedClock = clkGate.gatedClk; + upperGatedClock = clkGate.gatedClk; + + count <= + flop( + clkGate.gatedClk, + summer.sum, + reset: reset, + resetValue: initialValueLogic, + ); + } else { + final lowerClkGate = ClockGate(clk, + enable: _lowerEnable, + reset: reset, + controlIntf: _clockGateControlInterface, + name: 'lower_clock_gate'); + + final upperClkGate = ClockGate(clk, + enable: _upperEnable, + reset: reset, + controlIntf: _clockGateControlInterface, + name: 'upper_clock_gate'); + + final lowerCount = flop( + lowerClkGate.gatedClk, + summer.sum.getRange(0, clkGatePartitionIndex), + reset: reset, + resetValue: initialValueLogic.getRange(0, clkGatePartitionIndex), + ); + + final upperCount = flop( + upperClkGate.gatedClk, + summer.sum.getRange(clkGatePartitionIndex), + reset: reset, + resetValue: initialValueLogic.getRange(clkGatePartitionIndex), + ); + + lowerGatedClock = lowerClkGate.gatedClk; + upperGatedClock = upperClkGate.gatedClk; + + count <= [upperCount, lowerCount].swizzle(); + } + } +} diff --git a/lib/src/summation/sum_interface.dart b/lib/src/summation/sum_interface.dart index 5e5ff7b0f..534c93434 100644 --- a/lib/src/summation/sum_interface.dart +++ b/lib/src/summation/sum_interface.dart @@ -7,6 +7,8 @@ // 2024 August 26 // Author: Max Korbel +import 'dart:math'; + import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; @@ -59,7 +61,10 @@ class SumInterface extends PairInterface { : width = (width == null && fixedAmount == null) ? throw RohdHclException( 'Must provide either a fixedAmount or width.') - : width ?? LogicValue.ofInferWidth(fixedAmount).width { + : width ?? max(LogicValue.ofInferWidth(fixedAmount).width, 1) { + if (this.width <= 0) { + throw RohdHclException('Width must be positive.'); + } setPorts([ if (fixedAmount == null) Port('amount', this.width), if (hasEnable) Port('enable'), @@ -76,4 +81,12 @@ class SumInterface extends PairInterface { width: other.width, hasEnable: other.hasEnable, ); + + @override + String toString() => [ + 'SumInterface[$width]', + if (fixedAmount != null) ' = $fixedAmount', + if (increments) ' ++ ' else ' -- ', + if (hasEnable) ' (enable)', + ].join(); } diff --git a/lib/src/summation/summation.dart b/lib/src/summation/summation.dart index d1c994b68..0c9ef97f2 100644 --- a/lib/src/summation/summation.dart +++ b/lib/src/summation/summation.dart @@ -2,5 +2,6 @@ // SPDX-License-Identifier: BSD-3-Clause export 'counter.dart'; +export 'gated_counter.dart'; export 'sum.dart'; export 'sum_interface.dart'; diff --git a/lib/src/summation/summation_base.dart b/lib/src/summation/summation_base.dart index 53e7e11b6..c8a48ea01 100644 --- a/lib/src/summation/summation_base.dart +++ b/lib/src/summation/summation_base.dart @@ -58,6 +58,16 @@ abstract class SummationBase extends Module { /// equal to the minimum. Logic get equalsMin => output('equalsMin'); + /// If the provided `maxValue` was a constant, then this will be the constant + /// value. Otherwise, it will be `null`. + @protected + late final BigInt? constantMaxValue; + + /// If the provided `minValue` was a constant, then this will be the constant + /// value. Otherwise, it will be `null`. + @protected + late final BigInt? constantMinValue; + /// Sums the values across the provided [interfaces] within the bounds of the /// [saturates] behavior, [initialValue], [maxValue], and [minValue], with the /// specified [width], if provided. @@ -81,10 +91,32 @@ abstract class SummationBase extends Module { uniquify: (original) => '${original}_$i')) .toList(); + initialValue ??= 0; + maxValue ??= biggestVal(this.width); + minValue ??= 0; + + if (maxValue is! Logic) { + constantMaxValue = LogicValue.ofInferWidth(maxValue).toBigInt(); + } else { + constantMaxValue = null; + } + + if (minValue is! Logic) { + constantMinValue = LogicValue.ofInferWidth(minValue).toBigInt(); + } else { + constantMinValue = null; + } + + if (constantMaxValue != null && + constantMinValue != null && + constantMaxValue! < constantMinValue!) { + throw RohdHclException( + 'Max value must be greater or equal to min value.'); + } + initialValueLogic = _dynamicInputToLogic('initialValue', initialValue); minValueLogic = _dynamicInputToLogic('minValue', minValue); - maxValueLogic = - _dynamicInputToLogic('maxValue', maxValue ?? biggestVal(this.width)); + maxValueLogic = _dynamicInputToLogic('maxValue', maxValue); addOutput('overflowed'); addOutput('underflowed'); diff --git a/lib/src/toggle_gate.dart b/lib/src/toggle_gate.dart new file mode 100644 index 000000000..2fa978c50 --- /dev/null +++ b/lib/src/toggle_gate.dart @@ -0,0 +1,84 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// toggle_gate.dart +// A gate to reduce toggling for power savings. +// +// 2024 October +// Author: Max Korbel + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/src/clock_gating.dart'; + +/// A gate for signals to avoid toggling when not needed. +class ToggleGate extends Module { + /// The gated version of original data which will only toggle when the gate is + /// enabled. + Logic get gatedData => output('gatedData'); + + /// Constructs a [ToggleGate] to reduce power consumption by reducing toggling + /// of the [gatedData] signal when [enable] is not asserted. + /// + /// When [enable] is high, the [gatedData] signal will be the same as the + /// [data] signal. When [enable] is low, the [gatedData] signal will be the + /// same as the last value of [data] when [enable] was high. + /// + /// If [resetValue] is provided, then the [gatedData] signal will be set to + /// that when [reset]. + /// + /// If no [clockGateControlIntf] is provided (left `null`), then a default + /// clock gating implementation will be included for the internal sequential + /// elements. + ToggleGate({ + required Logic enable, + required Logic data, + required Logic clk, + required Logic reset, + dynamic resetValue, + ClockGateControlInterface? clockGateControlIntf, + super.name = 'toggle_gate', + }) { + enable = addInput('enable', enable); + data = addInput('data', data, width: data.width); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + if (resetValue != null && resetValue is Logic) { + resetValue = addInput('resetValue', resetValue, width: resetValue.width); + } + + if (clockGateControlIntf != null) { + clockGateControlIntf = + ClockGateControlInterface.clone(clockGateControlIntf) + ..pairConnectIO(this, clockGateControlIntf, PairRole.consumer); + } + + addOutput('gatedData', width: data.width); + + final lastData = Logic(name: 'lastData', width: data.width); + + final gateEnable = enable & (lastData.neq(data)); + + final clkGate = ClockGate( + clk, + enable: gateEnable, + reset: reset, + controlIntf: clockGateControlIntf, + ); + + lastData <= + flop( + clkGate.gatedClk, + en: clkGate.isPresent ? null : gateEnable, + reset: reset, + resetValue: resetValue, + data); + + gatedData <= + mux( + enable, + data, + lastData, + ); + } +} diff --git a/test/configurator_test.dart b/test/configurator_test.dart index 3e2561956..2bcdd7206 100644 --- a/test/configurator_test.dart +++ b/test/configurator_test.dart @@ -305,6 +305,14 @@ void main() { expect(mod.saturates, true); }); + test('gated counter configurator', () async { + final cfg = CounterConfigurator(); + cfg.clockGatingKnob.value = true; + + final mod = cfg.createModule() as GatedCounter; + await mod.build(); + }); + group('configurator builds', () { for (final componentConfigurator in componentRegistry) { test(componentConfigurator.name, () async { diff --git a/test/serialization_test.dart b/test/serialization_test.dart index 16ac89dd5..9c9623e21 100644 --- a/test/serialization_test.dart +++ b/test/serialization_test.dart @@ -155,8 +155,6 @@ void main() { await mod2.build(); unawaited(Simulator.run()); - WaveDumper(mod2); - start.inject(0); reset.inject(0); for (var i = 0; i < len; i++) { diff --git a/test/summation/counter_test.dart b/test/summation/counter_test.dart index 5a8565ef3..6fcd565a9 100644 --- a/test/summation/counter_test.dart +++ b/test/summation/counter_test.dart @@ -8,11 +8,15 @@ // Author: Max Korbel import 'dart:async'; +import 'dart:math'; import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:rohd_vf/rohd_vf.dart'; import 'package:test/test.dart'; +import 'summation_test_utils.dart'; + void main() { tearDown(() async { await Simulator.reset(); @@ -206,7 +210,6 @@ void main() { ); await counter.build(); - WaveDumper(counter); Simulator.setMaxSimTime(1000); unawaited(Simulator.run()); @@ -268,4 +271,102 @@ void main() { await Simulator.endSimulation(); }); + + group('random counter', () { + const numRandCounters = 20; + const restartProbability = 0.05; + + final counterTypes = ['normal', 'gated']; + + for (final counterType in counterTypes) { + group(counterType, () { + for (var counterIdx = 0; counterIdx < numRandCounters; counterIdx++) { + test('$counterIdx', () async { + const numCycles = 500; + + final rand = Random(456 + counterIdx ^ counterType.hashCode); + + final cfg = genRandomSummationConfiguration(rand); + + final clk = SimpleClockGenerator(10).clk; + Simulator.setMaxSimTime(numCycles * 10 * 2 + 100); + + final reset = Logic()..inject(1); + final restart = rand.nextBool() ? Logic() : null; + + final dut = counterType == 'normal' + ? Counter( + cfg.interfaces, + clk: clk, + reset: reset, + restart: restart, + minValue: cfg.minValue, + maxValue: cfg.maxValue, + saturates: cfg.saturates, + width: cfg.width, + resetValue: cfg.initialValue, + ) + : GatedCounter(cfg.interfaces, + clk: clk, + reset: reset, + restart: restart, + minValue: cfg.minValue, + maxValue: cfg.maxValue, + saturates: cfg.saturates, + width: cfg.width, + resetValue: cfg.initialValue, + gateToggles: rand.nextBool(), + clkGatePartitionIndex: + rand.nextBool() ? null : rand.nextInt(11) - 1); + + await dut.build(); + + unawaited(Simulator.run()); + + // reset flow + reset.inject(1); + restart?.inject(0); + for (final intf in cfg.interfaces) { + if (intf.hasEnable) { + intf.enable!.inject(0); + } + intf.amount.inject(0); + } + await clk.waitCycles(3); + reset.inject(0); + await clk.waitCycles(3); + + // set up checking on edges + checkCounter(dut); + + await clk.waitCycles(3); + + // randomize inputs on the interfaces of the counter + for (var i = 0; i < numCycles; i++) { + await clk.nextPosedge; + + for (final intf in cfg.interfaces) { + if (intf.hasEnable) { + intf.enable!.inject(rand.nextBool()); + } + + if (intf.fixedAmount == null) { + intf.amount.inject( + // put 0 sometimes, for clk gating to trigger more + rand.nextBool() ? rand.nextInt(1 << intf.width) : 0, + ); + } + } + + restart?.inject(rand.nextDouble() < restartProbability); + } + + await clk.waitCycles(10); + + await Simulator.endSimulation(); + }); + } + }); + } + }); } diff --git a/test/summation/gated_counter_test.dart b/test/summation/gated_counter_test.dart new file mode 100644 index 000000000..c1bd14a8b --- /dev/null +++ b/test/summation/gated_counter_test.dart @@ -0,0 +1,313 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// gated_counter_test.dart +// Tests for the gated counter. +// +// 2024 October +// Author: Max Korbel + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:rohd_vf/rohd_vf.dart'; +import 'package:test/test.dart'; + +import 'summation_test_utils.dart'; + +class ClockToggleCounter { + final GatedCounter dut; + + int upperToggles = 0; + int lowerToggles = 0; + int totalToggles = 0; + + ClockToggleCounter(this.dut) { + dut.upperGatedClock.posedge.listen((_) => upperToggles++); + dut.lowerGatedClock.posedge.listen((_) => lowerToggles++); + dut.clk.posedge.listen((_) => totalToggles++); + } + + double get upperActivity => upperToggles / totalToggles; + double get lowerActivity => lowerToggles / totalToggles; +} + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + Future testCounter( + GatedCounter Function(Logic clk, Logic reset) genCounter, { + int numCycles = 150, + bool dumpWaves = false, + bool printActivity = false, + bool doChecks = true, + bool printSv = false, + Future Function()? stimulus, + }) async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic()..inject(1); + + final dut = genCounter(clk, reset); + + await dut.build(); + + if (dumpWaves) { + WaveDumper(dut); + } + + if (printSv) { + // ignore: avoid_print + print(dut.generateSynth()); + } + + if (doChecks) { + checkCounter(dut); + } + + final toggleCounter = ClockToggleCounter(dut); + + Simulator.setMaxSimTime(2 * numCycles * 10); + unawaited(Simulator.run()); + + await clk.waitCycles(3); + reset.inject(0); + + await (stimulus?.call() ?? clk.waitCycles(numCycles)); + + await Simulator.endSimulation(); + + if (printActivity) { + // ignore: avoid_print + print('Upper activity: ${toggleCounter.upperActivity}'); + // ignore: avoid_print + print('Lower activity: ${toggleCounter.lowerActivity}'); + } + + return toggleCounter; + } + + test('simple 1-counter incrementing always, with rollover', () async { + final toggleCounter = await testCounter( + (clk, reset) => GatedCounter( + [SumInterface(fixedAmount: 1)], + clk: clk, + reset: reset, + width: 6, + ), + ); + + expect(toggleCounter.lowerActivity, greaterThan(0.95)); + expect(toggleCounter.upperActivity, lessThan(0.75)); + }); + + test( + 'simple 1-counter incrementing always, with rollover,' + ' clock gating disabled', () async { + final toggleCounter = await testCounter( + (clk, reset) => GatedCounter( + [SumInterface(fixedAmount: 1)], + clk: clk, + reset: reset, + width: 6, + clockGateControlInterface: ClockGateControlInterface(isPresent: false), + ), + ); + + expect(toggleCounter.lowerActivity, greaterThan(0.95)); + expect(toggleCounter.upperActivity, greaterThan(0.95)); + }); + + test( + 'simple 1-counter incrementing sometimes, with rollover,' + ' clock gating all together', () async { + final toggleCounter = await testCounter( + (clk, reset) { + final enable = Logic()..inject(0); + var count = 0; + clk.posedge.listen((_) { + enable.inject(count++ % 20 > 5); + }); + return GatedCounter( + [ + SumInterface(width: 1, hasEnable: true) + ..enable!.gets(enable) + ..amount.inject(1) + ], + clk: clk, + reset: reset, + width: 6, + clkGatePartitionIndex: 6, + ); + }, + ); + + expect(toggleCounter.lowerActivity, greaterThan(0.65)); + expect(toggleCounter.upperActivity, greaterThan(0.65)); + expect(toggleCounter.lowerActivity, toggleCounter.upperActivity); + }); + + test('simple 1-counter incrementing always, with saturation', () async { + final toggleCounter = await testCounter( + (clk, reset) => GatedCounter( + [SumInterface(fixedAmount: 1)], + clk: clk, + reset: reset, + width: 6, + clkGatePartitionIndex: 3, + saturates: true, + ), + ); + + expect(toggleCounter.lowerActivity, lessThan(0.45)); + expect(toggleCounter.upperActivity, lessThan(0.25)); + }); + + test('simple 1-down-counter decrementing always, with rollover', () async { + final toggleCounter = await testCounter( + (clk, reset) => GatedCounter( + [SumInterface(fixedAmount: 1, increments: false)], + resetValue: 63, + clk: clk, + reset: reset, + width: 8, + clkGatePartitionIndex: 4, + ), + numCycles: 1000, + ); + + expect(toggleCounter.lowerActivity, greaterThan(0.95)); + expect(toggleCounter.upperActivity, lessThan(0.25)); + }); + + test('simple 1-down-counter decrementing always, with saturation', () async { + final toggleCounter = await testCounter( + (clk, reset) => GatedCounter( + [SumInterface(fixedAmount: 1, increments: false)], + saturates: true, + resetValue: 63, + clk: clk, + reset: reset, + width: 8, + clkGatePartitionIndex: 4, + ), + numCycles: 1000, + ); + + expect(toggleCounter.lowerActivity, lessThan(0.50)); + expect(toggleCounter.upperActivity, lessThan(0.15)); + }); + + test('simple big-fixed counter incrementing only upper bits', () async { + final toggleCounter = await testCounter( + (clk, reset) => GatedCounter( + [SumInterface(fixedAmount: 8)], + clk: clk, + reset: reset, + width: 6, + clkGatePartitionIndex: 3, + ), + ); + + expect(toggleCounter.lowerActivity, lessThan(0.51)); + expect(toggleCounter.upperActivity, greaterThan(0.95)); + }); + + test('disabled increment turns off whole counter', () async { + final toggleCounter = await testCounter( + (clk, reset) { + final enable = Logic()..inject(0); + + var clkCount = 0; + var mod = 2; + clk.posedge.listen((_) { + if (clkCount >= mod) { + enable.inject(~enable.value); + clkCount = 0; + mod++; + } else { + clkCount++; + } + }); + + return GatedCounter( + [SumInterface(fixedAmount: 1, hasEnable: true)..enable!.gets(enable)], + clk: clk, + reset: reset, + width: 6, + clkGatePartitionIndex: 3, + ); + }, + ); + + expect(toggleCounter.lowerActivity, lessThan(0.5)); + expect(toggleCounter.upperActivity, lessThan(0.4)); + }); + + test('increment by variable amount, properly gates', () async { + final toggleCounter = await testCounter( + (clk, reset) { + final intf = SumInterface(width: 9, hasEnable: true)..enable!.inject(1); + + var clkCount = 0; + clk.posedge.listen((_) { + clkCount++; + + intf.amount.inject(clkCount % 3); + }); + + return GatedCounter( + [intf], + clk: clk, + reset: reset, + width: 9, + clkGatePartitionIndex: 6, + ); + }, + numCycles: 1000, + ); + + expect(toggleCounter.lowerActivity, lessThan(0.7)); + expect(toggleCounter.upperActivity, lessThan(0.5)); + }); + + test('multiple interfaces', () async { + final toggleCounter = await testCounter( + (clk, reset) { + final intf1 = SumInterface(width: 10, hasEnable: true) + ..enable!.inject(1); + final intf2 = SumInterface(width: 1, hasEnable: true) + ..enable!.inject(1); + final intf3 = SumInterface(fixedAmount: 3, hasEnable: true) + ..enable!.inject(1); + + var clkCount = 0; + clk.posedge.listen((_) { + clkCount++; + + intf1.amount.inject(clkCount % 3); + intf1.enable!.inject(clkCount % 5 > 2); + + intf2.amount.inject(clkCount % 2); + intf2.enable!.inject(clkCount % 7 > 2); + + intf3.enable!.inject(clkCount % 3 > 1); + }); + + return GatedCounter( + [intf1, intf2, intf3], + clk: clk, + reset: reset, + width: 10, + clkGatePartitionIndex: 6, + ); + }, + numCycles: 1000, + ); + + expect(toggleCounter.lowerActivity, lessThan(0.65)); + expect(toggleCounter.upperActivity, lessThan(0.6)); + }); +} diff --git a/test/summation/sum_test.dart b/test/summation/sum_test.dart index 536c0d471..506b8bf3b 100644 --- a/test/summation/sum_test.dart +++ b/test/summation/sum_test.dart @@ -13,95 +13,7 @@ import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; import 'package:test/test.dart'; -int goldenSumOfLogics( - List logics, { - required int width, - bool saturates = false, - int? maxVal, - int minVal = 0, -}) => - goldenSum( - logics.map((e) => SumInterface(width: e.width)..amount.gets(e)).toList(), - width: width, - saturates: saturates, - minVal: minVal, - maxVal: maxVal, - ); - -int goldenSum( - List interfaces, { - required int width, - bool saturates = false, - int? maxVal, - int? minVal, - int initialValue = 0, - bool debug = false, -}) { - void log(String message) { - if (debug) { - // ignore: avoid_print - print(message); - } - } - - log('width: $width'); - - var sum = initialValue; - - log('min $minVal -> max $maxVal'); - - maxVal ??= (1 << width) - 1; - if (maxVal > (1 << width) - 1) { - // ignore: parameter_assignments - maxVal = (1 << width) - 1; - } - minVal ??= 0; - - log('min $minVal -> max $maxVal [adjusted]'); - - if (minVal > maxVal) { - throw Exception('minVal must be less than or equal to maxVal'); - } - - log('init: $initialValue'); - - for (final intf in interfaces) { - final amount = intf.amount.value.toInt(); - final enabled = !intf.hasEnable || intf.enable!.value.toBool(); - - log('${intf.increments ? '+' : '-'}' - '$amount${enabled ? '' : ' [disabled]'}'); - - if (enabled) { - if (intf.increments) { - sum += amount; - } else { - sum -= amount; - } - } - } - - log('=$sum'); - - if (saturates) { - if (sum > maxVal) { - sum = maxVal; - } else if (sum < minVal) { - sum = minVal; - } - log('saturates to $sum'); - } else { - final range = maxVal - minVal + 1; - if (sum > maxVal) { - sum = (sum - maxVal - 1) % range + minVal; - } else if (sum < minVal) { - sum = maxVal - (minVal - sum - 1) % range; - } - log('rolls-over to $sum'); - } - - return sum; -} +import 'summation_test_utils.dart'; void main() { test('simple sum of 1 ofLogics', () async { @@ -369,56 +281,13 @@ void main() { expect(dut.width, greaterThan(80)); }); - test('random', () { + test('random sum', () { final rand = Random(123); - SumInterface genRandomInterface() { - final isFixed = rand.nextBool(); - return SumInterface( - fixedAmount: isFixed ? rand.nextInt(100) : null, - width: isFixed ? null : rand.nextInt(8), - increments: rand.nextBool(), - hasEnable: rand.nextBool(), - ); - } - - List genRandomInterfaces() { - final numInterfaces = rand.nextInt(8) + 1; - return List.generate(numInterfaces, (_) => genRandomInterface()); - } - for (var i = 0; i < 1000; i++) { - final interfaces = genRandomInterfaces(); - - final width = rand.nextBool() ? null : rand.nextInt(10) + 1; + final cfg = genRandomSummationConfiguration(rand); - final saturates = rand.nextBool(); - var minVal = rand.nextBool() ? rand.nextInt(30) : 0; - var maxVal = rand.nextBool() - ? rand.nextInt(width == null ? 70 : ((1 << width) - 1)) + minVal + 1 - : null; - var initialValue = rand.nextBool() ? rand.nextInt(maxVal ?? 100) : 0; - - if (maxVal != null && width != null) { - // truncate to width - maxVal = max(1, LogicValue.ofInt(maxVal, width).toInt()); - } - - if (width != null) { - // truncate to width - initialValue = LogicValue.ofInt(initialValue, width).toInt(); - } - - if (maxVal == null || minVal >= maxVal) { - if (maxVal == null && width == null) { - minVal = 0; - } else { - minVal = - rand.nextInt(maxVal ?? (width == null ? 0 : (1 << width) - 1)); - } - } - - for (final intf in interfaces) { + for (final intf in cfg.interfaces) { if (intf.hasEnable) { intf.enable!.put(rand.nextBool()); } @@ -428,37 +297,21 @@ void main() { } } - int safeWidthFor(int val) { - final lv = LogicValue.ofInferWidth(val); - final inferredWidth = lv.width; - - return min(max(inferredWidth, 1), width ?? inferredWidth); - } - - final dut = Sum(interfaces, - saturates: saturates, - maxValue: maxVal != null && rand.nextBool() - ? Const(LogicValue.ofInferWidth(maxVal), - width: safeWidthFor(maxVal)) - : maxVal, - minValue: rand.nextBool() - ? Const(LogicValue.ofInferWidth(minVal), - width: safeWidthFor(minVal)) - : minVal, - width: width, - initialValue: rand.nextBool() - ? Const(LogicValue.ofInferWidth(initialValue), - width: safeWidthFor(initialValue)) - : initialValue); + final dut = Sum(cfg.interfaces, + saturates: cfg.saturates, + maxValue: cfg.maxValue, + minValue: cfg.minValue, + width: cfg.width, + initialValue: cfg.initialValue); final actual = dut.sum.value.toInt(); final expected = goldenSum( - interfaces, + cfg.interfaces, width: dut.width, - saturates: saturates, - maxVal: maxVal, - minVal: minVal, - initialValue: initialValue, + saturates: cfg.saturates, + maxVal: cfg.maxVal, + minVal: cfg.minVal, + initialValue: cfg.initialVal, ); expect(actual, expected); diff --git a/test/summation/summation_test_utils.dart b/test/summation/summation_test_utils.dart new file mode 100644 index 000000000..f4edeb35e --- /dev/null +++ b/test/summation/summation_test_utils.dart @@ -0,0 +1,249 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// summation_test_utils.dart +// Utilities for summation testing. +// +// 2024 October +// Author: Max Korbel + +// ignore_for_file: invalid_use_of_protected_member + +import 'dart:math'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:test/test.dart'; + +/// Computes the proper sum of a list of [Logic]s. +int goldenSumOfLogics( + List logics, { + required int width, + bool saturates = false, + int? maxVal, + int minVal = 0, +}) => + goldenSum( + logics.map((e) => SumInterface(width: e.width)..amount.gets(e)).toList(), + width: width, + saturates: saturates, + minVal: minVal, + maxVal: maxVal, + ); + +/// Computes the proper sum of a list of [SumInterface]s. +int goldenSum( + List interfaces, { + required int width, + bool saturates = false, + int? maxVal, + int? minVal, + int initialValue = 0, + bool debug = false, +}) { + void log(String message) { + if (debug) { + // ignore: avoid_print + print(message); + } + } + + log('width: $width'); + + var sum = initialValue; + + log('min $minVal -> max $maxVal'); + + maxVal ??= (1 << width) - 1; + if (maxVal > (1 << width) - 1) { + // ignore: parameter_assignments + maxVal = (1 << width) - 1; + } + minVal ??= 0; + + log('min $minVal -> max $maxVal [adjusted]'); + + if (minVal > maxVal) { + throw Exception('minVal must be less than or equal to maxVal'); + } + + log('init: $initialValue'); + + for (final intf in interfaces) { + final amount = intf.amount.value.toInt(); + final enabled = !intf.hasEnable || intf.enable!.value.toBool(); + + log('${intf.increments ? '+' : '-'}' + '$amount${enabled ? '' : ' [disabled]'}'); + + if (enabled) { + if (intf.increments) { + sum += amount; + } else { + sum -= amount; + } + } + } + + log('=$sum'); + + if (saturates) { + if (sum > maxVal) { + sum = maxVal; + } else if (sum < minVal) { + sum = minVal; + } + log('saturates to $sum'); + } else { + final range = maxVal - minVal + 1; + if (sum > maxVal) { + sum = (sum - maxVal - 1) % range + minVal; + } else if (sum < minVal) { + sum = maxVal - (minVal - sum - 1) % range; + } + log('rolls-over to $sum'); + } + + return sum; +} + +/// Generates a random [SumInterface]. +SumInterface genRandomInterface(Random rand) { + final isFixed = rand.nextBool(); + return SumInterface( + fixedAmount: isFixed ? rand.nextInt(100) : null, + width: isFixed ? null : rand.nextInt(8) + 1, + increments: rand.nextBool(), + hasEnable: rand.nextBool(), + ); +} + +/// Generates a list of random [SumInterface]s. +List genRandomInterfaces(Random rand) { + final numInterfaces = rand.nextInt(8) + 1; + return List.generate(numInterfaces, (_) => genRandomInterface(rand)); +} + +/// Sets up a listener on clock edges to check that counters are functioning +/// properly. +void checkCounter(Counter counter) { + final sub = counter.clk.posedge.listen((_) async { + final errPrefix = '@${Simulator.time}: '; + + if (counter is GatedCounter && !counter.saturates) { + if (counter.summer.underflowed.previousValue!.isValid && + counter.summer.underflowed.previousValue!.toBool() && + !counter.mayUnderflow.previousValue!.toBool()) { + fail('$errPrefix Unexpectedly underflowed, bad clock gating.'); + } + + if (counter.summer.overflowed.previousValue!.isValid && + counter.summer.overflowed.previousValue!.toBool() && + !counter.mayOverflow.previousValue!.toBool()) { + fail('$errPrefix Unexpectedly overflowed, bad clock gating.'); + } + } + + final expected = counter.reset.previousValue!.toBool() + ? 0 + : goldenSum( + counter.interfaces, + width: counter.width, + saturates: counter.saturates, + minVal: counter.minValueLogic.value.toInt(), + maxVal: counter.maxValueLogic.value.toInt(), + initialValue: (counter.restart?.previousValue!.toBool() ?? false) + ? counter.initialValueLogic.value.toInt() + : counter.count.previousValue!.toInt(), + ); + + if (!counter.reset.previousValue!.toBool()) { + final actual = counter.count.value.toInt(); + + // print('$expected -- $actual'); + expect( + actual, + expected, + reason: '$errPrefix' + ' expected = 0x${expected.toRadixString(16)},' + ' actual = 0x${actual.toRadixString(16)}', + ); + } + }); + + Simulator.registerEndOfSimulationAction(() async { + await sub.cancel(); + }); +} + +/// Generates a random summation configuration. +({ + List interfaces, + int? width, + bool saturates, + int? minVal, + int? maxVal, + int initialVal, + dynamic minValue, + dynamic maxValue, + dynamic initialValue, +}) genRandomSummationConfiguration(Random rand) { + final interfaces = genRandomInterfaces(rand); + + final width = rand.nextBool() ? null : rand.nextInt(10) + 1; + + final saturates = rand.nextBool(); + var minVal = rand.nextBool() ? rand.nextInt(30) : 0; + var maxVal = rand.nextBool() + ? rand.nextInt(width == null ? 70 : ((1 << width) - 1)) + minVal + 1 + : null; + var initialVal = rand.nextBool() ? rand.nextInt(maxVal ?? 100) : 0; + + if (maxVal != null && width != null) { + // truncate to width + maxVal = max(1, LogicValue.ofInt(maxVal, width).toInt()); + } + + if (width != null) { + // truncate to width + initialVal = LogicValue.ofInt(initialVal, width).toInt(); + } + + if (maxVal == null || minVal >= maxVal) { + if (maxVal == null && width == null) { + minVal = 0; + } else { + minVal = rand.nextInt(maxVal ?? (width == null ? 0 : (1 << width) - 1)); + } + } + + int safeWidthFor(int val) { + final lv = LogicValue.ofInferWidth(val); + final inferredWidth = lv.width; + + return min(max(inferredWidth, 1), width ?? inferredWidth); + } + + final maxValue = maxVal != null && rand.nextBool() + ? Const(LogicValue.ofInferWidth(maxVal), width: safeWidthFor(maxVal)) + : maxVal; + final minValue = rand.nextBool() + ? Const(LogicValue.ofInferWidth(minVal), width: safeWidthFor(minVal)) + : minVal; + final initialValue = rand.nextBool() + ? Const(LogicValue.ofInferWidth(initialVal), + width: safeWidthFor(initialVal)) + : initialVal; + + return ( + interfaces: interfaces, + width: width, + saturates: saturates, + minVal: minVal, + maxVal: maxVal, + initialVal: initialVal, + minValue: minValue, + maxValue: maxValue, + initialValue: initialValue, + ); +} diff --git a/test/toggle_gate_test.dart b/test/toggle_gate_test.dart new file mode 100644 index 000000000..9bf95c9ac --- /dev/null +++ b/test/toggle_gate_test.dart @@ -0,0 +1,96 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// toggle_gate_test.dart +// Tests for the toggle gate. +// +// 2024 October +// Author: Max Korbel + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd_hcl/rohd_hcl.dart'; +import 'package:rohd_vf/rohd_vf.dart'; +import 'package:test/test.dart'; + +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + for (final withClockGating in [true, false]) { + for (final resetValue in [null, Const(0x12, width: 8), 0x34]) { + test( + 'gates data toggles to only when enabled,' + ' withClockGating=$withClockGating, resetValue=$resetValue', + () async { + final enable = Logic()..inject(0); + final data = Logic(width: 8)..inject(0xab); + final clk = SimpleClockGenerator(10).clk; + final reset = Logic()..inject(0); + final toggleGate = ToggleGate( + enable: enable, + data: data, + clk: clk, + reset: reset, + resetValue: resetValue, + clockGateControlIntf: withClockGating + ? null + : ClockGateControlInterface(isPresent: false), + ); + + await toggleGate.build(); + + Simulator.setMaxSimTime(1000); + unawaited(Simulator.run()); + + reset.inject(1); + await clk.waitCycles(3); + + if (resetValue == null) { + expect(toggleGate.gatedData.value.toInt(), 0); + } else if (resetValue is Logic) { + expect(toggleGate.gatedData.value, resetValue.value); + } else { + expect(toggleGate.gatedData.value.toInt(), resetValue); + } + + enable.inject(1); + + LogicValue? lastEnabledVal; + clk.posedge.listen((_) { + if (enable.value.toBool()) { + expect(toggleGate.gatedData.value, data.value); + lastEnabledVal = data.value; + } else { + expect(toggleGate.gatedData.value, lastEnabledVal); + } + }); + + await clk.waitCycles(3); + + reset.inject(0); + + await clk.waitCycles(2); + + for (var i = 0; i < 5; i++) { + data.inject(i + 7); + await clk.waitCycles(1); + } + + await clk.waitCycles(2); + + for (var i = 0; i < 20; i++) { + data.inject(2 * i + 9); + enable.inject(i % 7 > 2); + await clk.waitCycles(1); + } + + await clk.waitCycles(2); + + await Simulator.endSimulation(); + }); + } + } +} diff --git a/tool/gh_actions/check_tmp_test.sh b/tool/gh_actions/check_tmp_test.sh new file mode 100755 index 000000000..ec517cf03 --- /dev/null +++ b/tool/gh_actions/check_tmp_test.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Copyright (C) 2022-2024 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause +# +# check_tmp_test.sh +# GitHub Actions step: Check temporary test files. +# +# 2022 October 12 +# Author: Chykon + +set -euo pipefail + +# Make sure there are no VCD files in the root directory. +if [ -n "$(find . -maxdepth 1 -name '*.vcd' -print -quit)" ]; then + echo "Failure: VCD files found in the root directory!" + exit 1 +else + echo "Success: no VCD files found in the root directory!" +fi diff --git a/tool/run_checks.sh b/tool/run_checks.sh index 402a154b1..4b66df9ae 100755 --- a/tool/run_checks.sh +++ b/tool/run_checks.sh @@ -50,5 +50,9 @@ tool/gh_actions/generate_documentation.sh print_step 'Run project tests' tool/gh_actions/run_tests.sh +# Check temporary test files +print_step 'Check temporary test files' +tool/gh_actions/check_tmp_test.sh + # Successful script execution notification printf '\n%s\n\n' "${form_bold}${color_yellow}Result: ${color_green}SUCCESS${text_reset}"