2. Outline
• Introduction: background, motivation
• Dataflow HDLs: A novel hardware design abstraction layer
• DFiant HDL: Syntax, semantics, and various features
• Live demo
2
3. Motivation: The fallacy of RTL
“behavioral design”
• Bounded to either a combinational or a single
cycle description. What if one cycle is not
enough?
• Retiming as a solution?
• Still requires added registers in RTL.
• What happens during reset?
• What if we need backpressure supported?
• What about other common non-inferable
behaviors: FIFOs, PLLs/DCMs, De/Serializers
• The description changes for non-functional
properties:
• Clock and Reset polarities
• Synchronous vs. Asynchronous Reset
• Clock-Enable vs. Clock-Gating
3
always @(*)
begin
case(ALU_Sel)
4'b0000: // Addition
ALU_Result = A + B;
4'b0001: // Subtraction
ALU_Result = A - B;
4'b0010: // Multiplication
ALU_Result = A * B;
4'b0011: // Division
ALU_Result = A / B;
//...
//...
end
ALU Verilog Code Example (Combinational)
4. Motivation: The fallacy of HLS
hardware description
• HLS languages are typically sequential languages (e.g., C).
• Designed to speed up algorithms for FPGAs/ASICs with auto-pipelining;
not as effective or clear for architectures.
• No concurrent semantics (other than applying pragmas).
• No clear hierarchies and IO connectivity.
• May not have composable state (static values are accessed globally).
• Can you design a UART or a RISC-V core in HLS?
• Yes, but with loads of pain with little gain.
4
6. Key Observation:
What is pipelining for?
6
Pipelining is for
Performance
Correctness
always_ff @(posedge clk)
begin
case(ALU_Sel)
4'b0000: // Addition
ALU_Result = A + B;
4'b0001: // Subtraction
ALU_Result = A - B;
4'b0010: // Multiplication
ALU_Result = A * B;
4'b0011: // Division
ALU_Result = A / B;
//...
//...
end
ALU Verilog Code Example (Sequential)
10. DFiant HDL Goals
• Clear separation of non/synthesizable constructs
• Concise and type-safe syntax
• VHDL type-safe but too verbose.
• Verilog less verbose but unsafe.
• Good IDE integration and compile-time errors
• Expandable and composable language constructs
• Generate readable RTL and maintain source fidelity
• Opensource
10
12. Hello DFiant HDL World
12
import dfhdl.*
class MyDesign extends DFDesign:
val i = UInt(8) <> IN init 0
val o = UInt(8) <> OUT init 0
val v = UInt(8) <> VAR init 0
val c = UInt(8) const 0
v := c
v := i
o := o + v
13. Hello DFiant HDL World
13
import dfhdl.*
class MyDesign extends DFDesign:
val i = UInt(8) <> IN init 0
val o = UInt(8) <> OUT init 0
val v = UInt(8) <> VAR init 0
val c = UInt(8) const 0
v := c
v := i
o := o + v
This summons all the dataflow HDL functionality to
the current namespace
Import the DFiant HDL library
14. Hello DFiant HDL World
14
import dfhdl.*
class MyDesign extends DFDesign:
val i = UInt(8) <> IN init 0
val o = UInt(8) <> OUT init 0
val v = UInt(8) <> VAR init 0
val c = UInt(8) const 0
v := c
v := i
o := o + v
class _designType_ extends DFDesign: /*...*/
Declare your dataflow design
15. Hello DFiant HDL World
15
import dfhdl.*
class MyDesign extends DFDesign:
val i = UInt(8) <> IN init 0
val o = UInt(8) <> OUT init 0
val v = UInt(8) <> VAR init 0
val c = UInt(8) const 0
v := c
v := i
o := o + v
Populate your design with
ports, variables, and logic
Declarations are actually construction statements. E.g.:
Construct an unsigned 8-bit input port with name `i`
16. Dataflow Port/Variable Declaration
16
val i = UInt(8) <> IN init 0
val o = Bit <> OUT init (0,1,0)
val _name_ = _dftype_ <> _modifier_ [init _token_]
VAR
IN
OUT
INOUT
Optional dataflow history
initialization value or a tuple of
values to represent a sequence.
The type must match the given
dataflow type
Wide range of
dataflow types are
available and are
also composable
19. Boolean or Bit Dataflow Values
Boolean / Bit
val vbit = Bit <> VAR
val vbool = Boolean <> VAR
// both accept: true / false / 1 / 0
vbit := 1; vbool := false
vbit := true; vbool := 0
// explicit conversion
vbit := vbool.bit; vbool := vbit.bool
// implicit conversion
vbit := vbool; vbool := vbit
// logical operations: ||, &&, ^, !
val x = vbit || !vbool
20. Bit Vector Dataflow Values
• The given `width` must be a
positive integer.
• Most-significant bit is on the left
and its index value is `width-1`.
• For most operations, bit vectors
only accept bit vectors that have
the same width. E.g.:
Bits(width)
val i = Bits(3) <> IN
val v = Bits(8) <> VAR
// bits string interpolation tokens
v := b"10000001"; v := h"81"
// all bits are the same
v := all(0); v := all(true)
// bit-range and single bit selections
val x1 = v(3, 0); val x2 = v(6)
// concatenation tuple of known width values
(v(3, 0), v(7, 4)) := (i, b"1001", 1)
// logical operations: |, &, ^, ~, >>, <<
val x3 = ~v | h"22"
// resize truncates or (zero-)extends
val x4 = v.resize(4); val x5 = v.resize(16)
21. Bit Vector Token Binary/Hex String Interpolator
b"width'bin" / h"width'hex"
b"1" // value = 1
b"1000" // value = 1000
b"8'1000" // value = 00001000
b"3'0100" // value = 100
b"3'1100" // error
b"1?11" // value = 1?11
b"11_00" // value = 1100
h"1" // value = 0001
h"27" // value = 00100111
h"6'27" // value = 100111
h"5'27" // error
h"2?" // value = 0010????
h"F{00}F" // value = 1111001111
h"3_3" // value = 00110011
22. Error Examples
22
import dfhdl.*
class Example extends DFDesign:
val vu = UInt(8) <> VAR
val vs = SInt(8) <> VAR
vs := vu.signed
vu := vs
val x = vs + vu
23. Enumeration Dataflow Values
enum _name_ extends Encode: /*..*/
enum State extends Encode:
case Idle, Run, Stop
import State.*
val state = State <> VAR init Idle
state match
case Idle => state := Run
case Run => state := Stop
case Stop => state := Idle
24. Enumeration Encoding
enum _name_ extends Encode: /*..*/
enum MyEnum1 extends Encode:
case Foo, Bar, Baz //values: 00, 01, 10
enum MyEnum2 extends Encode.StartAt(20):
case Foo, Bar, Baz //values: 10100, 10101, 10110
enum MyEnum3 extends Encode.OneHot:
case Foo, Bar, Baz //values: 001, 010, 100
enum MyEnum4 extends Encode.Grey:
case Foo, Bar, Baz //values: 000, 001, 011
enum MyEnum5(val value: UInt[8] <> TOKEN)
extends Encode.Manual(8):
case Foo extends MyEnum5(200)
case Bar extends MyEnum5(100)
case Baz extends MyEnum5(0)
25. Generic Vector Dataflow Values
T X length
// 8 elements vector of 10-bit unsigned int
val v = UInt(10) X 8 <> VAR init all(0)
// assign to specific element
v(0) := 22
// mux selection assignment
val i = UInt(3) <> IN
v(i) := 70
// assign complete vector: 0, 10, 20, ..., 70
v := Vector.tabulate(8)(i => i * 10)
// 4x4 matrix of 8-bit unsigned int
val v44 = UInt(8) X 4 X 4 <> VAR init all(all(0))
v44(3)(1) := 22
26. Structure Dataflow Values
case class _name_(fields) extends Struct
// field selection and assignment
pixel.x := pixel.y + 5
// struct constant composition
pixel := Pixel(d"22", 22)
// struct value composition
pixel := Pixel(i(15, 8), i(7, 0))
// casted assignment
val i = Bits(16) <> IN
pixel := i.as(Pixel)
// declare the struct
case class Pixel(
x : UInt[8] <> VAL,
y : UInt[8] <> VAL
) extends Struct
// construct the struct variable
val pixel = Pixel <> VAR init Pixel(5, 7)
28. If Conditional Expression/Statement
if (cond) ??? else if (cond2) ??? else ???
val i = UInt(8) <> IN
val v = Bits(2) <> OUT
// `if` as a statement
if (i > 5) v := b"00"
else if (i > 20)
sim.report(msg"found i > 20")
if (i.bits(0) == 0) v := b"01" else v := b"10"
else v := b"11"
29. Match Conditional Expression/Statement
selector match case pattern => ???
val i = Bits(8) <> IN
i match
// bit vector string interpolator
case b"10010001" | h"AB"=>
// interpolators with wild-cards
case b"1001????" | h"?F" =>
// value extractor
case b"11${x : B[4]}11" =>
sim.report(msg"Found value $x")
// value extractors with a guard
case h"${x : B[2]}F${y : B[2]}" if x == y =>
case _ =>
30. Struct Pattern Matching
case class Packet(
header: Bits[8] <> VAL,
cnt: UInt[8] <> VAL,
data: Bits[8] <> VAL,
checksum: Bits[8] <> VAL
) extends Struct
val i = Packet <> IN
i match
case Packet(h"FA", cnt, data, _) if cnt < 22 =>
sim.report(msg"Found data $data")
case _ =>
32. Instantiate and Connect
• <> Connectivity direction is automatic
(:= assignment is directional from right to left)
• Directly connect sibling design ports
32
class ID extends DFDesign:
val x = SInt(16) <> IN
val y = SInt(16) <> OUT
y := x
class IDTop extends DFDesign:
val x = SInt(16) <> IN
val y = SInt(16) <> OUT
val id1 = new ID
val id2 = new ID
id1.x <> x
id1.y <> id2.x
id2.y <> y
34. Dataflow HDL Execution Model: Concurrency
Constructed Hardware
A and B are
independent
C is dependent
on both
Dataflow HDL Code
34
35. Dataflow HDL Execution Model: Concurrency
Constructed Hardware
* 3 +2
A-B
2
1
3
0
1
2
2
3
4
6
3
9
4
0
5
Dataflow HDL Code (DFiant)
class MyDesign extends DFDesign:
val inA = UInt(32) <> IN
val inB = UInt(32) <> IN
val outC = UInt(32) <> OUT
val A = inA * 3
val B = inB + 2
val C = A - B
outC := C
35
36. Dataflow HDL Execution Model: State
Constructed Hardware
inA := outC.prev
outC.prev outC
State
Element
Dataflow HDL Code
36
37. Dataflow HDL Execution Model: State
Constructed Hardware
* 3 +2
A-B
2
4
9
0
1
2
2
3
4
6
12
27
4
9
23
Dataflow HDL Code (DFiant)
class MyDesign extends DFDesign:
val inA = UInt(32) <> VAR
val inB = UInt(32) <> IN
val outC = UInt(32) <> OUT init 2
inA := outC.prev
val A = inA * 3
val B = inB + 2
val C = A - B
outC := C
37
39. class Dsn extends
DFDesign:
class Dsn extends
RTDesign(cfg):
class Dsn extends
EDDesign:
Dataflow
Domain
Register-Transfer
Domain
Event-Driven
Domain
Correctness:
Synthesizability:
✓
✓ ✓
? ?
?
40. class Fib extends DFDesign:
val f = UInt(32) <> VAR init (1, 0)
val o = UInt(32) <> OUT
f := f.prev(1) + f.prev(2)
o := f.prev(2)
val clkCfg = ClkCfg(ClkCfg.Edge.Rising)
val rstCfg = RstCfg(
RstCfg.Mode.Async, RstCfg.Active.Low
)
val cfg = RTDomainCfg(clkCfg, rstCfg)
class Fib extends RTDesign(cfg):
val o = UInt(32) <> OUT
val f = UInt(32) <> REG init 1
val f_reg = UInt(32) <> REG init 0
f_reg.din := f
f.din := f + f_reg
o := f_reg
class Fib extends EDDesign:
val clk = Bit <> IN
val rst = Bit <> IN
val o = UInt(32) <> OUT
val f = UInt(32) <> VAR init 1
val f_reg = UInt(32) <> VAR init 0
val f_din = UInt(32) <> VAR
val f_reg_din = UInt(32) <> VAR
process(all) {
f_reg_din :== f
f_din :== f + f_reg
o :== f_reg
}
process(clk, rst) {
if (rst == 0)
f :== d"32'1"
f_reg :== d"32'0"
else if (clk.rising)
f :== f_din
f_reg :== f_reg_din
}
41. Automatic Pipelining / Path-Balancing
• Pipelining can be completely automatic, according to a user defined constraint
and a pipeline estimation function.
• Pipelining can be partially manual, and the compiler can both add path balancing
stages or extra pipe stages.
• Pipelining can be manual only, and the compiler will only balance the data path.
41
//Simple Moving Average (N=4)
@df class SMA extends DFDesign {
val x = DFUInt(16) <> IN init 0
val y = DFUInt(16) <> OUT
val s1 = x +^ x.prev
val s2 = x.prev(2) +^ x.prev(3)
val sum = s1.pipe +^ s2.pipe
y := (sum / 4).resize(16)
}
s1 s2
42. Summary
• Registers serve various roles that bind the design when applied directly.
• DF-HDLs separate design functionality from its constraints by abstracting over
registers.
• DF-HDLs provide seamless concurrency, state control, and flow control through
their unique semantics.
• DF-HDLs allow mixing automatic pipelining and path-balancing along with
traditional hardware description capabilities.
• DFiant, a DF-HDL language, has other productivity-boosting traits like expandable
strongly typed system, concise and powerful syntax (up to 70% LoC reduction),
and good integration with Scala tooling.
42