diff options
| author | jackkoenig | 2016-10-26 16:40:36 -0700 |
|---|---|---|
| committer | Jack Koenig | 2017-02-02 22:53:03 -0800 |
| commit | dd51b917566e6b30c3f123ca22a0393e73c2afe8 (patch) | |
| tree | f81fce104ccc3405c7924c76618483750a4350bb /src/test | |
| parent | b0a328492383108509c322189ed2803f671d7a59 (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.v | 44 | ||||
| -rw-r--r-- | src/test/scala/chiselTests/VendingMachine.scala | 69 | ||||
| -rw-r--r-- | src/test/scala/cookbook/FSM.scala | 2 | ||||
| -rw-r--r-- | src/test/scala/examples/ImplicitStateVendingMachine.scala | 31 | ||||
| -rw-r--r-- | src/test/scala/examples/SimpleVendingMachine.scala | 95 | ||||
| -rw-r--r-- | src/test/scala/examples/VendingMachineGenerator.scala | 127 | ||||
| -rw-r--r-- | src/test/scala/examples/VendingMachineUtils.scala | 39 |
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 + } +} |
