0%

2.1 Chisel Module

Module

1
2
3
4
5
6
7
8
9
10
11
// a class extends to built-in Chisel class 'Module'
class Passthrough extends Module {
// io: must be called 'io' and be an 'IO' object or instance
val io = IO(new Bundle {
// Type: Uint, Width: 4
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
// ':=' is a Chisel operator (assign)
io.out := io.in
}

Module Generator

1
2
3
4
5
6
7
8
9
10
11
// parameterize
class PassthroughGenerator(width: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(width.W))
val out = Output(UInt(width.W))
})
io.out := io.in
}
// generate module with different widths
println(getVerilog(new PassthroughGenerator(10)))
println(getVerilog(new PassthroughGenerator(20)))

Tester

poke, expect, peek

1
2
3
4
5
6
7
8
test(new Passthrough()) { c =>
c.io.in.poke(0.U) // Set our input to value 0
c.io.out.expect(0.U) // Assert that the output correctly has 0
c.io.in.poke(1.U) // Set our input to value 1
c.io.out.expect(1.U) // Assert that the output correctly has 1
}
println("SUCCESS!!")
println(c.io.out.peek())

Generate Verilog/ firrtl

1
2
println(getVerilog(new Passthrough))
println(getFirrtl(new Passthrough))

println/ printf/ log

scala print

  • s 插值器

    在任何字符串前加上s,就可以直接在串中使用变量了。

    1
    2
    3
    val name="James"
    println(s"Hello,$name")
    println(s"1+1=${1+1}")
  • f 插值器

    在任何字符串字面前加上 f,就可以生成简单的格式化串。

    1
    2
    3
    val height=1.9d
    val name="James"
    println(f"$name%s is $height%2.2f meters tall")
    • raw 插值器

除了对字面值中的字符不做编码外,raw 插值器与 s 插值器在功能上是相同的。

1
2
3
println(s"a\nb")	// a
// b
println(raw"a\nb") // a\nb

chisel print

  • Chisel generator prints during circuit generation
  • Circuit prints during circuit simulation
  • Tester prints during testing
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
class PrintingModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
io.out := io.in

// chisel p interpolator
// only when simulation, print every setp(1)
printf(p"Print during simulation: IO is $io\n")

// only when simulation, print every setp(1)
printf("Print during simulation: Input is %d\n", io.in)

// only when generation
// println is a built-in Scala function that prints to the console.
// It cannot be used to print during circuit simulation,
// because the generated circuit is FIRRTL or Verilog- not Scala.
println(s"Print during generation: Input is ${io.in}")
}

test(new PrintingModule ) { c =>
c.io.in.poke(3.U)
c.clock.step(2) // circuit will print

// only when testing
println(s"Print during testing: Input is ${c.io.in.peek()}")
}
1
2
3
4
5
6
7
8
9
10
11
Elaborating design...
Print during generation: Input is UInt<4>(IO in unelaborated PrintingModule)
Done elaborating.
Print during simulation: Input is 3
Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
Print during simulation: Input is 3
Print during simulation: IO is AnonymousBundle(in -> 3, out -> 3)
Print during testing: Input is UInt<4>(3)
Print during simulation: Input is 0
Print during simulation: IO is AnonymousBundle(in -> 0, out -> 0)
test PrintingModule Success: 0 tests passed in 4 cycles in 0.020066 seconds 199.34 Hz

2.2 Combinational Logic

Operator

  • Scala and Chisel Operators Look the Same
1
2
3
4
5
6
7
8
// scala
val two = 1 + 1 // Scala: Int + Int
println(two) // 2
// chisel: hardware node
val utwo = 1.U + 1.U // Chisel: Uint + Uint
println(utwo) // UInt<1>(OpResult in MyModule)
// error
val ertwo = 1 + 1.U
  • Width
1
2
3
4
5
io.out_add    := 1.U + 4.U        // max(w(x),w(y))
io.out_add1 := 1.U +& 4.U // max(w(x),w(y))+1
io.out_sub := 2.U - 1.U // max(w(x),w(y))
io.out_sub1 := 1.U -& 4.U // max(w(x),w(y))+1
io.out_mul := 4.U * 2.U // w(x)+w(y)
  • Mux & Cat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyOperatorsTwo extends Module {
val io = IO(new Bundle {
val in = Input(Bool())
val out_mux = Output(UInt(4.W))
val out_cat = Output(UInt(4.W))
})

// Generate Verilog containing constants instead of actual mux or concatenation logic
// FIRRTL transformations have simplified the circuit, eliminating obvious logic.
val s = true.B
io.out_mux := Mux(s, 3.U, 0.U)
// assign io_out_mux = 4'h3;

// actual mux
io.out_mux := Mux(io.in, 3.U, 0.U)
// wire [1:0] _T = io_in ? 2'h3 : 2'h0;
// assign io_out_mux = {{2'd0}, _T};

// concatenates 2 (b10) with 1 (b1) to give 5 (101)
io.out_cat := Cat(2.U, 1.U)
// assign io_out_cat = 4'h5
}
  • More operators

Example

Arbiter

The following circuit arbitrates data coming from a FIFO into two parallel processing units. The FIFO and processing elements (PEs) communicate with ready-valid interfaces. Construct the arbiter to send data to whichever PE is ready to receive data, prioritizing PE0 if both are ready to receive data. Remember that the arbiter should tell the FIFO that it’s ready to receive data when at least one of the PEs can receive data. Also, wait for a PE to assert that it’s ready before asserting that the data are valid. You will likely need binary operators to complete this exercise.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Arbiter extends Module {
val io = IO(new Bundle {
// FIFO
val fifo_valid = Input(Bool())
val fifo_ready = Output(Bool())
val fifo_data = Input(UInt(16.W))

// PE0
val pe0_valid = Output(Bool())
val pe0_ready = Input(Bool())
val pe0_data = Output(UInt(16.W))

// PE1
val pe1_valid = Output(Bool())
val pe1_ready = Input(Bool())
val pe1_data = Output(UInt(16.W))
})

io.fifo_ready := io.pe0_ready || io.pe1_ready
io.pe0_valid := io.fifo_valid && io.pe0_ready
io.pe1_valid := io.fifo_valid && io.pe1_ready && !io.pe0_ready
io.pe0_data := io.fifo_data
io.pe1_data := io.fifo_data
}

Paramiterized Adder

This optional exercise exposes you to one of the most powerful features of Chisel, it’s parameterization capabilities. To demonstrate this, we’ll construct a parameterized adder that can either saturate the output when overflow occurs, or truncate the results (i.e. wrap around).

First, look at the Module below. The parameter we pass into it is called saturate and has type Scala Boolean. This is not a Chisel Bool. So, we’re not creating a single hardware adder that can either saturate or truncate, but rather we’re creating a generator that produces either a saturating hardware adder or a truncating hardware adder. The decision is made at compile time.

Next, notice the inputs and outputs are all 4-bit UInts. Chisel has built-in width inferencing, and if you look at the cheatsheet, you’ll see that the bitwidth of a normal summation is equal to the maximum bitwidth of the two inputs. This means that

1
val sum = io.in_a + io.in_b

will make sum a 4-bit wire, and the value will be the truncated result for 4-bit inputs. To check if the summation should saturate, you need to place the result in a 5-bit wire. This can be done with the +& summation, as seen on the cheatsheet.

1
val sum = io.in_a +& io.in_b

Finally, note that connecting a 4-bit UInt wire to a 5-bit UInt wire will truncate the MSB by default. You can use this to easily truncate the 5-bit sum for the non-saturating adder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ParameterizedAdder(saturate: Boolean) extends Module {
val io = IO(new Bundle {
val in_a = Input(UInt(4.W))
val in_b = Input(UInt(4.W))
val out = Output(UInt(4.W))
})

val sum = io.in_a +& io.in_b
if (saturate) {
io.out := Mux(sum > 15.U, 15.U, sum)
} else {
io.out := sum
}
}
1
2
3
4
5
6
7
8
9
10
11
module ParameterizedAdder(
input clock,
input reset,
input [3:0] io_in_a,
input [3:0] io_in_b,
output [3:0] io_out
);
wire [4:0] sum = io_in_a + io_in_b; // @[cmd9.sc 8:21]
wire [4:0] _T_1 = sum > 5'hf ? 5'hf : sum; // @[cmd9.sc 10:18]
assign io_out = _T_1[3:0]; // @[cmd9.sc 10:12]
endmodule
1
2
3
4
5
6
7
8
9
10
module ParameterizedAdder(
input clock,
input reset,
input [3:0] io_in_a,
input [3:0] io_in_b,
output [3:0] io_out
);
wire [4:0] sum = io_in_a + io_in_b; // @[cmd10.sc 8:21]
assign io_out = sum[3:0]; // @[cmd10.sc 12:12]
endmodule

2.3 Control Flow

‘:=’ Last Connect Semantics

It is possible to issue multiple connect statements to the same component. When this happens, the last statement wins.

1
2
3
4
5
6
7
8
9
10
class LastConnect extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
io.out := 1.U
io.out := 2.U
io.out := 3.U
io.out := 4.U // last statement wins
}

Chisel Conditional Logic

  • Actions taken in the bodies of the the three can be complex blocks and may contain nested when and allies.
  • when (chiselBoolType), if (scalaBooleanType)
1
2
3
4
5
6
7
when(chiselBoolType) {
// if (scalaBooleanType)
}.elsewhen(someOtherBooleanCondition) {
// else if
}.otherwise {
// else
}
  • Unlike Scala if, values are not returned by the blocks associated with when.
1
2
// error
val result = when(squareIt) { x * x }.otherwise { x }

We will discuss the solution to this in the Wires section.

Wire

Wire defines a circuit component that can appear on the right hand side or left hand side of a connect := operator.

1
val wire = Wire(Uint(12.W))

example

  • 4-Input Sort with Wires
image-20221221164557228 v2-cdda3f11c6efbc01577f5c29a9066772_b
  • Polynomial Circuit

Use Wire so that the $x^2$ computation appears only once in the module and so that there is a single connection to the output.

  • $x^2 - 2x + 1$
  • $2x^2 + 6x + 3$
  • $4x^2 - 10x -5$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Polynomial extends Module {
val io = IO(new Bundle {
val select = Input(UInt(2.W))
val x = Input(SInt(32.W))
val fOfX = Output(SInt(32.W))
})

val result = Wire(SInt(32.W))
val square = Wire(SInt(32.W))

square := io.x * io.x // $x^2$ computation appears only once

when(io.select === 0.U){
result := square - 2.S * io.x + 1.S
}.elsewhen(io.select === 1.U){
result := 2.S * square + 6.S * io.x + 3.S
}.otherwise{
result := 4.S * square - 10.S * io.x - 5.S
}

io.fOfX := result // there is a single connection to the output
}
  • Finite State Machine

Precedence: coffee > idea > pressure

image-20221221173650661

Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// state map
def states = Map("idle" -> 0, "coding" -> 1, "writing" -> 2, "grad" -> 3)

// life is full of question marks
def gradLife (state: Int, coffee: Boolean, idea: Boolean, pressure: Boolean): Int = {
var nextState = states("idle")
if (state == states("idle")) {
if (coffee) { nextState = states("coding") }
else if (idea) { nextState = states("idle") }
else if (pressure) { nextState = states("writing") }
} else if (state == states("coding")) {
if (coffee) { nextState = states("coding") }
else if (idea || pressure) { nextState = states("writing") }
} else if (state == states("writing")) {
if (coffee || idea) { nextState = states("writing") }
else if (pressure) { nextState = states("grad") }
}
nextState
}

Module

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
// life gets hard-er
class GradLife extends Module {
val io = IO(new Bundle {
val state = Input(UInt(2.W))
val coffee = Input(Bool())
val idea = Input(Bool())
val pressure = Input(Bool())
val nextState = Output(UInt(2.W))
})

val idle :: coding :: writing :: grad :: Nil = Enum(4)

when (io.state === idle) {
when (io.coffee) { io.nextState := coding }
.elsewhen (io.idea) { io.nextState := idle}
.elsewhen (io.pressure) { io.nextState := writing}
.otherwise { io.nextState := idle}
}.elsewhen (io.state === coding) {
when (io.coffee) { io.nextState := coding }
.elsewhen (io.idea || io.pressure) { io.nextState := writing}
.otherwise { io.nextState := idle}
}.elsewhen (io.state === writing) {
when (io.coffee || io.idea) { io.nextState := writing }
.elsewhen (io.pressure) { io.nextState := grad}
.otherwise { io.nextState := idle}
}.otherwise { io.nextState := idle}
}

2.4 Sequential Logic

Register

Declaration

Reg()

  • A Reg holds its output value until the rising edge of its clock, at which time it takes on the value of its input.
1
val register = Reg(UInt(12.W))
  • By default, every Chisel Module has an implicit clock that is used by every register in the design.
    The implicit clock can be overridden for multi-clock designs.

  • Operations called on a register are performed on the output of the register, and the kind of operations depend on the register’s type.

  • You aren’t restricted to using UInts with registers, you can use any subclass of the base type chisel3.Data. This includes SInt for signed integers and a lot of other things.

1
2
3
4
// It means the value reg is of type UInt,
// and you can do things you can normally do with UInts, like +, -, etc.
val reg: UInt = Reg(UInt(4.W))
val reg: SInt = Reg(SInt(4.W))

RegNext()

  • We didn’t need to specify the register bitwidth this time. It gets inferred from the register’s output connection, in this case io.out.
1
2
3
4
5
6
7
8
9
10
11
12
class RegNextModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(12.W))
val out = Output(UInt(12.W))
})

// With name and init value
val register = RegNext(io.in + 1.U, 0.U)

// register bitwidth is inferred from io.out
io.out := RegNext(io.in + 1.U)
}
  • In the latter case, register name is generated instead of explicity defined.
1
2
3
4
5
reg [11:0] REG; // @[cmd4.sc 8:20]
assign io_out = REG; // @[cmd4.sc 8:10]
always @(posedge clock) begin
REG <= io_in + 12'h1; // @[cmd4.sc 8:27]
end

RegInit()

  • Create a register that resets to a given value with RegInit.
1
2
val myReg = RegInit(UInt(12.W), 0.U)
val myReg = RegInit(0.U(12.W))
  • The generated verilog now has a block that checks if (reset) to reset the register to 0, inside the always @(posedge clock) block.
  • The register is still initialized to random junk before reset is called.
  • The PeekPokeTesters always call reset before running your test, but you can manually call reset as well using the reset(n) function, where reset is high for n cycles.
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
module RegInitModule(
input clock,
input reset,
input [11:0] io_in,
output [11:0] io_out
);
`ifdef RANDOMIZE_REG_INIT
reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
reg [11:0] register; // @[cmd6.sc 7:25]
wire [11:0] _T_1 = io_in + 12'h1; // @[cmd6.sc 8:21]
assign io_out = register; // @[cmd6.sc 9:10]
always @(posedge clock) begin
if (reset) begin // @[cmd6.sc 7:25]
register <= 12'h0; // @[cmd6.sc 7:25]
end else begin
register <= _T_1; // @[cmd6.sc 8:12]
end
end
// Register and memory initialization
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif
`ifndef RANDOM
`define RANDOM $random
`endif
`ifdef RANDOMIZE_MEM_INIT
integer initvar;
`endif
`ifndef SYNTHESIS
`ifdef FIRRTL_BEFORE_INITIAL
`FIRRTL_BEFORE_INITIAL
`endif
initial begin
`ifdef RANDOMIZE
`ifdef INIT_RANDOM
`INIT_RANDOM
`endif
`ifndef VERILATOR
`ifdef RANDOMIZE_DELAY
#`RANDOMIZE_DELAY begin end
`else
#0.002 begin end
`endif
`endif
`ifdef RANDOMIZE_REG_INIT
_RAND_0 = {1{`RANDOM}};
register = _RAND_0[11:0];
`endif // RANDOMIZE_REG_INIT
`endif // RANDOMIZE
end // initial
`ifdef FIRRTL_AFTER_INITIAL
`FIRRTL_AFTER_INITIAL
`endif
`endif // SYNTHESIS
endmodule

Test

  • Between calls to poke() and expect(), there is a call to step(1).
    This tells the test harness to tick the clock once, which will cause the register to pass its input to its output.
  • Calling poke() on an input immediately propagates the updated values through combinational logic.
    Calling step() is only needed to update state elements in sequential logic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RegisterModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(12.W))
val out = Output(UInt(12.W))
})

val register = Reg(UInt(12.W))
register := io.in + 1.U
io.out := register
}

test(new RegisterModule) { c =>
for (i <- 0 until 100) {
c.io.in.poke(i.U)
c.clock.step(1) // tick
c.io.out.expect((i + 1).U)
}
}

Implicit clock

  • The module has an input for clock (and reset) that you didn’t add - this is the implicit clock.
  • There is a block sectioned off by ifdef Randomize that initialized the register to some random variable before simulation starts. (x state ?)
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
module RegisterModule(
input clock,
input reset,
input [11:0] io_in,
output [11:0] io_out
);
`ifdef RANDOMIZE_REG_INIT
reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
reg [11:0] register; // @[cmd2.sc 7:21]
assign io_out = register; // @[cmd2.sc 9:10]
always @(posedge clock) begin
register <= io_in + 12'h1; // @[cmd2.sc 8:21]
end
// Register and memory initialization
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif
`ifndef RANDOM
`define RANDOM $random
`endif
`ifdef RANDOMIZE_MEM_INIT
integer initvar;
`endif
`ifndef SYNTHESIS
`ifdef FIRRTL_BEFORE_INITIAL
`FIRRTL_BEFORE_INITIAL
`endif
initial begin
`ifdef RANDOMIZE
`ifdef INIT_RANDOM
`INIT_RANDOM
`endif
`ifndef VERILATOR
`ifdef RANDOMIZE_DELAY
#`RANDOMIZE_DELAY begin end
`else
#0.002 begin end
`endif
`endif
`ifdef RANDOMIZE_REG_INIT
_RAND_0 = {1{`RANDOM}};
register = _RAND_0[11:0];
`endif // RANDOMIZE_REG_INIT
`endif // RANDOMIZE
end // initial
`ifdef FIRRTL_AFTER_INITIAL
`FIRRTL_AFTER_INITIAL
`endif
`endif // SYNTHESIS
endmodule

Control Flow

Registers are very similar to wires in terms of control flow. They have last connect semantics and can be assigned to conditionally with when, elsewhen, and otherwise.

Shift Register

  • Each element is a single bit wide.
  • Has a 4-bit output signal.
  • Takes a single input bit, which is the next value into the shift register.
  • Outputs the parallel output of the shift register, with the most significant bit being the last element of the shift register and the least significant bit being the first element of the shift register. Cat may come in handy.
  • The output initializes at b0001.
  • Shifts each clock cycle (no enable signal).
  • Note in Chisel, subword assignment IS ILLEGAL; something like out(0) := in will not work.
1
2
3
4
5
6
7
8
9
10
11
class MyShiftRegister(val init: Int = 1) extends Module {
val io = IO(new Bundle {
val in = Input(Bool())
val out = Output(UInt(4.W))
})

val state = RegInit(UInt(4.W), init.U)
val nextState = (state << 1) | io.in
state := nextState
io.out := state
}
1
2
3
4
5
6
7
8
9
10
11
12
reg [3:0] state; // @[cmd14.sc 7:22]
wire [4:0] _T = {state, 1'h0}; // @[cmd14.sc 16:26]
wire [4:0] _GEN_0 = {{4'd0}, io_in}; // @[cmd14.sc 16:32]
wire [4:0] nextState = _T | _GEN_0; // @[cmd14.sc 16:32]
assign io_out = state; // @[cmd14.sc 18:10]
always @(posedge clock) begin
if (reset) begin // @[cmd14.sc 7:22]
state <= 4'h1; // @[cmd14.sc 7:22]
end else begin
state <= nextState[3:0]; // @[cmd14.sc 17:9]
end
end

Parameterized Shift Register

1
2
3
4
5
6
7
8
9
10
11
12
class MyOptionalShiftRegister(val n: Int, val init: BigInt = 1) extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val in = Input(Bool())
val out = Output(UInt(n.W))
})

val state = RegInit(init.U(n.W))
val nextState = (state << 1) | io.in
when (io.en) { state := nextState }
io.out := state
}
1
2
3
4
5
6
7
8
9
10
11
12
13
reg [4:0] state; // @[cmd21.sc 8:22]
wire [5:0] _T = {state, 1'h0}; // @[cmd21.sc 10:26]
wire [5:0] _GEN_1 = {{5'd0}, io_in}; // @[cmd21.sc 10:32]
wire [5:0] nextState = _T | _GEN_1; // @[cmd21.sc 10:32]
wire [5:0] _GEN_0 = io_en ? nextState : {{1'd0}, state}; // @[cmd21.sc 11:16 cmd21.sc 11:24 cmd21.sc 8:22]
assign io_out = state; // @[cmd21.sc 13:10]
always @(posedge clock) begin
if (reset) begin // @[cmd21.sc 8:22]
state <= 5'h1; // @[cmd21.sc 8:22]
end else begin
state <= _GEN_0[4:0];
end
end

Explicit clock and reset

  • Clocks and resets can be overridden separately or together with withClock() {}, withReset() {}, and withClockAndReset() {}.
  • Clocks have their own type in Chisel Clock and should be declared as such.
  • Reset is always synchronous and of type Bool. Bool can be converted to Clocks by calling asClock() on them, but you should be careful that you aren’t doing something silly.
  • (?) Also note that chisel-testers do not currently have complete support for multi-clock designs.

Multi-Clock Module

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
// we need to import multi-clock features
import chisel3.experimental.{withClock, withReset, withClockAndReset}

class ClockExamples extends Module {
val io = IO(new Bundle {
val in = Input(UInt(10.W))
val alternateReset = Input(Bool())
val alternateClock = Input(Clock())
val outImplicit = Output(UInt())
val outAlternateReset = Output(UInt())
val outAlternateClock = Output(UInt())
val outAlternateBoth = Output(UInt())
})

val imp = RegInit(0.U(10.W))
imp := io.in
io.outImplicit := imp

withReset(io.alternateReset) {
// everything in this scope with have alternateReset as the reset
val altRst = RegInit(0.U(10.W))
altRst := io.in
io.outAlternateReset := altRst
}

withClock(io.alternateClock) {
val altClk = RegInit(0.U(10.W))
altClk := io.in
io.outAlternateClock := altClk
}

withClockAndReset(io.alternateClock, io.alternateReset) {
val alt = RegInit(0.U(10.W))
alt := io.in
io.outAlternateBoth := alt
}
}
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
module ClockExamples(
input clock, // implicit clock
input reset, // implicit reset
input [9:0] io_in,
input io_alternateReset, // alternate reset
input io_alternateClock, // alternate clock
output [9:0] io_outImplicit,
output [9:0] io_outAlternateReset,
output [9:0] io_outAlternateClock,
output [9:0] io_outAlternateBoth
);
reg [9:0] imp; // @[cmd22.sc 14:20]
reg [9:0] REG; // @[cmd22.sc 20:25] altRst
reg [9:0] REG_1; // @[cmd22.sc 26:25] altClk
reg [9:0] REG_2; // @[cmd22.sc 32:22] alt
assign io_outImplicit = imp; // @[cmd22.sc 16:18]
assign io_outAlternateReset = REG; // @[cmd22.sc 22:26]
assign io_outAlternateClock = REG_1; // @[cmd22.sc 28:26]
assign io_outAlternateBoth = REG_2; // @[cmd22.sc 34:25]
always @(posedge clock) begin // for registers with no alternateClock
if (reset) begin // @[cmd22.sc 14:20] // imp, altRst
imp <= 10'h0; // @[cmd22.sc 14:20]
end else begin
imp <= io_in; // @[cmd22.sc 15:7]
end
if (io_alternateReset) begin // @[cmd22.sc 20:25]
REG <= 10'h0; // @[cmd22.sc 20:25]
end else begin
REG <= io_in; // @[cmd22.sc 21:12]
end
end
always @(posedge io_alternateClock) begin // registers with alternateClock
if (reset) begin // @[cmd22.sc 26:25] // altClk, alt
REG_1 <= 10'h0; // @[cmd22.sc 26:25]
end else begin
REG_1 <= io_in; // @[cmd22.sc 27:12]
end
if (io_alternateReset) begin // @[cmd22.sc 32:22]
REG_2 <= 10'h0; // @[cmd22.sc 32:22]
end else begin
REG_2 <= io_in; // @[cmd22.sc 33:9]
end
end
endmodule

2.5 Exercise

FIR Filter

$y[n] = b_0 x[n] + b_1 x[n-1] + b_2 x[n-2] + …$

  • $y[n]$ is the output signal at time $n$
  • $x[n]$ is the input signal
  • $b_i$ are the filter coefficients or impulse response
  • $n-1$, $n-2$, … are time $n$ delayed by 1, 2, … cycles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class My4ElementFir(b0: Int, b1: Int, b2: Int, b3: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
// val shiftReg1 = RegInit(0.U(8.W))
// val shiftReg2 = RegInit(0.U(8.W))
// val shiftReg3 = RegInit(0.U(8.W))
// shiftReg1 := io.in
// shiftReg2 := shiftReg1
// shiftReg3 := shiftReg2

val shiftReg1 = RegNext(io.in, 0.U)
val shiftReg2 = RegNext(shiftReg1, 0.U)
val shiftReg3 = RegNext(shiftReg2, 0.U)

io.out := b0.U(8.W) * io.in
+ b1.U(8.W) * shiftReg1
+ b2.U(8.W) * shiftReg2
+ b3.U(8.W) * shiftReg3
}
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
module My4ElementFir(
input clock,
input reset,
input [7:0] io_in,
output [7:0] io_out
);
reg [7:0] shiftReg1; // @[cmd58.sc 7:26]
reg [7:0] shiftReg2; // @[cmd58.sc 8:26]
reg [7:0] shiftReg3; // @[cmd58.sc 9:26]
wire [15:0] _T = 8'h1 * io_in; // @[cmd58.sc 19:23]
wire [15:0] _T_1 = 8'h1 * shiftReg1; // @[cmd58.sc 19:43]
wire [15:0] _T_3 = _T + _T_1; // @[cmd58.sc 19:31]
wire [15:0] _T_4 = 8'h1 * shiftReg2; // @[cmd58.sc 19:67]
wire [15:0] _T_6 = _T_3 + _T_4; // @[cmd58.sc 19:55]
wire [15:0] _T_7 = 8'h1 * shiftReg3; // @[cmd58.sc 19:91]
wire [15:0] _T_9 = _T_6 + _T_7; // @[cmd58.sc 19:79]
assign io_out = _T_9[7:0]; // @[cmd58.sc 19:10]
always @(posedge clock) begin
if (reset) begin // @[cmd58.sc 7:26]
shiftReg1 <= 8'h0; // @[cmd58.sc 7:26]
end else begin
shiftReg1 <= io_in; // @[cmd58.sc 7:26]
end
if (reset) begin // @[cmd58.sc 8:26]
shiftReg2 <= 8'h0; // @[cmd58.sc 8:26]
end else begin
shiftReg2 <= shiftReg1; // @[cmd58.sc 8:26]
end
if (reset) begin // @[cmd58.sc 9:26]
shiftReg3 <= 8'h0; // @[cmd58.sc 9:26]
end else begin
shiftReg3 <= shiftReg2; // @[cmd58.sc 9:26]
end
end
endmodule

FIR Filter Generator

The generator has 3 inputs:

  • in, the input to the filter
  • valid, a boolean that says when the input is valid
  • consts, a vector for all the taps

and 1 output:

  • out, the filtered input
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyManyDynamicElementVecFir(length: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val valid = Input(Bool())
val out = Output(UInt(8.W))
val consts = Input(Vec(length, UInt(8.W)))
})

// Such concision! You'll learn what all this means later.
val taps = Seq(io.in) ++ Seq.fill(io.consts.length - 1)(RegInit(0.U(8.W)))
taps.zip(taps.tail).foreach { case (a, b) => when (io.valid) { b := a } }

io.out := taps.zip(io.consts).map { case (a, b) => a * b }.reduce(_ + _)
}

visualize(() => new MyManyDynamicElementVecFir(4))

2.6 Chisel Test

Basic

ChiselTest’s test method requires a bit less boiler plate. What was the PeekPokeTester is now built into the process.

  • The poke and expect methods are now part of each individual io element. This gives important hints to the tester to make better checking of types. The peek and step operations are also now methods on io elements.
  • Another difference is that values poked and expected are Chisel literals. Although pretty simple here, it also provides stronger checking in more advanced and interesting examples. This will be further enhanced with coming improvements in the ability to specify Bundle literals
iotesters (deprived ?) ChiselTest
poke poke(c.io.in1, 6) c.io.in1.poke(6.U)
peek peek(c.io.out1) c.io.out1.peek()
expect expect(c.io.out1, 6) c.io.out1.expect(6.U)
step step(1) c.io.clock.step(1)
initiate Driver.execute(…) { c => test(…) { c =>

Modules with Decoupled Interfaces

1
2
3
4
5
class QueueModule[T <: Data](ioType: T, entries: Int) extends MultiIOModule {
val in = IO(Flipped(Decoupled(ioType)))
val out = IO(Decoupled(ioType))
out <> Queue(in, entries)
}

3.1 Chisel Standard Library

Chisel cheatsheet

Chisel3 cheatsheet

Decoupled

DecoupledIO provides a ready-valid interface for transferring data. Any Chisel data can be wrapped in a DecoupledIO (used as the bits field).

  • (master)The source drives the bits signal with the data to be transferred and the valid signal when there is data to be transferred.
    (slave)The sink drives the ready signal when it is ready to accept data, and data is considered transferred when both ready and valid are asserted on a cycle.

  • ready and valid should not be combinationally coupled, otherwise this may result in unsynthesizable combinational loops.
    ready should only be dependent on whether the sink is able to receive data, and valid should only be dependent on whether the source has data. Only after the transaction (on the next clock cycle) should the values update.

  • Output: Decoupled(UInt(8.W)) Bundle with fields:

    1
    2
    3
    4
    5
    val out = new Bundle {
    val valid = Output(Bool())
    val ready = Input(Bool())
    val bits = Output(UInt(8.W))
    }

    Input: Flipped(Decoupled(UInt(8.W)))Bundle with fields:

    1
    2
    3
    4
    5
    val in = new Bundle {
    val valid = Input(Bool())
    val ready = Output(Bool())
    val bits = Input(UInt(8.W))
    }

Arbiter

  • Arbiters routes data from n DecoupledIO sources to one DecoupledIO sink, given a prioritization. There are two types included in Chisel:
    • Arbiter: prioritizes lower-index producers
    • RRArbiter: runs in round-robin order
  • Arbiter routing is implemented in combinational logic.
1
2
3
4
5
6
7
8
9
10
11
class Mod extends Module {
// Example circuit using a priority arbiter
val io = IO(new Bundle {
val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
val out = Decoupled(UInt(8.W))
})
// Arbiter doesn't have a convenience constructor, so it's built like any Module
val arbiter = Module(new Arbiter(UInt(8.W), 2)) // 2 to 1 Priority Arbiter
arbiter.io.in <> io.in
io.out <> arbiter.io.out
}

Queue

Queue creates a FIFO (first-in, first-out) queue with Decoupled interfaces on both sides, allowing backpressure. Both the data type and number of elements are configurable.

1
2
3
4
5
6
7
8
class Queue extends Module {
val io = IO(new Bundle {
val in = Flipped(Decoupled(UInt(8.W)))
val out = Decoupled(UInt(8.W))
})
val queue = Queue(io.in, 2) // 2-element queue
io.out <> queue
}

Bitwise Utilities

PopCount

PopCount returns the number of high (1) bits in the input as a UInt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test(new Module {
// Example circuit using PopCount
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
io.out := PopCount(io.in)
}) { c =>
// Integer.parseInt is used create an Integer from a binary specification
c.io.in.poke(Integer.parseInt("00000000", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")

c.io.in.poke(Integer.parseInt("11001010", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=${c.io.out.peek().litValue}")
}

Reverse

Reverse returns the bit-reversed sequence input. NOT ~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test(new Module {
// Example circuit using Reverse
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
})
io.out := Reverse(io.in)
}) { c =>
// Integer.parseInt is used create an Integer from a binary specification
c.io.in.poke(Integer.parseInt("00001111", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")

c.io.in.poke(Integer.parseInt("11001010", 2).U)
println(s"in=0b${c.io.in.peek().litValue.toInt.toBinaryString}, out=0b${c.io.out.peek().litValue.toInt.toBinaryString}")
}

OneHot encoding utilities

OneHot is an encoding of integers where there is one wire for each value, and exactly one wire is high.

The below two functions provide conversion between binary (UInt) and OneHot encodings, and are inverses of each other:

  • UInt to OneHot: UIntToOH
  • OneHot to UInt: OHToUInt
1
2
3
4
5
6
7
8
// UIntToOH
val in = Input(UInt(4.W))
val out = Output(UInt(16.W))
io.out := UIntToOH(io.in)
// OHToUInt
val in = Input(UInt(16.W))
val out = Output(UInt(4.W))
io.out := OHToUInt(io.in)

Mux

  • These muxes take in a list of values with select signals, and output the value associated with the lowest-index select signal.
  • These can either take a list of (select: Bool, value: Data) tuples, or corresponding lists of selects and values as arguments.

Priority Mux

A PriorityMux outputs the value associated with the lowest-index asserted select signal.

1
2
PriorityMux(io.in_sels, io.in_bits)
PriorityMux(List[Bool, Bits])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
test(new Module {
// Example circuit using PriorityMux
val io = IO(new Bundle {
val in_sels = Input(Vec(2, Bool()))
val in_bits = Input(Vec(2, UInt(8.W)))
val out = Output(UInt(8.W))
})
io.out := PriorityMux(io.in_sels, io.in_bits)
}) { c =>
c.io.in_bits(0).poke(10.U)
c.io.in_bits(1).poke(20.U)

c.io.in_sels(0).poke(false.B)
c.io.in_sels(1).poke(true.B) // Select higher index only

c.io.in_sels(0).poke(true.B) // Select both - arbitration needed
c.io.in_sels(1).poke(true.B)

c.io.in_sels(0).poke(true.B) // Select lower index only
c.io.in_sels(1).poke(false.B)
}

OneHot Mux

An Mux1H provides an efficient implementation when it is guaranteed that exactly one of the select signals will be high. Behavior is undefined if the assumption is not true.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
test(new Module {
// Example circuit using Mux1H
val io = IO(new Bundle {
val in_sels = Input(Vec(2, Bool()))
val in_bits = Input(Vec(2, UInt(8.W)))
val out = Output(UInt(8.W))
})
io.out := Mux1H(io.in_sels, io.in_bits)
}) { c =>
c.io.in_bits(0).poke(10.U)
c.io.in_bits(1).poke(20.U)

c.io.in_sels(0).poke(false.B)
c.io.in_sels(1).poke(true.B) // Select index 1

c.io.in_sels(0).poke(true.B) // Select index 0
c.io.in_sels(1).poke(false.B)

c.io.in_sels(0).poke(false.B) // Select none (invalid)
c.io.in_sels(1).poke(false.B)

c.io.in_sels(0).poke(true.B) // Select both (invalid)
c.io.in_sels(1).poke(true.B)
}

Counter

Counter is a counter that can be incremented once every cycle, up to some specified limit, at which point it overflows. Note that it is not a Module, and its value is accessible.

1
2
3
4
5
6
7
8
9
10
11
12
class Cnt extends Module {
// Example circuit using Mux1H
val io = IO(new Bundle {
val count = Input(Bool())
val out = Output(UInt(2.W))
})
val counter = Counter(3) // 3-count Counter (outputs range [0...2])
when(io.count) {
counter.inc() // increasement
}
io.out := counter.value
}

3.2 Generators: Parameters

Parameter Passing

  • require(...)are pre-elaboration assertions, which are useful when your generator only works with certain parameterizations or when some parameterizations are mutually exclusive or nonsensical.
  • There is a separate construct for simulation-time assertions called assert(...).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int) extends Module {
require(in0Width >= 0) // Pre-elaboration assertions
require(in1Width >= 0)
require(sumWidth >= 0)
val io = IO(new Bundle {
val in0 = Input(UInt(in0Width.W))
val in1 = Input(UInt(in1Width.W))
val sum = Output(UInt(sumWidth.W))
})
// a +& b includes the carry, a + b does not
io.sum := io.in0 +& io.in1
}

println(getVerilog(new ParameterizedWidthAdder(1, 4, 6)))

Parameterized 4-Input Sort

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
class Sort4(ascending: Boolean) extends Module {
val io = IO(new Bundle {
val in0 = Input(UInt(16.W))
val in1 = Input(UInt(16.W))
val in2 = Input(UInt(16.W))
val in3 = Input(UInt(16.W))
val out0 = Output(UInt(16.W))
val out1 = Output(UInt(16.W))
val out2 = Output(UInt(16.W))
val out3 = Output(UInt(16.W))
})

// this comparison funtion decides < or > based on the module's parameterization
def comp(l: UInt, r: UInt): Bool = {
if (ascending) {
l < r
} else {
l > r
}
}

val row10 = Wire(UInt(16.W))
val row11 = Wire(UInt(16.W))
val row12 = Wire(UInt(16.W))
val row13 = Wire(UInt(16.W))

when(comp(io.in0, io.in1)) {
row10 := io.in0 // preserve first two elements
row11 := io.in1
}.otherwise {
row10 := io.in1 // swap first two elements
row11 := io.in0
}

when(comp(io.in2, io.in3)) {
row12 := io.in2 // preserve last two elements
row13 := io.in3
}.otherwise {
row12 := io.in3 // swap last two elements
row13 := io.in2
}

val row21 = Wire(UInt(16.W))
val row22 = Wire(UInt(16.W))

when(comp(row11, row12)) {
row21 := row11 // preserve middle 2 elements
row22 := row12
}.otherwise {
row21 := row12 // swap middle two elements
row22 := row11
}

val row20 = Wire(UInt(16.W))
val row23 = Wire(UInt(16.W))
when(comp(row10, row13)) {
row20 := row10 // preserve the first and the forth elements
row23 := row13
}.otherwise {
row20 := row13 // swap the first and the forth elements
row23 := row10
}

when(comp(row20, row21)) {
io.out0 := row20 // preserve first two elements
io.out1 := row21
}.otherwise {
io.out0 := row21 // swap first two elements
io.out1 := row20
}

when(comp(row22, row23)) {
io.out2 := row22 // preserve first two elements
io.out3 := row23
}.otherwise {
io.out2 := row23 // swap first two elements
io.out3 := row22
}
}

Option

1
2
3
val map = Map("a" -> 1)
val a = map("a") // 1
val b = map("b") // Errors!

Map.get returns a key’s value of abstract class Option. Option has two subclasses, **Some **and None.

1
2
val a = map.get("a")	// Some(1)
val b = map.get("b") // None

Option.get returns a value, which errors if called on None.

Option.getOrElse provides a default value.

1
2
3
4
Some(1).get        		// 1
// None.get // Errors!
Some(1).getOrElse(2) // 1
None.getOrElse(2) // 2

Sometimes, a parameter doesn’t have a good default value. Option can be used with a default value of None in these situations.

Optional Reset by Input: Option

If resetValue = None, which is the default, the register will have no reset value and be initialized to garbage.

1
2
3
4
5
6
7
8
9
class DelayBy1(resetValue: Option[UInt] = None) extends Module {
...
val reg = if (resetValue.isDefined) { // resetValue = Some(number)
RegInit(resetValue.get)
} else { //resetValue = None
Reg(UInt())
}
...
}

Match - Case

  • Each code block that follows a => operator continues until it reaches either the ending brace of the match or the next case statement.
  • A match is searched in the order of the case statements, once a case statement has been matched, no other checks against other case statements are made.
  • case _ wildcard handle any value not found. (default)

Multiple Value Matching

1
2
3
4
5
6
7
8
(val1, val2) match {
case (true, true) => "wolverine"
case (true, false) => "elephant"
case (false, true) =>
println("here")
"shrew"
case _ => "puppy"
}

Type Matching

1
2
3
4
5
6
7
8
9
val sequence = Seq("abc", 1, 0.0, 'a', 'b')
sequence.foreach { x =>
x match {
case s: String => println(s"$x is a String")
case _: Int | _: Double => println(s"$x is an Int or Double")
case 'a' | _: Char => println(s"$x is a Char")
case _ => println(s"$x is an unknown type!")
}
}

Type Matching and Erasure

Type matching has some limitations. Because Scala runs on the JVM, and the JVM does not maintain polymorphic types, you cannot match on them at runtime (because they are all erased). Note that the following example always matches the first case statement, because the [String], [Int], and [Double] polymorphic types are erased, and the case statements are actually matching on just a Seq.

1
2
3
4
5
6
7
8
val sequence = Seq(Seq("a"), Seq(1), Seq(0.0))
sequence.foreach { x =>
x match {
case s: Seq[String] => println(s"$x is a String")
case s: Seq[Int] => println(s"$x is an Int")
case s: Seq[Double] => println(s"$x is a Double")
}
}

Optional Reset by Match

1
2
3
4
5
6
7
8
class DelayBy1(resetValue: Option[UInt] = None) extends Module {
...
val reg = resetValue match {
case Some(r) => RegInit(r)
case None => Reg(UInt())
}
...
}

IO with Optional Fields

Optional IO with Option

1
2
3
4
5
6
7
8
9
class HalfFullAdder(val hasCarry: Boolean) extends Module {
val io = IO(new Bundle {
...
val carryIn = if (hasCarry) Some(Input(UInt(1.W))) else None
...
})
val sum = io.a +& io.b +& io.carryIn.getOrElse(0.U)
...
}

Optional IO with Zero-Width Wires

Chisel types are allowed to have widths of zero. An IO with width zero is pruned from the emitted Verilog.
Anything that tries to use the value of a zero-width wire gets a constant zero.

1
2
3
4
5
6
7
8
9
class HalfFullAdder(val hasCarry: Boolean) extends Module {
val io = IO(new Bundle {
...
val carryIn = Input(if (hasCarry) UInt(1.W) else UInt(0.W))
...
})
val sum = io.a +& io.b +& io.carryIn
...
}

Implicit Argument

  • There can only be one implicit value of a given type.
  • When we call tooManyCats, we either omit the second implicit argument list (letting the compiler find it for us), or explicitly provide an argument (which can be different than the implicit value).
1
2
3
4
5
6
7
8
9
object CatDog {
implicit val numberOfCats: Int = 3
// implicit val numberOfDogs: String = "5"

def tooManyCats(nDogs: Int)(implicit nCats: Int): Boolean = nCats > nDogs

val imp = tooManyCats(2) // Argument passed implicitly
val exp = tooManyCats(2)(1) // Argument passed explicitly
}

Implicit Log

Note: there are better ways to do logging in Scala!

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
sealed trait Verbosity
implicit case object Silent extends Verbosity
case object Verbose extends Verbosity

class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int)(implicit verbosity: Verbosity)
extends Module {
def log(msg: => String): Unit = verbosity match {
case Silent =>
case Verbose => println(msg)
}
require(in0Width >= 0)
log(s"in0Width of $in0Width OK")
require(in1Width >= 0)
log(s"in1Width of $in1Width OK")
require(sumWidth >= 0)
log(s"sumWidth of $sumWidth OK")
val io = IO(new Bundle {
val in0 = Input(UInt(in0Width.W))
val in1 = Input(UInt(in1Width.W))
val sum = Output(UInt(sumWidth.W))
})
log("Made IO")
io.sum := io.in0 + io.in1
log("Assigned output")
}

println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)))
println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)(Verbose)))

Implicit Conversions

Implicit Conversions are used to automatically convert one Scala object into another.

Generally, implicits can make your code confusing, so we recommend you use them as a last resort. First try inheritance, traits, or method overloading.

1
2
3
4
5
class Animal(val name: String, val species: String)
class Human(val name: String)
implicit def human2animal(h: Human): Animal = new Animal(h.name, "Homo sapiens")
val me = new Human("Adam")
println(me.species)

Mealy Machine

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
// Mealy machine has
case class BinaryMealyParams(
// number of states
nStates: Int,
// initial state
s0: Int,
// function describing state transition
stateTransition: (Int, Boolean) => Int,
// function describing output
output: (Int, Boolean) => Int
) {
require(nStates >= 0)
require(s0 < nStates && s0 >= 0)
}

class BinaryMealy(val mp: BinaryMealyParams) extends Module {
val io = IO(new Bundle {
val in = Input(Bool())
val out = Output(UInt())
})

val state = RegInit(UInt(), mp.s0.U)

// output zero if no states
io.out := 0.U
for (i <- 0 until mp.nStates) {
when (state === i.U) {
when (io.in) {
state := mp.stateTransition(i, true).U
io.out := mp.output(i, true).U
}.otherwise {
state := mp.stateTransition(i, false).U
io.out := mp.output(i, false).U
}
}
}
}

// example from https://en.wikipedia.org/wiki/Mealy_machine
val nStates = 3
val s0 = 2
def stateTransition(state: Int, in: Boolean): Int = {
if (in) {
1
} else {
0
}
}
def output(state: Int, in: Boolean): Int = {
if (state == 2) {
return 0
}
if ((state == 1 && !in) || (state == 0 && in)) {
return 1
} else {
return 0
}
}

val testParams = BinaryMealyParams(nStates, s0, (state: Int, in: Boolean) => if (in) 1 else 0, output) // anonymous function
println(getVerilog(new BinaryMealy(testParams)))
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
module BinaryMealy(
input clock,
input reset,
input io_in,
output io_out
);
`ifdef RANDOMIZE_REG_INIT
reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
reg [1:0] state; // @[cmd41.sc 21:22]
wire _GEN_2 = state == 2'h0 & io_in; // @[cmd41.sc 26:26 cmd41.sc 24:10]
wire _GEN_3 = io_in ? 1'h0 : 1'h1; // @[cmd41.sc 27:20 cmd41.sc 29:16 cmd41.sc 32:16]
wire _GEN_5 = state == 2'h1 ? _GEN_3 : _GEN_2; // @[cmd41.sc 26:26]
assign io_out = state == 2'h2 ? 1'h0 : _GEN_5; // @[cmd41.sc 26:26]
always @(posedge clock) begin
if (reset) begin // @[cmd41.sc 21:22]
state <= 2'h2; // @[cmd41.sc 21:22]
end else if (state == 2'h2) begin // @[cmd41.sc 26:26]
state <= {{1'd0}, io_in};
end else if (state == 2'h1) begin // @[cmd41.sc 26:26]
state <= {{1'd0}, io_in};
end else if (state == 2'h0) begin // @[cmd41.sc 26:26]
state <= {{1'd0}, io_in};
end
end
endmodule

3.3 Generators: Collections

Parameterized FIR Filter Generator

  • Build a software Golden Model of a tap configurable FIR.
  • Redesign our test to use this model, and confirm that it works.
  • Refactor our My4ElementFir to allow an configurable number of taps.
  • Test the new circuit using our new test harness.

Golden Model

1
2
3
4
5
6
7
8
9
10
11
12
class ScalaFirFilter(taps: Seq[Int]) {
var pseudoRegisters = List.fill(taps.length)(0)

def poke(value: Int): Int = {
pseudoRegisters = value :: pseudoRegisters.take(taps.length - 1)
var accumulator = 0
for(i <- taps.indices) {
accumulator += taps(i) * pseudoRegisters(i)
}
accumulator
}
}

mutable.ArrayBuffer[]

  • We are using a Scala collection type called ArrayBuffer.
    ArrayBuffer allows you to append elements using the **+= **operator (also insert and delete, but we don’t need this).
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
class MyManyElementFir(consts: Seq[Int], bitWidth: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(bitWidth.W))
val out = Output(UInt(bitWidth.W))
})

val regs = mutable.ArrayBuffer[UInt]()
for(i <- 0 until consts.length) {
if(i == 0) regs += io.in // not register
else regs += RegNext(regs.last, 0.U) // register
}

val muls = mutable.ArrayBuffer[UInt]()
for(i <- 0 until consts.length) {
muls += regs(i) * consts(i).U
}

val scan = mutable.ArrayBuffer[UInt]()
for(i <- 0 until consts.length) {
if(i == 0) scan += muls(i)
else scan += muls(i) + scan.last
}

io.out := scan.last
}
println(getVerilog(new MyManyElementFir(Seq(1, 1, 1, 1), 8)))

Vec & VecInit

Vec supports many of the scala collection methods but it can only contain Chisel hardware elements.

Vec should only be used in situations where ordinary Scala collections won’t work, basically where:

  1. You need a collection of elements in a Bundle, typically a Bundle that will be used as IO.
  2. You need to access the collection via an index that is part of the hardware (think Register File).
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
class MyManyDynamicElementVecFir(length: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
val consts = Input(Vec(length, UInt(8.W)))
})

// Reference solution
val regs = RegInit(VecInit(Seq.fill(length - 1)(0.U(8.W))))
// val regs = Reg(Vec(length - 1, UInt(8.W))) // ?
for(i <- 0 until length - 1) {
if(i == 0) regs(i) := io.in
else regs(i) := regs(i - 1)
}

val muls = Wire(Vec(length, UInt(8.W)))
for(i <- 0 until length) {
if(i == 0) muls(i) := io.in * io.consts(i)
else muls(i) := regs(i - 1) * io.consts(i)
}

val scan = Wire(Vec(length, UInt(8.W)))
for(i <- 0 until length) {
if(i == 0) scan(i) := muls(i)
else scan(i) := muls(i) + scan(i - 1)
}

io.out := scan(length - 1)
}

Register File

  • Writes will only be performed when wen (write enable) is asserted.

  • The register at index 0 (the first register) is always zero when you read from it, regardless of what you write to it (it’s often useful to have 0 handy).

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
class RegisterFile(readPorts: Int) extends Module {
require(readPorts >= 0)
val io = IO(new Bundle {
val wen = Input(Bool())
val waddr = Input(UInt(5.W))
val wdata = Input(UInt(32.W))
val raddr = Input(Vec(readPorts, UInt(5.W)))
val rdata = Output(Vec(readPorts, UInt(32.W)))
})

// A Register of a vector of UInts
val reg = RegInit(VecInit(Seq.fill(32)(0.U(32.W))))

when (io.wen) {
reg(io.waddr) := io.wdata
printf(p"waddr = ${io.waddr}, wdata = ${io.wdata}, reg(${io.waddr - 1.U}) = ${reg(io.waddr - 1.U)}\n")
}

// when (io.wen === true.B) {
// for (i <- 1 until 32) {
// when (io.waddr === i.U) {
// reg(i) := io.wdata
// printf(p"waddr = ${io.waddr}, wdata = ${io.wdata}, reg(${i - 1}) = ${reg(i - 1)}\n")
// }
// }
// }//.otherwise { reg := reg }

for (i <- 0 until readPorts) {
when (io.raddr(i) === 0.U) {
io.rdata(i) := 0.U
} .otherwise {
io.rdata(i) := reg(io.raddr(i))
}
}

// for (i <- 0 until readPorts) {
// for (j <- 0 until 32) {
// when (io.raddr(i) === j.U) {
// io.rdata(i) := reg(j)
// // printf(p"raddr = ${io.raddr}, rdata = ${io.rdata}, reg(${j}) = ${reg(j)}\n")
// }.otherwise { io.rdata(i) := 0.U }
// }
// }
}

3.4 High-Order Functions

zip, map, reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
val muls = Wire(Vec(length, UInt(8.W)))
for(i <- 0 until length) {
if(i == 0) muls(i) := io.in * io.consts(i)
else muls(i) := regs(i - 1) * io.consts(i)
}

val scan = Wire(Vec(length, UInt(8.W)))
for(i <- 0 until length) {
if(i == 0) scan(i) := muls(i)
else scan(i) := muls(i) + scan(i - 1)
}

io.out := scan(length - 1)

equals to:

1
io.out := (taps zip io.consts).map { case (a, b) => a * b }.reduce(_ + _)
  • taps is the list of all samples, with taps(0) = io.in, taps(1) = regs(0), etc.
  • (taps zip io.consts) or (taps.zip(io.consts)) : combines two lists, taps and io.consts, into one list, where each element is a tuple of the elements at the inputs at the corresponding position. Concretely, its value would be [(taps(0), io.consts(0)), (taps(1), io.consts(1)), ..., (taps(n), io.consts(n))].
  • .map { case (a, b) => a * b } : applies the anonymous function (takes a tuple of two elements returns their product) to the elements of the list, and returns the result. In this case, the result is equivalent to muls in the verbose example, and has the value [taps(0) * io.consts(0), taps(1) * io.consts(1), ..., taps(n) * io.consts(n)].
  • .reduce(_ + _) : applies the function (addition of elements) to elements of the list. However, it takes two arguments: the first is the current accumulation, and the second is the list element (in the first iteration, it just adds the first two elements). These are given by the two underscores in the parentheses. The result would then be, assuming left-to-right traversal, (((muls(0) + muls(1)) + muls(2)) + ...) + muls(n), with the result of deeper-nested parentheses evaluated first. This is the output of the convolution.

Functions as Arguments

Different ways of specifying functions

  • _ + _: For functions where each argument is referred to exactly once, you may be able to use an underscore (_) to refer to each argument.
    In the example above, the reduce argument function took two arguments and could be specified as _ + _.

  • (a, b) => a * b: Specifying the inputs argument list explicitly.
    With the general form of putting the argument list in parentheses, followed by =>, followed by the function body referring to those arguments.

  • case (a, b) => a * b: When tuple unpacking is needed, using a case statement, as in case (a, b) => a * b.
    That takes a single argument, a tuple of two elements, and unpacks it into variables a and b, which can then be used in the function body.

Scala Collections API

map

  • List[A].map type signature: map[B](f: (A) ⇒ B): List[B]

  • It takes an argument of type (f: (A) ⇒ B), or a function that takes one argument of type A (the same type as the element of the input List) and returns a value of type B (which can be anything). map then returns a new list of type B (the return type of the argument function).

1
2
3
4
5
6
7
8
9
println(List(1, 2, 3, 4).map(_.toString + "a"))
println(List((1, 5), (2, 6), (3, 7), (4, 8)).map { case (x, y) => x*y })

println(0 to 10) // Range 0 to 10
println(0 until 10) // Range 0 until 10

// Those largely behave like lists, and can be useful for generating indices:
val myList = List("a", "b", "c", "d")
println((0 until 4).map(myList(_))) // Vector(a, b, c, d)
  • case
1
2
3
4
5
6
val myAnyList = List(1, 2, "3", 4L, myList)
myAnyList.map {
case (_:Int | _:Long) => "Number"
case _:String => "String"
case _ => "error"
}

zipWithIndex

  • List.zipWithIndex type signature: zipWithIndex: List[(A, Int)]

  • It returns a list where each element is a tuple of the original elements, and the index (with the first one being zero). So List(“a”, “b”, “c”, “d”).zipWithIndex would return List((“a”, 0), (“b”, 1), (“c”, 2), (“d”, 3))

1
2
println(List(("a", "b"), ("c", "d"), ("e", "f"), ("g", "h")).zipWithIndex)
// List(((a,b),0), ((c,d),1), ((e,f),2), ((g,h),3))

reduce

  • List[A].reduce type signature: reduce(op: (A, A) ⇒ A): A
1
2
3
4
5
println(List(1, 2, 3, 4).reduce((a, b) => a + b))  // returns the sum of all the elements
println(List(1, 2, 3, 4).reduce(_ * _)) // returns the product of all the elements
println(List(1, 2, 3, 4).map(_ + 1).reduce(_ + _)) // you can chain reduce onto the result of a map
// Important note: reduce will fail with an empty list
println(List[Int]().reduce(_ * _)) // Error
  • reduceLeft reduceRight : direction, type
1
2
reduceLeft[B >: A](op: (B, A) => B): B
reduceRight[B >: A](op: (A, B) => B): B

fold

  • List[A].fold type signature: fold(z: A)(op: (A, A) ⇒ A): A
    It is very similar to reduce, except that you can specify the initial accumulation value.
1
2
3
println(List(1, 2, 3, 4).fold(0)(_ + _))  // equivalent to the sum using reduce
println(List(1, 2, 3, 4).fold(1)(_ + _)) // like above, but accumulation starts at 1
println(List().fold(1)(_ + _)) // unlike reduce, does not fail on an empty input

scan

(B, B) : same type

1
def scan[B >: A, That](z: B)(op: (B, B) ⇒ B)(implicit cbf: CanBuildFrom[Repr, B, That]): That

(A, B) : can be different type

1
def scanLeft[B, That](z: B)(op: (B, A) ⇒ B)(implicit bf: CanBuildFrom[Repr, B, That]): That
1
def scanRight[B, That](z: B)(op: (A, B) ⇒ B)(implicit bf: CanBuildFrom[Repr, B, That]): That

Exercise: Decoupled Arbiter

  • Architecturally:
    • io.out.valid is true if any of the inputs are valid
    • Consider having an internal wire of the selected channel
    • Each input’s ready is true if the output is ready, AND that channel is selected (this does combinationally couple ready and valid, but we’ll ignore it for now…)
  • These constructs may help:
    • map, especially for returning a Vec of sub-elements, for example io.in.map(_.valid) returns a list of valid signals of the input Bundles
    • PriorityMux(List[Bool, Bits]), which takes in a list of valid signals and bits, returning the first element that is valid
    • Dynamic index on a Vec, by indexing with a UInt, for example io.in(0.U)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyRoutingArbiter(numChannels: Int) extends Module {
val io = IO(new Bundle {
val in = Vec(numChannels, Flipped(Decoupled(UInt(8.W))))
val out = Decoupled(UInt(8.W))
} )

// YOUR CODE BELOW
io.out.valid := io.in.map(_.valid).reduce(_ || _)
val channel = PriorityMux(
io.in.map(_.valid).zipWithIndex.map { case (valid, index) => (valid, index.U) }
)
io.out.bits := io.in(channel).bits
io.in.map(_.ready).zipWithIndex.foreach { case (ready, index) =>
ready := io.out.ready && channel === index.U
}
// for => foreach + collection
// for (i <- 0 until numChannels) {
// io.in(i).ready := io.out.ready && i.U === selChannel
// }
}