diff --git a/doc/tutorials/README.md b/doc/tutorials/README.md index ff8a182e5..c33d0a2c7 100644 --- a/doc/tutorials/README.md +++ b/doc/tutorials/README.md @@ -81,8 +81,10 @@ ## Chapter 8: Abstractions -- Pipelines: Normally Delayed exists in between a big circuit, abstractions split the logic across multiple cycles and let us decide what logic do you want it to occur in each of the cycle. -- Finite state machines +- [ROHD Abstraction](./chapter_8/00_abstraction.md#rohd-abstraction) +- [Interface](./chapter_8/01_interface.md) +- [Finite State Machine](./chapter_8/02_finite_state_machine.md) +- [Pipeline](./chapter_8/03_pipeline.md) ## Chapter 9: ROHD-COSIM External SystemVerilog Modules (Coming Soon!) @@ -97,7 +99,7 @@ - ROHD Cosim ---------------- -2023 June 9 +2023 September 4 Author: Yao Jing Quek <> Copyright (C) 2021-2023 Intel Corporation diff --git a/doc/tutorials/chapter_8/00_abstraction.md b/doc/tutorials/chapter_8/00_abstraction.md new file mode 100644 index 000000000..6e1b103dd --- /dev/null +++ b/doc/tutorials/chapter_8/00_abstraction.md @@ -0,0 +1,28 @@ +# Content + +- [ROHD Abstraction](#rohd-abstraction) +- [Interface](#interface) +- [Finite State Machine](#finite-state-machine-fsm) +- [Pipeline](#pipeline) + +## Learning Outcome + +In this chapter: + +- You will learn how to use ROHD abstraction to speed up the development process. + +## ROHD Abstraction + +ROHD provides several abstraction layer to accelerate the development work. As of now, ROHD provides several abstraction catalog which listed as below: + +### [Interface](01_interface.md) + +Interface make it easier to define port connections of a module in a reusable way. + +### [Finite State Machine (FSM)](02_finite_state_machine.md) + +An easy and fast implementation of Finite State Machine with ROHD API. + +### [Pipeline](03_pipeline.md) + +Pipeline can be build with ROHD API in a simple and refactorable way. diff --git a/doc/tutorials/chapter_8/01_interface.md b/doc/tutorials/chapter_8/01_interface.md new file mode 100644 index 000000000..82ee6bdde --- /dev/null +++ b/doc/tutorials/chapter_8/01_interface.md @@ -0,0 +1,191 @@ +# Content + +- [ROHD Interfaces](#rohd-interfaces) +- [Counter Module](#counter-module) +- [Counter Module Interface](#counter-module-interface) +- [Exercise](#exercise) + +## Learning Outcome + +In this chapter: + +- You will learn how to use ROHD interface abstraction API to group and reuse port easily. + +## ROHD Interfaces + +Interfaces make it easier to define port connections of a module in a reusable way. An example of the counter re-implemented using interfaces is shown below. + +`Interface` takes a generic parameter for direction type. This enables you to group signals so make adding them as inputs/outputs easier for different modules sharing this interface. + +The `Port` class extends `Logic`, but has a constructor that takes width as a positional argument to make interface port definitions a little cleaner. + +When connecting an `Interface` to a `Module`, you should always create a new instance of the `Interface` so you don't modify the one being passed in through the constructor. Modifying the same `Interface` as was passed would have negative consequences if multiple `Modules` were consuming the same `Interface`, and also breaks the rules for `Module` input and output connectivity. + +The `connectIO` function under the hood calls `addInput` and `addOutput` directly on the `Module` and connects those `Module` ports to the correct ports on the `Interfaces`. Connection is based on signal names. You can use the `uniquify` Function argument in `connectIO` to uniquify inputs and outputs in case you have multiple instances of the same `Interface` connected to your module. You can also use the `setPort` function to directly set individual ports on the `Interface` instead of via tagged set of ports. + +## Counter Module + +In ROHD, the [`Counter` module](../../../example/example.dart) reside in the example is one of the most basic example. Let us try to understand the counter module and see how we can modified it with ROHD interface instead. + +In the `Counter` module, its take in inputs `enable`, `reset`, `clk` and output `val`. + +On every positive edge of the clock, the value of `val` will be increment by 1 if enable `en` is set to true. + +```dart +// Define a class Counter that extends ROHD's abstract Module class. +class Counter extends Module { + // For convenience, map interesting outputs to short variable names for + // consumers of this module. + Logic get val => output('val'); + + // This counter supports any width, determined at run-time. + final int width; + + Counter(Logic en, Logic reset, Logic clk, + {this.width = 8, super.name = 'counter'}) { + // Register inputs and outputs of the module in the constructor. + // Module logic must consume registered inputs and output to registered + // outputs. + en = addInput('en', en); + reset = addInput('reset', reset); + clk = addInput('clk', clk); + + final val = addOutput('val', width: width); + + // A local signal named 'nextVal'. + final nextVal = Logic(name: 'nextVal', width: width); + + // Assignment statement of nextVal to be val+1 + // ('<=' is the assignment operator). + nextVal <= val + 1; + + // `Sequential` is like SystemVerilog's always_ff, in this case trigger on + // the positive edge of clk. + Sequential(clk, [ + // `If` is a conditional if statement, like `if` in SystemVerilog + // always blocks. + If(reset, then: [ + // The '<' operator is a conditional assignment. + val < 0 + ], orElse: [ + If(en, then: [val < nextVal]) + ]) + ]); + } +} +``` + +## Counter Module Interface + +Let us see how we can change the `ROHD` module to `Counter` interface. First, we can create a enum `CounterDirection` that have tags of `inward`, `outward` and `misc`. You can think of this as what is the category you want to group your ports. This category can be reuse between modules. `inward` port group all inputs port, `outward` group all outputs port and `misc` group all miscellanous port such as `clk`. + +Then, we can create our interface `CounterInterface` that extends from parents `Interface`. The `TagType` is the enum that we create earlier. Let create the getters to all ports for `Counter` to allows us to send signals to the interface. + +Let start by creating a constructor `CounterInterface`. Inside the constructor, add `setPorts()` function to group our common port. `setPorts` have function signature of `void setPorts(List ports, [List? tags])` which received a List of `Logic` and `tags`. + +Hence, the `CounterInterface` will look something like this: + +```dart +enum CounterDirection { inward, outward, misc } + +/// A simple [Interface] for [Counter]. +class CounterInterface extends Interface { + Logic get en => port('en'); + Logic get reset => port('reset'); + Logic get val => port('val'); + Logic get clk => port('clk'); + + final int width; + CounterInterface({this.width = 8}) { + setPorts([Port('en'), Port('reset')], [CounterDirection.inward]); + + setPorts([ + Port('val', width), + ], [ + CounterDirection.outward + ]); + + setPorts([Port('clk')], [CounterDirection.misc]); + } +} +``` + +Next, we want to modify the `Counter` module constructor to receive the interface. Then, we **MUST** create a new instance of the interface to avoid modify the interface inside the constructor. + +```dart +// create a new interface instance. Let make it a private variable. +late final CounterInterface _intf; +Counter(CounterInterface intf): super('counter') {} +``` + +Now, let use the `connectIO()` function. As mentioned [previously](#rohd-interfaces), this function called `addInput` and `addOutput` that help us register the port. Therefore, we can pass the `module`, `interface`, `inputTags`, and `outputTags` as the arguments of the `connectIO` function. + +```dart +Counter(CounterInterface intf) : super(name: 'counter') { + _intf = CounterInterface(width: intf.width) + ..connectIO(this, intf, + inputTags: {CounterDirection.inward, CounterDirection.misc}, + outputTags: {CounterDirection.outward}); + + final nextVal = Logic(name: 'nextVal', width: intf.width); + + nextVal <= _intf.val + 1; + + Sequential(_intf.clk, [ + If.block([ + Iff(_intf.reset, [ + _intf.val < 0, + ]), + ElseIf(_intf.en, [ + _intf.val < nextVal, + ]) + ]), + ]); +} +``` + +Yup, that all you need to use the ROHD interface. Now, let see how to do simulation or pass value to perform test with interface module. + +The only different here is instead of passing the `Logic` value through constructor, we are going to instantiate the interface object and perform assignment directly through the getter function we create earlier. + +```dart +Future main() async { + // instantiate the counter interface + final counterInterface = CounterInterface(); + + // Assign SimpleClockGenerator to the clk through assesing the getter function + counterInterface.clk <= SimpleClockGenerator(10).clk; + + final counter = Counter(counterInterface); + await counter.build(); + + // Inject value to en and reset through interface + counterInterface.en.inject(0); + counterInterface.reset.inject(1); + + print(counter.generateSynth()); + + WaveDumper(counter, + outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); + Simulator.registerAction(25, () { + counterInterface.en.put(1); + counterInterface.reset.put(0); + }); + + Simulator.setMaxSimTime(100); + + await Simulator.run(); +} +``` + +Thats it for the ROHD interface. By using interface, you code can be a lot cleaner and readable. Hope you enjoy the tutorials. You can find the executable version of code at [counter_interface.dart](./counter_interface.dart). + +## Exercise + +1. Serial Peripheral Interface (SPI) + +Serial Peripheral Interface (SPI) is an interface bus commonly used to send data between microcontrollers and small peripherals such as shift registers, sensors, and SD cards. It uses separate clock and data lines, along with a select line to choose the device you wish to talk to. + +Build a SPI using ROHD interface. You can use the shift register as the peripheral, you can just build the unit test for peripheral. + +Answer to this exercise can be found at [answers/exercise_1_spi.dart](./answers/exercise_1_spi.dart) diff --git a/doc/tutorials/chapter_8/02_finite_state_machine.md b/doc/tutorials/chapter_8/02_finite_state_machine.md new file mode 100644 index 000000000..7ebeaa443 --- /dev/null +++ b/doc/tutorials/chapter_8/02_finite_state_machine.md @@ -0,0 +1,348 @@ +# Content + +- [Microwave Oven in Finite State Machine (FSM)](#microwave-oven-in-finite-state-machine-fsm) +- [State](#state) +- [Transitions](#transitions) +- [Microwave Oven State Diagram](#microwave-oven-state-diagram) +- [ROHD FSM](#rohd-fsm) +- [FSM Simulation](#fsm-simulation) +- [Exercise](#exercise) + +## Learning Outcome + +In this chapter: + +- You will learn how to create a microwave oven finite state machine using ROHD abstraction API. + +## Microwave Oven in Finite State Machine (FSM) + +Today, we going to develop an Microwave Oven FSM using ROHD. Since its just a case study, let make this microwave oven only consists of 4 states (standby, cooking, paused and completed). + +### State + +- Standby: LED Blue. The initial state, where the oven is idle and waiting for user input +- Cooking: LED Yellow. The state in which the oven is actively cooking the food +- Paused: LED Red. The state in which cooking is temporarily suspended, but can be resumed. +- Completed: LED Green. The state in which the oven has finished cooking the food. + +### Transitions + +Transitions between states would be triggered by events such as button presses or sensor readings. For example, when the "start" button is pressed, the FSM would transition from "standby" to "cooking." When the cooking time has expired, it would transition from "cooking" to "completed." If the "pause" button is pressed while in the "cooking" state, the FSM would transition to "paused." And if the "resume" button is pressed while in the "paused" state, the FSM would transition back to "cooking." + +## Microwave Oven State Diagram + +![Oven FSM](./assets/oven_fsm.png) + +## ROHD FSM + +In ROHD, there are abstraction level of writting FSM. Yes, you can definitely wrote the code using Sequential and Combinational like previous chapter. But, today we want to see how we can leverage the abstraction layer provided in ROHD to quickly create the Oven FSM above. + +First, we want to import the ROHD package and also `counter` module. We can use the counter interface we created last session. + +```dart +import 'package:rohd/rohd.dart'; +import './counter_interface.dart'; +``` + +We also want to represent the standby, cooking, paused, and completed states. + +Next, we can also use enums to represent the LED light on the oven. Similar to buttons, we want to also encoded the LED light with customs value instead. + +```dart +enum OvenState { standby, cooking, paused, completed } +``` + +Then, its time to create our `OvenModule`. Let start by creating the Module class. + +```dart +class OvenModule extends Module { + OvenModule(): super(name: 'OvenModule') { + // logic here + } +} +``` + +In ROHD, we can use `StateMachine` API library. The `StateMachine` constructs a simple FSM, using the `clk` and `reset` signals. Also accepts the `reset` state to transition to `resetState` along with the List of _states of the FSM. Later, we will also need to create a List of `state` and send to the StateMachine. + +Let start by intitialize a variable called `_oven` that is `StateMachine` with `StateIdentifier` as `OvenState`. + +Besides, we can use a simple hashmap to map over the button and LED value to integer. + +```dart +class OvenModule extends Module { + late StateMachine _oven; + + // A hashmap that represent button value + final Map btnVal = { + 'start': 0, + 'pause': 1, + 'resume': 2, + }; + + // A hashmap that represent LED value + final Map ledLight = { + 'yellow': 0, + 'blue': 1, + 'red': 2, + 'green': 3 + }; + + OvenModule(): super(name: 'OvenModule') { + // logic here + } +} +``` + +This oven will receive button and reset signal as input and output a led light. Let add the inputs and output port now. Also create a getter for the LED as the output. + +Let also create an internal clock generator `clk` inside the module. This clk generator will be shared with the FSM and `counter` module. Let instantiate the counter module, together with internal signals reset `counterReset` and enable `en`. + +```dart +class OvenModule extends Module { + late StateMachine _oven; + Logic get led => output('led'); + + OvenModule(): super(name: 'OvenModule') { + // FSM input and output + button = addInput('button', button, width: button.width); + reset = addInput('reset', reset); + final led = addOutput('led', width: 8); + + // Counter internal signals + final clk = SimpleClockGenerator(10).clk; + final counterReset = Logic(name: 'counter_reset'); + final en = Logic(name: 'counter_en'); + final counterInterface = CounterInterface(); + counterInterface.clk <= clk; + counterInterface.en <= en; + counterInterface.reset <= counterReset; + + final counter = Counter(counterInterface); + } +} +``` + +Let start creating the FSM `State`. FSM `State` represent a state named `identifier` with a definition of `events` and `actions` associated with that state. + +- `identifier`: Identifer or name of the state. +- `events`: A map of the possible conditions that might be true and the next state that the FSM needs to transition to in each of those cases. +- `actions`: Actions to perform while the FSM is in this state. + +```dart +State(StateIdentifier identifier, {required Map events, required List actions}); +``` + +Example, State 0 - Standby State will have the below properties: + +1. identifier: `OvenState.standby` +2. events: +key: `Logic(name: 'button_start')..gets(button.eq(Const(Button.start.value, width: button.width)))` +value: `OvenState.cooking` +3. actions: `[led < LEDLight.blue.value, counterReset < 1, end < 0 ]` + +The other states are coded as below. The code are well documented with comments below. + +```dart +final states = [ + // identifier: standby state, represent by `OvenState.standby`. + State(OvenState.standby, + // events: + // When the button `start` is pressed during standby state, + // OvenState will changed to `OvenState.cooking` state. + events: { + Logic(name: 'button_start') + ..gets(button + .eq(Const(Button.start.value, width: button.width))): + OvenState.cooking, + }, + // actions: + // During the standby state, `led` is change to blue; timer's + // `counterReset` is set to 1 (Reset the timer); + // timer's `en` is set to 0 (Disable value update). + actions: [ + led < LEDLight.blue.value, + counterReset < 1, + en < 0, + ]), + + // identifier: cooking state, represent by `OvenState.cooking`. + State(OvenState.cooking, + // events: + // When the button `paused` is pressed during cooking state, + // OvenState will changed to `OvenState.paused` state. + // + // When the button `counter` time is elapsed during cooking state, + // OvenState will changed to `OvenState.completed` state. + events: { + Logic(name: 'button_pause') + ..gets(button + .eq(Const(Button.pause.value, width: button.width))): + OvenState.paused, + Logic(name: 'counter_time_complete')..gets(counterInterface.val.eq(4)): + OvenState.completed + }, + // actions: + // During the cooking state, `led` is change to yellow; timer's + // `counterReset` is set to 0 (Do not reset); + // timer's `en` is set to 1 (Enable value update). + actions: [ + led < LEDLight.yellow.value, + counterReset < 0, + en < 1, + ]), + + // identifier: paused state, represent by `OvenState.paused`. + State(OvenState.paused, + // events: + // When the button `resume` is pressed during paused state, + // OvenState will changed to `OvenState.cooking` state. + events: { + Logic(name: 'button_resume') + ..gets(button + .eq(Const(Button.resume.value, width: button.width))): + OvenState.cooking + }, + // actions: + // During the paused state, `led` is change to red; timer's + // `counterReset` is set to 0 (Do not reset); + // timer's `en` is set to 0 (Disable value update). + actions: [ + led < LEDLight.red.value, + counterReset < 0, + en < 0, + ]), + + // identifier: completed state, represent by `OvenState.completed`. + State(OvenState.completed, + // events: + // When the button `start` is pressed during completed state, + // OvenState will changed to `OvenState.standby` state. + events: { + Logic(name: 'button_start') + ..gets(button + .eq(Const(Button.start.value, width: button.width))): + OvenState.standby + }, + // actions: + // During the start state, `led` is change to green; timer's + // `counterReset` is set to 1 (Reset value); + // timer's `en` is set to 0 (Disable value update). + actions: [ + led < LEDLight.green.value, + counterReset < 1, + en < 0, + ]) +]; +``` + +By now, you already have a list of `state` ready to be passed to the `StateMachine`. Let assign the the `state` to the StateMachine declared. Note that, we also passed `OvenState.standby` to the StateMachine to understand that is the State when reset signal is given. + +ROHD FSM abstraction come with state diagram generator using mermaid. We can create a markdown file using the function `generateDiagram()`. You can install mermaid extension in VSCode to preview the diagram. + +```dart +_oven = StateMachine(clk, reset, OvenState.standby, states); + +_oven.generateDiagram(outputPath: 'doc/tutorials/chapter_8/oven_fsm.md'); +``` + +Then, you can preview the generated fsm diagram. + +![FSM Mermaid](./assets/fsm_mermaid_output.png) + +## FSM Simulation + +Let test and simulate our FSM module. First, let create a main function and instantiate the FSM module together with the inputs logic. + +```dart +Future main({bool noPrint = false}) async { + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + final oven = OvenModule(button, reset); +} +``` + +Before we start the simulation, we need to build the module using `await oven.build()`. Let start by inject value of 1 to `reset` to not allows the FSM start. + +```dart +await oven.build(); +reset.inject(1); +``` + +Let also attach a `WaveDumper` to preview what is the waveform and what happened during the Simulation. + +```dart +if (!noPrint) { + WaveDumper(oven, outputPath: 'oven.vcd'); +} +``` + +Let add a listener to the to listen to the `Steam` and print the outputs when the value changed. This can be achieved by `.changed.listen()` function. + +```dart +if (!noPrint) { + // We can listen to the streams on LED light changes based on time. + oven.led.changed.listen((event) { + // Get the led light enum name from LogicValue. + final ledVal = LEDLight.values[event.newValue.toInt()].name; + + // Print the Simulator time when the LED light changes. + print('@t=${Simulator.time}, LED changed to: $ledVal'); + }); +} +``` + +Now, let start the simulation. At time 25, we want to drop the reset and press on the start button. + +```dart +// Drop reset at time 25. +Simulator.registerAction(25, () => reset.put(0)); + +// Press button start => `00` at time 25. +Simulator.registerAction(25, () { + button.put(Button.start.value); +}); +``` + +Then, we want to press the pause button at time 50 and press resume button at time 70. + +```dart +// Press button pause => `01` at time 50. +Simulator.registerAction(50, () { + button.put(Button.pause.value); +}); + +// Press button resume => `10` at time 70. +Simulator.registerAction(70, () { + button.put(Button.resume.value); +}); +``` + +Finally, we want to `setMaxSimTime` to 120 and register a print function on time 120 to indicate that the Simulation completed. + +```dart +// Set a maximum time for the simulation so it doesn't keep running forever. +Simulator.setMaxSimTime(120); + +// Kick off the simulation. +await Simulator.run(); +``` + +Well, that is for the FSM. Hope you enjoyed the tutorials. You can find the executable version of code at [oven_fsm.dart](./oven_fsm.dart). + +## Exercise + +1. Toy Capsule Finite State Machine + +Let create a toy capsule vending machine. Consider a toy capsule vending machine that dispenses a single type of toy capsule. The vending machine can be in one of three states. + +- Idle: This is the initial state of the vending machine, where it is waiting for a customer to insert coin. +- Coin Inserted: Once the customer inserts a coin, the vending machine moves into this state. The machine now waits for the customer to press dispense. +- Dispensing: If the customer press the dispense button, the machine moves into this state, where it dispenses the toy capsule and then returns to the "Idle" state. + +In this case study, the transitions between the states are as follows: + +- If the vending machine is in "idle" state and a coin is inserted, It transitions to the "Coin Inserted" State. +- If the vending machine is in "Coin Inserted" state and a dispense button is pressed, it transitions to the "Dispensing" state. +- If the vending machine is in the "Dispensing" state, it dispenses the toy capsule and transitions back to the Idle State. + +Answer to this exercise can be found at [answers/exercise_2_toycapsule_fsm.dart](./answers/exercise_2_toycapsule_fsm.dart) diff --git a/doc/tutorials/chapter_8/03_pipeline.md b/doc/tutorials/chapter_8/03_pipeline.md new file mode 100644 index 000000000..09ec7bffc --- /dev/null +++ b/doc/tutorials/chapter_8/03_pipeline.md @@ -0,0 +1,323 @@ +# Content + +- [ROHD Pipelines](#rohd-pipelines) +- [Carry Save Multiplier 4 x 4](#carry-save-multiplier-4-x-4) +- [Exercise](#exercise) + +## Learning Outcome + +In this chapter: + +- You will learn how to use ROHD pipeline abstraction API to build Carry Save Multiplier (CSM). + +## ROHD Pipelines + +ROHD has a built-in syntax for handling pipelines in a simple & refactorable way. The example below shows a three-stage pipeline which adds 1 three times. Note that `Pipeline` consumes a clock and a list of stages, which are each a `List Function(PipelineStageInfo p)`, where `PipelineStageInfo` has information on the value of a given signal in that stage. The `List` the same type of procedural code that can be placed in `Combinational`. + +```dart +Logic a; +var pipeline = Pipeline(clk, + stages: [ + (p) => [ + // the first time `get` is called, `a` is automatically pipelined + p.get(a) < p.get(a) + 1 + ], + (p) => [ + p.get(a) < p.get(a) + 1 + ], + (p) => [ + p.get(a) < p.get(a) + 1 + ], + ] +); +var b = pipeline.get(a); // the output of the pipeline +``` + +This pipeline is very easy to refractor. If we wanted to merge the last two stages, we could simple rewrite it as: + +```dart +Logic a; +var pipeline = Pipeline(clk, + stages: [ + (p) => [ + p.get(a) < p.get(a) + 1 + ], + (p) => [ + p.get(a) < p.get(a) + 1, + p.get(a) < p.get(a) + 1 + ], + ] +); +var b = pipeline.get(a); +``` + +You can also optionally add stalls and reset values for signals in the pipeline. Any signal not accessed via the `PipelineStageInfo` object is just accessed as normal, so other logic can optionally sit outside of the pipeline object. + +ROHD also includes a version of `Pipeline` that support a ready/valid protocol called `ReadyValidPipeline`. The syntax looks the same, but has some additional parameters for readys and valids. + +## Carry Save Multiplier (4 x 4) + +Carry Save Multiplier is a digital circuit used for multiplying two binary numbers. It is a specialized multiplication technique that is commonly employed in high-performance arithmetic units, particularly in digital signal processing (DSP) applications. + +The carry-save multiplier approach aims to enhance the speed and efficiency of the multiplication operation by breaking it down into smaller parallel operations. Instead of directly multiplying the etire multiplicand and mulltiplier, the carry-save multiplier splits them into smaller components and performs independent multiplications on each components. + +We can build carry save multiplier using carry save adder built in [chapter 5](../chapter_5/00_basic_modules.md). The diagram below shows the architectures of the carry save multiplier built using Full Adder and N-Bit Adder. + +![carrysave multiplier](./assets/4x4-bits-Carry-Save-Multiplier-2.png) + +Assuming that we have binary input A = 1010 (decimal: 10) and B = 1110 (decimal: 14), the final result would be 10001100 (decimal: 140). + +The **first stage** of the carry-save multiplier consists of a Full Adder that takes in the AND gate of `Ax` and `B0`, where x is the bit position of the input a. + +In stage 0, the full adder takes in: + +- Inputs + - A: 0 + - B: AND(Ax, B0) + - C-IN: 0 + +In the stage 2 to 4, the full adder takes: + +- Inputs + - A: Output **Sum** from previous stage + - B: AND(Ax, By), where x is the single bit of A, while y is the bits based on stage. + - C-IN: Output **carry-out** from previous stage + +As shown in the diagram, the first index of the FA always takes 0 as input A, and the **final stage** consists of the **N-Bit Adder** we created previously. +Note that n-bit adder is also called as ripple carry adder. + +Let's start by creating the `CarrySaveMultiplier` module. The module takes inputs `a`, `b`, `reset` and a `clk`, and returns an output port named `product`. We will also need two internal signals, `rCarryA` and `rCarryB`, which will contain the signals to be passed to the nBitAdder or ripple carry adder later in the final stage. + +Let's also created two List of Logic that save `sum` and `carry` from each of the stage. Since we will be using ROHD pipeline for this module, let also created a global variable for ROHD pipeline. + +We can also create a getter function that get the output of `product`. + +```dart +class CarrySaveMultiplier extends Module { + final List sum = + List.generate(8, (index) => Logic(name: 'sum_$index')); + final List carry = + List.generate(8, (index) => Logic(name: 'carry_$index')); + + late final Pipeline pipeline; + + CarrySaveMultiplier(Logic valA, Logic valB, Logic clk, Logic reset, + {super.name = 'carry_save_multiplier'}) { + // Declare Input Node + valA = addInput('a', valA, width: valA.width); + valB = addInput('b', valB, width: valB.width); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + final product = addOutput('product', width: a.width + b.width + 1); + + final rCarryA = Logic(name: 'rcarry_a', width: valA.width); + final rCarryB = Logic(name: 'rcarry_b', width: valB.width); + } + + Logic get product => output('product'); +} +``` + +Since we will be using `FullAdder` and `NBitAdder` modules in chapter 5, we need to import them. + +```dart +import '../chapter_5/n_bit_adder.dart'; +``` + +To implement the pipeline, we need to declare a `Pipeline` object with a clock `clk` and a list of stages. Each stage can be thought of as a row, and each row will have `(a.width - 1) + row` columns. + +For the first stage or row of `FullAdder`, we need to set `a` and `c-in` to 0. Additionally, for every first column, `a` will also be 0. Therefore, we can represent `a` as `column == (a.width - 1) + row || row == 0 ? Const(0) : p.get(sum[column])`. Similarly, we can represent `carryIn` as `row == 0 ? Const(0) : p.get(carry[column - 1])`. + +Note that we use `p.get()` to retrieve data from the previous pipeline stage. As for `b`, we can represent it using a simple `AND` operation: `a[column - row] & b[row]`. + +Summary: + +- a: `column == (a.width - 1) + row || row == 0 ? Const(0) : p.get(sum[column])` +- b: `a[column - row] & b[row]` +- c-in: `row == 0 ? Const(0) : p.get(carry[column - 1])` + +In each pipeline stage, we can use `...List.generate()` to generate the `FullAdder`. + +```dart +pipeline = Pipeline(clk, + stages: [ + ...List.generate( + valB.width, + (row) => (p) { + final columnAdder = []; + final maxIndexA = (valA.width - 1) + row; + + for (var column = maxIndexA; column >= row; column--) { + final fullAdder = FullAdder( + a: column == maxIndexA || row == 0 + ? Const(0) + : p.get(sum[column]), + b: p.get(valA)[column - row] & p.get(valB)[row], + carryIn: row == 0 ? Const(0) : p.get(carry[column - 1])) + .fullAdderRes; + + columnAdder + ..add(p.get(carry[column]) < fullAdder.cOut) + ..add(p.get(sum[column]) < fullAdder.sum); + } + + return columnAdder; + }, + )], +); +``` + +We have successfully created stages 0 to 3. Next, we manually add the final stage where we swizzle the `sum` and `carry` and connect them to `rCarryA` and `rCarryB`, respectively. + +```dart +(p) => [ + p.get(rCarryA) < + [ + Const(0), + ...List.generate( + valA.width - 1, + (index) => + p.get(sum[(valA.width + valB.width - 2) - index])) + ].swizzle(), + p.get(rCarryB) < + [ + ...List.generate( + valA.width, + (index) => + p.get(carry[(valA.width + valB.width - 2) - index])) + ].swizzle() +], +``` + +Also not to forget to set the reset signals in pipeline. Your final version of the pipeline will look like this. + +```dart +pipeline = Pipeline( + clk, + stages: [ + ...List.generate( + valB.width, + (row) => (p) { + final columnAdder = []; + final maxIndexA = (valA.width - 1) + row; + + for (var column = maxIndexA; column >= row; column--) { + final fullAdder = FullAdder( + a: column == maxIndexA || row == 0 + ? Const(0) + : p.get(sum[column]), + b: p.get(valA)[column - row] & p.get(valB)[row], + carryIn: row == 0 ? Const(0) : p.get(carry[column - 1])) + .fullAdderRes; + + columnAdder + ..add(p.get(carry[column]) < fullAdder.cOut) + ..add(p.get(sum[column]) < fullAdder.sum); + } + + return columnAdder; + }, + ), + (p) => [ + p.get(rCarryA) < + [ + Const(0), + ...List.generate( + valA.width - 1, + (index) => + p.get(sum[(valA.width + valB.width - 2) - index])) + ].swizzle(), + p.get(rCarryB) < + [ + ...List.generate( + valA.width, + (index) => + p.get(carry[(valA.width + valB.width - 2) - index])) + ].swizzle() + ], + ], + reset: reset, + resetValues: {product: Const(0)}, +); +``` + +To obtain our final result, we can instantiate the `NBitAdder` module and pass `rCarryA` and `rCarryB` to the module. Finally, we need to swizzle the results from `nBitAdder` and the last four bits from the pipeline. + +```dart +final nBitAdder = NBitAdder( + pipeline.get(rCarryA), + pipeline.get(rCarryB), +); + +product <= +[ + ...List.generate( + valA.width + 1, + (index) => nBitAdder.sum[(valA.width) - index], + ), + ...List.generate( + valA.width, + (index) => pipeline.get(sum[valA.width - index - 1]), + ) +].swizzle(); +``` + +We can test and simulate the final output by creating the `main()` function as below: + +```dart +void main() async { + final a = Logic(name: 'a', width: 4); + final b = Logic(name: 'b', width: 4); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final csm = CarrySaveMultiplier(a, b, clk, reset); + + await csm.build(); + + // after one cycle, change the value of a and b + a.inject(10); + b.inject(14); + reset.inject(1); + + // Attach a waveform dumper so we can see what happens. + WaveDumper(csm, outputPath: 'csm.vcd'); + + Simulator.registerAction(10, () { + reset.inject(0); + }); + + Simulator.registerAction(30, () { + a.put(10); + b.put(11); + }); + + Simulator.registerAction(60, () { + a.put(10); + b.put(6); + }); + + csm.product.changed.listen((event) { + print('@t=${Simulator.time}, product is: ${event.newValue.toInt()}'); + }); + + Simulator.setMaxSimTime(150); + + await Simulator.run(); +} +``` + +Well, that is for the pipeline. Hope you enjoyed the tutorials. You can find the executable version of code at [carry_save_multiplier.dart](./carry_save_multiplier.dart). + +## Exercise + +1. Can you create a simple 4-stages pipeline that perform the following operation on each stage: + +`a + (a * stage_num)` + +where `a` is an input variable from user, and `stage_num` takes the values of 0 to 3 based on it current stage. + +In each stage, the pipeline should multiply the input `a` by the stage number and add the result to the input `a` to obtain the stage output. + +Answer to this exercise can be found at [answers/exercise_3_pipeline.dart](./answers/exercise_3_pipeline.dart) diff --git a/doc/tutorials/chapter_8/answers/exercise_1_spi.dart b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart new file mode 100644 index 000000000..9325f4990 --- /dev/null +++ b/doc/tutorials/chapter_8/answers/exercise_1_spi.dart @@ -0,0 +1,170 @@ +// ignore_for_file: avoid_print + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; + +// Define a set of legal directions for SPI interface, will +// be pass as parameter to Interface +enum SPIDirection { controllerOutput, peripheralOutput } + +// Create an interface for Serial Peripheral Interface +class SPIInterface extends Interface { + // include the getter to the function + Logic get sck => port('sck'); // serial clock + Logic get sdi => port('sdi'); // serial data in (mosi) + Logic get sdo => port('sdo'); // serial data out (miso) + Logic get cs => port('cs'); // chip select + + SPIInterface() { + // Output from Controller, Input to Peripheral + setPorts([ + Port('sck'), + Port('sdi'), + Port('cs'), + ], [ + SPIDirection.controllerOutput + ]); + + // Output from Peripheral, Input to Controller + setPorts([ + Port('sdo'), + ], [ + SPIDirection.peripheralOutput + ]); + } +} + +class Controller extends Module { + late final Logic _reset; + late final Logic _sin; + + Controller(SPIInterface intf, Logic reset, Logic sin) + : super(name: 'controller') { + // set input port to private variable instead, + // we don't want other class to access this + _reset = addInput('reset', reset); + _sin = addInput('sin', sin); + + // define a new interface, and connect it + // to the interface passed in. + intf = SPIInterface() + ..connectIO( + this, + intf, + inputTags: {SPIDirection.peripheralOutput}, // Add inputs + outputTags: {SPIDirection.controllerOutput}, // Add outputs + ); + + intf.cs <= Const(1); + + Sequential(intf.sck, [ + If.block([ + Iff(_reset, [ + intf.sdi < 0, + ]), + Else([ + intf.sdi < _sin, + ]), + ]) + ]); + } +} + +class Peripheral extends Module { + Logic get sck => input('sck'); + Logic get sdi => input('sdi'); + Logic get cs => input('cs'); + + Logic get sdo => output('sdo'); + Logic get sout => output('sout'); + + late final SPIInterface shiftRegIntF; + + Peripheral(SPIInterface periIntF) : super(name: 'shift_register') { + shiftRegIntF = SPIInterface() + ..connectIO( + this, + periIntF, + inputTags: {SPIDirection.controllerOutput}, + outputTags: {SPIDirection.peripheralOutput}, + ); + + const regWidth = 8; + final data = Logic(name: 'data', width: regWidth); + final sout = addOutput('sout', width: 8); + + Sequential(sck, [ + If(shiftRegIntF.cs, then: [ + data < [data.slice(regWidth - 2, 0), sdi].swizzle() + ], orElse: [ + data < 0 + ]) + ]); + + sout <= data; + shiftRegIntF.sdo <= data.getRange(0, 1); + } +} + +class TestBench extends Module { + Logic get sout => output('sout'); + + final spiInterface = SPIInterface(); + final clk = SimpleClockGenerator(10).clk; + + TestBench(Logic reset, Logic sin) { + reset = addInput('reset', reset); + sin = addInput('sin', sin); + + final sout = addOutput('sout', width: 8); + + // ignore: unused_local_variable + final ctrl = Controller(spiInterface, reset, clk); + final peripheral = Peripheral(spiInterface); + + sout <= peripheral.sout; + } +} + +void main() async { + final testInterface = SPIInterface(); + testInterface.sck <= SimpleClockGenerator(10).clk; + + final peri = Peripheral(testInterface); + await peri.build(); + + final reset = Logic(); + final sin = Logic(); + final tb = TestBench(reset, sin); + + await tb.build(); + + print(tb.generateSynth()); + + testInterface.cs.inject(0); + testInterface.sdi.inject(0); + + void printFlop([String message = '']) { + print('@t=${Simulator.time}:\t' + ' input=${testInterface.sdi.value}, output ' + '=${peri.sout.value.toString(includeWidth: false)}\t$message'); + } + + Future drive(LogicValue val) async { + for (var i = 0; i < val.width; i++) { + peri.cs.put(1); + peri.sdi.put(val[i]); + await peri.sck.nextPosedge; + + printFlop(); + } + } + + Simulator.setMaxSimTime(100); + unawaited(Simulator.run()); + + WaveDumper(peri, outputPath: 'doc/tutorials/chapter_8/spi-new.vcd'); + + await drive(LogicValue.ofString('01010101')); +} diff --git a/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart new file mode 100644 index 000000000..9edb1afb4 --- /dev/null +++ b/doc/tutorials/chapter_8/answers/exercise_2_toycapsule_fsm.dart @@ -0,0 +1,77 @@ +// ignore_for_file: avoid_print + +import 'package:rohd/rohd.dart'; + +enum ToyCapsuleState { idle, coinInserted, dispensing } + +class ToyCapsuleFSM extends Module { + late StateMachine _state; + + ToyCapsuleFSM(Logic clk, Logic reset, Logic btnDispense, Logic coin) + : super(name: 'toy_capsule_fsm') { + clk = addInput('clk', clk); + reset = addInput(reset.name, reset); + btnDispense = addInput(btnDispense.name, btnDispense); + + final toyCapsule = addOutput('toy_capsule'); + + final states = [ + State(ToyCapsuleState.idle, events: { + coin: ToyCapsuleState.coinInserted, + }, actions: [ + toyCapsule < 0, + ]), + State(ToyCapsuleState.coinInserted, events: { + btnDispense: ToyCapsuleState.dispensing + }, actions: [ + toyCapsule < 0, + ]), + State(ToyCapsuleState.dispensing, events: { + Const(1): ToyCapsuleState.idle + }, actions: [ + toyCapsule < 1, + ]), + ]; + + _state = StateMachine(clk, reset, ToyCapsuleState.idle, states); + } + + StateMachine get toyCapsuleStateMachine => _state; + Logic get toyCapsule => output('toy_capsule'); +} + +Future main(List args) async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(name: 'reset'); + final dispenseBtn = Logic(name: 'dispense_btn'); + final coin = Logic(name: 'coin_sensor'); + + final toyCap = ToyCapsuleFSM(clk, reset, dispenseBtn, coin); + await toyCap.build(); + + print(toyCap.generateSynth()); + + toyCap.toyCapsuleStateMachine.generateDiagram(); + + reset.inject(1); + + WaveDumper(toyCap, outputPath: 'toyCapsuleFSM.vcd'); + + Simulator.setMaxSimTime(100); + Simulator.registerAction(25, () { + reset.put(0); + }); + + Simulator.registerAction(30, () { + coin.put(1); + }); + + Simulator.registerAction(35, () => dispenseBtn.put(1)); + + Simulator.registerAction(50, () { + dispenseBtn.put(0); + coin.put(0); + }); + + await Simulator.run(); +} diff --git a/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart new file mode 100644 index 000000000..ef93701b3 --- /dev/null +++ b/doc/tutorials/chapter_8/answers/exercise_3_pipeline.dart @@ -0,0 +1,54 @@ +import 'package:rohd/rohd.dart'; +import 'package:test/test.dart'; + +class Pipeline4Stages extends Module { + late final Pipeline pipeline; + + Logic get result => output('result'); + + Pipeline4Stages(Logic clk, Logic reset, Logic a) + : super(name: 'pipeline_4_stages') { + clk = addInput('clk', clk); + reset = addInput(reset.name, reset); + a = addInput(a.name, a, width: a.width); + + final result = addOutput('result', width: 64); + + pipeline = Pipeline(clk, reset: reset, resetValues: { + result: Const(0) + }, stages: [ + ...List.generate( + 4, (stage) => (p) => [p.get(a) < p.get(a) + (p.get(a) * stage)]) + ]); + + result <= pipeline.get(a).zeroExtend(result.width); + } +} + +void main(List args) async { + test('should return the the matching stage result if input is 5.', () async { + final a = Logic(name: 'a', width: 8); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final pipe = Pipeline4Stages(clk, reset, a); + await pipe.build(); + + // print(pipe.generateSynth()); + + a.inject(5); + reset.inject(1); + + Simulator.registerAction(10, () => reset.put(0)); + + WaveDumper(pipe, outputPath: 'answer_1.vcd'); + + Simulator.registerAction(50, () async { + // stage 4 / result: 30 + (30 * 3) = 120 + expect(pipe.result.value.toInt(), 120); + }); + + Simulator.setMaxSimTime(100); + await Simulator.run(); + }); +} diff --git a/doc/tutorials/chapter_8/assets/4x4-bits-Carry-Save-Multiplier-2.png b/doc/tutorials/chapter_8/assets/4x4-bits-Carry-Save-Multiplier-2.png new file mode 100644 index 000000000..87556fc8e Binary files /dev/null and b/doc/tutorials/chapter_8/assets/4x4-bits-Carry-Save-Multiplier-2.png differ diff --git a/doc/tutorials/chapter_8/assets/fsm_mermaid_output.png b/doc/tutorials/chapter_8/assets/fsm_mermaid_output.png new file mode 100644 index 000000000..e1dfbb54a Binary files /dev/null and b/doc/tutorials/chapter_8/assets/fsm_mermaid_output.png differ diff --git a/doc/tutorials/chapter_8/assets/oven_fsm.png b/doc/tutorials/chapter_8/assets/oven_fsm.png new file mode 100644 index 000000000..f6a24b6a5 Binary files /dev/null and b/doc/tutorials/chapter_8/assets/oven_fsm.png differ diff --git a/doc/tutorials/chapter_8/carry_save_multiplier.dart b/doc/tutorials/chapter_8/carry_save_multiplier.dart new file mode 100644 index 000000000..82b9521da --- /dev/null +++ b/doc/tutorials/chapter_8/carry_save_multiplier.dart @@ -0,0 +1,134 @@ +// ignore_for_file: avoid_print + +import 'package:rohd/rohd.dart'; +import '../chapter_5/n_bit_adder.dart'; + +class CarrySaveMultiplier extends Module { + // Add Input and output port for FA sum and Carry + final List sum = + List.generate(8, (index) => Logic(name: 'sum_$index')); + final List carry = + List.generate(8, (index) => Logic(name: 'carry_$index')); + + late final Pipeline pipeline; + CarrySaveMultiplier(Logic valA, Logic valB, Logic clk, Logic reset, + {super.name = 'carry_save_multiplier'}) { + // Declare Input Node + valA = addInput('a', valA, width: valA.width); + valB = addInput('b', valB, width: valB.width); + clk = addInput('clk', clk); + reset = addInput('reset', reset); + + final product = addOutput('product', width: valA.width + valB.width + 1); + final rCarryA = Logic(name: 'rcarry_a', width: valA.width); + final rCarryB = Logic(name: 'rcarry_b', width: valB.width); + + pipeline = Pipeline( + clk, + stages: [ + ...List.generate( + valB.width, + (row) => (p) { + final columnAdder = []; + final maxIndexA = (valA.width - 1) + row; + + for (var column = maxIndexA; column >= row; column--) { + final fullAdder = FullAdder( + a: column == maxIndexA || row == 0 + ? Const(0) + : p.get(sum[column]), + b: p.get(valA)[column - row] & p.get(valB)[row], + carryIn: row == 0 ? Const(0) : p.get(carry[column - 1])) + .fullAdderRes; + + columnAdder + ..add(p.get(carry[column]) < fullAdder.cOut) + ..add(p.get(sum[column]) < fullAdder.sum); + } + + return columnAdder; + }, + ), + (p) => [ + p.get(rCarryA) < + [ + Const(0), + ...List.generate( + valA.width - 1, + (index) => + p.get(sum[(valA.width + valB.width - 2) - index])) + ].swizzle(), + p.get(rCarryB) < + [ + ...List.generate( + valA.width, + (index) => + p.get(carry[(valA.width + valB.width - 2) - index])) + ].swizzle() + ], + ], + reset: reset, + resetValues: {product: Const(0)}, + ); + + final nBitAdder = NBitAdder( + pipeline.get(rCarryA), + pipeline.get(rCarryB), + ); + + product <= + [ + ...List.generate( + valA.width + 1, + (index) => nBitAdder.sum[(valA.width) - index], + ), + ...List.generate( + valA.width, + (index) => pipeline.get(sum[valA.width - index - 1]), + ) + ].swizzle(); + } + + Logic get product => output('product'); +} + +void main() async { + final a = Logic(name: 'a', width: 4); + final b = Logic(name: 'b', width: 4); + final reset = Logic(name: 'reset'); + final clk = SimpleClockGenerator(10).clk; + + final csm = CarrySaveMultiplier(a, b, clk, reset); + + await csm.build(); + + // after one cycle, change the value of a and b + a.inject(10); + b.inject(14); + reset.inject(1); + + // Attach a waveform dumper so we can see what happens. + WaveDumper(csm, outputPath: 'csm.vcd'); + + Simulator.registerAction(10, () { + reset.inject(0); + }); + + Simulator.registerAction(30, () { + a.put(10); + b.put(11); + }); + + Simulator.registerAction(60, () { + a.put(10); + b.put(6); + }); + + csm.product.changed.listen((event) { + print('@t=${Simulator.time}, product is: ${event.newValue.toInt()}'); + }); + + Simulator.setMaxSimTime(150); + + await Simulator.run(); +} diff --git a/doc/tutorials/chapter_8/counter_interface.dart b/doc/tutorials/chapter_8/counter_interface.dart new file mode 100644 index 000000000..06f265a8b --- /dev/null +++ b/doc/tutorials/chapter_8/counter_interface.dart @@ -0,0 +1,84 @@ +// ignore_for_file: avoid_print + +import 'package:rohd/rohd.dart'; + +enum CounterDirection { inward, outward, misc } + +class CounterInterface extends Interface { + Logic get en => port('en'); + Logic get reset => port('reset'); + Logic get val => port('val'); + Logic get clk => port('clk'); + + final int width; + CounterInterface({this.width = 8}) { + setPorts([ + Port('en'), + Port('reset'), + ], [ + CounterDirection.inward + ]); + + setPorts([ + Port('val', width), + ], [ + CounterDirection.outward + ]); + + setPorts([ + Port('clk'), + ], [ + CounterDirection.misc + ]); + } +} + +class Counter extends Module { + late final CounterInterface _intf; + + Counter(CounterInterface intf) : super(name: 'counter') { + _intf = CounterInterface(width: intf.width) + ..connectIO(this, intf, + inputTags: {CounterDirection.inward, CounterDirection.misc}, + outputTags: {CounterDirection.outward}); + + final nextVal = Logic(name: 'nextVal', width: intf.width); + + nextVal <= _intf.val + 1; + + Sequential(_intf.clk, [ + If.block([ + Iff(_intf.reset, [ + _intf.val < 0, + ]), + ElseIf(_intf.en, [ + _intf.val < nextVal, + ]) + ]), + ]); + } +} + +Future main() async { + final intf = CounterInterface(); + intf.clk <= SimpleClockGenerator(10).clk; + intf.en.inject(0); + intf.reset.inject(1); + + final counter = Counter(intf); + + await counter.build(); + + print(counter.generateSynth()); + + WaveDumper(counter, + outputPath: 'doc/tutorials/chapter_8/counter_interface.vcd'); + Simulator.registerAction(25, () { + intf.en.put(1); + intf.reset.put(0); + }); + + Simulator.setMaxSimTime(100); + + await Simulator.run(); +} diff --git a/doc/tutorials/chapter_8/oven_fsm.dart b/doc/tutorials/chapter_8/oven_fsm.dart new file mode 100644 index 000000000..939e033c0 --- /dev/null +++ b/doc/tutorials/chapter_8/oven_fsm.dart @@ -0,0 +1,245 @@ +// ignore_for_file: avoid_print + +// Import the ROHD package. +import 'package:rohd/rohd.dart'; + +// Import the counter module interface. +import './counter_interface.dart'; + +// Enumerated type named `OvenState` with four possible states: +// `standby`, `cooking`,`paused`, and `completed`. +enum OvenState { standby, cooking, paused, completed } + +// Define a class OvenModule that extends ROHD's abstract Module class. +class OvenModule extends Module { + // A private variable with type StateMachine `_oven`. + // + // Use `late` to indicate that the value will not be null + // and will be assign in the later section. + late StateMachine _oven; + + // A hashmap that represent button value + final Map btnVal = { + 'start': 0, + 'pause': 1, + 'resume': 2, + }; + + // A hashmap that represent LED value + final Map ledLight = { + 'yellow': 0, + 'blue': 1, + 'red': 2, + 'green': 3 + }; + + // We can expose an LED light output as a getter to retrieve it value. + Logic get led => output('led'); + + // This oven module receives a `button` and a `reset` input from runtime. + OvenModule(Logic button, Logic reset) : super(name: 'OvenModule') { + // Register inputs and outputs of the module in the constructor. + // Module logic must consume registered inputs and output to registered + // outputs. `led` output also added as the output port. + button = addInput('button', button, width: button.width); + reset = addInput('reset', reset); + final led = addOutput('led', width: 8); + + // An internal clock generator. + final clk = SimpleClockGenerator(10).clk; + + // Register local signals, `counterReset` and `en` + // for Counter module. + final counterReset = Logic(name: 'counter_reset'); + final en = Logic(name: 'counter_en'); + + // An internal counter module that will be used to time the cooking state. + // Receive `en`, `counterReset` and `clk` as input. + final counterInterface = CounterInterface(); + counterInterface.clk <= clk; + counterInterface.en <= en; + counterInterface.reset <= counterReset; + + // A list of `OvenState` that describe the FSM. Note that + // `OvenState` consists of identifier, events and actions. We + // can think of `identifier` as the state name, `events` is a map of event + // that trigger next state. `actions` is the behaviour of current state, + // like what is the actions need to be shown separate current state with + // other state. Represented as List of conditionals to be executed. + final states = [ + // identifier: standby state, represent by `OvenState.standby`. + State(OvenState.standby, + // events: + // When the button `start` is pressed during standby state, + // OvenState will changed to `OvenState.cooking` state. + events: { + Logic(name: 'button_start') + ..gets( + button.eq(Const(btnVal['start'], width: button.width))): + OvenState.cooking, + }, + // actions: + // During the standby state, `led` is change to blue; timer's + // `counterReset` is set to 1 (Reset the timer); + // timer's `en` is set to 0 (Disable value update). + actions: [ + led < ledLight['blue'], + counterReset < 1, + en < 0, + ]), + + // identifier: cooking state, represent by `OvenState.cooking`. + State(OvenState.cooking, + // events: + // When the button `paused` is pressed during cooking state, + // OvenState will changed to `OvenState.paused` state. + // + // When the button `counter` time is elapsed during cooking state, + // OvenState will changed to `OvenState.completed` state. + events: { + Logic(name: 'button_pause') + ..gets( + button.eq(Const(btnVal['pause'], width: button.width))): + OvenState.paused, + Logic(name: 'counter_time_complete') + ..gets(counterInterface.val.eq(4)): OvenState.completed + }, + // actions: + // During the cooking state, `led` is change to yellow; timer's + // `counterReset` is set to 0 (Do not reset); + // timer's `en` is set to 1 (Enable value update). + actions: [ + led < ledLight['yellow'], + counterReset < 0, + en < 1, + ]), + + // identifier: paused state, represent by `OvenState.paused`. + State(OvenState.paused, + // events: + // When the button `resume` is pressed during paused state, + // OvenState will changed to `OvenState.cooking` state. + events: { + Logic(name: 'button_resume') + ..gets( + button.eq(Const(btnVal['resume'], width: button.width))): + OvenState.cooking + }, + // actions: + // During the paused state, `led` is change to red; timer's + // `counterReset` is set to 0 (Do not reset); + // timer's `en` is set to 0 (Disable value update). + actions: [ + led < ledLight['red'], + counterReset < 0, + en < 0, + ]), + + // identifier: completed state, represent by `OvenState.completed`. + State(OvenState.completed, + // events: + // When the button `start` is pressed during completed state, + // OvenState will changed to `OvenState.standby` state. + events: { + Logic(name: 'button_start') + ..gets( + button.eq(Const(btnVal['start'], width: button.width))): + OvenState.standby + }, + // actions: + // During the start state, `led` is change to green; timer's + // `counterReset` is set to 1 (Reset value); + // timer's `en` is set to 0 (Disable value update). + actions: [ + led < ledLight['green'], + counterReset < 1, + en < 0, + ]) + ]; + + // Assign the _oven StateMachine object to private variable declared. + _oven = StateMachine(clk, reset, OvenState.standby, states); + + // Generate a Mermaid FSM diagram and save as the name `oven_fsm.md`. + // Note that the extension of the files is recommend as .md or .mmd. + // + // Check on https://mermaid.js.org/intro/ to view the diagram generated. + // If you are using vscode, you can download the mermaid extension. + _oven.generateDiagram(outputPath: 'doc/tutorials/chapter_8/oven_fsm.md'); + } +} + +Future main({bool noPrint = false}) async { + // Signals `button` and `reset` that mimic user's behaviour of button pressed + // and reset. + // + // Width of button is 2 because button is represent by 2-bits signal. + final button = Logic(name: 'button', width: 2); + final reset = Logic(name: 'reset'); + + // Build an Oven Module and passed the `button` and `reset`. + final oven = OvenModule(button, reset); + + // Before we can simulate or generate code with the counter, we need + // to build it. + await oven.build(); + + // Now let's try simulating! + + // Let's start off with asserting reset to Oven. + reset.inject(1); + + // Attach a waveform dumper so we can see what happens. + if (!noPrint) { + WaveDumper(oven, outputPath: 'doc/tutorials/chapter_8/oven.vcd'); + } + + if (!noPrint) { + // We can listen to the streams on LED light changes based on time. + oven.led.changed.listen((event) { + // Get the led light enum name from LogicValue. + final ledVal = oven.ledLight.keys.firstWhere( + (element) => oven.ledLight[element] == event.newValue.toInt()); + + // Print the Simulator time when the LED light changes. + print('@t=${Simulator.time}, LED changed to: $ledVal'); + }); + } + + // Drop reset at time 25. + Simulator.registerAction(25, () => reset.put(0)); + + // Press button start => `00` at time 25. + Simulator.registerAction(25, () { + button.put(oven.btnVal['start']); + }); + + // Press button pause => `01` at time 50. + Simulator.registerAction(50, () { + button.put(oven.btnVal['pause']); + }); + + // Press button resume => `10` at time 70. + Simulator.registerAction(70, () { + button.put(oven.btnVal['resume']); + }); + + // Print a message when we're done with the simulation! + Simulator.registerAction(120, () { + if (!noPrint) { + print('Simulation completed!'); + } + }); + + // Set a maximum time for the simulation so it doesn't keep running forever. + Simulator.setMaxSimTime(120); + + // Kick off the simulation. + await Simulator.run(); + + // We can take a look at the waves now + if (!noPrint) { + print('To view waves, check out waves.vcd with a' + ' waveform viewer (e.g. `gtkwave waves.vcd`).'); + } +}