summaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorjackkoenig2016-10-26 16:40:36 -0700
committerJack Koenig2017-02-02 22:53:03 -0800
commitdd51b917566e6b30c3f123ca22a0393e73c2afe8 (patch)
treef81fce104ccc3405c7924c76618483750a4350bb /src/test
parentb0a328492383108509c322189ed2803f671d7a59 (diff)
Revamp VendingMachine.scala as cookbook example
* Move to cookbook * Change FSM implementation to use switch & is * Add non-FSM implementation * Add execution-driven test
Diffstat (limited to 'src/test')
-rw-r--r--src/test/resources/VerilogVendingMachine.v44
-rw-r--r--src/test/scala/chiselTests/VendingMachine.scala69
-rw-r--r--src/test/scala/cookbook/FSM.scala2
-rw-r--r--src/test/scala/examples/ImplicitStateVendingMachine.scala31
-rw-r--r--src/test/scala/examples/SimpleVendingMachine.scala95
-rw-r--r--src/test/scala/examples/VendingMachineGenerator.scala127
-rw-r--r--src/test/scala/examples/VendingMachineUtils.scala39
7 files changed, 337 insertions, 70 deletions
diff --git a/src/test/resources/VerilogVendingMachine.v b/src/test/resources/VerilogVendingMachine.v
new file mode 100644
index 00000000..c01259bd
--- /dev/null
+++ b/src/test/resources/VerilogVendingMachine.v
@@ -0,0 +1,44 @@
+// See LICENSE for license details.
+
+// A simple Verilog FSM vending machine implementation
+module VerilogVendingMachine(
+ input clock,
+ input reset,
+ input nickel,
+ input dime,
+ output dispense
+);
+ parameter sIdle = 3'd0, s5 = 3'd1, s10 = 3'd2, s15 = 3'd3, sOk = 3'd4;
+ reg [2:0] state;
+ wire [2:0] next_state;
+
+ assign dispense = (state == sOk) ? 1'd1 : 1'd0;
+
+ always @(*) begin
+ case (state)
+ sIdle: if (nickel) next_state <= s5;
+ else if (dime) next_state <= s10;
+ else next_state <= state;
+ s5: if (nickel) next_state <= s10;
+ else if (dime) next_state <= s15;
+ else next_state <= state;
+ s10: if (nickel) next_state <= s15;
+ else if (dime) next_state <= sOk;
+ else next_state <= state;
+ s15: if (nickel) next_state <= sOk;
+ else if (dime) next_state <= sOk;
+ else next_state <= state;
+ sOk: next_state <= sIdle;
+ endcase
+ end
+
+ // Go to next state
+ always @(posedge clock) begin
+ if (reset) begin
+ state <= sIdle;
+ end else begin
+ state <= next_state;
+ end
+ end
+endmodule
+
diff --git a/src/test/scala/chiselTests/VendingMachine.scala b/src/test/scala/chiselTests/VendingMachine.scala
deleted file mode 100644
index 712b5b7a..00000000
--- a/src/test/scala/chiselTests/VendingMachine.scala
+++ /dev/null
@@ -1,69 +0,0 @@
-// See LICENSE for license details.
-
-package chiselTests
-
-import chisel3._
-import chisel3.util._
-
-class VendingMachine extends Module {
- val io = IO(new Bundle {
- val nickel = Input(Bool())
- val dime = Input(Bool())
- val valid = Output(Bool())
- })
- val c = 5.U(3.W)
- val sIdle :: s5 :: s10 :: s15 :: sOk :: Nil = Enum(5)
- val state = Reg(init = sIdle)
- when (state === sIdle) {
- when (io.nickel) { state := s5 }
- when (io.dime) { state := s10 }
- }
- when (state === s5) {
- when (io.nickel) { state := s10 }
- when (io.dime) { state := s15 }
- }
- when (state === s10) {
- when (io.nickel) { state := s15 }
- when (io.dime) { state := sOk }
- }
- when (state === s15) {
- when (io.nickel) { state := sOk }
- when (io.dime) { state := sOk }
- }
- when (state === sOk) {
- state := sIdle
- }
- io.valid := (state === sOk)
-}
-
-/*
-class VendingMachineTester(c: VendingMachine) extends Tester(c) {
- var money = 0
- var isValid = false
- for (t <- 0 until 20) {
- val coin = rnd.nextInt(3)*5
- val isNickel = coin == 5
- val isDime = coin == 10
-
- // Advance circuit
- poke(c.io.nickel, int(isNickel))
- poke(c.io.dime, int(isDime))
- step(1)
-
- // Advance model
- money = if (isValid) 0 else (money + coin)
- isValid = money >= 20
-
- // Compare
- expect(c.io.valid, int(isValid))
- }
-}
-*/
-
-class VendingMachineSpec extends ChiselPropSpec {
- property("VendingMachine should elaborate") {
- elaborate { new VendingMachine }
- }
-
- ignore("VendingMachineTester should return the correct result") { }
-}
diff --git a/src/test/scala/cookbook/FSM.scala b/src/test/scala/cookbook/FSM.scala
index 58f6a9a2..45359c9e 100644
--- a/src/test/scala/cookbook/FSM.scala
+++ b/src/test/scala/cookbook/FSM.scala
@@ -6,7 +6,7 @@ import chisel3._
import chisel3.util._
/* ### How do I create a finite state machine?
-
+ *
* Use Chisel Enum to construct the states and switch & is to construct the FSM
* control logic
*/
diff --git a/src/test/scala/examples/ImplicitStateVendingMachine.scala b/src/test/scala/examples/ImplicitStateVendingMachine.scala
new file mode 100644
index 00000000..ae1e28dd
--- /dev/null
+++ b/src/test/scala/examples/ImplicitStateVendingMachine.scala
@@ -0,0 +1,31 @@
+// See LICENSE for license details.
+
+package examples
+
+import chiselTests.ChiselFlatSpec
+import chisel3._
+
+// Vending machine implemented with an implicit state machine
+class ImplicitStateVendingMachine extends SimpleVendingMachine {
+ // We let the value of nickel be 1 and dime be 2 for efficiency reasons
+ val value = Reg(init = 0.asUInt(3.W))
+ val incValue = Wire(init = 0.asUInt(3.W))
+ val doDispense = value >= 4.U // 4 * nickel as 1 == $0.20
+
+ when (doDispense) {
+ value := 0.U // No change given
+ } .otherwise {
+ value := value + incValue
+ }
+
+ when (io.nickel) { incValue := 1.U }
+ when (io.dime) { incValue := 2.U }
+
+ io.dispense := doDispense
+}
+
+class ImplicitStateVendingMachineSpec extends ChiselFlatSpec {
+ "An vending machine implemented with implicit state" should "work" in {
+ assertTesterPasses { new SimpleVendingMachineTester(new ImplicitStateVendingMachine) }
+ }
+}
diff --git a/src/test/scala/examples/SimpleVendingMachine.scala b/src/test/scala/examples/SimpleVendingMachine.scala
new file mode 100644
index 00000000..d935b611
--- /dev/null
+++ b/src/test/scala/examples/SimpleVendingMachine.scala
@@ -0,0 +1,95 @@
+// See LICENSE for license details.
+
+package examples
+
+import chiselTests.ChiselFlatSpec
+import chisel3.testers.BasicTester
+import chisel3._
+import chisel3.util._
+
+class SimpleVendingMachineIO extends Bundle {
+ val nickel = Input(Bool())
+ val dime = Input(Bool())
+ val dispense = Output(Bool())
+}
+
+// Superclass for vending machines with very simple IO
+abstract class SimpleVendingMachine extends Module {
+ val io = IO(new SimpleVendingMachineIO)
+ assert(!(io.nickel && io.dime), "Only one of nickel or dime can be input at a time!")
+}
+
+// Vending machine implemented with a Finite State Machine
+class FSMVendingMachine extends SimpleVendingMachine {
+ val sIdle :: s5 :: s10 :: s15 :: sOk :: Nil = Enum(5)
+ val state = Reg(init = sIdle)
+
+ switch (state) {
+ is (sIdle) {
+ when (io.nickel) { state := s5 }
+ when (io.dime) { state := s10 }
+ }
+ is (s5) {
+ when (io.nickel) { state := s10 }
+ when (io.dime) { state := s15 }
+ }
+ is (s10) {
+ when (io.nickel) { state := s15 }
+ when (io.dime) { state := sOk }
+ }
+ is (s15) {
+ when (io.nickel) { state := sOk }
+ when (io.dime) { state := sOk }
+ }
+ is (sOk) {
+ state := sIdle
+ }
+ }
+ io.dispense := (state === sOk)
+}
+
+class VerilogVendingMachine extends BlackBox {
+ // Because this is a blackbox, we must explicity add clock and reset
+ val io = IO(new SimpleVendingMachineIO {
+ val clock = Input(Clock())
+ val reset = Input(Bool())
+ })
+}
+
+// Shim because Blackbox io is slightly different than normal Chisel Modules
+class VerilogVendingMachineWrapper extends SimpleVendingMachine {
+ val impl = Module(new VerilogVendingMachine)
+ impl.io.clock := clock
+ impl.io.reset := reset
+ impl.io.nickel := io.nickel
+ impl.io.dime := io.dime
+ io.dispense := impl.io.dispense
+}
+
+// Accept a reference to a SimpleVendingMachine so it can be constructed inside
+// the tester (in a call to Module.apply as required by Chisel
+class SimpleVendingMachineTester(mod: => SimpleVendingMachine) extends BasicTester {
+
+ val dut = Module(mod)
+
+ val (cycle, done) = Counter(true.B, 10)
+ when (done) { stop(); stop() } // Stop twice because of Verilator
+
+ val nickelInputs = Vec(true.B, true.B, true.B, true.B, true.B, false.B, false.B, false.B, true.B, false.B)
+ val dimeInputs = Vec(false.B, false.B, false.B, false.B, false.B, true.B, true.B, false.B, false.B, true.B)
+ val expected = Vec(false.B, false.B, false.B, false.B, true.B , false.B, false.B, true.B, false.B, false.B)
+
+ dut.io.nickel := nickelInputs(cycle)
+ dut.io.dime := dimeInputs(cycle)
+ assert(dut.io.dispense === expected(cycle))
+}
+
+class SimpleVendingMachineSpec extends ChiselFlatSpec {
+ "An FSM implementation of a vending machine" should "work" in {
+ assertTesterPasses { new SimpleVendingMachineTester(new FSMVendingMachine) }
+ }
+ "An Verilog implementation of a vending machine" should "work" in {
+ assertTesterPasses(new SimpleVendingMachineTester(new VerilogVendingMachineWrapper),
+ List("/VerilogVendingMachine.v"))
+ }
+}
diff --git a/src/test/scala/examples/VendingMachineGenerator.scala b/src/test/scala/examples/VendingMachineGenerator.scala
new file mode 100644
index 00000000..fc44a47e
--- /dev/null
+++ b/src/test/scala/examples/VendingMachineGenerator.scala
@@ -0,0 +1,127 @@
+// See LICENSE for license details.
+
+package examples
+
+import chiselTests.ChiselFlatSpec
+import chisel3.testers.BasicTester
+import chisel3._
+import chisel3.util._
+
+import VendingMachineUtils._
+
+class VendingMachineIO(legalCoins: Seq[Coin]) extends Bundle {
+ require(legalCoins.size >= 1, "The vending machine must accept at least 1 coin!")
+ // Order of coins by value
+ val coins: Seq[Coin] = legalCoins sortBy (_.value)
+ // Map of coin names to their relative position in value (ie. index in inputs)
+ val indexMap: Map[String, Int] = coins.map(_.name).zipWithIndex.toMap
+
+ require(coins map (_.value % coins.head.value == 0) reduce (_ && _),
+ "All coins must be a multiple of the lowest value coin!")
+
+ val inputs = Input(Vec(legalCoins.size, Bool()))
+ val dispense = Output(Bool())
+
+ def apply(coin: String): Unit = {
+ val idx = indexMap(coin)
+ inputs(idx) := true.B
+ }
+}
+
+// Superclass for parameterized vending machines
+abstract class ParameterizedVendingMachine(legalCoins: Seq[Coin], val sodaCost: Int) extends Module {
+ val io = IO(new VendingMachineIO(legalCoins))
+ // Enforce one hot
+ if (io.inputs.size > 1) {
+ for (input <- io.inputs) {
+ when (input) {
+ assert(io.inputs.filterNot(_ == input).map(!_).reduce(_ && _),
+ "Only 1 coin can be input in a given cycle!")
+ }
+ }
+ }
+}
+
+class VendingMachineGenerator(
+ legalCoins: Seq[Coin],
+ sodaCost: Int) extends ParameterizedVendingMachine(legalCoins, sodaCost) {
+ require(sodaCost > 0, "Sodas must actually cost something!")
+
+ // All coin values are normalized to a multiple of the minimum coin value
+ val minCoin = io.coins.head.value
+ val maxCoin = io.coins.last.value
+ val maxValue = (sodaCost + maxCoin - minCoin) / minCoin // normalize to minimum value
+
+ val width = log2Up(maxValue + 1).W
+ val value = Reg(init = 0.asUInt(width))
+ val incValue = Wire(init = 0.asUInt(width))
+ val doDispense = value >= (sodaCost / minCoin).U
+
+ when (doDispense) {
+ value := 0.U // No change given
+ } .otherwise {
+ value := value + incValue
+ }
+
+ for ((coin, index) <- io.coins.zipWithIndex) {
+ when (io.inputs(index)) { incValue := (coin.value / minCoin).U }
+ }
+ io.dispense := doDispense
+}
+
+class ParameterizedVendingMachineTester(
+ mod: => ParameterizedVendingMachine,
+ testLength: Int) extends BasicTester {
+ require(testLength > 0, "Test length must be positive!")
+
+ // Construct the module
+ val dut = Module(mod)
+ val coins = dut.io.coins
+
+ // Inputs and expected results
+ // Do random testing
+ private val _rand = scala.util.Random
+ val inputs: Seq[Option[Coin]] = Seq.fill(testLength)(coins.lift(_rand.nextInt(coins.size + 1)))
+ val expected: Seq[Boolean] = getExpectedResults(inputs, dut.sodaCost)
+
+ val inputVec: Vec[UInt] = Vec(inputs map {
+ case Some(coin) => (1 << dut.io.indexMap(coin.name)).asUInt(coins.size.W)
+ case None => 0.asUInt(coins.size.W)
+ })
+ val expectedVec: Vec[Bool] = Vec(expected map (_.B))
+
+ val (idx, done) = Counter(true.B, testLength + 1)
+ when (done) { stop(); stop() } // Two stops for Verilator
+
+ dut.io.inputs := inputVec(idx).toBools
+ assert(dut.io.dispense === expectedVec(idx))
+}
+
+class VendingMachineGeneratorSpec extends ChiselFlatSpec {
+ behavior of "The vending machine generator"
+
+ it should "generate a vending machine that accepts only nickels and dimes and costs $0.20" in {
+ val coins = Seq(Nickel, Dime)
+ assertTesterPasses {
+ new ParameterizedVendingMachineTester(new VendingMachineGenerator(coins, 20), 100)
+ }
+ }
+ it should "generate a vending machine that only accepts one kind of coin" in {
+ val coins = Seq(Nickel)
+ assertTesterPasses {
+ new ParameterizedVendingMachineTester(new VendingMachineGenerator(coins, 30), 100)
+ }
+ }
+ it should "generate a more realistic vending machine that costs $1.50" in {
+ val coins = Seq(Penny, Nickel, Dime, Quarter)
+ assertTesterPasses {
+ new ParameterizedVendingMachineTester(new VendingMachineGenerator(coins, 150), 100)
+ }
+ }
+ it should "generate a Harry Potter themed vending machine" in {
+ val coins = Seq(Knut, Sickle) // Galleons are worth too much
+ assertTesterPasses {
+ new ParameterizedVendingMachineTester(new VendingMachineGenerator(coins, Galleon.value), 100)
+ }
+ }
+}
diff --git a/src/test/scala/examples/VendingMachineUtils.scala b/src/test/scala/examples/VendingMachineUtils.scala
new file mode 100644
index 00000000..e229dab5
--- /dev/null
+++ b/src/test/scala/examples/VendingMachineUtils.scala
@@ -0,0 +1,39 @@
+// See LICENSE for license details.
+
+package examples
+
+import scala.collection.mutable
+
+/* Useful utilities for testing vending machines */
+object VendingMachineUtils {
+ abstract class Coin(val name: String, val value: Int)
+ // US Coins
+ case object Penny extends Coin("penny", 1)
+ case object Nickel extends Coin("nickel", 5)
+ case object Dime extends Coin("dime", 10)
+ case object Quarter extends Coin("quarter", 25)
+
+ // Harry Potter Coins
+ case object Knut extends Coin("knut", Penny.value * 2) // Assuming 1 Knut is worth $0.02
+ case object Sickle extends Coin("sickle", Knut.value * 29)
+ case object Galleon extends Coin("galleon", Sickle.value * 17)
+
+ def getExpectedResults(inputs: Seq[Option[Coin]], sodaCost: Int): Seq[Boolean] = {
+ var value = 0
+ val outputs = mutable.ArrayBuffer.empty[Boolean]
+ for (input <- inputs) {
+ val incValue = input match {
+ case Some(coin) => coin.value
+ case None => 0
+ }
+ if (value >= sodaCost) {
+ outputs.append(true)
+ value = 0
+ } else {
+ outputs.append(false)
+ value += incValue
+ }
+ }
+ outputs
+ }
+}