Repository: intel/rohd (dart)

Issue:
Add reset signal to `FlipFlop`
## Motivation
It would be nice to use `FlipFlop` with a reset signal (optionally synchronous or asynchronous)

## Desired solution
Add optional named parameter(s) to `FlipFlop` that, when specified, add reset behavior.

## Alternatives considered  
People can already achieve this using the `Sequential` block, but `FlipFlop` is convenient.

Pull request:
<!-- Please make sure you check out the contribution guidelines before submitting a pull request! -->

## Description & Motivation

This PR has a couple things in one:
- "try" ports, where it will return `null` if a port doesn't exist (on a `Module` or an `Interface`)
- Reset (and reset value) on `flop` and `FlipFlop`.

## Related Issue(s)

Fix #69
Fix #405

## Testing

Added tests for new functionality

## Backwards-compatibility

> Is this a breaking change that will not be backwards-compatible? If yes, how so?

No

## Documentation

> Does the change require any updates to documentation? If so, where? Are they included?

Updated API docs

Patch:
diff --git a/lib/src/interfaces/interface.dart b/lib/src/interfaces/interface.dart
index 41f8e0218..a5d0b8557 100644
--- a/lib/src/interfaces/interface.dart
+++ b/lib/src/interfaces/interface.dart
@@ -48,6 +48,9 @@ class Interface<TagType> {
       ? _ports[name]!
       : throw Exception('Port name "$name" not found on this interface.');
 
+  /// Provides the [port] named [name] if it exists, otherwise `null`.
+  Logic? tryPort(String name) => _ports[name];
+
   /// Connects [module]'s inputs and outputs up to [srcInterface] and this
   /// [Interface].
   ///
diff --git a/lib/src/module.dart b/lib/src/module.dart
index 86caa90b5..8b6388db3 100644
--- a/lib/src/module.dart
+++ b/lib/src/module.dart
@@ -91,13 +91,19 @@ abstract class Module {
   /// Accesses the [Logic] associated with this [Module]s input port
   /// named [name].
   ///
-  /// Logic within this [Module] should consume this signal.
+  /// Only logic within this [Module] should consume this signal.
   @protected
   Logic input(String name) => _inputs.containsKey(name)
       ? _inputs[name]!
       : throw Exception(
           'Input name "$name" not found as an input to this Module.');
 
+  /// Provides the [input] named [name] if it exists, otherwise `null`.
+  ///
+  /// Only logic within this [Module] should consume this signal.
+  @protected
+  Logic? tryInput(String name) => _inputs[name];
+
   /// Accesses the [Logic] associated with this [Module]s output port
   /// named [name].
   ///
@@ -108,6 +114,9 @@ abstract class Module {
       : throw Exception(
           'Output name "$name" not found as an output of this Module.');
 
+  /// Provides the [output] named [name] if it exists, otherwise `null`.
+  Logic? tryOutput(String name) => _outputs[name];
+
   /// Returns true iff [net] is the same [Logic] as the input port of this
   /// [Module] with the same name.
   bool isInput(Logic net) =>
diff --git a/lib/src/modules/conditional.dart b/lib/src/modules/conditional.dart
index 6860c16aa..db3b8f064 100644
--- a/lib/src/modules/conditional.dart
+++ b/lib/src/modules/conditional.dart
@@ -2,7 +2,8 @@
 // SPDX-License-Identifier: BSD-3-Clause
 //
 // conditional.dart
-// Definitions of conditionallly executed hardware constructs (if/else statements, always_comb, always_ff, etc.)
+// Definitions of conditionallly executed hardware constructs
+// (if/else statements, always_comb, always_ff, etc.)
 //
 // 2021 May 7
 // Author: Max Korbel <max.korbel@intel.com>
@@ -1548,25 +1549,49 @@ ${padding}end ''');
 
 /// Constructs a positive edge triggered flip flop on [clk].
 ///
-/// It returns [FlipFlop.q]. When optional [en] is provided, an additional
-/// input will be created for flop. If optional [en] is high or not provided,
-/// output will vary as per input[d]. For low [en], output remains frozen
-/// irrespective of input [d]
-Logic flop(Logic clk, Logic d, {Logic? en}) => FlipFlop(clk, d, en: en).q;
+/// It returns [FlipFlop.q].
+///
+/// When the optional [en] is provided, an additional input will be created for
+/// flop. If optional [en] is high or not provided, output will vary as per
+/// input[d]. For low [en], output remains frozen irrespective of input [d].
+///
+/// When the optional [reset] is provided, the flop will be reset (active-high).
+/// If no [resetValue] is provided, the reset value is always `0`. Otherwise,
+/// it will reset to the provided [resetValue].
+Logic flop(
+  Logic clk,
+  Logic d, {
+  Logic? en,
+  Logic? reset,
+  dynamic resetValue,
+}) =>
+    FlipFlop(
+      clk,
+      d,
+      en: en,
+      reset: reset,
+      resetValue: resetValue,
+    ).q;
 
 /// Represents a single flip-flop with no reset.
 class FlipFlop extends Module with CustomSystemVerilog {
   /// Name for the enable input of this flop
-  late final String _enName;
+  final String _enName = Module.unpreferredName('en');
 
   /// Name for the clk of this flop.
-  late final String _clkName;
+  final String _clkName = Module.unpreferredName('clk');
 
   /// Name for the input of this flop.
-  late final String _dName;
+  final String _dName = Module.unpreferredName('d');
 
   /// Name for the output of this flop.
-  late final String _qName;
+  final String _qName = Module.unpreferredName('q');
+
+  /// Name for the reset of this flop.
+  final String _resetName = Module.unpreferredName('reset');
+
+  /// Name for the reset value of this flop.
+  final String _resetValueName = Module.unpreferredName('resetValue');
 
   /// The clock, posedge triggered.
   late final Logic _clk = input(_clkName);
@@ -1576,7 +1601,10 @@ class FlipFlop extends Module with CustomSystemVerilog {
   /// If enable is  high or enable is not provided then flop output will vary
   /// on the basis of clock [_clk] and input [_d]. If enable is low, then
   /// output of the flop remains frozen irrespective of the input [_d].
-  late final Logic _en = input(_enName);
+  late final Logic? _en = tryInput(_enName);
+
+  /// Optional reset input to the flop.
+  late final Logic? _reset = tryInput(_resetName);
 
   /// The input to the flop.
   late final Logic _d = input(_dName);
@@ -1584,77 +1612,114 @@ class FlipFlop extends Module with CustomSystemVerilog {
   /// The output of the flop.
   late final Logic q = output(_qName);
 
-  /// To track if optional enable is provided or not.
-  late final bool _isEnableProvided;
+  /// The reset value for this flop, if it was a port.
+  Logic? _resetValuePort;
+
+  /// The reset value for this flop, if it was a constant.
+  ///
+  /// Only initialized if a constant value is provided.
+  late LogicValue _resetValueConst;
 
   /// Constructs a flip flop which is positive edge triggered on [clk].
   ///
   /// When optional [en] is provided, an additional input will be created for
   /// flop. If optional [en] is high or not provided, output will vary as per
   /// input[d]. For low [en], output remains frozen irrespective of input [d]
-  FlipFlop(Logic clk, Logic d, {Logic? en, super.name = 'flipflop'}) {
+  ///
+  /// When the optional [reset] is provided, the flop will be reset active-high.
+  /// If no [resetValue] is provided, the reset value is always `0`. Otherwise,
+  /// it will reset to the provided [resetValue]. The type of [resetValue] must
+  /// be a valid driver of a [ConditionalAssign] (e.g. [Logic], [LogicValue],
+  /// [int], etc.).
+  FlipFlop(
+    Logic clk,
+    Logic d, {
+    Logic? en,
+    Logic? reset,
+    dynamic resetValue,
+    super.name = 'flipflop',
+  }) {
     if (clk.width != 1) {
       throw Exception('clk must be 1 bit');
     }
 
-    _clkName = Module.unpreferredName('clk');
-    _dName = Module.unpreferredName('d');
-    _qName = Module.unpreferredName('q');
-
     addInput(_clkName, clk);
     addInput(_dName, d, width: d.width);
     addOutput(_qName, width: d.width);
 
     if (en != null) {
-      if (en.width != 1) {
-        throw PortWidthMismatchException(en, 1);
-      }
-      _enName = Module.unpreferredName('en');
       addInput(_enName, en);
-      _isEnableProvided = true;
+    }
 
-      _setupWithEnable();
-    } else {
-      _isEnableProvided = false;
+    if (reset != null) {
+      addInput(_resetName, reset);
 
-      _setup();
+      if (resetValue != null && resetValue is Logic) {
+        _resetValuePort = addInput(_resetValueName, resetValue, width: d.width);
+      } else {
+        _resetValueConst = LogicValue.of(resetValue ?? 0, width: d.width);
+      }
     }
+
+    _setup();
   }
 
   /// Performs setup for custom functional behavior.
   void _setup() {
-    Sequential(_clk, [q < _d]);
-  }
+    var contents = [q < _d];
+
+    if (_en != null) {
+      contents = [If(_en!, then: contents)];
+    }
 
-  /// Performs setup for custom functional behavior with enable
-  void _setupWithEnable() {
-    Sequential(_clk, [
-      If(_en, then: [q < _d])
-    ]);
+    Sequential(
+      _clk,
+      contents,
+      reset: _reset,
+      resetValues:
+          _reset != null ? {q: _resetValuePort ?? _resetValueConst} : null,
+    );
   }
 
   @override
   String instantiationVerilog(String instanceType, String instanceName,
       Map<String, String> inputs, Map<String, String> outputs) {
-    if (_isEnableProvided) {
-      if (inputs.length != 3 || outputs.length != 1) {
-        throw Exception('FlipFlop has exactly three inputs and one output.');
-      }
-    } else {
-      if (inputs.length != 2 || outputs.length != 1) {
-        throw Exception('FlipFlop has exactly two inputs and one output.');
-      }
+    var expectedInputs = 2;
+    if (_en != null) {
+      expectedInputs++;
+    }
+    if (_reset != null) {
+      expectedInputs++;
+    }
+    if (_resetValuePort != null) {
+      expectedInputs++;
+    }
+
+    if (inputs.length != expectedInputs || outputs.length != 1) {
+      throw Exception(
+          'FlipFlop has exactly $expectedInputs inputs and one output.');
     }
 
     final clk = inputs[_clkName]!;
     final d = inputs[_dName]!;
     final q = outputs[_qName]!;
 
-    if (_isEnableProvided) {
-      final en = inputs[_enName]!;
-      return 'always_ff @(posedge $clk) if($en) $q <= $d;  // $instanceName';
-    } else {
-      return 'always_ff @(posedge $clk) $q <= $d;  // $instanceName';
+    final svBuffer = StringBuffer('always_ff @(posedge $clk) ');
+
+    if (_reset != null) {
+      final resetValueString = _resetValuePort != null
+          ? inputs[_resetValueName]!
+          : _resetValueConst.toString();
+      svBuffer
+          .write('if(${inputs[_resetName]}) $q <= $resetValueString; else ');
     }
+
+    if (_en != null) {
+      svBuffer.write('if(${inputs[_enName]!}) ');
+    }
+
+    svBuffer.write('$q <= $d;  // $instanceName');
+
+    return svBuffer.toString();
   }
 }
diff --git a/lib/src/values/logic_value.dart b/lib/src/values/logic_value.dart
index fa7462204..5c17c7eb7 100644
--- a/lib/src/values/logic_value.dart
+++ b/lib/src/values/logic_value.dart
@@ -256,6 +256,8 @@ abstract class LogicValue implements Comparable<LogicValue> {
           return LogicValue.ofIterable(val).zeroExtend(width);
         }
       }
+    } else if (val == null) {
+      throw LogicValueConstructionException('Cannot construct from `null`.');
     } else {
       throw LogicValueConstructionException('Unrecognized value type "$val" - '
           'Unknown type ${val.runtimeType}'


Test patch:
diff --git a/test/flop_test.dart b/test/flop_test.dart
index 97e020e7e..4e3def505 100644
--- a/test/flop_test.dart
+++ b/test/flop_test.dart
@@ -12,24 +12,26 @@ import 'package:rohd/src/utilities/simcompare.dart';
 import 'package:test/test.dart';
 
 class FlopTestModule extends Module {
-  Logic get a => input('a');
-  Logic get y => output('y');
-  Logic get en => input('en');
-
-  FlopTestModule(Logic a, {Logic? en}) : super(name: 'floptestmodule') {
+  FlopTestModule(Logic a, {Logic? en, Logic? reset, dynamic resetValue})
+      : super(name: 'floptestmodule') {
     a = addInput('a', a, width: a.width);
+
     if (en != null) {
-      en = addInput('en', en, width: en.width);
+      en = addInput('en', en);
+    }
+
+    if (reset != null) {
+      reset = addInput('reset', reset);
+    }
+
+    if (resetValue != null && resetValue is Logic) {
+      resetValue = addInput('resetValue', resetValue, width: a.width);
     }
 
     final y = addOutput('y', width: a.width);
     final clk = SimpleClockGenerator(10).clk;
 
-    if (en != null) {
-      y <= flop(clk, a, en: en);
-    } else {
-      y <= flop(clk, a);
-    }
+    y <= flop(clk, a, en: en, reset: reset, resetValue: resetValue);
   }
 }
 
@@ -50,9 +52,7 @@ void main() {
         Vector({'a': 0}, {'y': 0}),
       ];
       await SimCompare.checkFunctionalVector(ftm, vectors);
-      final simResult = SimCompare.iverilogVector(ftm, vectors);
-      expect(simResult, equals(true));
-      // expect(true, true);
+      SimCompare.checkIverilogVector(ftm, vectors);
     });
 
     test('flop bit with enable', () async {
@@ -73,9 +73,7 @@ void main() {
         Vector({'a': 1, 'en': 0}, {'y': 1}),
       ];
       await SimCompare.checkFunctionalVector(ftm, vectors);
-      final simResult = SimCompare.iverilogVector(ftm, vectors);
-      expect(simResult, equals(true));
-      // expect(true, true);
+      SimCompare.checkIverilogVector(ftm, vectors);
     });
 
     test('flop bus', () async {
@@ -89,8 +87,7 @@ void main() {
         Vector({'a': 0x1}, {'y': 0x55}),
       ];
       await SimCompare.checkFunctionalVector(ftm, vectors);
-      final simResult = SimCompare.iverilogVector(ftm, vectors);
-      expect(simResult, equals(true));
+      SimCompare.checkIverilogVector(ftm, vectors);
     });
 
     test('flop bus with enable', () async {
@@ -113,8 +110,90 @@ void main() {
         Vector({'a': 0x1, 'en': 1}, {'y': 0x1}),
       ];
       await SimCompare.checkFunctionalVector(ftm, vectors);
-      final simResult = SimCompare.iverilogVector(ftm, vectors);
-      expect(simResult, equals(true));
+      SimCompare.checkIverilogVector(ftm, vectors);
+    });
+
+    test('flop bus reset, no reset value', () async {
+      final ftm = FlopTestModule(Logic(width: 8), reset: Logic());
+      await ftm.build();
+      final vectors = [
+        Vector({'reset': 1}, {}),
+        Vector({'reset': 0, 'a': 0xa5}, {'y': 0}),
+        Vector({'a': 0xff}, {'y': 0xa5}),
+        Vector({}, {'y': 0xff}),
+      ];
+      await SimCompare.checkFunctionalVector(ftm, vectors);
+      SimCompare.checkIverilogVector(ftm, vectors);
+    });
+
+    test('flop bus reset, const reset value', () async {
+      final ftm = FlopTestModule(
+        Logic(width: 8),
+        reset: Logic(),
+        resetValue: 3,
+      );
+      await ftm.build();
+      final vectors = [
+        Vector({'reset': 1}, {}),
+        Vector({'reset': 0, 'a': 0xa5}, {'y': 3}),
+        Vector({'a': 0xff}, {'y': 0xa5}),
+        Vector({}, {'y': 0xff}),
+      ];
+      await SimCompare.checkFunctionalVector(ftm, vectors);
+      SimCompare.checkIverilogVector(ftm, vectors);
+    });
+
+    test('flop bus reset, logic reset value', () async {
+      final ftm = FlopTestModule(
+        Logic(width: 8),
+        reset: Logic(),
+        resetValue: Logic(width: 8),
+      );
+      await ftm.build();
+      final vectors = [
+        Vector({'reset': 1, 'resetValue': 5}, {}),
+        Vector({'reset': 0, 'a': 0xa5}, {'y': 5}),
+        Vector({'a': 0xff}, {'y': 0xa5}),
+        Vector({}, {'y': 0xff}),
+      ];
+      await SimCompare.checkFunctionalVector(ftm, vectors);
+      SimCompare.checkIverilogVector(ftm, vectors);
+    });
+
+    test('flop bus no reset, const reset value', () async {
+      final ftm = FlopTestModule(
+        Logic(width: 8),
+        resetValue: 9,
+      );
+      await ftm.build();
+      final vectors = [
+        Vector({}, {}),
+        Vector({'a': 0xa5}, {}),
+        Vector({'a': 0xff}, {'y': 0xa5}),
+        Vector({}, {'y': 0xff}),
+      ];
+      await SimCompare.checkFunctionalVector(ftm, vectors);
+      SimCompare.checkIverilogVector(ftm, vectors);
+    });
+
+    test('flop bus, enable, reset, const reset value', () async {
+      final ftm = FlopTestModule(
+        Logic(width: 8),
+        en: Logic(),
+        reset: Logic(),
+        resetValue: 12,
+      );
+      await ftm.build();
+      final vectors = [
+        Vector({'reset': 1, 'en': 0}, {}),
+        Vector({'reset': 0, 'a': 0xa5}, {'y': 12}),
+        Vector({}, {'y': 12}),
+        Vector({'en': 1}, {'y': 12}),
+        Vector({'a': 0xff}, {'y': 0xa5}),
+        Vector({}, {'y': 0xff}),
+      ];
+      await SimCompare.checkFunctionalVector(ftm, vectors);
+      SimCompare.checkIverilogVector(ftm, vectors);
     });
   });
 }
diff --git a/test/interface_test.dart b/test/interface_test.dart
index f0844c2db..b2e7421c9 100644
--- a/test/interface_test.dart
+++ b/test/interface_test.dart
@@ -37,6 +37,16 @@ class UncleanPortInterface extends Interface<MyDirection> {
   }
 }
 
+class MaybePortInterface extends Interface<MyDirection> {
+  Logic? get p => tryPort('p');
+
+  MaybePortInterface({required bool includePort}) {
+    if (includePort) {
+      setPorts([Port('p')], {MyDirection.dir1});
+    }
+  }
+}
+
 void main() {
   tearDown(() async {
     await Simulator.reset();
@@ -56,4 +66,16 @@ void main() {
       UncleanPortInterface();
     }, throwsException);
   });
+
+  group('maybe port', () {
+    test('tryPort, exists', () {
+      final intf = MaybePortInterface(includePort: true);
+      expect(intf.p, isNotNull);
+    });
+
+    test('tryPort, doesnt exist', () {
+      final intf = MaybePortInterface(includePort: false);
+      expect(intf.p, null);
+    });
+  });
 }
diff --git a/test/module_test.dart b/test/module_test.dart
new file mode 100644
index 000000000..78bc9aecd
--- /dev/null
+++ b/test/module_test.dart
@@ -0,0 +1,46 @@
+// Copyright (C) 2023 Intel Corporation
+// SPDX-License-Identifier: BSD-3-Clause
+//
+// module_test.dart
+// Unit tests for Module APIs
+//
+// 2023 September 11
+// Author: Max Korbel <max.korbel@intel.com>
+
+import 'package:rohd/rohd.dart';
+import 'package:test/test.dart';
+
+class ModuleWithMaybePorts extends Module {
+  Logic? get i => tryInput('i');
+  Logic? get o => tryOutput('o');
+  ModuleWithMaybePorts({required bool addIn, required bool addOut}) {
+    if (addIn) {
+      addInput('i', Logic());
+    }
+    if (addOut) {
+      addOutput('o');
+    }
+  }
+}
+
+void main() {
+  test('tryInput, exists', () {
+    final mod = ModuleWithMaybePorts(addIn: true, addOut: false);
+    expect(mod.i, isNotNull);
+  });
+
+  test('tryInput, doesnt exist', () {
+    final mod = ModuleWithMaybePorts(addIn: false, addOut: false);
+    expect(mod.i, null);
+  });
+
+  test('tryOutput, exists', () {
+    final mod = ModuleWithMaybePorts(addIn: false, addOut: true);
+    expect(mod.o, isNotNull);
+  });
+
+  test('tryOutput, doesnt exist', () {
+    final mod = ModuleWithMaybePorts(addIn: false, addOut: false);
+    expect(mod.o, null);
+  });
+}
diff --git a/test/sequential_test.dart b/test/sequential_test.dart
index 2e97920df..834494c86 100644
--- a/test/sequential_test.dart
+++ b/test/sequential_test.dart
@@ -96,6 +96,35 @@ class ShorthandSeqModule extends Module {
   }
 }
 
+class SeqResetValTypes extends Module {
+  SeqResetValTypes(Logic reset, Logic i) : super(name: 'seqResetValTypes') {
+    reset = addInput('reset', reset);
+    i = addInput('i', i, width: 8);
+    final clk = SimpleClockGenerator(10).clk;
+
+    final a = addOutput('a', width: 8); // int
+    final b = addOutput('b', width: 8); // LogicValue
+    final c = addOutput('c', width: 8); // Logic
+    final d = addOutput('d', width: 8); // none
+
+    Sequential(
+      clk,
+      reset: reset,
+      resetValues: {
+        a: 7,
+        b: LogicValue.ofInt(12, 8),
+        c: i,
+      },
+      [
+        a < 8,
+        b < LogicValue.ofInt(13, 8),
+        c < i + 1,
+        d < 4,
+      ],
+    );
+  }
+}
+
 void main() {
   tearDown(() async {
     await Simulator.reset();
@@ -161,4 +190,22 @@ void main() {
           throwsException);
     });
   });
+
+  test('reset and value types', () async {
+    final dut = SeqResetValTypes(
+      Logic(),
+      Logic(width: 8),
+    );
+    await dut.build();
+
+    final vectors = [
+      Vector({'reset': 1, 'i': 17}, {}),
+      Vector({'reset': 1}, {'a': 7, 'b': 12, 'c': 17, 'd': 0}),
+      Vector({'reset': 0}, {'a': 7, 'b': 12, 'c': 17, 'd': 0}),
+      Vector({'reset': 0}, {'a': 8, 'b': 13, 'c': 18, 'd': 4}),
+    ];
+
+    await SimCompare.checkFunctionalVector(dut, vectors);
+    SimCompare.checkIverilogVector(dut, vectors);
+  });
 }