|
| 1 | +# How to Build a Great Component in ROHD |
| 2 | + |
| 3 | +Since ROHD is an extension of the Dart programming language, please follow all |
| 4 | +Dart programming and documentation conventions. |
| 5 | + |
| 6 | +The `Module` class is the base class used in ROHD to build components, and |
| 7 | +calling the constructor the `Module` instantiates the component and connects it |
| 8 | +to signals passed into the constructor. |
| 9 | + |
| 10 | +Since ROHD is written in Dart, you should use the Dart best practices, such as |
| 11 | +camel-case variable naming, commenting patterns for all public APIs, and take |
| 12 | +advantage of dart format and dart analyze tools. |
| 13 | + |
| 14 | +## Port Construction and Connection |
| 15 | + |
| 16 | +A `Module` constructor takes `Logic` arguments and parameters to generate a |
| 17 | +hardware component. The `Logic` arguments are actually the external signals |
| 18 | +being connected to by the `Module`, and so internal copies must be constructed |
| 19 | +and connected to these arguments by the constructor and only then can other |
| 20 | +logic signals be connected to these copies. If you do not do this, a trace error |
| 21 | +will occur, but that only happens when this module is instantiated in another -- |
| 22 | +it will not show up while testing which just instantiates the module in a test |
| 23 | +environment. See [Modules](https://intel.github.io/rohd-website/docs/modules/) |
| 24 | +for more detail. |
| 25 | + |
| 26 | +A key pattern used in ROHD-HCL is to have the constructor take only input |
| 27 | +signals as arguments and generate the output signal widths based on these |
| 28 | +signals and other parameters. |
| 29 | + |
| 30 | +## Port Types |
| 31 | + |
| 32 | +Signals can take various forms in ROHD, and it is important to consider what form |
| 33 | +you want for the API of the `Module` you are building. ROHD supports the basic |
| 34 | +`Logic` signal which has its width encode (therefore you should not be using |
| 35 | +width as a parameter to a `Module`). ROHD provides basic cloning and accessor |
| 36 | +helper functions like `addInput` and `input`. |
| 37 | + |
| 38 | +`LogicArray` is a uniform multidimensional array of leaf `Logic` signals. Using |
| 39 | +this for input/output will require special routines like `addInputArray` to |
| 40 | +connect external and internal signals. Examples of using `LogicArray` are in the |
| 41 | +`Serializer` and `Deserializer` components. |
| 42 | + |
| 43 | +`LogicStructure` is a hierarchical concatenation of named `Logic` fields, where |
| 44 | +the `FloatingPoint` arithmetic type is an example used in the |
| 45 | +`FloatingPointMultiplierSimple` module. We can also pass in `LogicStructure` as |
| 46 | +a type for certain components so that the field structure is not lost on input |
| 47 | +and output. A good example of this is `Fifo`, which is templatized on |
| 48 | +`LogicType` to allow for us to generate a `Fifo` for a particular |
| 49 | +`LogicStructure` to use when pushing and popping the data in and out. Here, |
| 50 | +`addTypedInput` is a method used to help with creating the internal signals. |
| 51 | + |
| 52 | +`Interface` is similar to `LogicStructure`, yet it provides an ability to define |
| 53 | +directionality to the internal fields, useful in connecting modules that share a |
| 54 | +common protocol such as the `ApbInterface`. See |
| 55 | +[Interfaces](https://intel.github.io/rohd-website/docs/interfaces). A few |
| 56 | +examples of key general interface types that you can inherit from are the |
| 57 | +`PairInterface` and the `DataPortInterface`. the `Memory` module has a good |
| 58 | +example of how `DataPortInterface`s are cloned internally using its `connectIO` |
| 59 | +method. |
| 60 | +The `Fifo` has a good example of using an `Interface` to wrap a `LogicStructure`. |
| 61 | + |
| 62 | +When wrapping `LogicStructure` with `Interface`, don't name the `LogicStructure` |
| 63 | +as `Interface` will need to uniquify (a known bug in `Interface`). |
| 64 | + |
| 65 | +An important kind of `Interface` is the `PairInterface` which is designed for |
| 66 | +bidirectional communication and provides a `pairConnectIO` method for connecting |
| 67 | +external and internal ports based on producer/consumer filtering. |
| 68 | + |
| 69 | +## Logic Internals |
| 70 | + |
| 71 | +Signal logic is constructed in a ROHD component by assignment and simple logic |
| 72 | +operations like and (`&`) and or (`|`) as well as multiplexing (`mux`) and |
| 73 | +flopping (`flop`). See |
| 74 | +[operations](https://intel.github.io/rohd-website/docs/logic-math-compare/) for |
| 75 | +more detail. |
| 76 | + |
| 77 | + More complex logic can be constructed using |
| 78 | + [`Sequental`](https://intel.github.io/rohd-website/docs/sequentials/) and |
| 79 | + [`Combinational`](https://intel.github.io/rohd-website/docs/conditionals/) |
| 80 | + blocks similar to SystemVerilog `always` blocks. There is also a |
| 81 | + [`FiniteStateMachine`](https://intel.github.io/rohd-website/docs/fsm/) |
| 82 | + construct for state machines and a |
| 83 | + [`Pipeline`](https://intel.github.io/rohd-website/docs/pipelines/) construct |
| 84 | + for assisting with pipelined logic. |
| 85 | + |
| 86 | + Try to minimize the addition of new internal signals, by just reusing the |
| 87 | + signals created by the ports or by subcomponents. Use `.named` to create clean |
| 88 | + SystemVerilog names. |
| 89 | + |
| 90 | +### Debug |
| 91 | + |
| 92 | + If you want to expose internal signals onto the interface of a `Module` for |
| 93 | + debug, a simple method is to declare them as a field in the class (Use |
| 94 | +`@protected` in case this is exposed, so it doesn't become part of the API). This |
| 95 | + signal will be available in tests as module.field. |
| 96 | + |
| 97 | +## Unit Testing |
| 98 | + |
| 99 | + A good component has unit tests to validate the component and provide examples |
| 100 | + of use. We use the Dart testing framework which requires that tests are stored |
| 101 | + in the `test/` directory and are named ending in `_test.dart`. An example of |
| 102 | + unit tests for a component is shown below. Note that grouping of tests can |
| 103 | + reuse a common component built for multiple tests. Also note that each test |
| 104 | + with sequential logic will need a `SimpleClockGenerator`, a `Simulator.run()` |
| 105 | + and an `endSimulation`. Some helper methods (like `.waitCycles`) are available |
| 106 | + in the rohd-fv package. |
| 107 | + |
| 108 | + ```dart |
| 109 | +void main() { |
| 110 | + tearDown(() async { |
| 111 | + await Simulator.reset(); |
| 112 | + }); |
| 113 | +
|
| 114 | + group('test narrow component', () { |
| 115 | + final input = Logic(width: 5); |
| 116 | + final component = MyComponent(input); |
| 117 | + final output = component.out; |
| 118 | +
|
| 119 | + test('MyComponent smoke test', () async { |
| 120 | + final clk = SimpleClockGenerator(10).clk; |
| 121 | +
|
| 122 | + unawaited(Simulator.run()); |
| 123 | + reset.inject(1); |
| 124 | + await clk.waitCycles(3); |
| 125 | + reset.inject(0); |
| 126 | + await clk.waitCycles(1); |
| 127 | + input.inject(1); |
| 128 | + await clk.waitCycles(3); |
| 129 | + expect(output.value, equals(Const(1, width: output.width))); |
| 130 | +
|
| 131 | + await Simulator.endSimulation(); |
| 132 | + }); |
| 133 | + |
| 134 | + test('MyComponent second test', () async { |
| 135 | + final clk = SimpleClockGenerator(10).clk; |
| 136 | +
|
| 137 | + unawaited(Simulator.run()); |
| 138 | + reset.inject(1); |
| 139 | + await clk.waitCycles(3); |
| 140 | + reset.inject(0); |
| 141 | + await clk.waitCycles(1); |
| 142 | + input.inject(6); |
| 143 | + await clk.waitCycles(3); |
| 144 | + expect(output.value, equals(Const(6, width: output.width))); |
| 145 | +
|
| 146 | + await Simulator.endSimulation(); |
| 147 | + }); |
| 148 | + }); |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +Prefer using `waitCycles` instead of `nextPosedge` and use `inject` instead of |
| 153 | +`put` when working with sequential tests. |
| 154 | + |
| 155 | +When testing a combinational path, and you `inject` inputs after a positive |
| 156 | +clock edge, if you sample at the next clock edge, you will miss the |
| 157 | +combinational value. Instead, use the output `previousValue` at the next clock |
| 158 | +edge, or sample the output at `nextNegEdge` to look at the value midway through |
| 159 | +the clock cycle. |
| 160 | + |
| 161 | +While creating unit tests, you can just run the tests for your component instead |
| 162 | +of running the entire suite of ROHD-HCL tests. The entire regression suite |
| 163 | +takes quite a long time and is only necessary if you make changes to some core |
| 164 | +functionality. |
0 commit comments