From d8d72f8a637b3f532f297f563be7957d7537bdf4 Mon Sep 17 00:00:00 2001 From: Max Korbel Date: Wed, 20 Nov 2024 08:53:03 -0800 Subject: [PATCH 1/3] 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}" From bf4106e2e15518e6e9af9137246fb66797586bbc Mon Sep 17 00:00:00 2001 From: Josh Kimmel Date: Thu, 21 Nov 2024 13:04:55 -0800 Subject: [PATCH 2/3] [MultiCycleDivider] Patch of prior bug fix to properly support unsigned division (#141) * Fixing bug in Divider and updating tests and documentation accordingly. --- doc/components/divider.md | 10 +++++++--- lib/src/arithmetic/divider.dart | 7 ++----- test/arithmetic/divider_test.dart | 9 ++++++++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/doc/components/divider.md b/doc/components/divider.md index a2ebb689c..d2b564495 100644 --- a/doc/components/divider.md +++ b/doc/components/divider.md @@ -1,13 +1,13 @@ # Divider -ROHD HCL provides an integer divider module to get the dividend of numerator and denominator operands. The divider implementation is not pipelined and has a maximum latency of the bit width of the operands. +ROHD HCL provides an integer divider module to get the quotient and the remainder of dividend and divisor operands. The divider implementation is not pipelined and has a minimum latency of 3 cycles. The maximum latency is dependent on the width of the operands (upper bound of `O(WIDTH**2)`). Note that latency increases exponentially as the absolute difference between the dividend and the divisor increases (worst case: largest possible dividend and divisor of 1). ## Interface The inputs to the divider module are: * `clock` => clock for synchronous logic -* `reset` => reset for synchronous logic (active high) +* `reset` => reset for synchronous logic (active high, synchronous to `clock`) * `dividend` => the numerator operand * `divisor` => the denominator operand * `isSigned` => should the operands of the division be treated as signed integers @@ -30,7 +30,7 @@ To initiate a new request, it is expected that the requestor drive `validIn` to When the division is complete, the module will assert the `validOut` signal along with the numerical values of `quotient` and `remainder` representing the division result and the signal `divZero` to indicate whether or not a division by zero occurred. The module will hold these signal values until `readyOut` is driven high by the integrating environment. The integrating environment must assume that `quotient` and `remainder` are meaningless if `divZero` is asserted. -### Mathematical Properties +## Mathematical Properties For the division, implicit rounding towards 0 is always performed. I.e., a negative quotient will always be rounded up if the dividend is not evenly divisible by the divisor. Note that this behavior is not uniform across all programming languages (for example, Python rounds towards negative infinity). @@ -65,3 +65,7 @@ if (divIntf.validOut.value.toBool()) { } ``` + +## Future Considerations + +In the future, an optimization might be added in which the `remainder` output is optional and controlled by a build time constructor parameter. If the remainder does not need to be computed, the implementation's upper bound latency can be significantly improved (`O(WIDTH**2)` => `O(WIDTH)`). diff --git a/lib/src/arithmetic/divider.dart b/lib/src/arithmetic/divider.dart index c3016eee2..a15a1e918 100644 --- a/lib/src/arithmetic/divider.dart +++ b/lib/src/arithmetic/divider.dart @@ -258,10 +258,7 @@ class MultiCycleDivider extends Module { orElse: [ tmpDifference < (aBuf - tmpShift), // move to accumulate if tmpDifference <= 0 - If( - ~tmpShift.or() | - tmpDifference[dataWidth - 1] | - ~tmpDifference.or(), + If(~tmpShift.or() | tmpDifference[-1] | ~tmpDifference.or(), then: [nextState < _MultiCycleDividerState.accumulate], orElse: [nextState < _MultiCycleDividerState.process]) ]) @@ -375,7 +372,7 @@ class MultiCycleDivider extends Module { currentState.eq(_MultiCycleDividerState .process), // didn't exceed a_buf, so count as success [ - If(~tmpDifference[dataWidth - 1], then: [ + If(~tmpDifference[-1], then: [ lastSuccess < (Const(1, width: dataWidth + 1) << currIndex), // capture 2^i diff --git a/test/arithmetic/divider_test.dart b/test/arithmetic/divider_test.dart index 6574b1bfb..b98a60553 100644 --- a/test/arithmetic/divider_test.dart +++ b/test/arithmetic/divider_test.dart @@ -406,7 +406,14 @@ class MultiCycleDividerBasicSequence extends Sequence { mDivisor: 0, mValidIn: true, mIsSigned: true, - mReadyOut: true)); // divide by 0 + mReadyOut: true)) // divide by 0 + // long latency division + ..add(MultiCycleDividerInputSeqItem( + mDividend: 0xffffffec, + mDivisor: 0x6, + mValidIn: true, + mIsSigned: false, + mReadyOut: true)); } } From 2e70ca9caa5a78d2156c7d8eae9cb82281031306 Mon Sep 17 00:00:00 2001 From: Desmond Kirkpatrick Date: Wed, 4 Dec 2024 21:01:33 -0800 Subject: [PATCH 3/3] Full set of sign variants (signed/unsigned/selected) for multiplier operands (#144) * full set of sign variants (signed/unsigned/selected) for operands of integer multiplication * multiplier doc and configurator update * complete signage coverage at multiplier, better specification of getters for inputs, resultIsSigned API addition --- doc/components/multiplier.md | 48 +- doc/components/multiplier_components.md | 8 +- lib/src/arithmetic/carry_save_mutiplier.dart | 4 +- .../arithmetic/evaluate_partial_product.dart | 22 +- lib/src/arithmetic/multiplicand_selector.dart | 27 +- lib/src/arithmetic/multiplier.dart | 352 ++++++--- lib/src/arithmetic/multiplier_encoder.dart | 57 +- .../arithmetic/partial_product_generator.dart | 62 +- .../partial_product_sign_extend.dart | 152 ++-- .../config_carry_save_multiplier.dart | 6 +- .../config_compression_tree_multiplier.dart | 16 + lib/src/utils.dart | 13 + test/arithmetic/addend_compressor_test.dart | 242 +------ .../carry_save_multiplier_test.dart | 5 +- test/arithmetic/multiplier_encoder_test.dart | 563 ++++++++++----- test/arithmetic/multiplier_test.dart | 678 ++++++++++-------- test/configurator_test.dart | 38 +- 17 files changed, 1354 insertions(+), 939 deletions(-) diff --git a/doc/components/multiplier.md b/doc/components/multiplier.md index bbc5067c0..e34736329 100644 --- a/doc/components/multiplier.md +++ b/doc/components/multiplier.md @@ -4,7 +4,12 @@ ROHD-HCL provides an abstract `Multiplier` module which multiplies two numbers represented as two `Logic`s, potentially of different widths, treating them as either signed (2s complement) or unsigned. It produces the product as a `Logic` with width equal to the sum of the -widths of the inputs. As of now, we have the following implementations +widths of the inputs. The signs of the operands are either fixed by a parameter, +or runtime selectable, e.g.: `signedMultiplicand` or `selectSignedMultiplicand`. +The output of the multiplier also has a signal telling us if the result is to be +treated as signed. + +As of now, we have the following implementations of this abstract `Module`: - [Carry Save Multiplier](#carry-save-multiplier) @@ -13,7 +18,13 @@ of this abstract `Module`: An additional kind of abstract module provided is a `MultiplyAccumulate` module which multiplies two numbers represented as two `Logic`s and adds the result to a third `Logic` with width -equal to the sum of the widths of the main inputs. We have a +equal to the sum of the widths of the main inputs. Similar to the `Multiplier`, +the signs of the operands are either fixed by a parameter, +or runtime selectable, e.g.: `signedMultiplicand` or `selectSignedMultiplicand`. +The output of the multiply-accumulate also has a signal telling us if the result is to be +treated as signed. + +We have a high-performance implementation: - [Compression Tree Multiply Accumulate](#compression-tree-multiply-accumulate) @@ -22,7 +33,7 @@ The compression tree based arithmetic units are built from a set of components f ## Carry Save Multiplier -Carry save multiplier is a digital circuit used for performing multiplication operations. It +The carry-save multiplier is a digital circuit used for performing multiplication operations. It is particularly useful in applications that require high speed multiplication, such as digital signal processing. @@ -31,7 +42,8 @@ The module in ROHD-HCL accept input parameters the clock `clk` signal, reset `reset` signal, `Logic`s' a and b as the input pin and the name of the module `name`. Note that the width of the inputs must be the -same or `RohdHclException` will be thrown. +same or `RohdHclException` will be thrown. The output latency is equal to the width of the inputs +given by `latency` on the component. An example is shown below to multiply two inputs of signals that have 4-bits of width. @@ -82,15 +94,17 @@ digital signal processing. The parameters of the `CompressionTreeMultiplier` are: -- Two input terms `a` and `b` which can be different widths -- The radix used for Booth encoding (2, 4, 8, and 16 are currently supported) -- The type of `ParallelPrefix` tree used in the final `ParallelPrefixAdder` (optional) -- `signed` parameter: whether the operands should be treated as signed (2s complement) or unsigned +- Two input terms `a` and `b` which can be different widths. +- The radix used for Booth encoding (2, 4, 8, and 16 are currently supported). +- The type of `ParallelPrefix` tree used in the final `ParallelPrefixAdder` (optional). - `ppGen` parameter: the type of `PartialProductGenerator` to use which has derived classes for different styles of sign extension. In some cases this adds an extra row to hold a sign bit. -- An optional `selectSigned` control signal which overrides the `signed` configuration allowing for runtime control of signed or unsigned operation with the same hardware. `signed` must be false if using this control signal. +- `signedMultiplicand` parameter: whether the multiplicand (first arg) should be treated as signed (2s complement) or unsigned. +- `signedMultiplier` parameter: whether the multiplier (second arg) should be treated as signed (2s complement) or unsigned. +- An optional `selectSignedMultiplicand` control signal which overrides the `signedMultiplicand` parameter allowing for runtime control of signed or unsigned operation with the same hardware. `signedMultiplicand` must be false if using this control signal. +- An optional `selectSignedMultiplier` control signal which overrides the `signedMultiplier` parameter allowing for runtime control of signed or unsigned operation with the same hardware. `signedMultiplier` must be false if using this control signal. - An optional `clk`, as well as `enable` and `reset` that are used to add a pipestage in the `ColumnCompressor` to allow for pipelined operation. -Here is an example of use of the `CompressionTreeMultiplier`: +Here is an example of use of the `CompressionTreeMultiplier` with one signed input: ```dart const widthA = 6; @@ -103,7 +117,7 @@ Here is an example of use of the `CompressionTreeMultiplier`: b.put(3); final multiplier = - CompressionTreeMultiplier(a, b, radix, signed: true); + CompressionTreeMultiplier(a, b, radix, signedMultiplicand: true); final product = multiplier.product; @@ -123,12 +137,16 @@ The parameters of the - The accumulate input term `c` which must have width as sum of the two operand widths + 1. - The radix used for Booth encoding (2, 4, 8, and 16 are currently supported) - The type of `ParallelPrefix` tree used in the final `ParallelPrefixAdder` (default Kogge-Stone). -- `signed` parameter: whether the operands should be treated as signed (2s complement) or unsigned - `ppGen` parameter: the type of `PartialProductGenerator` to use which has derived classes for different styles of sign extension. In some cases this adds an extra row to hold a sign bit (default `PartialProductGeneratorCompactRectSignExtension`). -- An optional `selectSigned` control signal which overrides the `signed` configuration allowing for runtime control of signed or unsigned operation with the same hardware. `signed` must be false if using this control signal. +- `signedMultiplicand` parameter: whether the multiplicand (first arg) should be treated as signed (2s complement) or unsigned +- `signedMultiplier` parameter: whether the multiplier (second arg) should be treated as signed (2s complement) or unsigned +- `signedAddend` parameter: whether the addend (third arg) should be treated as signed (2s complement) or unsigned +- An optional `selectSignedMultiplicand` control signal which overrides the `signedMultiplicand` parameter allowing for runtime control of signed or unsigned operation with the same hardware. `signedMultiplicand` must be false if using this control signal. +- An optional `selectSignedMultiplier` control signal which overrides the `signedMultiplier` parameter allowing for runtime control of signed or unsigned operation with the same hardware. `signedMultiplier` must be false if using this control signal. +- An optional `selectSignedAddend` control signal which overrides the `signedAddend` parameter allowing for runtime control of signed or unsigned operation with the same hardware. `signedAddend` must be false if using this control signal. - An optional `clk`, as well as `enable` and `reset` that are used to add a pipestage in the `ColumnCompressor` to allow for pipelined operation. -Here is an example of using the `CompressionTreeMultiplyAccumulate`: +Here is an example of using the `CompressionTreeMultiplyAccumulate` with all inputs as signed: ```dart const widthA = 6; @@ -142,7 +160,7 @@ Here is an example of using the `CompressionTreeMultiplyAccumulate`: b.put(3); c.put(5); - final multiplier = CompressionTreeMultiplyAccumulate(a, b, c, radix, signed: true); + final multiplier = CompressionTreeMultiplyAccumulate(a, b, c, radix, signedMultiplicand: true, signedMultiplier: true, signedAddend: true); final accumulate = multiplier.accumulate; diff --git a/doc/components/multiplier_components.md b/doc/components/multiplier_components.md index 0db47ddfb..d0a9515e0 100644 --- a/doc/components/multiplier_components.md +++ b/doc/components/multiplier_components.md @@ -51,7 +51,7 @@ row slice mult A few things to note: first, that we are negating by 1s complement (so we need a -0) and second, these rows do not add up to (18: 10010). For Booth encoded rows to add up properly, they need to be in 2s complement form, and they need to be sign-extended. - Here is the matrix with crude sign extension (this formatting is available from our `PartialProductGenerator` component). With 2s complementation, and sign bits folded in (note the LSB of each row has a sign term from the previous row), these addends are correctly formed and add to (18: 10010). + Here is the matrix with a crude sign extension `brute` (the table formatting is available from our `PartialProductGenerator` component). With 2s complementation, and sign bits folded in (note the LSB of each row has a sign term from the previous row), these addends are correctly formed and add to (18: 10010). ```text 7 6 5 4 3 2 1 0 @@ -64,7 +64,7 @@ A few things to note: first, that we are negating by 1s complement (so we need a 0 0 0 1 0 0 1 0 : 00010010 = 18 (18) ``` - There are more compact ways of doing sign-extension which result in far fewer additions. Here is an example of compact sign-extension: + There are more compact ways of doing sign-extension which result in far fewer additions. Here is an example of `compact` sign-extension, where the last row which carries only a sign bit is folded into the previous row: ```text 7 6 5 4 3 2 1 0 @@ -86,7 +86,7 @@ And of course, with higher radix-encoding, we select more bits at a time from th 0 0 0 1 0 0 1 0 : 00010010 = 18 (18) ``` -Note that radix-4 shifts by 2 positions each row, but with only two rows and with sign-extension adding an LSB bit, you only see a shift of 1 in row 1. +Note that radix-4 shifts by 2 positions each row, but with only two rows and with sign-extension adding an LSB bit to each row, you only see a shift of 1 in row 1, but in a larger example you would see the two-bit shift in the following rows. ## Partial Product Generator @@ -222,7 +222,7 @@ Finally, we produce the product. ```dart final pp = - PartialProductGeneratorCompactRectSignExtension(a, b, RadixEncoder(radix), signed: true); + PartialProductGeneratorCompactRectSignExtension(a, b, RadixEncoder(radix), signedMultiplicand: true, signedMultiplier: true); final compressor = ColumnCompressor(pp)..compress(); final adder = ParallelPrefixAdder( compressor.exractRow(0), compressor.extractRow(1), BrentKung.new); diff --git a/lib/src/arithmetic/carry_save_mutiplier.dart b/lib/src/arithmetic/carry_save_mutiplier.dart index 3a8363d14..3ec4f410b 100644 --- a/lib/src/arithmetic/carry_save_mutiplier.dart +++ b/lib/src/arithmetic/carry_save_mutiplier.dart @@ -42,8 +42,8 @@ class CarrySaveMultiplier extends Multiplier { CarrySaveMultiplier(super.a, super.b, {required Logic clk, required Logic reset, - required super.signed, - super.name = 'carry_save_multiplier'}) { + super.name = 'carry_save_multiplier'}) + : super(signedMultiplicand: false, signedMultiplier: false) { if (a.width != b.width) { throw RohdHclException('inputs of a and b should have same width.'); } diff --git a/lib/src/arithmetic/evaluate_partial_product.dart b/lib/src/arithmetic/evaluate_partial_product.dart index 4b8209aab..97339bea9 100644 --- a/lib/src/arithmetic/evaluate_partial_product.dart +++ b/lib/src/arithmetic/evaluate_partial_product.dart @@ -10,6 +10,22 @@ import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; +/// The following routines are useful only during testing +extension TestPartialProductSignage on PartialProductGenerator { + /// Return true if multiplicand is truly signed (fixed or runtime) + bool isSignedMultiplicand() => (selectSignedMultiplicand == null) + ? signedMultiplicand + : !selectSignedMultiplicand!.value.isZero; + + /// Return true if multiplier is truly signed (fixed or runtime) + bool isSignedMultiplier() => (selectSignedMultiplier == null) + ? signedMultiplier + : !selectSignedMultiplier!.value.isZero; + + /// Return true if accumulate result is truly signed (fixed or runtime) + bool isSignedResult() => isSignedMultiplicand() | isSignedMultiplier(); +} + /// Debug routines for printing out partial product matrix during /// simulation with live logic values extension EvaluateLivePartialProduct on PartialProductGenerator { @@ -25,11 +41,9 @@ extension EvaluateLivePartialProduct on PartialProductGenerator { } } final sum = LogicValue.ofBigInt(accum, maxW).toBigInt(); - return signed + return isSignedMultiplicand() | isSignedMultiplier() ? sum.toSigned(maxW) - : (selectSigned != null && !selectSigned!.value.isZero) - ? sum.toSigned(maxW) - : sum; + : sum; } /// Print out the partial product matrix diff --git a/lib/src/arithmetic/multiplicand_selector.dart b/lib/src/arithmetic/multiplicand_selector.dart index 21522cd41..75b5c15dd 100644 --- a/lib/src/arithmetic/multiplicand_selector.dart +++ b/lib/src/arithmetic/multiplicand_selector.dart @@ -12,7 +12,7 @@ import 'package:rohd_hcl/rohd_hcl.dart'; /// A class accessing the multiples of the multiplicand at a position class MultiplicandSelector { - /// radix of the selector + /// The radix of the selector int radix; /// The bit shift of the selector (typically overlaps 1) @@ -21,17 +21,25 @@ class MultiplicandSelector { /// New width of partial products generated from the multiplicand int get width => multiplicand.width + shift - 1; - /// Access the multiplicand + /// The base multiplicand from which to generate multiples to select. Logic multiplicand = Logic(); - /// Place to store multiples of the multiplicand + /// Place to store [multiples] of the [multiplicand] (e.g. *1, *2, *-1, *-2..) late LogicArray multiples; - /// Generate required multiples of multiplicand + /// Build a [MultiplicandSelector] generationg required [multiples] of + /// [multiplicand] to [select] using a [RadixEncoder] argument. + /// + /// [multiplicand] is base multiplicand multiplied by Booth encodings of + /// the [RadixEncoder] during [select]. + /// + /// [signedMultiplicand] generates a fixed signed selector versus using + /// [selectSignedMultiplicand] which is a runtime sign selection [Logic] + /// in which case [signedMultiplicand] must be false. MultiplicandSelector(this.radix, this.multiplicand, - {Logic? selectSigned, bool signed = false}) + {Logic? selectSignedMultiplicand, bool signedMultiplicand = false}) : shift = log2Ceil(radix) { - if (signed && (selectSigned != null)) { + if (signedMultiplicand && (selectSignedMultiplicand != null)) { throw RohdHclException('sign reconfiguration requires signed=false'); } if (radix > 16) { @@ -41,15 +49,16 @@ class MultiplicandSelector { final numMultiples = radix ~/ 2; multiples = LogicArray([numMultiples], width); final Logic extendedMultiplicand; - if (selectSigned == null) { - extendedMultiplicand = signed + if (selectSignedMultiplicand == null) { + extendedMultiplicand = signedMultiplicand ? multiplicand.signExtend(width) : multiplicand.zeroExtend(width); } else { final len = multiplicand.width; final sign = multiplicand[len - 1]; final extension = [ - for (var i = len; i < width; i++) mux(selectSigned, sign, Const(0)) + for (var i = len; i < width; i++) + mux(selectSignedMultiplicand, sign, Const(0)) ]; extendedMultiplicand = (multiplicand.elements + extension).rswizzle(); } diff --git a/lib/src/arithmetic/multiplier.dart b/lib/src/arithmetic/multiplier.dart index 4bb3e7114..88d903f16 100644 --- a/lib/src/arithmetic/multiplier.dart +++ b/lib/src/arithmetic/multiplier.dart @@ -6,7 +6,8 @@ // need to inherit this module to ensure consistency. // // 2023 May 29 -// Author: Yao Jing Quek +// Author: Yao Jing Quek , Desmond Kirkpatrick +// import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; @@ -15,25 +16,79 @@ import 'package:rohd_hcl/src/arithmetic/partial_product_sign_extend.dart'; /// An abstract class for all multiplier implementations. abstract class Multiplier extends Module { - /// The input to the multiplier pin [a]. + /// The multiplicand input [a]. @protected - late final Logic a; + Logic get a => input('a'); - /// The input to the multiplier pin [b]. + /// The multiplier input [b]. + @protected + Logic get b => input('b'); + + /// The multiplier treats input [a] always as a signed input. + @protected + final bool signedMultiplicand; + + /// The multiplier treats input [b] always as a signed input. @protected - late final Logic b; + final bool signedMultiplier; - /// The multiplier treats operands and output as signed - bool signed; + /// If not null, use this signal to select between signed and unsigned + /// multiplicand [a]. + @protected + Logic? get selectSignedMultiplicand => tryInput('selectSignedMultiplicand'); + + /// If not null, use this signal to select between signed and unsigned + /// multiplier [b] + @protected + Logic? get selectSignedMultiplier => tryInput('selectSignedMultiplier'); /// The multiplication results of the multiplier. Logic get product; + /// Logic that tells us [product] is signed. + @protected + Logic get isProductSigned => output('isProductSigned'); + /// Take input [a] and input [b] and return the /// [product] of the multiplication result. - Multiplier(Logic a, Logic b, {required this.signed, super.name}) { - this.a = addInput('a', a, width: a.width); - this.b = addInput('b', b, width: b.width); + /// + /// [signedMultiplicand] parameter configures the multiplicand [a] as a signed + /// multiplier (default is unsigned). + /// + /// [signedMultiplier] parameter configures the multiplier [b] as a signed + /// multiplier (default is unsigned). + /// + /// Optional [selectSignedMultiplicand] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedMultiplicand] static + /// configuration. + /// + /// Optional [selectSignedMultiplier] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedMultiplier] static + /// configuration. + Multiplier(Logic a, Logic b, + {this.signedMultiplicand = false, + this.signedMultiplier = false, + Logic? selectSignedMultiplicand, + Logic? selectSignedMultiplier, + super.name}) { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + + selectSignedMultiplicand = (selectSignedMultiplicand != null) + ? addInput('selectSignedMultiplicand', selectSignedMultiplicand) + : null; + selectSignedMultiplier = (selectSignedMultiplier != null) + ? addInput('selectSignedMultiplier', selectSignedMultiplier) + : null; + + addOutput('isProductSigned') <= + (signedMultiplicand | signedMultiplier ? Const(1) : Const(0)) | + ((selectSignedMultiplicand != null) + ? selectSignedMultiplicand + : Const(0)) | + ((selectSignedMultiplier != null) + ? selectSignedMultiplier + : Const(0)); } } @@ -41,29 +96,95 @@ abstract class Multiplier extends Module { abstract class MultiplyAccumulate extends Module { /// The input to the multiplier pin [a]. @protected - late final Logic a; + Logic get a => input('a'); /// The input to the multiplier pin [b]. @protected - late final Logic b; + Logic get b => input('b'); /// The input to the addend pin [c]. @protected - late final Logic c; + Logic get c => input('c'); + + /// The MAC treats multiplicand [a] as always signed. + @protected + final bool signedMultiplicand; + + /// The MAC treats multiplier [b] as always signed. + @protected + final bool signedMultiplier; + + /// The MAC treats addend [c] as always signed. + @protected + final bool signedAddend; + + /// If not null, use this signal to select between signed and unsigned + /// multiplicand [a]. + @protected + Logic? get selectSignedMultiplicand => tryInput('selectSignedMultiplicand'); + + /// If not null, use this signal to select between signed and unsigned + /// multiplier [b] + @protected + Logic? get selectSignedMultiplier => tryInput('selectSignedMultiplier'); - /// The multiplier treats operands and output as signed - bool signed; + /// If not null, use this signal to select between signed and unsigned + /// addend [c] + @protected + Logic? get selectSignedAddend => tryInput('selectSignedAddend'); - /// The multiplication results of the multiply-accumulate. + /// The multiplication and addition or [accumulate] result. Logic get accumulate; + /// Logic that tells us [accumulate] is signed. + @protected + Logic get isAccumulateSigned => output('isAccumulateSigned'); + /// Take input [a] and input [b], compute their /// product, add input [c] to produce the [accumulate] result. + /// + /// Optional [selectSignedMultiplicand] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedMultiplicand] static + /// configuration. + /// + /// Optional [selectSignedMultiplier] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedMultiplier] static + /// configuration. + /// + /// Optional [selectSignedAddend] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedAddend] static + /// configuration. MultiplyAccumulate(Logic a, Logic b, Logic c, - {required this.signed, super.name}) { - this.a = addInput('a', a, width: a.width); - this.b = addInput('b', b, width: b.width); - this.c = addInput('c', c, width: c.width); + {this.signedMultiplicand = false, + this.signedMultiplier = false, + this.signedAddend = false, + Logic? selectSignedMultiplicand, + Logic? selectSignedMultiplier, + Logic? selectSignedAddend, + super.name}) { + a = addInput('a', a, width: a.width); + b = addInput('b', b, width: b.width); + c = addInput('c', c, width: c.width); + selectSignedMultiplicand = (selectSignedMultiplicand != null) + ? addInput('selectSignedMultiplicand', selectSignedMultiplicand) + : null; + selectSignedMultiplier = (selectSignedMultiplier != null) + ? addInput('selectSignedMultiplier', selectSignedMultiplier) + : null; + selectSignedAddend = (selectSignedAddend != null) + ? addInput('selectSignedAddend', selectSignedAddend) + : null; + addOutput('isAccumulateSigned') <= + (signedMultiplicand | signedMultiplier | signedAddend + ? Const(1) + : Const(0)) | + ((selectSignedMultiplicand != null) + ? selectSignedMultiplicand + : Const(0)) | + ((selectSignedMultiplier != null) + ? selectSignedMultiplier + : Const(0)) | + ((selectSignedAddend != null) ? selectSignedAddend : Const(0)); } } @@ -88,14 +209,22 @@ class CompressionTreeMultiplier extends Multiplier { /// Sign extension methodology is defined by the partial product generator /// supplied via [ppGen]. /// - /// [a] and [b] are the product terms and they can be different widths - /// allowing for rectangular multiplication. + /// [a] multiplicand and [b] multiplier are the product terms and they can + /// be different widths allowing for rectangular multiplication. + /// + /// [signedMultiplicand] parameter configures the multiplicand [a] as a signed + /// multiplier (default is unsigned). + /// + /// [signedMultiplier] parameter configures the multiplier [b] as a signed + /// multiplier (default is unsigned). /// - /// [signed] parameter configures the multiplier as a signed multiplier - /// (default is unsigned). + /// Optional [selectSignedMultiplicand] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedMultiplicand] static + /// configuration. /// - /// Optional [selectSigned] allows for runtime configuration of signed - /// or unsigned operation, overriding the [signed] static configuration. + /// Optional [selectSignedMultiplier] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedMultiplier] static + /// configuration. /// /// If [clk] is not null then a set of flops are used to latch the output /// after compression. [reset] and [enable] are optional @@ -105,26 +234,36 @@ class CompressionTreeMultiplier extends Multiplier { {this.clk, this.reset, this.enable, - Logic? selectSigned, + super.signedMultiplicand = false, + super.signedMultiplier = false, + super.selectSignedMultiplicand, + super.selectSignedMultiplier, ParallelPrefix Function(List, Logic Function(Logic, Logic)) ppTree = KoggeStone.new, PartialProductGenerator Function(Logic, Logic, RadixEncoder, - {required bool signed, Logic? selectSigned}) + {required bool signedMultiplier, + required bool signedMultiplicand, + Logic? selectSignedMultiplier, + Logic? selectSignedMultiplicand}) ppGen = PartialProductGeneratorCompactRectSignExtension.new, - super.signed = false, super.name = 'compression_tree_multiplier'}) { - final internalSelectSigned = - (selectSigned != null) ? addInput('selectSigned', selectSigned) : null; - final iClk = (clk != null) ? addInput('clk', clk!) : null; - final iReset = (reset != null) ? addInput('reset', reset!) : null; - final iEnable = (enable != null) ? addInput('enable', enable!) : null; + clk = (clk != null) ? addInput('clk', clk!) : null; + reset = (reset != null) ? addInput('reset', reset!) : null; + enable = (enable != null) ? addInput('enable', enable!) : null; final product = addOutput('product', width: a.width + b.width); - final pp = ppGen(a, b, RadixEncoder(radix), - selectSigned: internalSelectSigned, signed: signed); + final pp = ppGen( + a, + b, + RadixEncoder(radix), + selectSignedMultiplicand: selectSignedMultiplicand, + signedMultiplicand: signedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier, + signedMultiplier: signedMultiplier, + ); final compressor = - ColumnCompressor(clk: iClk, reset: iReset, enable: iEnable, pp) + ColumnCompressor(clk: clk, reset: reset, enable: enable, pp) ..compress(); final adder = ParallelPrefixAdder( compressor.extractRow(0), compressor.extractRow(1), @@ -136,13 +275,16 @@ class CompressionTreeMultiplier extends Multiplier { /// An implementation of an integer multiply-accumulate using compression trees class CompressionTreeMultiplyAccumulate extends MultiplyAccumulate { /// The clk for the pipelined version of column compression. - Logic? clk; + @protected + Logic? get clk => tryInput('clk'); /// Optional reset for configurable pipestage - Logic? reset; + @protected + Logic? get reset => tryInput('reset'); /// Optional enable for configurable pipestage. - Logic? enable; + @protected + Logic? get enable => tryInput('enable'); /// The final product of the multiplier module. @override @@ -154,48 +296,75 @@ class CompressionTreeMultiplyAccumulate extends MultiplyAccumulate { /// [a] and [b] are the product terms, [c] is the accumulate term which /// must be the sum of the widths plus 1. /// - /// [signed] parameter configures the multiplier as a signed multiplier - /// (default is unsigned). + /// [signedMultiplicand] parameter configures the multiplicand [a] as + /// always signed (default is unsigned). + /// + /// [signedMultiplier] parameter configures the multiplier [b] as + /// always signed (default is unsigned). + /// + /// [signedAddend] parameter configures the addend [c] as + /// always signed (default is unsigned). /// /// Sign extension methodology is defined by the partial product generator /// supplied via [ppGen]. /// - /// Optional [selectSigned] allows for runtime configuration of signed - /// or unsigned operation, overriding the [signed] static configuration. + /// Optional [selectSignedMultiplicand] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedMultiplicand] static + /// configuration. + /// + /// Optional [selectSignedMultiplier] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedMultiplier] static + /// configuration. + /// + /// Optional [selectSignedAddend] allows for runtime configuration of + /// signed or unsigned operation, overriding the [signedAddend] static + /// configuration. /// /// If[clk] is not null then a set of flops are used to latch the output /// after compression. [reset] and [enable] are optional /// inputs to control these flops when [clk] is provided. If [clk] is null, /// the [ColumnCompressor] is built as a combinational tree of compressors. CompressionTreeMultiplyAccumulate(super.a, super.b, super.c, int radix, - {this.clk, - this.reset, - this.enable, - super.signed = false, - Logic? selectSigned, + {Logic? clk, + Logic? reset, + Logic? enable, + super.signedMultiplicand = false, + super.signedMultiplier = false, + super.signedAddend = false, + super.selectSignedMultiplicand, + super.selectSignedMultiplier, + super.selectSignedAddend, ParallelPrefix Function(List, Logic Function(Logic, Logic)) ppTree = KoggeStone.new, PartialProductGenerator Function(Logic, Logic, RadixEncoder, - {required bool signed, Logic? selectSigned}) + {required bool signedMultiplier, + required bool signedMultiplicand, + Logic? selectSignedMultiplier, + Logic? selectSignedMultiplicand}) ppGen = PartialProductGeneratorCompactRectSignExtension.new, super.name = 'compression_tree_mac'}) { - final internalSelectSigned = - (selectSigned != null) ? addInput('selectSigned', selectSigned) : null; - final iClk = (clk != null) ? addInput('clk', clk!) : null; - final iReset = (reset != null) ? addInput('reset', reset!) : null; - final iEnable = (enable != null) ? addInput('enable', enable!) : null; + clk = (clk != null) ? addInput('clk', clk) : null; + reset = (reset != null) ? addInput('reset', reset) : null; + enable = (enable != null) ? addInput('enable', enable) : null; final accumulate = addOutput('accumulate', width: a.width + b.width + 1); - final pp = ppGen(a, b, RadixEncoder(radix), - selectSigned: internalSelectSigned, signed: signed); + final pp = ppGen( + a, + b, + RadixEncoder(radix), + selectSignedMultiplicand: selectSignedMultiplicand, + signedMultiplicand: signedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier, + signedMultiplier: signedMultiplier, + ); final lastLength = pp.partialProducts[pp.rows - 1].length + pp.rowShift[pp.rows - 1]; final sign = mux( - (internalSelectSigned != null) - ? internalSelectSigned - : (signed ? Const(1) : Const(0)), + (selectSignedAddend != null) + ? selectSignedAddend! + : (signedAddend ? Const(1) : Const(0)), c[c.width - 1], Const(0)); final l = [for (var i = 0; i < c.width; i++) c[i]]; @@ -212,7 +381,7 @@ class CompressionTreeMultiplyAccumulate extends MultiplyAccumulate { pp.rowShift.insert(0, 0); final compressor = - ColumnCompressor(clk: iClk, reset: iReset, enable: iEnable, pp) + ColumnCompressor(clk: clk, reset: reset, enable: enable, pp) ..compress(); final adder = ParallelPrefixAdder( compressor.extractRow(0), compressor.extractRow(1), @@ -223,38 +392,55 @@ class CompressionTreeMultiplyAccumulate extends MultiplyAccumulate { /// A MultiplyAccumulate which ignores the [c] term and applies the /// multiplier function -class MutiplyOnly extends MultiplyAccumulate { +class MultiplyOnly extends MultiplyAccumulate { @override Logic get accumulate => output('accumulate'); + static String _genName( + Multiplier Function(Logic a, Logic b, + {Logic? selectSignedMultiplicand, + Logic? selectSignedMultiplier}) + fn, + Logic a, + Logic b, + Logic? selectSignedMultiplicand, + Logic? selectSignedMultiplier) => + fn(a, b, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier) + .name; + /// Construct a MultiplyAccumulate that only multiplies to enable /// using the same tester with zero accumulate addend [c]. - MutiplyOnly(super.a, super.b, super.c, - Multiplier Function(Logic a, Logic b, {Logic? selectSigned}) mulGen, - {super.signed = false, - Logic? selectSigned}) // Will be overrwridden by multiplyGenerator - : super( - name: 'Multiply Only: ' - '${mulGen.call(a, b, selectSigned: selectSigned).name}') { - final Logic? internalSelectSigned; - - if (selectSigned != null) { - internalSelectSigned = addInput('selectSigned', selectSigned); - } else { - internalSelectSigned = null; - } + MultiplyOnly( + super.a, + super.b, + super.c, + Multiplier Function(Logic a, Logic b, + {Logic? selectSignedMultiplicand, Logic? selectSignedMultiplier}) + mulGen, { + super.signedMultiplicand = false, + super.signedMultiplier = false, + super.signedAddend = false, + super.selectSignedMultiplicand, + super.selectSignedMultiplier, + super.selectSignedAddend, + }) // Will be overrwridden by multiplyGenerator + : super( + // ignore: prefer_interpolation_to_compose_strings + name: 'Multiply Only: ' + + _genName(mulGen, a, b, selectSignedMultiplicand, + selectSignedMultiplier)) { final accumulate = addOutput('accumulate', width: a.width + b.width + 1); - final multiply = mulGen(a, b, selectSigned: internalSelectSigned); - signed = multiply.signed; + final multiply = mulGen(a, b, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier); accumulate <= mux( - (internalSelectSigned != null) - ? internalSelectSigned - : (signed ? Const(1) : Const(0)), - [multiply.product[multiply.product.width - 1], multiply.product] - .swizzle(), + multiply.isProductSigned, + multiply.product.signExtend(accumulate.width), multiply.product.zeroExtend(accumulate.width)); } } diff --git a/lib/src/arithmetic/multiplier_encoder.dart b/lib/src/arithmetic/multiplier_encoder.dart index 2b85426c3..e96317b4e 100644 --- a/lib/src/arithmetic/multiplier_encoder.dart +++ b/lib/src/arithmetic/multiplier_encoder.dart @@ -81,48 +81,55 @@ class RadixEncoder { } } -/// A class that generates the Booth encoding of the multipler +/// A class that generates the Booth encoding of the multipler. class MultiplierEncoder { /// Access the multiplier final Logic multiplier; - /// Number of row radixEncoders + /// Number of rows that are Booth-encoded. late final int rows; /// The multiplier value, sign extended as appropriate to be divisible - /// by the RadixEncoder overlapping bitslices. + /// by the RadixEncoder width using overlapping (by one) bitslices. Logic _extendedMultiplier = Logic(); + + /// Store the [RadixEncoder] used. late final RadixEncoder _encoder; - late final int _sliceWidth; - /// Generate an encoding of the input multiplier + /// Generate the Booth encoding of an input [multiplier] using + /// [radixEncoder]. + /// + /// [signedMultiplier] generates a fixed signed encoder versus using + /// [selectSignedMultiplier] which is a runtime sign selection [Logic] + /// in which case [signedMultiplier] must be false. MultiplierEncoder(this.multiplier, RadixEncoder radixEncoder, - {Logic? selectSigned, bool signed = false}) - : _encoder = radixEncoder, - _sliceWidth = log2Ceil(radixEncoder.radix) + 1 { - if (signed && (selectSigned != null)) { + {Logic? selectSignedMultiplier, bool signedMultiplier = false}) + : _encoder = radixEncoder { + if (signedMultiplier && (selectSignedMultiplier != null)) { throw RohdHclException('sign reconfiguration requires signed=false'); } // Unsigned encoding wants to overlap past the multipler - if (signed) { - rows = - ((multiplier.width + (signed ? 0 : 1)) / log2Ceil(radixEncoder.radix)) - .ceil(); + if (signedMultiplier) { + rows = ((multiplier.width + (signedMultiplier ? 0 : 1)) / + log2Ceil(radixEncoder.radix)) + .ceil(); } else { - rows = (((multiplier.width + 1) % (_sliceWidth - 1) == 0) ? 0 : 1) + + rows = (((multiplier.width + 1) % (log2Ceil(radixEncoder.radix)) == 0) + ? 0 + : 1) + ((multiplier.width + 1) ~/ log2Ceil(radixEncoder.radix)); } // slices overlap by 1 and start at -1a - if (selectSigned == null) { - _extendedMultiplier = (signed - ? multiplier.signExtend(rows * (_sliceWidth - 1)) - : multiplier.zeroExtend(rows * (_sliceWidth - 1))); + if (selectSignedMultiplier == null) { + _extendedMultiplier = (signedMultiplier + ? multiplier.signExtend(rows * (log2Ceil(radixEncoder.radix))) + : multiplier.zeroExtend(rows * (log2Ceil(radixEncoder.radix)))); } else { final len = multiplier.width; final sign = multiplier[len - 1]; final extension = [ - for (var i = len - 1; i < (rows * (_sliceWidth - 1)); i++) - mux(selectSigned, sign, Const(0)) + for (var i = len - 1; i < (rows * (log2Ceil(radixEncoder.radix))); i++) + mux(selectSignedMultiplier, sign, Const(0)) ]; _extendedMultiplier = (multiplier.elements + extension).rswizzle(); } @@ -133,13 +140,15 @@ class MultiplierEncoder { if (row >= rows) { throw RohdHclException('row $row is not < number of encoding rows $rows'); } - final base = row * (_sliceWidth - 1); + final base = row * log2Ceil(_encoder.radix); final multiplierSlice = [ if (row > 0) - _extendedMultiplier.slice(base + _sliceWidth - 2, base - 1) + _extendedMultiplier.slice(base + log2Ceil(_encoder.radix) - 1, base - 1) else - [_extendedMultiplier.slice(base + _sliceWidth - 2, base), Const(0)] - .swizzle() + [ + _extendedMultiplier.slice(base + log2Ceil(_encoder.radix) - 1, base), + Const(0) + ].swizzle() ]; return _encoder.encode(multiplierSlice.first); } diff --git a/lib/src/arithmetic/partial_product_generator.dart b/lib/src/arithmetic/partial_product_generator.dart index 59a7500df..048d3b92f 100644 --- a/lib/src/arithmetic/partial_product_generator.dart +++ b/lib/src/arithmetic/partial_product_generator.dart @@ -202,35 +202,52 @@ abstract class PartialProductGenerator extends PartialProductArray { /// multiples of the multiplicand and generate partial products late final MultiplicandSelector selector; - /// Operands are signed - late final bool signed; + /// [multiplicand] operand is always signed + final bool signedMultiplicand; + + /// [multiplier] operand is always signed + final bool signedMultiplier; /// Used to avoid sign extending more than once bool isSignExtended = false; /// If not null, use this signal to select between signed and unsigned - /// operation. - late final Logic? selectSigned; + /// [multiplicand]. + final Logic? selectSignedMultiplicand; - /// Construct a [PartialProductGenerator] -- the partial product matrix + /// If not null, use this signal to select between signed and unsigned + /// [multiplier]. + final Logic? selectSignedMultiplier; + + /// Construct a [PartialProductGenerator] -- the partial product matrix. + /// + /// [signedMultiplier] generates a fixed signed encoder versus using + /// [selectSignedMultiplier] which is a runtime sign selection [Logic] + /// in which case [signedMultiplier] must be false. PartialProductGenerator( Logic multiplicand, Logic multiplier, RadixEncoder radixEncoder, - {this.signed = false, this.selectSigned, super.name = 'ppg'}) { - if (signed && (selectSigned != null)) { + {this.signedMultiplicand = false, + this.selectSignedMultiplicand, + this.signedMultiplier = false, + this.selectSignedMultiplier, + super.name = 'ppg'}) { + if (signedMultiplier && (selectSignedMultiplier != null)) { throw RohdHclException('sign reconfiguration requires signed=false'); } encoder = MultiplierEncoder(multiplier, radixEncoder, - selectSigned: selectSigned, signed: signed); + signedMultiplier: signedMultiplier, + selectSignedMultiplier: selectSignedMultiplier); selector = MultiplicandSelector(radixEncoder.radix, multiplicand, - selectSigned: selectSigned, signed: signed); + signedMultiplicand: signedMultiplicand, + selectSignedMultiplicand: selectSignedMultiplicand); if (multiplicand.width < selector.shift) { throw RohdHclException('multiplicand width must be greater than ' - '${selector.shift}'); + 'or equal to ${selector.shift}'); } - if (multiplier.width < (selector.shift + (signed ? 1 : 0))) { + if (multiplier.width < (selector.shift + (signedMultiplier ? 1 : 0))) { throw RohdHclException('multiplier width must be greater than ' - '${selector.shift + (signed ? 1 : 0)}'); + 'or equal to ${selector.shift + (signedMultiplier ? 1 : 0)}'); } _build(); signExtend(); @@ -255,7 +272,7 @@ abstract class PartialProductGenerator extends PartialProductArray { /// Helper function for sign extension routines: /// For signed operands, set the MSB to [sign], otherwise add this [sign] bit. void addStopSign(List addend, SignBit sign) { - if (!signed) { + if (!signedMultiplicand) { addend.add(sign); } else { addend.last = sign; @@ -265,12 +282,12 @@ abstract class PartialProductGenerator extends PartialProductArray { /// Helper function for sign extension routines: /// For signed operands, flip the MSB, otherwise add this [sign] bit. void addStopSignFlip(List addend, SignBit sign) { - if (!signed) { - if (selectSigned == null) { + if (!signedMultiplicand) { + if (selectSignedMultiplicand == null) { addend.add(sign); } else { - addend.add(SignBit(mux(selectSigned!, ~addend.last, sign), - inverted: selectSigned != null)); + addend.add(SignBit(mux(selectSignedMultiplicand!, ~addend.last, sign), + inverted: selectSignedMultiplicand != null)); } } else { addend.last = SignBit(~addend.last, inverted: true); @@ -279,13 +296,14 @@ abstract class PartialProductGenerator extends PartialProductArray { } /// A Partial Product Generator with no sign extension -class PartialProductGeneratorNoSignExtension extends PartialProductGenerator { +class PartialProductGeneratorNoneSignExtension extends PartialProductGenerator { /// Construct a basic Partial Product Generator - PartialProductGeneratorNoSignExtension( + PartialProductGeneratorNoneSignExtension( super.multiplicand, super.multiplier, super.radixEncoder, - {required super.signed, - // ignore: avoid_unused_constructor_parameters - Logic? selectSigned}); + {super.signedMultiplicand, + super.signedMultiplier, + super.selectSignedMultiplicand, + super.selectSignedMultiplier}); @override void signExtend() {} diff --git a/lib/src/arithmetic/partial_product_sign_extend.dart b/lib/src/arithmetic/partial_product_sign_extend.dart index 7a8e6d644..33aa48ae7 100644 --- a/lib/src/arithmetic/partial_product_sign_extend.dart +++ b/lib/src/arithmetic/partial_product_sign_extend.dart @@ -21,7 +21,7 @@ enum SignExtension { brute, /// Extend using stop bits in each row (and an extra row for final sign) - stop, + stopBits, /// Fold in last row sign bit (Mohanty, B.K., Choubey, A.) compact, @@ -33,29 +33,24 @@ enum SignExtension { /// Used to test different sign extension methods typedef PPGFunction = PartialProductGenerator Function( Logic a, Logic b, RadixEncoder radixEncoder, - {Logic? selectSigned, bool signed}); + {bool signedMultiplicand, + Logic? selectSignedMultiplicand, + bool signedMultiplier, + Logic? selectSignedMultiplier}); /// Used to test different sign extension methods PPGFunction curryPartialProductGenerator(SignExtension signExtension) => - (a, b, encoder, {selectSigned, signed = false}) => switch (signExtension) { - SignExtension.none => PartialProductGeneratorNoSignExtension( - a, b, encoder, - signed: signed), - SignExtension.brute => PartialProductGeneratorBruteSignExtension( - a, b, encoder, - selectSigned: selectSigned, signed: signed), - SignExtension.stop => PartialProductGeneratorStopBitsSignExtension( - a, b, encoder, - selectSigned: selectSigned, signed: signed), - SignExtension.compact => PartialProductGeneratorCompactSignExtension( - a, b, encoder, - signed: signed), - SignExtension.compactRect => - PartialProductGeneratorCompactRectSignExtension(a, b, encoder, - signed: signed), - }; - -/// These other sign extensions are for asssisting with testing and debugging. + switch (signExtension) { + SignExtension.none => PartialProductGeneratorNoneSignExtension.new, + SignExtension.brute => PartialProductGeneratorBruteSignExtension.new, + SignExtension.stopBits => + PartialProductGeneratorStopBitsSignExtension.new, + SignExtension.compact => PartialProductGeneratorCompactSignExtension.new, + SignExtension.compactRect => + PartialProductGeneratorCompactRectSignExtension.new, + }; + +/// These other sign extensions are for assisting with testing and debugging. /// More robust and simpler sign extensions in case /// complex sign extension routines obscure other bugs. @@ -65,13 +60,18 @@ class PartialProductGeneratorBruteSignExtension /// Construct a brute-force sign extending Partial Product Generator PartialProductGeneratorBruteSignExtension( super.multiplicand, super.multiplier, super.radixEncoder, - {super.signed, super.selectSigned, super.name = 'brute'}); + {super.signedMultiplicand, + super.signedMultiplier, + super.selectSignedMultiplicand, + super.selectSignedMultiplier, + super.name = 'brute'}); /// Fully sign extend the PP array: useful for reference only @override void signExtend() { - if (signed && (selectSigned != null)) { - throw RohdHclException('sign reconfiguration requires signed=false'); + if (signedMultiplicand && (selectSignedMultiplicand != null)) { + throw RohdHclException('multiplicand sign reconfiguration requires ' + 'signedMultiplicand=false'); } if (isSignExtended) { throw RohdHclException('Partial Product array already sign-extended'); @@ -80,12 +80,11 @@ class PartialProductGeneratorBruteSignExtension final signs = [for (var r = 0; r < rows; r++) encoder.getEncoding(r).sign]; for (var row = 0; row < rows; row++) { final addend = partialProducts[row]; - // final sign = SignBit(signed ? addend.last : signs[row]); final Logic sign; - if (selectSigned != null) { - sign = mux(selectSigned!, addend.last, signs[row]); + if (selectSignedMultiplicand != null) { + sign = mux(selectSignedMultiplicand!, addend.last, signs[row]); } else { - sign = signed ? addend.last : signs[row]; + sign = signedMultiplicand ? addend.last : signs[row]; } addend.addAll(List.filled((rows - row) * shift, SignBit(sign))); if (row > 0) { @@ -108,7 +107,11 @@ class PartialProductGeneratorCompactSignExtension /// Construct a compact sign extending Partial Product Generator PartialProductGeneratorCompactSignExtension( super.multiplicand, super.multiplier, super.radixEncoder, - {super.signed, super.selectSigned, super.name = 'compact'}); + {super.signedMultiplicand, + super.selectSignedMultiplicand, + super.signedMultiplier, + super.selectSignedMultiplier, + super.name = 'compact'}); /// Sign extend the PP array using stop bits without adding a row. @override @@ -117,8 +120,9 @@ class PartialProductGeneratorCompactSignExtension // Mohanty, B.K., Choubey, A. Efficient Design for Radix-8 Booth Multiplier // and Its Application in Lifting 2-D DWT. Circuits Syst Signal Process 36, // 1129–1149 (2017). https://doi.org/10.1007/s00034-016-0349-9 - if (signed && (selectSigned != null)) { - throw RohdHclException('sign reconfiguration requires signed=false'); + if (signedMultiplicand && (selectSignedMultiplicand != null)) { + throw RohdHclException('multiplicand sign reconfiguration requires ' + 'signedMultiplicand=false'); } if (isSignExtended) { throw RohdHclException('Partial Product array already sign-extended'); @@ -129,7 +133,7 @@ class PartialProductGeneratorCompactSignExtension final firstAddend = partialProducts[0]; final lastAddend = partialProducts[lastRow]; - final firstRowQStart = selector.width - (signed ? 1 : 0); + final firstRowQStart = selector.width - (signedMultiplicand ? 1 : 0); final lastRowSignPos = shift * lastRow; final alignRow0Sign = firstRowQStart - lastRowSignPos; @@ -137,11 +141,19 @@ class PartialProductGeneratorCompactSignExtension final propagate = List.generate(rows, (i) => List.filled(0, Logic(), growable: true)); + for (var row = 0; row < rows; row++) { propagate[row].add(signs[row]); for (var col = 0; col < 2 * (shift - 1); col++) { propagate[row].add(partialProducts[row][col]); } + // Last row has extend sign propagation to Q start + if (row == lastRow) { + var col = 2 * (shift - 1); + while (propagate[lastRow].length <= alignRow0Sign) { + propagate[lastRow].add(SignBit(partialProducts[row][col++])); + } + } for (var col = 1; col < propagate[row].length; col++) { propagate[row][col] = propagate[row][col] & propagate[row][col - 1]; } @@ -154,6 +166,9 @@ class PartialProductGeneratorCompactSignExtension } m[row].addAll(List.filled(shift - 1, Logic())); } + while (m[lastRow].length < alignRow0Sign) { + m[lastRow].add(Logic()); + } for (var i = shift - 1; i < m[lastRow].length; i++) { m[lastRow][i] = lastAddend[i] ^ @@ -164,15 +179,16 @@ class PartialProductGeneratorCompactSignExtension for (var row = 0; row < lastRow; row++) { remainders[row] = propagate[row][shift - 1]; } - remainders[lastRow] <= propagate[lastRow][alignRow0Sign]; + remainders[lastRow] <= propagate[lastRow][max(alignRow0Sign, 0)]; // Compute Sign extension for row==0 - // final firstSign = !signed ? signs[0] : firstAddend.last; final Logic firstSign; - if (selectSigned == null) { - firstSign = signed ? SignBit(firstAddend.last) : SignBit(signs[0]); + if (selectSignedMultiplicand == null) { + firstSign = + signedMultiplicand ? SignBit(firstAddend.last) : SignBit(signs[0]); } else { - firstSign = SignBit(mux(selectSigned!, firstAddend.last, signs[0])); + firstSign = + SignBit(mux(selectSignedMultiplicand!, firstAddend.last, signs[0])); } final q = [ firstSign ^ remainders[lastRow], @@ -183,7 +199,7 @@ class PartialProductGeneratorCompactSignExtension for (var row = 0; row < rows; row++) { final addend = partialProducts[row]; if (row > 0) { - final mLimit = (row == lastRow) ? 2 * (shift - 1) : shift - 1; + final mLimit = (row == lastRow) ? alignRow0Sign : shift - 1; for (var i = 0; i < mLimit; i++) { addend[i] = m[row][i]; } @@ -196,7 +212,7 @@ class PartialProductGeneratorCompactSignExtension for (var i = 0; i < shift - 1; i++) { firstAddend[i] = m[0][i]; } - if (!signed) { + if (!signedMultiplicand) { firstAddend.add(q[0]); } else { firstAddend.last = q[0]; @@ -216,17 +232,23 @@ class PartialProductGeneratorStopBitsSignExtension /// Construct a stop bits sign extending Partial Product Generator PartialProductGeneratorStopBitsSignExtension( super.multiplicand, super.multiplier, super.radixEncoder, - {super.signed, super.selectSigned, super.name = 'stop_bits'}); + {super.signedMultiplicand, + super.selectSignedMultiplicand, + super.signedMultiplier, + super.selectSignedMultiplier, + super.name = 'stop_bits'}); /// Sign extend the PP array using stop bits. /// If possible, fold the final carry into another row (only when rectangular /// enough that carry bit lands outside another row). /// This technique can then be combined with a first-row extension technique /// for folding in the final carry. + /// @override void signExtend() { - if (signed && (selectSigned != null)) { - throw RohdHclException('sign reconfiguration requires signed=false'); + if (signedMultiplicand && (selectSignedMultiplicand != null)) { + throw RohdHclException('multiplicand sign reconfiguration requires ' + 'signedMultiplicand=false'); } if (isSignExtended) { throw RohdHclException('Partial Product array already sign-extended'); @@ -246,15 +268,16 @@ class PartialProductGeneratorStopBitsSignExtension for (var row = 0; row < rows; row++) { final addend = partialProducts[row]; final Logic sign; - if (selectSigned != null) { - sign = mux(selectSigned!, addend.last, signs[row]); + if (selectSignedMultiplicand != null) { + sign = mux(selectSignedMultiplicand!, addend.last, signs[row]); } else { - sign = signed ? addend.last : signs[row]; + sign = signedMultiplicand ? addend.last : signs[row]; } if (row == 0) { - if (!signed) { + if (!signedMultiplicand) { addend.addAll(List.filled(shift, SignBit(sign))); } else { + // either is signed? addend.addAll(List.filled(shift - 1, SignBit(sign))); // signed only? } addend.add(SignBit(~sign, inverted: true)); @@ -275,7 +298,7 @@ class PartialProductGeneratorStopBitsSignExtension finalCarryPos - (extensionRow.length + rowShift[finalCarryRow]), Const(0))) ..add(SignBit(signs[rows - 1])); - } else if (signed | (selectSigned != null)) { + } else if (signedMultiplier | (selectSignedMultiplier != null)) { // Create an extra row to hold the final carry bit partialProducts .add(List.filled(selector.width, Const(0), growable: true)); @@ -284,7 +307,8 @@ class PartialProductGeneratorStopBitsSignExtension // Hack for radix-2 if (shift == 1) { - partialProducts.last.last = ~partialProducts.last.last; + addStopSignFlip( + partialProducts.last, SignBit(Const(1), inverted: true)); } } } @@ -296,7 +320,11 @@ class PartialProductGeneratorCompactRectSignExtension /// Construct a compact rect sign extending Partial Product Generator PartialProductGeneratorCompactRectSignExtension( super.multiplicand, super.multiplier, super.radixEncoder, - {required super.signed, super.selectSigned, super.name = 'compact_rect'}); + {super.signedMultiplicand, + super.signedMultiplier, + super.selectSignedMultiplicand, + super.selectSignedMultiplier, + super.name = 'compact_rect'}); /// Sign extend the PP array using stop bits without adding a row /// This routine works with different widths of multiplicand/multiplier, @@ -304,8 +332,9 @@ class PartialProductGeneratorCompactRectSignExtension /// Desmond A. Kirkpatrick @override void signExtend() { - if (signed && (selectSigned != null)) { - throw RohdHclException('sign reconfiguration requires signed=false'); + if (signedMultiplicand && (selectSignedMultiplicand != null)) { + throw RohdHclException('multiplicand sign reconfiguration requires ' + 'signedMultiplicand=false'); } if (isSignExtended) { throw RohdHclException('Partial Product array already sign-extended'); @@ -316,7 +345,7 @@ class PartialProductGeneratorCompactRectSignExtension final firstAddend = partialProducts[0]; final lastAddend = partialProducts[lastRow]; - final firstRowQStart = selector.width - (signed ? 1 : 0); + final firstRowQStart = selector.width - (signedMultiplicand ? 1 : 0); final lastRowSignPos = shift * lastRow; final align = firstRowQStart - lastRowSignPos; @@ -354,6 +383,7 @@ class PartialProductGeneratorCompactRectSignExtension } m[row].addAll(List.filled(shift - 1, Logic())); } + while (m[lastRow].length < align) { m[lastRow].add(Logic()); } @@ -392,14 +422,13 @@ class PartialProductGeneratorCompactRectSignExtension // Insert the lastRow sign: Either in firstRow's Q if there is a // collision or in another row if it lands beyond the Q sign extension - - // final firstSign = signed ? SignBit(firstAddend.last) : SignBit(signs[0]); - final Logic firstSign; - if (selectSigned == null) { - firstSign = signed ? SignBit(firstAddend.last) : SignBit(signs[0]); + if (selectSignedMultiplicand == null) { + firstSign = + signedMultiplicand ? SignBit(firstAddend.last) : SignBit(signs[0]); } else { - firstSign = SignBit(mux(selectSigned!, firstAddend.last, signs[0])); + firstSign = + SignBit(mux(selectSignedMultiplicand!, firstAddend.last, signs[0])); } final lastSign = SignBit(remainders[lastRow]); // Compute Sign extension MSBs for firstRow @@ -423,13 +452,14 @@ class PartialProductGeneratorCompactRectSignExtension if (-align >= q.length) { q.last = SignBit(~firstSign, inverted: true); } - addStopSign(firstAddend, SignBit(q[0])); firstAddend.addAll(q.getRange(1, q.length)); if (-align >= q.length) { - final finalCarryRelPos = - lastRowSignPos - selector.width - shift + (signed ? 1 : 0); + final finalCarryRelPos = lastRowSignPos - + selector.width - + shift + + (signedMultiplicand ? 1 : 0); final finalCarryRow = (finalCarryRelPos / shift).floor(); final curRowLength = partialProducts[finalCarryRow].length + rowShift[finalCarryRow]; diff --git a/lib/src/component_config/components/config_carry_save_multiplier.dart b/lib/src/component_config/components/config_carry_save_multiplier.dart index 96374d80a..f75c02af7 100644 --- a/lib/src/component_config/components/config_carry_save_multiplier.dart +++ b/lib/src/component_config/components/config_carry_save_multiplier.dart @@ -16,20 +16,16 @@ class CarrySaveMultiplierConfigurator extends Configurator { /// A knob controlling the width of the inputs to a [CarrySaveMultiplier]. final IntConfigKnob logicWidthKnob = IntConfigKnob(value: 8); - /// A knob controling the sign of a [CarrySaveMultiplier] - final signChoiceKnob = ChoiceConfigKnob([false, true], value: false); - @override final String name = 'Carry Save Multiplier'; @override CarrySaveMultiplier createModule() => CarrySaveMultiplier( Logic(width: logicWidthKnob.value), Logic(width: logicWidthKnob.value), - clk: Logic(), reset: Logic(), signed: true); + clk: Logic(), reset: Logic()); @override late final Map> knobs = UnmodifiableMapView({ 'Width': logicWidthKnob, - 'Sign': signChoiceKnob, }); } diff --git a/lib/src/component_config/components/config_compression_tree_multiplier.dart b/lib/src/component_config/components/config_compression_tree_multiplier.dart index 3b63e8ad2..5b9e77649 100644 --- a/lib/src/component_config/components/config_compression_tree_multiplier.dart +++ b/lib/src/component_config/components/config_compression_tree_multiplier.dart @@ -40,6 +40,14 @@ class CompressionTreeMultiplierConfigurator extends Configurator { /// Controls the width of the multiplier.! final IntConfigKnob multiplierWidthKnob = IntConfigKnob(value: 5); + /// A knob controlling the sign of the multiplicand + final ChoiceConfigKnob signMultiplicandValueKnob = + ChoiceConfigKnob(['unsigned', 'signed', 'selected'], value: 'unsigned'); + + /// A knob controlling the sign of the multiplier + final ChoiceConfigKnob signMultiplierValueKnob = + ChoiceConfigKnob(['unsigned', 'signed', 'selected'], value: 'unsigned'); + /// Controls whether the adder is pipelined final ToggleConfigKnob pipelinedKnob = ToggleConfigKnob(value: false); @@ -49,6 +57,12 @@ class CompressionTreeMultiplierConfigurator extends Configurator { Logic(name: 'a', width: multiplicandWidthKnob.value), Logic(name: 'b', width: multiplierWidthKnob.value), radixKnob.value, + signedMultiplicand: signMultiplicandValueKnob.value == 'signed', + signedMultiplier: signMultiplierValueKnob.value == 'signed', + selectSignedMultiplicand: + signMultiplicandValueKnob.value == 'selected' ? Logic() : null, + selectSignedMultiplier: + signMultiplierValueKnob.value == 'selected' ? Logic() : null, ppTree: generatorMap[prefixTreeKnob.value]!); @override @@ -56,7 +70,9 @@ class CompressionTreeMultiplierConfigurator extends Configurator { 'Tree type': prefixTreeKnob, 'Radix': radixKnob, 'Multiplicand width': multiplicandWidthKnob, + 'Multiplicand sign': signMultiplicandValueKnob, 'Multiplier width': multiplierWidthKnob, + 'Multiplier sign': signMultiplierValueKnob, 'Pipelined': pipelinedKnob, }); diff --git a/lib/src/utils.dart b/lib/src/utils.dart index e27b7f083..102149e64 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -50,3 +50,16 @@ extension LogicValueMajority on LogicValue { return result; } } + +/// This extension will provide conversion to Signed or Unsigned BigInt +extension SignedBigInt on BigInt { + /// Convert a BigInt to Signed when [signed] is true + BigInt toCondSigned(int width, {bool signed = false}) => + signed ? toSigned(width) : toUnsigned(width); + + /// Construct a Signed BigInt from an int when [signed] is true + static BigInt fromSignedInt(int value, int width, {bool signed = false}) => + signed + ? BigInt.from(value).toSigned(width) + : BigInt.from(value).toUnsigned(width); +} diff --git a/test/arithmetic/addend_compressor_test.dart b/test/arithmetic/addend_compressor_test.dart index 77cf3833a..d479e940f 100644 --- a/test/arithmetic/addend_compressor_test.dart +++ b/test/arithmetic/addend_compressor_test.dart @@ -8,8 +8,6 @@ // Author: Desmond Kirkpatrick import 'dart:async'; -import 'dart:io'; -import 'dart:math'; import 'package:rohd/rohd.dart'; import 'package:rohd_hcl/rohd_hcl.dart'; import 'package:rohd_hcl/src/arithmetic/evaluate_compressor.dart'; @@ -17,6 +15,8 @@ import 'package:rohd_hcl/src/arithmetic/evaluate_partial_product.dart'; import 'package:rohd_hcl/src/arithmetic/partial_product_sign_extend.dart'; import 'package:test/test.dart'; +/// This [CompressorTestMod] module is used to test instantiation, where we can +/// catch trace errors (IO not added) not found in a simple test instantiation. class CompressorTestMod extends Module { late final PartialProductGenerator pp; @@ -37,7 +37,7 @@ class CompressorTestMod extends Module { } pp = PartialProductGeneratorCompactRectSignExtension(a, b, encoder, - signed: signed); + signedMultiplicand: signed, signedMultiplier: signed); compressor = ColumnCompressor(pp, clk: clk); compressor.compress(); final r0 = addOutput('r0', width: compressor.columns.length); @@ -48,159 +48,10 @@ class CompressorTestMod extends Module { } } -void testCompressionExhaustive(PartialProductGenerator pp) { - final widthX = pp.selector.multiplicand.width; - final widthY = pp.encoder.multiplier.width; - - final signed = - (pp.selectSigned == null) ? pp.signed : !pp.selectSigned!.value.isZero; - - final limitX = pow(2, widthX); - final limitY = pow(2, widthY); - for (var i = 0; i < limitX; i++) { - for (var j = 0; j < limitY; j++) { - final X = signed - ? BigInt.from(i).toSigned(widthX) - : BigInt.from(i).toUnsigned(widthX); - final Y = signed - ? BigInt.from(j).toSigned(widthY) - : BigInt.from(j).toUnsigned(widthY); - - checkCompressor(pp, X, Y); - } - } -} - -void testCompressionRandom(PartialProductGenerator pp, int iterations) { - final widthX = pp.selector.multiplicand.width; - final widthY = pp.encoder.multiplier.width; - - final value = Random(47); - for (var i = 0; i < iterations; i++) { - final X = pp.signed - ? value.nextLogicValue(width: widthX).toBigInt().toSigned(widthX) - : value.nextLogicValue(width: widthX).toBigInt().toUnsigned(widthX); - final Y = pp.signed - ? value.nextLogicValue(width: widthY).toBigInt().toSigned(widthY) - : value.nextLogicValue(width: widthY).toBigInt().toUnsigned(widthY); - - checkCompressor(pp, X, Y); - } -} - -void checkCompressor(PartialProductGenerator pp, BigInt X, BigInt Y) { - final widthX = pp.selector.multiplicand.width; - final widthY = pp.encoder.multiplier.width; - final compressor = ColumnCompressor(pp); - - final product = X * Y; - - pp.multiplicand.put(X); - pp.multiplier.put(Y); - final value = pp.evaluate(); - expect(value, equals(product), - reason: 'Fail: $X * $Y: $value ' - 'vs expected $product' - '\n$pp'); - final evaluateValue = compressor.evaluate(); - if (evaluateValue.$1 != product) { - stdout - ..write('Fail: $X)$widthX] * $Y[$widthY]: $evaluateValue ' - 'vs expected $product\n') - ..write(pp); - } - compressor.compress(); - final compressedValue = compressor.evaluate().$1; - expect(compressedValue, equals(product), - reason: 'Fail: $X[$widthX] * $Y[$widthY]: $compressedValue ' - 'vs expected $product' - '\n$pp'); - final compressedLogicValue = compressor.evaluate(logic: true).$1; - expect(compressedLogicValue, equals(product), - reason: 'Fail: $X[$widthX] * $Y[$widthY]: $compressedLogicValue ' - 'vs expected $product' - '\n$pp'); - - final a = compressor.extractRow(0); - final b = compressor.extractRow(1); - - final adder = ParallelPrefixAdder(a, b); - final adderValue = - adder.sum.value.toBigInt().toSigned(compressor.columns.length); - expect(adderValue, equals(product), - reason: 'Fail: $X[$widthX] * $Y[$widthY]: ' - '$adderValue vs expected $product' - '\n$pp'); -} - void main() { tearDown(() async { await Simulator.reset(); }); - test('ColumnCompressor: random evaluate: square radix-4, just CompactRect', - () async { - for (final signed in [false, true]) { - for (var radix = 4; radix < 8; radix *= 2) { - final encoder = RadixEncoder(radix); - // stdout.write('encoding with radix=$radix\n'); - final shift = log2Ceil(encoder.radix); - for (var width = shift + 1; width < 2 * shift + 1; width++) { - for (final signExtension in SignExtension.values) { - if (signExtension != SignExtension.compactRect) { - continue; - } - final ppg = curryPartialProductGenerator(signExtension); - for (final useSelect in [false, true]) { - final PartialProductGenerator pp; - if (useSelect) { - final selectSigned = Logic(); - // ignore: cascade_invocations - selectSigned.put(signed ? 1 : 0); - pp = ppg(Logic(name: 'X', width: width), - Logic(name: 'Y', width: width), encoder, - selectSigned: selectSigned); - } else { - pp = ppg(Logic(name: 'X', width: width), - Logic(name: 'Y', width: width), encoder, - signed: signed); - } - testCompressionRandom(pp, 10); - } - } - } - } - } - }); - test('Column Compressor: single compressor evaluate', () async { - const widthX = 6; - const widthY = 9; - final a = Logic(name: 'a', width: widthX); - final b = Logic(name: 'b', width: widthY); - - const av = 4; - const bv = 14; - for (final signed in [true, false]) { - final bA = signed - ? BigInt.from(av).toSigned(widthX) - : BigInt.from(av).toUnsigned(widthX); - final bB = signed - ? BigInt.from(bv).toSigned(widthY) - : BigInt.from(bv).toUnsigned(widthY); - - // Set these so that printing inside module build will have Logic values - a.put(bA); - b.put(bB); - const radix = 2; - final encoder = RadixEncoder(radix); - final pp = PartialProductGeneratorCompactRectSignExtension(a, b, encoder, - signed: signed); - expect(pp.evaluate(), equals(BigInt.from(av * bv))); - final compressor = ColumnCompressor(pp); - expect(compressor.evaluate().$1, equals(BigInt.from(av * bv))); - compressor.compress(); - expect(compressor.evaluate().$1, equals(BigInt.from(av * bv))); - } - }); test('Column Compressor: evaluate flopped', () async { final clk = SimpleClockGenerator(10).clk; @@ -211,8 +62,8 @@ void main() { var av = 3; const bv = 6; - var bA = BigInt.from(av).toSigned(widthX); - final bB = BigInt.from(bv).toSigned(widthY); + var bA = SignedBigInt.fromSignedInt(av, widthX, signed: true); + final bB = SignedBigInt.fromSignedInt(bv, widthY, signed: true); // Set these so that printing inside module build will have Logic values a.put(bA); @@ -236,79 +87,36 @@ void main() { equals(BigInt.from(av * bv))); await Simulator.endSimulation(); }); - - test('example multiplier', () async { - const widthX = 10; - const widthY = 10; + test('Column Compressor: single compressor evaluate', () async { + const widthX = 3; + const widthY = 3; final a = Logic(name: 'a', width: widthX); final b = Logic(name: 'b', width: widthY); - const av = 37; - const bv = 6; - for (final signed in [false, true]) { - final bA = signed - ? BigInt.from(av).toSigned(widthX) - : BigInt.from(av).toUnsigned(widthX); - final bB = signed - ? BigInt.from(bv).toSigned(widthY) - : BigInt.from(bv).toUnsigned(widthY); + const av = -3; + const bv = -3; + for (final signed in [true, false]) { + final bA = SignedBigInt.fromSignedInt(av, widthX, signed: signed); + final bB = SignedBigInt.fromSignedInt(bv, widthY, signed: signed); // Set these so that printing inside module build will have Logic values a.put(bA); b.put(bB); - const radix = 8; - final encoder = RadixEncoder(radix); - final selectSigned = Logic(); - // ignore: cascade_invocations - selectSigned.put(signed ? 1 : 0); - final pp = PartialProductGeneratorStopBitsSignExtension(a, b, encoder, - // final pp = PartialProductGeneratorCompactRectSignExtension(a, b, - // encoder, - // signed: signed); - selectSigned: selectSigned); - - expect(pp.evaluate(), equals(bA * bB)); - final compressor = ColumnCompressor(pp)..compress(); - expect(compressor.evaluate().$1, equals(bA * bB)); - } - }); - - test('single sign agnostic compressor evaluate', () async { - const widthX = 3; - const widthY = 3; - final a = Logic(name: 'a', width: widthX); - final b = Logic(name: 'b', width: widthY); - - const av = 1; - const bv = 4; - for (final signed in [false, true]) { - final bA = signed - ? BigInt.from(av).toSigned(widthX) - : BigInt.from(av).toUnsigned(widthX); - final bB = signed - ? BigInt.from(bv).toSigned(widthY) - : BigInt.from(bv).toUnsigned(widthY); - const radix = 4; final encoder = RadixEncoder(radix); - // for (final useSelect in [true]) { for (final useSelect in [false, true]) { - // Set these so that printing inside module build will have Logic values - a.put(bA); - b.put(bB); - - final selectSigned = Logic(); - // ignore: cascade_invocations - selectSigned.put(signed ? 1 : 0); - - final pp = useSelect - ? PartialProductGeneratorBruteSignExtension(a, b, encoder, - selectSigned: selectSigned) - : PartialProductGeneratorBruteSignExtension(a, b, encoder, - signed: signed); - - // print(pp.representation()); - + final selectSignedMultiplicand = useSelect ? Logic() : null; + final selectSignedMultiplier = useSelect ? Logic() : null; + if (useSelect) { + selectSignedMultiplicand!.put(signed ? 1 : 0); + selectSignedMultiplier!.put(signed ? 1 : 0); + } + final pp = PartialProductGeneratorCompactRectSignExtension( + a, b, encoder, + signedMultiplicand: !useSelect & signed, + signedMultiplier: !useSelect & signed, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier); expect(pp.evaluate(), equals(bA * bB)); final compressor = ColumnCompressor(pp); expect(compressor.evaluate().$1, equals(bA * bB)); diff --git a/test/arithmetic/carry_save_multiplier_test.dart b/test/arithmetic/carry_save_multiplier_test.dart index 9cffff90b..35778a806 100644 --- a/test/arithmetic/carry_save_multiplier_test.dart +++ b/test/arithmetic/carry_save_multiplier_test.dart @@ -25,8 +25,7 @@ void main() { final clk = SimpleClockGenerator(10).clk; final reset = Logic(name: 'reset'); - expect( - () => CarrySaveMultiplier(clk: clk, reset: reset, signed: true, a, b), + expect(() => CarrySaveMultiplier(clk: clk, reset: reset, a, b), throwsA(const TypeMatcher())); }); @@ -37,7 +36,7 @@ void main() { final reset = Logic(name: 'reset'); final clk = SimpleClockGenerator(10).clk; - final csm = CarrySaveMultiplier(clk: clk, reset: reset, signed: true, a, b); + final csm = CarrySaveMultiplier(clk: clk, reset: reset, a, b); await csm.build(); diff --git a/test/arithmetic/multiplier_encoder_test.dart b/test/arithmetic/multiplier_encoder_test.dart index 485728b16..13570b83f 100644 --- a/test/arithmetic/multiplier_encoder_test.dart +++ b/test/arithmetic/multiplier_encoder_test.dart @@ -16,134 +16,226 @@ import 'package:rohd_hcl/src/arithmetic/evaluate_partial_product.dart'; import 'package:rohd_hcl/src/arithmetic/partial_product_sign_extend.dart'; import 'package:test/test.dart'; -void checkEvaluateExhaustive(PartialProductGenerator pp) { +// Here are the variations needed testing: +// - Sign variants +// - Multiplicand: [unsigned, signed], [selectedUnsigned, selectedSigned] +// - Multiplier: [unsigned, signed], [selectedUnsigned, selectedSigned] +// - Radix Encodings: [2,4,8,16] +// - Widths: +// - Cross the shift intervals for each radix +// - Rectangular: again, need to cross a shift interval +// - Sign Extension: [brute, stop, compact, compactRect] + +void testPartialProductExhaustive(PartialProductGenerator pp) { final widthX = pp.selector.multiplicand.width; final widthY = pp.encoder.multiplier.width; - final signed = - (pp.selectSigned == null) ? pp.signed : !pp.selectSigned!.value.isZero; final limitX = pow(2, widthX); final limitY = pow(2, widthY); - for (var i = BigInt.zero; i < BigInt.from(limitX); i += BigInt.one) { - for (var j = BigInt.zero; j < BigInt.from(limitY); j += BigInt.one) { - final X = signed ? i.toSigned(widthX) : i.toUnsigned(widthX); - final Y = signed ? j.toSigned(widthY) : j.toUnsigned(widthY); - pp.multiplicand.put(X); - pp.multiplier.put(Y); - final value = pp.evaluate(); - expect(value, equals(X * Y), - reason: '$X * $Y = $value should be ${X * Y}'); + + final multiplicandSigns = pp.signedMultiplicand + ? [true] + : (pp.selectSignedMultiplicand != null) + ? [false, true] + : [false]; + final multiplierSigns = pp.signedMultiplier + ? [true] + : (pp.selectSignedMultiplier != null) + ? [false, true] + : [false]; + test( + 'exhaustive: ${pp.name} R${pp.selector.radix} ' + 'WD=${pp.multiplicand.width} WM=${pp.multiplier.width} ' + 'SD=${pp.signedMultiplicand ? 1 : 0} ' + 'SM=${pp.signedMultiplier ? 1 : 0} ' + 'SelD=${pp.selectSignedMultiplicand != null ? 1 : 0} ' + 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () async { + for (var i = 0; i < limitX; i++) { + for (var j = 0; j < limitY; j++) { + for (final multiplicandSign in multiplicandSigns) { + final X = + SignedBigInt.fromSignedInt(i, widthX, signed: multiplicandSign); + if (pp.selectSignedMultiplicand != null) { + pp.selectSignedMultiplicand!.put(multiplicandSign ? 1 : 0); + } + for (final multiplierSign in multiplierSigns) { + final Y = + SignedBigInt.fromSignedInt(j, widthY, signed: multiplierSign); + if (pp.selectSignedMultiplier != null) { + pp.selectSignedMultiplier!.put(multiplierSign ? 1 : 0); + } + checkPartialProduct(pp, X, Y); + } + } + } } - } + }); } -void checkEvaluateRandom(PartialProductGenerator pp, int nSamples) { +void testPartialProductRandom(PartialProductGenerator pp, int iterations) { final widthX = pp.selector.multiplicand.width; final widthY = pp.encoder.multiplier.width; - final signed = - (pp.selectSigned == null) ? pp.signed : !pp.selectSigned!.value.isZero; - - for (var i = 0; i < nSamples; ++i) { - final rX = Random().nextLogicValue(width: widthX).toBigInt(); - final rY = Random().nextLogicValue(width: widthY).toBigInt(); - final X = signed ? rX.toSigned(widthX) : rX; - final Y = signed ? rY.toSigned(widthY) : rY; - pp.multiplicand.put(X); - pp.multiplier.put(Y); - final value = pp.evaluate(); - expect(value, equals(X * Y), reason: '$X * $Y = $value should be ${X * Y}'); - } + + final multiplicandSigns = pp.signedMultiplicand + ? [true] + : (pp.selectSignedMultiplicand != null) + ? [false, true] + : [false]; + final multiplierSigns = pp.signedMultiplier + ? [true] + : (pp.selectSignedMultiplier != null) + ? [false, true] + : [false]; + + test( + 'random: ${pp.name} R${pp.selector.radix} ' + 'WD=${pp.multiplicand.width} WM=${pp.multiplier.width} ' + 'SD=${pp.signedMultiplicand ? 1 : 0} ' + 'SM=${pp.signedMultiplier ? 1 : 0} ' + 'SelD=${pp.selectSignedMultiplicand != null ? 1 : 0} ' + 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () async { + final value = Random(47); + for (var i = 0; i < iterations; i++) { + for (final multiplicandSign in multiplicandSigns) { + final X = value + .nextLogicValue(width: widthX) + .toBigInt() + .toCondSigned(widthX, signed: multiplicandSign); + if (pp.selectSignedMultiplicand != null) { + pp.selectSignedMultiplicand!.put(multiplicandSign ? 1 : 0); + } + for (final multiplierSign in multiplierSigns) { + final Y = value + .nextLogicValue(width: widthY) + .toBigInt() + .toCondSigned(widthY, signed: multiplierSign); + if (pp.selectSignedMultiplier != null) { + pp.selectSignedMultiplier!.put(multiplierSign ? 1 : 0); + } + checkPartialProduct(pp, X, Y); + } + } + } + }); } -void main() { - test('single MAC partial product test', () async { - final encoder = RadixEncoder(16); - const widthX = 8; - const widthY = 18; +void testPartialProductSingle(PartialProductGenerator pp, BigInt X, BigInt Y) { + test( + 'single: ${pp.name} R${pp.selector.radix} ' + 'WD=${pp.multiplicand.width} WM=${pp.multiplier.width} ' + 'SD=${pp.signedMultiplicand ? 1 : 0} ' + 'SM=${pp.signedMultiplier ? 1 : 0} ' + 'SelD=${pp.selectSignedMultiplicand != null ? 1 : 0} ' + 'SelM=${pp.selectSignedMultiplier != null ? 1 : 0}', () async { + if (pp.selectSignedMultiplicand != null) { + pp.selectSignedMultiplicand!.put(X.isNegative ? 1 : 0); + } + if (pp.selectSignedMultiplier != null) { + pp.selectSignedMultiplier!.put(Y.isNegative ? 1 : 0); + } + checkPartialProduct(pp, X, Y); + }); +} - const i = 1478; - const j = 9; - const k = 0; +void checkPartialProduct(PartialProductGenerator pp, BigInt iX, BigInt iY) { + final widthX = pp.selector.multiplicand.width; + final widthY = pp.encoder.multiplier.width; - final X = BigInt.from(i).toSigned(widthX); - final Y = BigInt.from(j).toSigned(widthY); - final Z = BigInt.from(k).toSigned(widthX + widthY); - // print('X=$X Y=$Y, Z=$Z'); - final product = X * Y + Z; + final X = iX.toCondSigned(widthX, signed: pp.isSignedMultiplicand()); + final Y = iY.toCondSigned(widthY, signed: pp.isSignedMultiplier()); - final logicX = Logic(name: 'X', width: widthX); - final logicY = Logic(name: 'Y', width: widthY); - final logicZ = Logic(name: 'Z', width: widthX + widthY); - logicX.put(X); - logicY.put(Y); - logicZ.put(Z); - final pp = PartialProductGeneratorCompactRectSignExtension( - logicX, logicY, encoder, - signed: true); + final product = X * Y; - final lastLength = - pp.partialProducts[pp.rows - 1].length + pp.rowShift[pp.rows - 1]; + pp.multiplicand.put(X); + pp.multiplier.put(Y); + final value = pp.evaluate(); + expect(value, equals(product), + reason: 'Fail1: $X * $Y: $value ' + 'vs expected $product' + '\n$pp'); +} - final sign = logicZ[logicZ.width - 1]; - // for unsigned versus signed testing - // final sign = signed ? logicZ[logicZ.width - 1] : Const(0); - final l = [for (var i = 0; i < logicZ.width; i++) logicZ[i]]; - while (l.length < lastLength) { - l.add(sign); +void main() { + tearDown(() async { + await Simulator.reset(); + }); + + group('PartialProduct: fixed sign variants', () { + for (final signedMultiplicand in [false, true]) { + for (final signedMultiplier in [false, true]) { + for (final radix in [2, 4]) { + final width = log2Ceil(radix) + (signedMultiplier ? 1 : 0); + for (final signExtension + in SignExtension.values.where((e) => e != SignExtension.none)) { + final ppg = curryPartialProductGenerator(signExtension); + final pp = ppg(Logic(name: 'X', width: width), + Logic(name: 'Y', width: width), RadixEncoder(radix), + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier); + testPartialProductExhaustive(pp); + } + } + } } - l - ..add(~sign) - ..add(Const(1)); - // print(pp.representation()); + }); + group('PartialProduct: singleton fixed sign variants', () { + const radix = 16; + final encoder = RadixEncoder(radix); - pp.partialProducts.insert(0, l); - pp.rowShift.insert(0, 0); - // print(pp.representation()); + for (final signedMultiplicand in [false, true]) { + for (final signedMultiplier in [false, true]) { + final width = log2Ceil(radix) + (signedMultiplier ? 1 : 0); + final a = Logic(name: 'X', width: width); + final b = Logic(name: 'Y', width: width); - if (pp.evaluate() != product) { - stdout.write('Fail: $X * $Y: ${pp.evaluate()} vs expected $product\n'); + const i = 0; + const j = 0; + final X = + SignedBigInt.fromSignedInt(i, width, signed: signedMultiplicand); + final Y = + SignedBigInt.fromSignedInt(j, width, signed: signedMultiplier); + a.put(X); + b.put(Y); + final PartialProductGenerator pp; + pp = PartialProductGeneratorStopBitsSignExtension(a, b, encoder, + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier); + testPartialProductSingle(pp, X, Y); + } } - expect(pp.evaluate(), equals(product)); }); - test('majority function', () async { - expect(LogicValue.ofBigInt(BigInt.from(7), 5).majority(), true); - expect(LogicValue.ofBigInt(BigInt.from(7) << 1, 5).majority(), true); - expect(LogicValue.ofBigInt(BigInt.from(11) << 1, 5).majority(), true); - expect(LogicValue.ofBigInt(BigInt.from(9) << 1, 5).majority(), false); - expect(LogicValue.ofBigInt(BigInt.from(7) << 3, 7).majority(), false); - }); + group('PartialProduct: fixed/select sign variants', () { + final selectSignMultiplicand = Logic(); + final selectSignMultiplier = Logic(); + for (final radix in [2, 4]) { + final encoder = RadixEncoder(radix); + for (final selectMultiplicand in [false, true]) { + for (final signMultiplicand + in (!selectMultiplicand ? [false, true] : [false])) { + for (final selectMultiplier in [false, true]) { + for (final signMultiplier + in (!selectMultiplier ? [false, true] : [false])) { + selectSignMultiplicand.put(selectMultiplicand ? 1 : 0); + selectSignMultiplier.put(selectMultiplier ? 1 : 0); + for (final signExtension in SignExtension.values + .where((e) => e != SignExtension.none)) { + final width = log2Ceil(radix) + (signMultiplier ? 1 : 0); + final ppg = curryPartialProductGenerator(signExtension); + final PartialProductGenerator pp; + pp = ppg( + Logic(name: 'X', width: width), + Logic(name: 'Y', width: width), + encoder, + signedMultiplicand: signMultiplicand, + signedMultiplier: signMultiplier, + selectSignedMultiplicand: + selectMultiplicand ? selectSignMultiplicand : null, + selectSignedMultiplier: + selectMultiplier ? selectSignMultiplier : null); - // This is a two-minute exhaustive but quick test - test('exhaustive partial product evaluate: square radix-4, all extension', - () async { - for (final signed in [false, true]) { - for (var radix = 4; radix < 8; radix *= 2) { - const radix = 4; - final encoder = RadixEncoder(radix); - final shift = log2Ceil(encoder.radix); - final minWidth = shift + (signed ? 1 : 0); - for (var width = minWidth; width < shift * 2 + 1; width++) { - for (final signExtension in SignExtension.values) { - if (signExtension == SignExtension.none) { - continue; - } - final ppg = curryPartialProductGenerator(signExtension); - for (final useSelect in [false, true]) { - final PartialProductGenerator pp; - if (useSelect) { - final selectSigned = Logic(); - // ignore: cascade_invocations - selectSigned.put(signed ? 1 : 0); - pp = ppg(Logic(name: 'X', width: width), - Logic(name: 'Y', width: width), encoder, - selectSigned: selectSigned); - } else { - pp = ppg(Logic(name: 'X', width: width), - Logic(name: 'Y', width: width), encoder, - signed: signed); + testPartialProductExhaustive(pp); } - checkEvaluateExhaustive(pp); } } } @@ -151,36 +243,121 @@ void main() { } }); - test('Multiplier encoder rectangular test,', () async { - for (final signed in [false, true]) { - for (var radix = 2; radix < 8; radix *= 2) { - final encoder = RadixEncoder(radix); - final shift = log2Ceil(encoder.radix); - for (var width = 3 + shift + 1; width < 3 + shift * 2 + 1; width++) { - for (var skew = -3; skew < shift * 2; skew++) { - // Only some sign extension routines have rectangular support - // Commented out rectangular extension routines for speedup + group('PartialProduct: singleton fixed/select sign variants', () { + const radix = 4; + final encoder = RadixEncoder(radix); + const width = 4; + + final selectSignMultiplicand = Logic(); + final selectSignMultiplier = Logic(); + + for (final selectMultiplicand in [false, true]) { + for (final selectMultiplier in [false, true]) { + selectSignMultiplicand.put(selectMultiplicand ? 1 : 0); + selectSignMultiplier.put(selectMultiplier ? 1 : 0); + final PartialProductGenerator pp; + pp = PartialProductGeneratorStopBitsSignExtension( + Logic(name: 'X', width: width), + Logic(name: 'Y', width: width), + encoder, + signedMultiplicand: !selectMultiplicand, + signedMultiplier: !selectMultiplier, + selectSignedMultiplicand: + selectMultiplicand ? selectSignMultiplicand : null, + selectSignedMultiplier: + selectMultiplier ? selectSignMultiplier : null); + + const i = 6; + const j = -6; + final X = SignedBigInt.fromSignedInt(i, width, + signed: pp.isSignedMultiplicand()); + final Y = SignedBigInt.fromSignedInt(j, width, + signed: pp.isSignedMultiplier()); + + testPartialProductSingle(pp, X, Y); + } + } + }); + + group('PartialProduct: width/radix/extension sweep', () { + for (var radix = 2; radix < 16; radix *= 2) { + final encoder = RadixEncoder(radix); + final shift = log2Ceil(encoder.radix); + for (var width = shift; width < min(5, 2 * shift); width++) { + for (final signExtension + in SignExtension.values.where((e) => e != SignExtension.none)) { + final ppg = curryPartialProductGenerator(signExtension); + final pp = ppg(Logic(name: 'X', width: width), + Logic(name: 'Y', width: width), encoder); + testPartialProductExhaustive(pp); + } + } + } + }); + group('PartialProduct: rectangle/radix/extension sweep', () { + for (var radix = 2; radix < 8; radix *= 2) { + final encoder = RadixEncoder(radix); + final shift = log2Ceil(encoder.radix); + for (final signedMultiplicand in [false, true]) { + final widthX = shift + 2; + for (final signedMultiplier in [false, true]) { + for (var widthY = shift + (signedMultiplier ? 1 : 0); + widthY < shift + 3 + (signedMultiplier ? 1 : 0); + widthY++) { + for (final signExtension in [ + SignExtension.stopBits, + SignExtension.compactRect + ]) { + final ppg = curryPartialProductGenerator(signExtension); + final pp = ppg(Logic(name: 'X', width: widthX), + Logic(name: 'Y', width: widthY), encoder, + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier); + testPartialProductExhaustive(pp); + } + } + } + } + } + }); + + group('PartialProduct: minimum width', () { + for (var radix = 2; radix < 32; radix *= 2) { + final encoder = RadixEncoder(radix); + final shift = log2Ceil(encoder.radix); + for (var width = shift; width < min(5, 2 * shift); width++) { + for (final signExtension + in SignExtension.values.where((e) => e != SignExtension.none)) { + final ppg = curryPartialProductGenerator(signExtension); + final pp = ppg(Logic(name: 'X', width: width), + Logic(name: 'Y', width: width), encoder); + testPartialProductExhaustive(pp); + } + } + } + }); + + group('PartialProduct: minimum rectangle', () { + for (var radix = 2; radix < 32; radix *= 2) { + final encoder = RadixEncoder(radix); + final shift = log2Ceil(encoder.radix); + for (final signedMultiplicand in [false, true]) { + final widthX = shift; + for (final signedMultiplier in [false, true]) { + for (var widthY = shift + (signedMultiplier ? 1 : 0); + widthY < shift + 1 + (signedMultiplier ? 1 : 0); + widthY++) { for (final signExtension in [ SignExtension.brute, + SignExtension.stopBits, SignExtension.compactRect ]) { final ppg = curryPartialProductGenerator(signExtension); - for (final useSelect in [false, true]) { - final PartialProductGenerator pp; - if (useSelect) { - final selectSigned = Logic(); - // ignore: cascade_invocations - selectSigned.put(signed ? 1 : 0); - pp = ppg(Logic(name: 'X', width: width), - Logic(name: 'Y', width: width), encoder, - selectSigned: selectSigned); - } else { - pp = ppg(Logic(name: 'X', width: width), - Logic(name: 'Y', width: width), encoder, - signed: signed); - } - checkEvaluateRandom(pp, 10); - } + final pp = ppg(Logic(name: 'X', width: widthX), + Logic(name: 'Y', width: widthY), encoder, + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier); + testPartialProductExhaustive(pp); } } } @@ -188,7 +365,7 @@ void main() { } }); - test('Rectangle Q collision tests,', () async { + group('Rectangle Q collision tests New,', () { // These collide with the normal q extension bits // These are unsigned tests @@ -207,69 +384,99 @@ void main() { final width = align.$2; final skew = align.$3; - final X = BigInt.from(29).toUnsigned(width); - final Y = BigInt.from(2060).toUnsigned(width + skew); - final product = X * Y; final pp = PartialProductGeneratorCompactRectSignExtension( Logic(name: 'X', width: width), Logic(name: 'Y', width: width + skew), - encoder, - signed: false); + encoder); - pp.multiplicand.put(X); - pp.multiplier.put(Y); - expect(pp.evaluate(), equals(product)); - checkEvaluateRandom(pp, 100); + testPartialProductRandom(pp, 10); } } }); - test('minimum width verification,', () async { + test('PartialProduct: flat test', () { + const radix = 4; + final radixEncoder = RadixEncoder(radix); + const widthX = 6; + const widthY = 3; + final limitX = pow(2, widthX); + final limitY = pow(2, widthY); + final multiplicand = Logic(width: widthX); + final multiplier = Logic(width: widthY); for (final signed in [false, true]) { - for (var radix = 2; radix < 32; radix *= 2) { - final encoder = RadixEncoder(radix); - final shift = log2Ceil(encoder.radix); - final width = shift + (signed ? 1 : 0); - const skew = 0; - // Only some sign extension routines have rectangular support - // Commented out rectangular extension routines for speedup - for (final signExtension in SignExtension.values) { - if (signExtension == SignExtension.none) { - continue; - } - final ppg = curryPartialProductGenerator(signExtension); - final pp = ppg(Logic(name: 'X', width: width), - Logic(name: 'Y', width: width + skew), encoder, - signed: signed); - checkEvaluateRandom(pp, 100); + final pp = PartialProductGeneratorCompactSignExtension( + multiplicand, multiplier, radixEncoder, + signedMultiplicand: signed, signedMultiplier: signed); + for (var i = BigInt.zero; i < BigInt.from(limitX); i += BigInt.one) { + for (var j = BigInt.zero; j < BigInt.from(limitY); j += BigInt.one) { + final X = signed ? i.toSigned(widthX) : i.toUnsigned(widthX); + final Y = signed ? j.toSigned(widthY) : j.toUnsigned(widthY); + multiplicand.put(X); + multiplier.put(Y); + final value = pp.evaluate(); + expect(value, equals(X * Y), + reason: '$X * $Y = $value should be ${X * Y}'); } } } }); - test('minimum rectangular width verification,', () async { - for (final signed in [false, true]) { - for (var radix = 2; radix < 32; radix *= 2) { - final encoder = RadixEncoder(radix); - final shift = log2Ceil(encoder.radix); - final width = shift; - final skew = (signed ? 1 : 0); - // Only some sign extension routines have rectangular support - // Commented out rectangular extension routines for speedup - for (final signExtension in [ - SignExtension.brute, - SignExtension.stop, - SignExtension.compactRect - ]) { - { - final ppg = curryPartialProductGenerator(signExtension); - final pp = ppg(Logic(name: 'X', width: width), - Logic(name: 'Y', width: width + skew), encoder, - signed: signed); - checkEvaluateExhaustive(pp); - } - } - } + + test('single MAC partial product test', () async { + final encoder = RadixEncoder(16); + const widthX = 8; + const widthY = 18; + + const i = 1478; + const j = 9; + const k = 0; + + final X = BigInt.from(i).toSigned(widthX); + final Y = BigInt.from(j).toSigned(widthY); + final Z = BigInt.from(k).toSigned(widthX + widthY); + // print('X=$X Y=$Y, Z=$Z'); + final product = X * Y + Z; + + final logicX = Logic(name: 'X', width: widthX); + final logicY = Logic(name: 'Y', width: widthY); + final logicZ = Logic(name: 'Z', width: widthX + widthY); + logicX.put(X); + logicY.put(Y); + logicZ.put(Z); + final pp = PartialProductGeneratorCompactRectSignExtension( + logicX, logicY, encoder, + signedMultiplicand: true, signedMultiplier: true); + + final lastLength = + pp.partialProducts[pp.rows - 1].length + pp.rowShift[pp.rows - 1]; + + final sign = logicZ[logicZ.width - 1]; + // for unsigned versus signed testing + // final sign = signed ? logicZ[logicZ.width - 1] : Const(0); + final l = [for (var i = 0; i < logicZ.width; i++) logicZ[i]]; + while (l.length < lastLength) { + l.add(sign); } + l + ..add(~sign) + ..add(Const(1)); + // print(pp.representation()); + + pp.partialProducts.insert(0, l); + pp.rowShift.insert(0, 0); + // print(pp.representation()); + + if (pp.evaluate() != product) { + stdout.write('Fail: $X * $Y: ${pp.evaluate()} vs expected $product\n'); + } + expect(pp.evaluate(), equals(product)); + }); + + test('majority function', () async { + expect(LogicValue.ofBigInt(BigInt.from(7), 5).majority(), true); + expect(LogicValue.ofBigInt(BigInt.from(7) << 1, 5).majority(), true); + expect(LogicValue.ofBigInt(BigInt.from(11) << 1, 5).majority(), true); + expect(LogicValue.ofBigInt(BigInt.from(9) << 1, 5).majority(), false); + expect(LogicValue.ofBigInt(BigInt.from(7) << 3, 7).majority(), false); }); } diff --git a/test/arithmetic/multiplier_test.dart b/test/arithmetic/multiplier_test.dart index 479c3fda0..629804bfe 100644 --- a/test/arithmetic/multiplier_test.dart +++ b/test/arithmetic/multiplier_test.dart @@ -7,6 +7,8 @@ // 2024 August 7 // Author: Desmond Kirkpatrick +// ignore_for_file: invalid_use_of_protected_member + import 'dart:async'; import 'dart:math'; import 'package:rohd/rohd.dart'; @@ -15,46 +17,78 @@ import 'package:rohd_hcl/src/arithmetic/evaluate_compressor.dart'; import 'package:rohd_hcl/src/arithmetic/partial_product_sign_extend.dart'; import 'package:test/test.dart'; +/// The following routines are useful only during testing +extension TestMultiplierSignage on Multiplier { + /// Return true if multiplicand [a] is truly signed (fixed or runtime) + bool isSignedMultiplicand() => (selectSignedMultiplicand == null) + ? signedMultiplicand + : !selectSignedMultiplicand!.value.isZero; + + /// Return true if multiplier [b] is truly signed (fixed or runtime) + bool isSignedMultiplier() => (selectSignedMultiplier == null) + ? signedMultiplier + : !selectSignedMultiplier!.value.isZero; + + /// Return true if accumulate result is truly signed (fixed or runtime) + bool isSignedResult() => isSignedMultiplicand() | isSignedMultiplier(); +} + +/// The following routines are useful only during testing +extension TestMultiplierAccumulateSignage on MultiplyAccumulate { + /// Return true if multiplicand [a] is truly signed (fixed or runtime) + bool isSignedMultiplicand() => (selectSignedMultiplicand == null) + ? signedMultiplicand + : !selectSignedMultiplicand!.value.isZero; + + /// Return true if multiplier [b] is truly signed (fixed or runtime) + bool isSignedMultiplier() => (selectSignedMultiplier == null) + ? signedMultiplier + : !selectSignedMultiplier!.value.isZero; + + /// Return true if addend [c] is truly signed (fixed or runtime) + bool isSignedAddend() => (selectSignedAddend == null) + ? signedAddend + : !selectSignedAddend!.value.isZero; + + /// Return true if accumulate result is truly signed (fixed or runtime) + bool isSignedResult() => + isSignedAddend() | isSignedMultiplicand() | isSignedMultiplier(); +} + /// Simple multiplier to demonstrate instantiation of CompressionTreeMultiplier -class SimpleMultiplier extends Module { +class SimpleMultiplier extends Multiplier { /// The output of the simple multiplier - late final Logic product; + @override + Logic get product => output('product'); /// Construct a simple multiplier with runtime sign operation - SimpleMultiplier(Logic a, Logic b, Logic multASigned) - : super(name: 'my_test_module') { - a = addInput('a', a, width: a.width); - b = addInput('b', b, width: b.width); - multASigned = addInput('multASigned', multASigned); - product = addOutput('product', width: a.width + b.width); - - final mult = CompressionTreeMultiplier(a, b, 4, selectSigned: multASigned); + SimpleMultiplier(Logic a, Logic b, Logic? selSignedMultiplicand, + Logic? selSignedMultiplier) + : super(a, b) { + addOutput('product', width: a.width + b.width); + final mult = CompressionTreeMultiplier(a, b, 4, + selectSignedMultiplicand: selSignedMultiplicand, + selectSignedMultiplier: selSignedMultiplier); product <= mult.product; } } // Inner test of a multipy accumulate unit void checkMultiplyAccumulate( - MultiplyAccumulate mod, BigInt bA, BigInt bB, BigInt bC, - {bool signedTest = false}) { + MultiplyAccumulate mod, BigInt bA, BigInt bB, BigInt bC) { final golden = bA * bB + bC; - // ignore: invalid_use_of_protected_member mod.a.put(bA); - // ignore: invalid_use_of_protected_member mod.b.put(bB); - // ignore: invalid_use_of_protected_member mod.c.put(bC); - final result = signedTest - ? mod.accumulate.value.toBigInt().toSigned(mod.accumulate.width) - : mod.accumulate.value.toBigInt().toUnsigned(mod.accumulate.width); + final result = mod.accumulate.value + .toBigInt() + .toCondSigned(mod.accumulate.width, signed: mod.isSignedResult()); expect(result, equals(golden)); } -// Random testing of a mutiplier or multiplier/accumulate unit -void testMultiplyAccumulateRandom(int width, int iterations, - MultiplyAccumulate Function(Logic a, Logic b, Logic c) fn, - {bool signedTest = false}) { +void testMultiplyAccumulateSingle(int width, BigInt ibA, BigInt ibB, BigInt ibC, + MultiplyAccumulate Function(Logic a, Logic b, Logic c) fn) { final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); final c = Logic(name: 'c', width: width * 2); @@ -62,31 +96,56 @@ void testMultiplyAccumulateRandom(int width, int iterations, b.put(0); c.put(0); final mod = fn(a, b, c); - test('random_${mod.name}_S${mod.signed}_W${width}_I$iterations', () async { - final multiplyOnly = mod is MutiplyOnly; + test('single_W${width}_${mod.name}', () async { + final multiplyOnly = mod is MultiplyOnly; await mod.build(); + final bA = ibA.toCondSigned(width, signed: mod.isSignedMultiplicand()); + final bB = ibB.toCondSigned(width, signed: mod.isSignedMultiplier()); + final bC = multiplyOnly + ? BigInt.zero + : ibC.toCondSigned(width * 2, signed: mod.isSignedAddend()); + + checkMultiplyAccumulate(mod, bA, bB, bC); + }); +} + +void testMultiplyAccumulateRandom(int width, int iterations, + MultiplyAccumulate Function(Logic a, Logic b, Logic c) fn) { + final a = Logic(name: 'a', width: width); + final b = Logic(name: 'b', width: width); + final c = Logic(name: 'c', width: width * 2); + a.put(0); + b.put(0); + c.put(0); + final mod = fn(a, b, c); + test('random_W${width}_I${iterations}_${mod.name}', () { + final multiplyOnly = mod is MultiplyOnly; + final value = Random(47); for (var i = 0; i < iterations; i++) { - final bA = signedTest - ? value.nextLogicValue(width: width).toBigInt().toSigned(width) - : value.nextLogicValue(width: width).toBigInt().toUnsigned(width); - final bB = signedTest - ? value.nextLogicValue(width: width).toBigInt().toSigned(width) - : value.nextLogicValue(width: width).toBigInt().toUnsigned(width); + final bA = value + .nextLogicValue(width: width) + .toBigInt() + .toCondSigned(width, signed: mod.isSignedMultiplicand()); + final bB = value + .nextLogicValue(width: width) + .toBigInt() + .toCondSigned(width, signed: mod.isSignedMultiplier()); + final bC = multiplyOnly ? BigInt.zero - : signedTest - ? value.nextLogicValue(width: width).toBigInt().toSigned(width) - : value.nextLogicValue(width: width).toBigInt().toUnsigned(width); - checkMultiplyAccumulate(mod, bA, bB, bC, signedTest: signedTest); + : value + .nextLogicValue(width: width) + .toBigInt() + .toCondSigned(width, signed: mod.isSignedAddend()); + + checkMultiplyAccumulate(mod, bA, bB, bC); } }); } -// Exhaustive testing of a mutiplier or multiplier/accumulate unit void testMultiplyAccumulateExhaustive( - int width, MultiplyAccumulate Function(Logic a, Logic b, Logic c) fn, - {bool signedTest = false}) { + int width, MultiplyAccumulate Function(Logic a, Logic b, Logic c) fn) { final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); final c = Logic(name: 'c', width: 2 * width); @@ -94,27 +153,22 @@ void testMultiplyAccumulateExhaustive( b.put(0); c.put(0); final mod = fn(a, b, c); - test('exhaustive_${mod.name}_S${mod.signed}_W$width', () async { + test('exhaustive_W${width}_${mod.name}', () async { await mod.build(); - final multiplyOnly = mod is MutiplyOnly; - - final cLimit = multiplyOnly ? 1 : (1 << (2 * width)); + final multiplyOnly = mod is MultiplyOnly; for (var aa = 0; aa < (1 << width); ++aa) { for (var bb = 0; bb < (1 << width); ++bb) { - for (var cc = 0; cc < cLimit; ++cc) { - final bA = signedTest - ? BigInt.from(aa).toSigned(width) - : BigInt.from(aa).toUnsigned(width); - final bB = signedTest - ? BigInt.from(bb).toSigned(width) - : BigInt.from(bb).toUnsigned(width); + for (var cc = 0; cc < (multiplyOnly ? 1 : (1 << (2 * width))); ++cc) { + final bA = SignedBigInt.fromSignedInt(aa, width, + signed: mod.isSignedMultiplicand()); + final bB = SignedBigInt.fromSignedInt(bb, width, + signed: mod.isSignedMultiplier()); final bC = multiplyOnly ? BigInt.zero - : signedTest - ? BigInt.from(cc).toSigned(2 * width) - : BigInt.from(cc).toUnsigned(2 * width); - checkMultiplyAccumulate(mod, bA, bB, bC, signedTest: signedTest); + : SignedBigInt.fromSignedInt(bb, width * 2, + signed: mod.isSignedAddend()); + checkMultiplyAccumulate(mod, bA, bB, bC); } } } @@ -125,195 +179,160 @@ typedef MultiplyAccumulateCallback = MultiplyAccumulate Function( Logic a, Logic b, Logic c); typedef MultiplierCallback = Multiplier Function(Logic a, Logic b, - {Logic? selectSigned}); + {Logic? selectSignedMultiplicand, Logic? selectSignedMultiplier}); void main() { tearDown(() async { await Simulator.reset(); }); - MultiplierCallback curryCompressionTreeMultiplier( - int radix, - ParallelPrefix Function(List, Logic Function(Logic, Logic)) - ppTree, - {PartialProductGenerator Function(Logic, Logic, RadixEncoder, - {required bool signed, Logic? selectSigned}) - ppGen = PartialProductGeneratorCompactRectSignExtension.new, - bool signed = false, - Logic? selectSigned}) => - (a, b, {selectSigned}) => CompressionTreeMultiplier(a, b, radix, - selectSigned: selectSigned, - ppTree: ppTree, - ppGen: ppGen, - signed: signed, - name: 'Compression Tree Multiplier: ${ppTree.call([ - Logic() - ], (a, b, {selectSigned}) => Logic()).name}' - ' Sel=${selectSigned != null}R${radix}_E' - '${ppGen.call(a, b, RadixEncoder(radix), signed: signed).name}'); - - MultiplyAccumulateCallback curryMultiplierAsMultiplyAccumulate( - int radix, - ParallelPrefix Function(List, Logic Function(Logic, Logic)) - ppTree, - {PartialProductGenerator Function(Logic, Logic, RadixEncoder, - {required bool signed, Logic? selectSigned}) - ppGen = PartialProductGeneratorCompactRectSignExtension.new, - bool signed = false, - Logic? selectSign}) => - (a, b, c) => MutiplyOnly( + MultiplierCallback curryCompressionTreeMultiplier(int radix, + ParallelPrefix Function(List, Logic Function(Logic, Logic)) ppTree, + {PPGFunction ppGen = PartialProductGeneratorCompactRectSignExtension.new, + bool signedMultiplicand = false, + bool signedMultiplier = false, + Logic? selectSignedMultiplicand, + Logic? selectSignedMultiplier}) { + String genName(Logic a, Logic b) => ppGen( + a, + b, + RadixEncoder(radix), + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier, + selectSignedMultiplicand: + selectSignedMultiplicand != null ? Logic() : null, + selectSignedMultiplier: + selectSignedMultiplier != null ? Logic() : null, + ).name; + final signage = ' SD=${signedMultiplicand ? 1 : 0}' + ' SM=${signedMultiplier ? 1 : 0}' + ' SelD=${(selectSignedMultiplicand != null) ? 1 : 0}' + ' SelM=${(selectSignedMultiplier != null) ? 1 : 0}'; + return (a, b, {selectSignedMultiplicand, selectSignedMultiplier}) => + CompressionTreeMultiplier(a, b, radix, + ppTree: ppTree, + ppGen: ppGen, + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier, + name: 'Compression Tree Multiplier: ' + '${ppTree([Logic()], (a, b) => Logic()).name}' + '$signage R${radix}_E${genName(a, b)}'); + } + + MultiplyAccumulateCallback curryMultiplierAsMultiplyAccumulate(int radix, + {ParallelPrefix Function(List, Logic Function(Logic, Logic)) + ppTree = KoggeStone.new, + PPGFunction ppGen = + PartialProductGeneratorCompactRectSignExtension.new, + bool signedMultiplicand = false, + bool signedMultiplier = false, + Logic? selectSignedMultiplicand, + Logic? selectSignedMultiplier}) => + (a, b, c) => MultiplyOnly( a, b, c, - selectSigned: selectSign, - curryCompressionTreeMultiplier(radix, ppTree, - ppGen: ppGen, selectSigned: selectSign, signed: signed)); + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier, + curryCompressionTreeMultiplier( + radix, + ppTree, + ppGen: ppGen, + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier, + )); MultiplyAccumulateCallback curryMultiplyAccumulate( - int radix, - ParallelPrefix Function(List, Logic Function(Logic, Logic)) ppTree, { - PartialProductGenerator Function(Logic, Logic, RadixEncoder, - {required bool signed, Logic? selectSigned}) - ppGen = PartialProductGeneratorCompactRectSignExtension.new, - bool signed = false, - Logic? selectSign, - }) => - (a, b, c) => CompressionTreeMultiplyAccumulate(a, b, c, radix, - selectSigned: selectSign, - ppTree: ppTree, - ppGen: ppGen, - signed: signed, - name: 'Compression Tree MAC: ${ppTree.call([ - Logic() - ], (a, b) => Logic()).name}' - ' Sel=${selectSign != null}R${radix}_E' - '${ppGen.call(a, b, RadixEncoder(radix), signed: signed).name}'); - - group('Compression Tree Multiplier: curried random radix/width', () { - for (final signedTest in [false, true]) { - for (final signedOperands in [false, true]) { - final Logic? signedSelect; - if (signedOperands) { - signedSelect = Logic()..put(signedTest ? 1 : 0); - } else { - signedSelect = null; - } - for (final radix in [2, 16]) { - for (final width in [5, 6]) { - for (final ppTree in [KoggeStone.new]) { - testMultiplyAccumulateRandom( - width, - 10, - curryMultiplierAsMultiplyAccumulate(radix, ppTree, - ppGen: PartialProductGeneratorStopBitsSignExtension.new, - signed: !signedOperands && signedTest, - selectSign: signedSelect), - signedTest: signedTest); - } - } + int radix, { + ParallelPrefix Function(List, Logic Function(Logic, Logic)) ppTree = + KoggeStone.new, + PPGFunction ppGen = PartialProductGeneratorCompactRectSignExtension.new, + bool signedMultiplicand = false, + bool signedMultiplier = false, + bool signedAddend = false, + Logic? selectSignedMultiplicand, + Logic? selectSignedMultiplier, + Logic? selectSignedAddend, + }) { + String genName(Logic a, Logic b) => ppGen( + a, + b, + RadixEncoder(radix), + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier, + selectSignedMultiplicand: + selectSignedMultiplicand != null ? Logic() : null, + selectSignedMultiplier: + selectSignedMultiplier != null ? Logic() : null, + ).name; + final signage = ' SD=${signedMultiplicand ? 1 : 0}' + ' SM=${signedMultiplier ? 1 : 0}' + ' SelD=${(selectSignedMultiplicand != null) ? 1 : 0}' + ' SelM=${(selectSignedMultiplier != null) ? 1 : 0}'; + + return (a, b, c) => CompressionTreeMultiplyAccumulate(a, b, c, radix, + ppTree: ppTree, + ppGen: ppGen, + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier, + signedAddend: signedAddend, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier, + selectSignedAddend: selectSignedAddend, + name: 'Compression Tree MAC: ${ppTree.call([ + Logic() + ], (a, b) => Logic()).name}' + ' $signage R$radix E${genName(a, b)}'); + } + + group('Compression Tree Multiplier: curried random radix/ptree/width', () { + for (final radix in [2, 4]) { + for (final width in [3, 4]) { + for (final ppTree in [KoggeStone.new, BrentKung.new, Sklansky.new]) { + testMultiplyAccumulateRandom(width, 10, + curryMultiplierAsMultiplyAccumulate(radix, ppTree: ppTree)); } } } }); - - test('Compression Tree Multiplier: pipelined test', () async { - final clk = SimpleClockGenerator(10).clk; - final Logic? signedSelect; - signedSelect = Logic()..put(1); - const width = 5; - final a = Logic(name: 'a', width: width); - final b = Logic(name: 'b', width: width); - final bA = BigInt.from(-10).toSigned(width); - final bB = BigInt.from(-10).toSigned(width); - final mod = CompressionTreeMultiplier(a, b, 4, - clk: clk, selectSigned: signedSelect); - unawaited(Simulator.run()); - a.put(bA); - b.put(bB); - - await clk.nextNegedge; - final golden = bA * bB; - a.put(0); - b.put(0); - - final result = mod.product.value.toBigInt().toSigned(mod.product.width); - expect(result, equals(golden)); - await Simulator.endSimulation(); - }); - - group('Compression Tree Multiplier: curried exhaustive sign/select/extension', + group('Compression Tree Multiplier: curried random radix/extension/width', () { - for (final signedTest in [false, true]) { - for (final signedOperands in [false, true]) { - final Logic? signedSelect; - if (signedOperands) { - signedSelect = Logic()..put(signedTest ? 1 : 0); - } else { - signedSelect = null; - } - for (final radix in [4]) { - for (final ppTree in [KoggeStone.new]) { - for (final ppGen in [ - PartialProductGeneratorCompactSignExtension.new, - PartialProductGeneratorCompactRectSignExtension.new, - PartialProductGeneratorStopBitsSignExtension.new, - PartialProductGeneratorBruteSignExtension.new - ]) { - for (final width in [1 + log2Ceil(radix)]) { - testMultiplyAccumulateExhaustive( - width, - curryMultiplierAsMultiplyAccumulate(radix, ppTree, - ppGen: ppGen, - signed: !signedOperands && signedTest, - selectSign: signedSelect), - signedTest: signedTest); - } - } - } + for (final radix in [2, 4]) { + for (final width in [3, 4]) { + for (final signExtension + in SignExtension.values.where((e) => e != SignExtension.none)) { + final ppg = curryPartialProductGenerator(signExtension); + testMultiplyAccumulateRandom(width, 10, + curryMultiplierAsMultiplyAccumulate(radix, ppGen: ppg)); } } } }); - test('Trim Curried Test of Compression Tree Multiplier', () async { - final Logic? signedSelect; - signedSelect = Logic()..put(1); - const width = 5; - final a = Logic(name: 'a', width: width); - final b = Logic(name: 'b', width: width); - final c = Logic(name: 'c', width: width * 2 + 1); - a.put(0); - b.put(0); - c.put(0); - final bA = BigInt.from(-10).toSigned(width); - final bB = BigInt.from(-10).toSigned(width); - a.put(bA); - b.put(bB); - final mod = curryMultiplierAsMultiplyAccumulate(4, KoggeStone.new, - ppGen: PartialProductGeneratorCompactSignExtension.new, - selectSign: signedSelect)(a, b, c); - checkMultiplyAccumulate(mod, bA, bB, BigInt.zero, signedTest: true); - }); - - group('Compression Tree Multiplier Accumulate: random radix/width', () { - for (final signedTest in [false, true]) { - for (final signedOperands in [false, true]) { - final Logic? signedSelect; - if (signedOperands) { - signedSelect = Logic()..put(signedTest ? 1 : 0); - } else { - signedSelect = null; - } - for (final radix in [4, 16]) { - for (final width in [5, 6]) { - for (final ppTree in [KoggeStone.new]) { - testMultiplyAccumulateRandom( - width, - 10, - curryMultiplyAccumulate(radix, ppTree, - signed: !signedOperands && signedTest, - selectSign: signedSelect), - signedTest: signedTest); + group('Compression Tree Multiplier: curried random sign/select', () { + for (final selectSignedMultiplicand in [null, Const(0), Const(1)]) { + for (final signedMultiplicand + in (selectSignedMultiplicand == null) ? [false, true] : [false]) { + for (final selectSignedMultiplier in [null, Const(0), Const(1)]) { + for (final signedMultiplier + in (selectSignedMultiplier == null) ? [false, true] : [false]) { + for (final radix in [4]) { + for (final width in [1 + log2Ceil(radix)]) { + testMultiplyAccumulateRandom( + width, + 10, + curryMultiplierAsMultiplyAccumulate(radix, + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier)); + } } } } @@ -321,33 +340,30 @@ void main() { } }); - group( - 'Compression Tree Multiplier Accumulate: ' - 'curried exhaustive sign/select/extension', () { - for (final signedTest in [false, true]) { - for (final signedOperands in [false, true]) { - final Logic? signedSelect; - if (signedOperands) { - signedSelect = Logic()..put(signedTest ? 1 : 0); - } else { - signedSelect = null; - } - for (final radix in [4]) { - for (final ppTree in [KoggeStone.new]) { - for (final ppGen in [ - PartialProductGeneratorCompactSignExtension.new, - PartialProductGeneratorCompactRectSignExtension.new, - PartialProductGeneratorStopBitsSignExtension.new, - PartialProductGeneratorBruteSignExtension.new - ]) { - for (final width in [1 + log2Ceil(radix)]) { - testMultiplyAccumulateExhaustive( - width, - curryMultiplyAccumulate(radix, ppTree, - ppGen: ppGen, - signed: !signedOperands && signedTest, - selectSign: signedSelect), - signedTest: signedTest); + group('Compression Tree MAC: curried random sign/select', () { + for (final selectSignedMultiplicand in [null, Const(0), Const(1)]) { + for (final signedMultiplicand + in (selectSignedMultiplicand == null) ? [false, true] : [false]) { + for (final selectSignedMultiplier in [null, Const(0), Const(1)]) { + for (final signedMultiplier + in (selectSignedMultiplier == null) ? [false, true] : [false]) { + for (final selectSignedAddend in [null, Const(0), Const(1)]) { + for (final signedAddend + in (selectSignedAddend == null) ? [false, true] : [false]) { + for (final radix in [4]) { + for (final width in [1 + log2Ceil(radix)]) { + testMultiplyAccumulateRandom( + width, + 10, + curryMultiplyAccumulate(radix, + signedMultiplicand: signedMultiplicand, + signedMultiplier: signedMultiplier, + signedAddend: signedAddend, + selectSignedMultiplicand: selectSignedMultiplicand, + selectSignedMultiplier: selectSignedMultiplier, + selectSignedAddend: selectSignedAddend)); + } + } } } } @@ -356,25 +372,32 @@ void main() { } }); - test('Trim Curried Test of Compression Tree MAC', () async { + test('Compression Tree Multiplier: pipelined test', () async { + final clk = SimpleClockGenerator(10).clk; final Logic? signedSelect; signedSelect = Logic()..put(1); const width = 5; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); - final c = Logic(name: 'c', width: width * 2 + 1); final bA = BigInt.from(-10).toSigned(width); final bB = BigInt.from(-10).toSigned(width); - final bC = BigInt.from(0).toSigned(width); + final mod = CompressionTreeMultiplier(a, b, 4, + clk: clk, + selectSignedMultiplicand: signedSelect, + selectSignedMultiplier: signedSelect); + unawaited(Simulator.run()); a.put(bA); b.put(bB); - c.put(bC); - final mod = curryMultiplyAccumulate(4, KoggeStone.new, - ppGen: PartialProductGeneratorCompactSignExtension.new, - selectSign: signedSelect)(a, b, c); - checkMultiplyAccumulate(mod, bA, bB, bC, signedTest: true); - }); + await clk.nextNegedge; + final golden = bA * bB; + a.put(0); + b.put(0); + + final result = mod.product.value.toBigInt().toSigned(mod.product.width); + expect(result, equals(golden)); + await Simulator.endSimulation(); + }); test('Compression Tree MAC: pipelined test', () async { final clk = SimpleClockGenerator(10).clk; final Logic? signedSelect; @@ -382,13 +405,19 @@ void main() { const width = 5; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); - final c = Logic(name: 'c', width: width * 2 + 1); - final bA = BigInt.from(-10).toSigned(width); - final bB = BigInt.from(-10).toSigned(width); - final bC = BigInt.from(100).toSigned(width); + final c = Logic(name: 'c', width: width * 2); + final bA = BigInt.from(0).toSigned(width); + final bB = BigInt.from(0).toSigned(width); + final bC = BigInt.from(-512).toSigned(width * 2); + a.put(0); + b.put(0); + c.put(0); final mod = CompressionTreeMultiplyAccumulate(a, b, c, 4, - clk: clk, selectSigned: signedSelect); + clk: clk, + selectSignedMultiplicand: signedSelect, + selectSignedMultiplier: signedSelect, + selectSignedAddend: signedSelect); unawaited(Simulator.run()); a.put(bA); b.put(bB); @@ -414,12 +443,8 @@ void main() { const bv = 13; for (final signed in [true, false]) { for (final useSignedLogic in [true, false]) { - final bA = signed - ? BigInt.from(av).toSigned(width) - : BigInt.from(av).toUnsigned(width); - final bB = signed - ? BigInt.from(bv).toSigned(width) - : BigInt.from(bv).toUnsigned(width); + final bA = SignedBigInt.fromSignedInt(av, width, signed: signed); + final bB = SignedBigInt.fromSignedInt(bv, width, signed: signed); final Logic? signedSelect; @@ -434,11 +459,13 @@ void main() { b.put(bB); final mod = CompressionTreeMultiplier(a, b, 4, - signed: !useSignedLogic && signed, selectSigned: signedSelect); + signedMultiplier: !useSignedLogic && signed, + selectSignedMultiplicand: signedSelect, + selectSignedMultiplier: signedSelect); await mod.build(); mod.generateSynth(); final golden = bA * bB; - final result = mod.signed + final result = mod.isSignedResult() ? mod.product.value.toBigInt().toSigned(mod.product.width) : mod.product.value.toBigInt().toUnsigned(mod.product.width); expect(result, equals(golden)); @@ -447,8 +474,8 @@ void main() { }); test('trivial instantiated multiplier', () async { + // Using this to find trace errors when instantiating multipliers const dataWidth = 5; - final av = BigInt.from(-16).toSigned(dataWidth); final bv = BigInt.from(-6).toSigned(dataWidth); @@ -461,30 +488,31 @@ void main() { multA.put(av); multB.put(bv); - final mod = curryMultiplierAsMultiplyAccumulate(4, KoggeStone.new, - selectSign: signedOperands)(multA, multB, Const(0)); - - checkMultiplyAccumulate(mod, av, bv, BigInt.zero, signedTest: true); + final mod = MultiplyOnly( + multA, + multB, + Logic(), + (a, b, {selectSignedMultiplicand, selectSignedMultiplier}) => + SimpleMultiplier( + a, b, selectSignedMultiplicand, selectSignedMultiplier), + selectSignedMultiplicand: signedOperands, + selectSignedMultiplier: signedOperands); + + checkMultiplyAccumulate(mod, av, bv, BigInt.zero); }); test('single mac', () async { const width = 8; final a = Logic(name: 'a', width: width); final b = Logic(name: 'b', width: width); - final c = Logic(name: 'c', width: 2 * width + 1); - const av = 0; - const bv = 0; - const cv = -512; - for (final signed in [true, false]) { - final bA = signed - ? BigInt.from(av).toSigned(width) - : BigInt.from(av).toUnsigned(width); - final bB = signed - ? BigInt.from(bv).toSigned(width) - : BigInt.from(bv).toUnsigned(width); - final bC = signed - ? BigInt.from(cv).toSigned(2 * width + 1) - : BigInt.from(cv).toUnsigned(width * 2 + 1); + final c = Logic(name: 'c', width: 2 * width); + const av = 10; + const bv = 6; + const cv = 0; + for (final signed in [false, true]) { + final bA = SignedBigInt.fromSignedInt(av, width, signed: signed); + final bB = SignedBigInt.fromSignedInt(bv, width, signed: signed); + final bC = SignedBigInt.fromSignedInt(cv, width * 2, signed: signed); final signedOperands = Logic(name: 'signedOperands'); // ignore: cascade_invocations @@ -496,39 +524,39 @@ void main() { c.put(bC); final mod = CompressionTreeMultiplyAccumulate(a, b, c, 4, - selectSigned: signedOperands); - checkMultiplyAccumulate(mod, bA, bB, bC, signedTest: signed); + selectSignedMultiplicand: signedOperands, + selectSignedMultiplier: signedOperands, + selectSignedAddend: signedOperands); + checkMultiplyAccumulate(mod, bA, bB, bC); } }); test('single rectangular mac', () async { - const widthA = 6; - const widthB = 9; + const widthA = 8; + const widthB = 8; + const widthC = widthA + widthB; final a = Logic(name: 'a', width: widthA); final b = Logic(name: 'b', width: widthB); - final c = Logic(name: 'c', width: widthA + widthB + 1); + final c = Logic(name: 'c', width: widthC); - const av = 0; + const av = 10; const bv = 0; - const cv = -512; + const cv = 0; for (final signed in [true, false]) { - final bA = signed - ? BigInt.from(av).toSigned(widthA) - : BigInt.from(av).toUnsigned(widthA); - final bB = signed - ? BigInt.from(bv).toSigned(widthB) - : BigInt.from(bv).toUnsigned(widthB); - final bC = signed - ? BigInt.from(cv).toSigned(widthA + widthB) - : BigInt.from(cv).toUnsigned(widthA + widthB); + final bA = SignedBigInt.fromSignedInt(av, widthA, signed: signed); + final bB = SignedBigInt.fromSignedInt(bv, widthB, signed: signed); + final bC = SignedBigInt.fromSignedInt(cv, widthC, signed: signed); // Set these so that printing inside module build will have Logic values a.put(bA); b.put(bB); c.put(bC); - final mod = CompressionTreeMultiplyAccumulate(a, b, c, 4, signed: signed); - checkMultiplyAccumulate(mod, bA, bB, bC, signedTest: signed); + final mod = CompressionTreeMultiplyAccumulate(a, b, c, 4, + signedMultiplicand: signed, + signedMultiplier: signed, + signedAddend: signed); + checkMultiplyAccumulate(mod, bA, bB, bC); } }); test('trivial compression tree multiply-accumulate test', () async { @@ -537,18 +565,48 @@ void main() { const radix = 8; final a = Logic(name: 'a', width: widthA); final b = Logic(name: 'b', width: widthB); - final c = Logic(name: 'c', width: widthA + widthB + 1); + final c = Logic(name: 'c', width: widthA + widthB); a.put(15); b.put(3); c.put(5); - final multiplier = - CompressionTreeMultiplyAccumulate(a, b, c, radix, signed: true); + final multiplier = CompressionTreeMultiplyAccumulate(a, b, c, radix, + signedMultiplier: true); final accumulate = multiplier.accumulate; expect(accumulate.value.toBigInt(), equals(BigInt.from(15 * 3 + 5))); }); + test('trivial compression MAC signed', () async { + const widthA = 6; + const widthB = 6; + const widthC = widthA + widthB; + const radix = 4; + final a = Logic(name: 'a', width: widthA); + final b = Logic(name: 'b', width: widthB); + final c = Logic(name: 'c', width: widthC); + + const av = 10; + const bv = 6; + const cv = 0; + for (final signed in [false, true]) { + final bA = SignedBigInt.fromSignedInt(av, widthA, signed: signed); + final bB = SignedBigInt.fromSignedInt(bv, widthB, signed: signed); + final bC = SignedBigInt.fromSignedInt(cv, widthC, signed: signed); + + final golden = bA * bB + bC; + + a.put(bA); + b.put(bB); + c.put(bC); + + final multiplier = CompressionTreeMultiplyAccumulate(a, b, c, radix, + signedMultiplicand: signed, signedMultiplier: signed); + final accumulate = multiplier.accumulate; + expect(accumulate.value.toBigInt(), equals(golden)); + } + }); + test('setting PPG', () async { const width = 8; final a = Logic(name: 'a', width: width); @@ -561,7 +619,7 @@ void main() { final ppG0 = PartialProductGeneratorCompactRectSignExtension( a, b, RadixEncoder(4), - signed: true); + signedMultiplicand: true, signedMultiplier: true); final bit_0_5 = ppG0.getAbsolute(0, 5); expect(bit_0_5.value, equals(LogicValue.one)); diff --git a/test/configurator_test.dart b/test/configurator_test.dart index 2bcdd7206..f0dda0990 100644 --- a/test/configurator_test.dart +++ b/test/configurator_test.dart @@ -136,8 +136,6 @@ void main() { test('should return both Int knobs to be configured', () { final multiplier = CarrySaveMultiplierConfigurator(); expect(multiplier.knobs.values.whereType().length, 1); - expect(multiplier.knobs.values.whereType>().length, - 1); }); test('should return rtl code when invoke generate() with default value', @@ -160,6 +158,42 @@ void main() { }); }); + group('compression_tree_multiplier', () { + test('should return Compression Tree Multiplier for component name', () { + final multiplier = CompressionTreeMultiplierConfigurator(); + expect(multiplier.name, 'Comp. Tree Multiplier'); + }); + + test('should return both Int knobs to be configured', () { + final multiplier = CompressionTreeMultiplierConfigurator(); + expect(multiplier.knobs.values.whereType().length, 2); + expect( + multiplier.knobs.values.whereType>().length, + 4); + expect(multiplier.knobs.values.whereType().length, 1); + }); + + test('should return rtl code when invoke generate() with default value', + () async { + final multiplier = CompressionTreeMultiplierConfigurator(); + expect( + await multiplier.generateSV(), contains('CompressionTreeMultiplier')); + }); + + test('should return rtl code when invoke generate() with custom value', + () async { + final multiplierDefault = CompressionTreeMultiplierConfigurator(); + final multiplierCustom = CompressionTreeMultiplierConfigurator(); + multiplierCustom.knobs.values.toList()[1].value = 4; + + final multiplierDefaultRTL = await multiplierDefault.generateSV(); + final multiplierCustomRTL = await multiplierCustom.generateSV(); + + expect(multiplierDefaultRTL.length > multiplierCustomRTL.length, + equals(false)); + }); + }); + group('fifo configurator', () { test('should generate FIFO', () async { final cfg = FifoConfigurator()