summaryrefslogtreecommitdiff
path: root/src/test/scala/examples/VendingMachineGenerator.scala
blob: 4adae987630929628137606b663ffbaefa1195b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// SPDX-License-Identifier: Apache-2.0

package examples

import chiselTests.ChiselFlatSpec
import chisel3.testers.BasicTester
import chisel3._
import chisel3.util._

import VendingMachineUtils._

class VendingMachineIO(val 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 = log2Ceil(maxValue + 1).W
  val value = RegInit(0.asUInt(width))
  val incValue = WireDefault(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] = VecInit(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] = VecInit(expected.map(_.B))

  val (idx, done) = Counter(true.B, testLength + 1)
  when(done) { stop(); stop() } // Two stops for Verilator

  dut.io.inputs := inputVec(idx).asBools
  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)
    }
  }
}