Presentation
Yodl is a simple and modern behavioural Hardware Description Language (HDL) which acts as a lightweight abstraction layer over the FIRRTL intermediate representation to describe digital circuits.
Design goals
Simple
- Yodl designs should be easy to read, even for hobbyists without any prior experience with HDLs, this is mainly achieved by using a familiar C/Rust-like syntax and limiting the amount of constructs (no always blocks / processes).
- Minimise implementation complexity by choosing an easy to parse syntax and by focusing on a restricted set of features.
Explicit
- Established HDLs like Verilog suffer from implicit behaviours which can lead to subtle, hard-to-diagnose bugs like implicit signal declarations or implicit wire narrowing and widening. Yodl enforces strict rules with explicit intent to prevent common mistakes.
Robust
- Yodl's type-system can catch many classes of errors before elaboration even begins.
Getting started
To try Yodl directly in your browser, visit the playground.
Installation
The simplest way to get yodl is to install the bundled npm package:
$ npm install --global yodl
$ yodl examples/Hello.yodl "write_firrtl Hello.fir"
.fir files can then be converted to SystemVerilog using firtool (tested with version 1.111.1).
Feature support for SystemVerilog/Verilog differs from tool to tool, check this page to customise the output.
When using Yosys for instance, specify --lowering-options=disallowPackedArrays,disallowLocalVariables,emitBindComments
.
firtool --format=fir --verilog Hello.fir -o Hello.sv
The project can also be compiled to wasm or wasm-gc by installing Moonbit and running:
$ git clone https://github.com/nathsou/yodl
$ cd yodl
$ moon build --target wasm-gc
$ moon run src/main examples/Hello.yodl "write_firrtl Hello.fir"
Editor support
Visual Studio Code
A syntax-highlighting extension is embedded in the repository, to install it, do:
- Open the command palette (Ctrl | Cmd + Shift + P)
- Select 'Developer: Install Extension from Location'
- Open 'extensions/yodl-vscode-syntax'
Your first Yodl design
Create a file called Blink.yodl
and insert the following text:
module Top(
// inputs
clk: clock,
rst: bool,
) -> (
// outputs
leds: uint<8>,
) {
let counter = Reg<uint<24>>(clk, rst)
counter.d = counter.q + 1'd1
leds = counter.q[23:16]
}
This simple design creates a 24-bit counter and displays the 8 most-significant bits to the output leds
port.
To convert this Yodl design to FIRRTL:
moon run src/main Blink.yodl "write_firrtl Blink.fir"
Which should output:
FIRRTL version 4.1.0
circuit Top:
public module Top:
input clk: Clock
input rst: UInt<1>
output leds: UInt<8>
wire counter: { flip d: UInt<24>, q: UInt<24> }
regreset reg_0: UInt<24>, clk, rst, UInt<24>(0)
connect counter.q, reg_0
connect reg_0, counter.d
connect counter.d, counter.q
node temp_1 = bits(add(counter.q, UInt<1>(1)), 24, 0)
connect counter.d, temp_1
node temp_2 = bits(counter.q, 23, 16)
connect leds, temp_2
Now to generate a SystemVerilog file:
firtool --format=fir -O=release --verilog \
-disable-all-randomization -strip-debug-info \
--lowering-options=disallowPackedArrays,disallowLocalVariables \
Blink.fir -o Blink.sv
Which should output a file similar to:
// Generated by CIRCT firtool-1.105.0
module Top(
input clk,
rst,
output [7:0] leds
);
reg [23:0] reg_0;
always @(posedge clk) begin
if (rst)
reg_0 <= 24'h0;
else
reg_0 <= reg_0 + 24'h1;
end // always @(posedge)
assign leds = reg_0[23:16];
endmodule
Data Types
Ground Types
Integer types
Yodl supports two integer types, which require a width specifier.
Unsigned Integers
module Test() -> () {
// a 16-bit unsigned integer
let a: uint<16>
a = 16'd1721
}
Signed Integers
module Test() -> () {
// a 7-bit signed ingeger
let b: sint<7> = -6'd8
let c: sint<32> = sint(32'd11)
}
Integer literals can be specified in decimal, binary, octal, or hexadecimal format.
The syntax is <width in bits>'[base prefix]<value>
The decimal value 1621
which requires at least 11 bits can be represented as follows:
prefix | base | example |
---|---|---|
b | 2 | 11'b11001010101 |
o | 8 | 11'o3125 |
d | 10 | 11'd1621 |
h | 16 | 11'h655 |
Booleans
The bool
type is an alias for the uint<1>
type.
For readability, the true
and false
keywords are supported and correspond to 1'b1
and 1'b0
respectively.
module Test() -> () {
let is_yodl_neat: bool = true
}
Clocks
The clock
type is required for clock signals used in Reg
and Memory
module instances.
module Counter(clk: clock) -> (count: uint<24>) {
let counter = Reg<uint<24>>(clk)
counter.d = counter.q + 1'd1
count = counter.q
}
Aggregate Types
Ground types can be combined to create more complex data structures.
Vectors
Vectors are ordered and sized collections of elements.
module Test() -> () {
// a vector of eight booleans
let neighbours: bool[8] = [false, false, true, false, true, false, true, false]
// a vector of four 16-bit unsigned integers
let ints: uint<16>[4] = [16'd1, 16'd12, 16'd3, 16'd4]
}
Characters and Strings
Characters use the ISO 8601 encoding (extended ASCII).
module Test() -> () {
let char: uint<8> = 'a'
}
Escape sequences
Sequence | Description |
---|---|
\n | Newline |
\t | Tab |
\\ | Backslash |
\' | Single quote |
Strings
Strings are fixed-length vectors of characters.
module Test() -> () {
let message: uint<8>[9] = "Yo, Yodl!"
}
Individual characters can be accessed using the following syntax:
module Test(clk: clock) -> () {
let first_char = "Yo!"[0]
$assert(first_char == 'Y')
}
Tuples
Tuples group a fixed number of ordered values of potentially different types.
module Test() -> () {
let pair: (bool, uint<8>) = (true, 8'hFF)
let triplet: ({ a: uint<4>[1], b: bool }, (bool, uint<8>), uint<1>) = ({ a: [4'd1], b: false }, pair, 1'b0)
let first: bool = pair.0 // true
let second: uint<8> = pair.1 // 8'hFF
}
Structs
Structures (also known as Bundles in Chisel/FIRRTL) group multiple named values under a single name.
module Test() -> () {
let colour: { r: uint<8>, g: uint<8>, b: uint<8> } = { r: 8'd17, g: 8'd128, b: 8'd211 }
let red = colour.r;
}
Instance Types
The type of a module instance is similar to a structure corresponding to the port signature of the module.
Note: Instance types are not compatible with structure types because only the ports which are accessed on a particular instance are included in the resulting type.
module Adder(a: uint<8>, b: uint<8>) -> (sum: uint<9>) {
sum = a + b;
}
module Top() -> () {
let adder = Adder()
adder.a = 8'd1
adder.b = 8'd2
let sum = adder.sum
}
Structural Typing
Yodl's type system is structural (except for instance types), which means that two types are compatible if they have the same shape.
Type A | Type B | Compatible? |
---|---|---|
uint<8> | uint<8> | Yes |
uint<1> | bool | Yes |
{ a: uint<16>, b: bool } | { b: bool, a: uint<16> } | Yes |
{ a: uint<16>, b: bool } | { a: uint<16> } | No |
{ a: uint<16>, b: bool } | { a: uint<16>, b: bool, c: bool[7] } | No |
uint<8>[8][8] | uint<8>[64] | No |
Adder | Adder | Yes |
Adder | { a: uint<8>, b: uint<8>, sum: uint<9> } | No |
Language Constructs
Packages
Packages group related top-level declarations under a common namespace.
package VGA {
const SCREEN_WIDTH = 640
const SCREEN_HEIGHT = 480
module SyncPulses(
clk: clock,
) -> (
hsync: bool,
vsync: bool,
row: uint<$clog2(SCREEN_HEIGHT)>,
col: uint<$clog2(SCREEN_WIDTH)>,
) {
// ...
hsync = 1'b0
vsync = 1'b0
row = 9'd0
col = 10'd0
}
}
module Top(
clk: clock,
) -> (
hsync: bool,
vsync: bool,
vga_color: { r: uint<8>, g: uint<8>, b: uint<8> },
) {
let pulses = VGA::SyncPulses(clk, hsync, vsync)
vga_color = {
r: pulses.row[7:0],
g: pulses.col[7:0],
b: 8'd127,
}
}
-
Members declared within a package are accessed using the
::
operator. -
Imported files are implicitly wrapped in a package with the same name as the file.
Constant Declarations
Constants are named fixed values known at compile-time.
const BAUD_RATE = 115200
const CLOCK_FREQ = 100 * 1_000_000 // 100 MHz
const CYCLES_PER_BIT = CLOCK_FREQ / BAUD_RATE
module Top() -> () {}
Type Alias Declarations
Type aliases are used to give a name to a commonly used type.
type U8 = uint<8>
type RGB = { r: U8, g: U8, b: U8 }
module Top() -> () {}
Parameterised type aliases
Type aliases can be parameterised by providing a list of type parameters.
type Triplet<T> = (T, T, T)
module Top() -> () {
let triplet: Triplet<bool> = (true, false, false)
}
Module Declarations
-
Modules enable hierarchical design by encapsulating reusable functionality.
-
A module is defined by a name, an optional list of parameters, and a list of input and output ports.
module FullAdder(
a: bool,
b: bool,
carry_in: bool,
) -> (
sum: bool,
carry_out: bool,
) {
let xor1 = a xor b
sum = carry_in xor xor1
carry_out = (carry_in and xor1) or (a and b)
}
module Top() -> () {}
Parameterised Modules
Modules can be parameterised by specifying a list of parameters surrounded by <
and >
after the module name.
module Adder<N: uint>(
a: uint<N>,
b: uint<N>,
carry_in: bool,
) -> (
sum: uint<N>,
carry_out: bool,
) {
let carry_chain: bool[N + 1]
carry_chain[0] = carry_in
let bits: bool[N]
for i in 0..<N {
FullAdder(
a: a[i],
b: b[i],
carry_in: carry_chain[i],
carry_out: carry_chain[i + 1],
sum: bits[i],
)
}
carry_out = carry_chain[N]
sum = uint(bits)
}
module Top() -> () {}
Module instances
-
Module instances are first-class values that can be assigned to variables.
-
If a module is parameterised, all the parameters must be specified when creating an instance.
-
Parameter names can be omitted if the parameters are specified in the same order as the declaration.
module Adder<N: uint>(
a: uint<N>,
b: uint<N>,
carry_in: bool,
) -> (
sum: uint<N>,
carry_out: bool,
) {
let total: uint<N + 1> = a + b + carry_in
sum = total[N - 1 : 0]
carry_out = total[N]
}
module Top(clk: clock) -> () {
let counter = Reg<uint<32>>(clk)
counter.d = counter.q + 1
let adder = Adder<32>( // or Adder<N: 32>(..)
a: counter.q,
b: 32'1,
carry_in: 1'0,
)
}
-
A module instance need not assign all ports as the module's ports are accessible as fields of the instance using the
.
operator. -
Ports whose values are local variables with the same name can omit the right-hand side of the assignment.
All three Reg
instances in the following example are equivalent.
module Top(clk: clock, rst: bool) -> () {
let reg1 = Reg<uint<32>>(clk: clk, rst: rst)
let reg2 = Reg<uint<32>>(clk, rst)
let reg3 = Reg<uint<32>>()
reg3.clk = clk
reg3.rst = rst
}
Let Bindings
- A let binding introduces a named value in the current scope.
- Let bindings must be defined inside a module declaration.
- The type of the binding is inferred from the right-hand side of the assignment if present otherwise, the type must be explicitly specified.
module Test() -> () {
let message1: uint<8>[3]
message1 = "Yo!"
let message2 = "Hi!"
let message3: uint<8>[3] = "Hey"
}
Assignments
If a binding is assigned multiple times, only the last assignment is used.
module Test() -> () {
let a = true // this first assignment is ignored
a = false
}
Block Expressions
Block expressions group multiple statements in a new lexical scope and optionally return a value:
module Test(clk: clock) -> () {
let result = {
let a = 5
let b = 10
(a + b) // Last expression becomes the block's value
}
$assert(result == 15)
}
Operators
Unary Operators
Operator | Description | Example |
---|---|---|
- | Arithmetic negation | -x |
not | Bitwise NOT | not x |
andr | AND reduction | andr x |
orr | OR reduction | orr x |
xorr | XOR reduction | xorr x |
Reduction Operators
Reduction operators apply the corresponding logic operation across all bits of the operand, returning a single bit result.
module Test(clk: clock) -> () {
let all_ones = andr 8'b11111111
$assert(all_ones == 1)
let any_one = orr 8'b00000001
$assert(any_one == 1)
let parity = xorr 8'b10101010
$assert(parity == 0)
}
Binary Operators
Arithmetic Operators
Operator | Description | Example |
---|---|---|
+ | Addition | a + b |
- | Subtraction | a - b |
* | Multiplication | a * b |
/ | Division | a / b |
mod | Modulo | a mod b |
Bitwise Operators
Operator | Description | Example |
---|---|---|
and | Bitwise AND | a and b |
or | Bitwise OR | a or b |
xor | Bitwise XOR | a xor b |
nand | Bitwise NAND | a nand b |
nor | Bitwise NOR | a nor b |
xnor | Bitwise XNOR | a xnor b |
shl | Shift Left | a shl b |
shr | Shift Right | a shr b |
Note 1: When the shift amount (the right hand side) in a shr
operation is signed (sint
type), the operation corresponds to an arithmetic shift right.
Note 2: The and
, or
, xor
, nand
, nor
, and xnor
operators are used both to perform bitwise and logical operations.
Comparison Operators
Operator | Description | Example |
---|---|---|
== | Equal | a == b |
!= | Not Equal | a != b |
<: | Less Than | a <: b |
>: | Greater Than | a >: b |
<= | Less Than or Equal | a <= b |
>= | Greater Than or Equal | a >= b |
Ternary Operator
The ternary operator is a concise way to express conditional expressions, generally spanning a single line.
module Test(a: uint<8>, b: uint<8>) -> () {
let max = a >= b ? a : b
}
Concatenation
Integer concatenation can be performed by wrapping a list of values in curly braces:
module Test(clk: clock) -> () {
let upper_nibble = 8'hAB
let lower_nibble = 8'hCD
let word = {upper_nibble, lower_nibble}
$assert(word == 16'hABCD)
}
The concatenation operator also accepts Vectors of integers.
module Test(clk: clock) -> () {
let value = {[1'1, 1'0, 1'0, 1'0]} // 4'b1000
$assert(value == 4'b1000)
}
Slicing and Indexing
Elements of vectors and bits of integers can be accessed using the []
operator:
module Test(clk: clock) -> () {
let bits = [..8'd233]
let first = bits[0] // Access the first element
let nibble = bits[7:4] // Extract a range of bits (inclusive)
let byte = bits[7-:8] // Extract 8 bits starting from bit 7 (equivalent to data[7:0])
$assert(first == 1'b1)
$assert(uint(nibble) == 4'hE)
$assert(uint(byte) == 8'hE9)
}
There are two forms of bit slicing:
[high:low]
- Extract bits from positionhigh
down tolow
(inclusive)[start-:width]
- Extractwidth
bits starting from positionstart
Note:
- Integers are indexed from the least significant bit (LSB) to the most significant bit (MSB) (right to left).
- Vectors follow standard array-indexing conventions, with the first element at index 0 (left to right).
When a bit vector (bool[N]
i.e. uint<1>[N]
) is used as the argument of the uint
and sint
built-in functions,
the first element of the vector becomes the MSB of the resulting integer:
module Test(clk: clock) -> () {
let n = uint([1'b1, 1'b0, 1'b0])
$assert(n == 3'b0001)
}
Replication
Replication expressions <uint>*[<expr-list>]
and <uint>*{<expr-list>}
create a vector by repeating a value multiple times:
const a = 1'b1
const b = 1'b0
module Test(clk: clock) -> () {
let zeros = 4*[1'b0] // Expands to [1'b0, 1'b0, 1'b0, 1'b0]
let ones = 3*{1'b1} // Expands to {1'b, 1'b, 1'b1}
$assert(uint(zeros) == 4'd0)
$assert(uint(ones) == 3'b111)
}
The repeated expressions can contain any value, including instances:
module Cell(clk: clock, rst: bool) -> () {}
const Rows = 1
const Cols = 1
module Test(clk: clock, rst: bool) -> () {
// initialise a Rows by Cols grid of cells
let cells = Cols * [Rows * [Cell(clk, rst)]]
}
Concatenation
Concatenation expressions {<expr-list>}
create an integer from a list of smaller integers.
The width of the resulting integer is the sum of the widths of the operands.
If any operand is a signed integer (sint), then all operands are required to be signed.
module Test(clk: clock) -> () {
let concat_args = {16'hBABA, 16'hFABE}
let concat_vec = {[16'hBABA, 16'hFABE]}
$assert(concat_args == 32'hBABAFABE)
$assert(concat_vec == 32'hBABAFABE)
}
Spread
The spread operator ..
can only appear inside a vector expression and is used to decompose a value into its individual elements.
module Test(clk: clock) -> () {
let bits: uint<1>[4] = [..4'b1100] // [1'b0, 1'b0, 1'b1, 1'b1]
let chars: uint<8>[3] = [.."Yo!"] // [8'h59, 8'h6F, 8'h21]
let flat: uint<2>[3] = [..[2'd1, 2'd2], 2'd3] // [2'd1, 2'd2, 2'd3]
$assert(bits[0] == 1'b0)
$assert(bits[1] == 1'b0)
$assert(bits[2] == 1'b1)
$assert(bits[3] == 1'b1)
$assert(chars[0] == 8'h59)
$assert(chars[1] == 8'h6F)
$assert(chars[2] == 8'h21)
$assert(flat[0] == 2'd1)
$assert(flat[1] == 2'd2)
$assert(flat[2] == 2'd3)
}
Operator Precedence
Operators are evaluated in the following order (from highest to lowest precedence):
- Unary operators (
not
,-
,andr
,orr
,xorr
) - Multiplication, division, modulo (
*
,/
,mod
) - Addition, subtraction (
+
,-
) - Shift operations (
shl
,shr
) - Comparisons (
<:
,>:
,<=
,>=
) - Equality operators (
==
,!=
) - Bitwise AND and NAND (
and
,nand
) - Bitwise XOR and XNOR (
xor
,xnor
) - Bitwise OR and NOR (
or
,nor
)
Parentheses can be used to override the default precedence order.
Resulting Type
The output type of a binary operation is determined by the types of the operands and the operation being performed.
It matches the FIRRTL specification
The following table summarises the resulting type for each operation:
operation | lhs type | rhs type | output type |
---|---|---|---|
+ , - | uint<A> | uint<B> | uint<max(A, B) + 1> |
+ , - | sint<A> | sint<B> | sint<max(A, B) + 1> |
* | uint<A> | uint<B> | uint<A + B> |
* | sint<A> | sint<B> | sint<A + B> |
/ | uint<A> | uint<B> | uint<A> |
/ | sint<A> | sint<B> | sint<A + 1> |
mod | uint<A> | uint<B> | uint<min(A, B)> |
mod | sint<A> | sint<B> | sint<min(A, B)> |
== , != , <: , >: | uint<A> | uint<B> | uint<1> |
== , != , <: , >: | sint<A> | sint<B> | uint<1> |
and , nand , or , nor , xor , xnor | uint<A> | uint<B> | uint<max(A, B)> |
and , nand , or , nor , xor , xnor | sint<A> | sint<B> | uint<max(A, B)> |
Shift Operations
When the shift amount is known at compile time, the output type is determined as follows:
operation | lhs type | shift amount | output type |
---|---|---|---|
shl | uint<A> | n | uint<A + n> |
shl | sint<A> | n | sint<A + n> |
shr | uint<A> | n | uint<max(A - n, 0)> |
shr | sint<A> | n | sint<max(A - n, 1)> |
When the shift amount is not known at compile time, the output type is determined as follows:
operation | lhs type | rhs type | output type |
---|---|---|---|
shl | uint<A> | uint<B> | uint<A + 2^B - 1> |
shl | sint<A> | uint<B> | sint<A + 2^B - 1> |
shr | uint<A> | uint<B> | uint<A> |
shr | sint<A> | uint<B> | sint<A> |
Control Flow
Yodl supports several control flow constructs that help express complex behaviours in digital circuits.
Conditional Statements
If Expressions
If expressions evaluate a condition and execute one of two branches based on the result:
module Test(a: uint<8>, b: uint<8>) -> () {
let max = if a >: b {
a
} else {
b
}
}
If expressions always return a value. When used as statements (without assigning the result), the result is discarded.
Match Expressions
Match expressions provide a compact way of handling multiple conditions based on a single value:
module Cell(clk: clock) -> () {
let alive = Reg<bool>(clk);
const count = 2
// compute next state in the Game of Life
alive.d = match count {
3'd2 => alive.q // stable
3'd3 => true // reproduction
_ => false // overpopulation or underpopulation
}
}
A match expression must handle all possible values of the match condition, either by explicitly listing all cases or by providing a default case with the _
wildcard.
Iteration
For Loops
The for
loop iterates over a range of values:
// Simple parallel hexadecimal to character decoder
module Hex<Bits: uint>(n: uint<Bits>) -> (chars: uint<8>[$cdiv(Bits, 4)]) {
const Len = $cdiv(Bits, 4)
for i in 0..<Len {
chars[Len - 1 - i] = match n[(i + 1) * 4 - 1 -: 4] {
4'h0 => '0'
4'h1 => '1'
4'h2 => '2'
4'h3 => '3'
4'h4 => '4'
4'h5 => '5'
4'h6 => '6'
4'h7 => '7'
4'h8 => '8'
4'h9 => '9'
4'hA => 'A'
4'hB => 'B'
4'hC => 'C'
4'hD => 'D'
4'hE => 'E'
4'hF => 'F'
}
}
}
module Top() -> () {}
The range syntax uses:
a..<b
for exclusive upper bound (iterates from a to b-1)a..=b
for inclusive upper bound (iterates from a to b)
When to Use Each Control Flow Construct
- If Expressions: Use for simple binary decisions, especially when the logic is straightforward.
- Match Expressions: Use when you need to handle multiple distinct cases based on a single value.
- For Loops: Use to repeat operations a known number of times, especially when manipulating vectors or generating parallel hardware.
Control Flow in Hardware Context
In hardware description languages, control flow constructs don't create sequential execution as in software—they describe hardware structures that implement the specified behavior. Yodl's compiler unrolls loops and elaborates conditional statements into multiplexers.
Built-in Functions and Operations
Yodl provides several built-in functions and operations to facilitate hardware design.
Type Conversion Functions
uint(x)
Reinterprets a value as an unsigned integer.
module Test() -> () {
let a: sint<8> = -7'd11
let b: uint<8> = uint(a) // 8'd11
}
When applied to a vector of bits, it concatenates them into a single unsigned integer.
Note that the first element of the vector becomes the least significant bit (LSB) of the resulting integer. If you would like the order to be preserved, use the {<expr_list>}
concatenation operator.
module Test(clk: clock) -> () {
let bits = [true, false, true, true]
$assert(uint(bits) == 4'b1101)
}
sint(x)
Reinterprets a value as a signed integer.
module Test() -> () {
let a: sint<8> = sint(8'b10101010)
}
clock(x)
Converts a boolean signal to a clock signal.
module Test(clk: clock) -> () {
let counter = Reg<uint<24>>(clk)
counter.d = counter.q + 1
let slow_clk = clock(counter.q[23]) // Divide the clock by 2^23
}
Mathematical Functions
$clog2(n)
Computes the ceiling of the base-2 logarithm of n
.
Often used to determine the minimum number of bits required to represent a value.
module Test(clk: clock) -> () {
const AddrWidth = $clog2(1024)
let addr: uint<AddrWidth> = 10'd0
$assert(AddrWidth == 10)
}
$pow(base, exp)
Computes the power of base
raised to exp
.
module Test(clk: clock) -> () {
const kilobyte = $pow(2, 10)
$assert(kilobyte == 1024)
}
$cdiv(a, b)
Computes the ceiling of the division of a
by b
.
module Test(clk: clock) -> () {
const data_size = 1024
const block_size = 100
const blocks_needed = $cdiv(data_size, block_size)
$assert(blocks_needed == 11)
}
Bit Manipulation
$flip(x)
Reverses the bit order of x
.
module Test(clk: clock) -> () {
const flipped = $flip(5'b11100)
$assert(flipped == 5'b00111)
}
Vector Functions
$rev(vec)
Reverses the order of elements in a vector.
Particularly useful when constructing a bit vector from a list of bits from most to least significant
since vectors are indexed from least to most significant, i.e. vec[0]
is the first element.
module Test(clk: clock) -> () {
let reversed = $rev([1'1, 1'0, 1'0, 1'0])
$assert(reversed[0] == 1'0)
$assert(reversed[1] == 1'0)
$assert(reversed[2] == 1'0)
$assert(reversed[3] == 1'1)
}
Memory Functions
Yodl provides familiar $readmemb
and $readmemh
functions to initialize Memory instances from files.
$readmemb(file, memory)
Initializes a memory from a binary format file.
module Test(clk: clock, addr: uint<8>) -> () {
let rom = Memory<
T: uint<8>,
Depth: 256,
ReadPorts: 1,
WritePorts: 0,
>(
read: [{ clk: clk, en: true, addr: addr }],
)
// Initialize memory from binary file
$readmemb("rom_data.bin", rom)
let data = rom.q[0]
}
$readmemh(file, memory)
Initializes a memory from a hexadecimal format file.
module Test(clk: clock, addr: uint<8>) -> () {
let rom = Memory<
T: uint<8>,
Depth: 256,
ReadPorts: 1,
WritePorts: 0,
>(
read: [{ clk: clk, en: true, addr: addr }],
)
// Initialize memory from hex file
$readmemh("rom_data.hex", rom)
let data = rom.q[0]
}
Debug Functions
$printf(format_string, args..)
Prints formatted text during simulation. Similar to C's printf.
module Test(clk: clock, data: uint<8>) -> () {
$printf("Value of data: %d", data)
}
$assert(predicate, [format_string, args..])
Asserts that the predicate is true. If the predicate is false, the simulation stops and prints an optional error message.
module Test(clk: clock) -> () {
$assert(true == 1'b1)
const two_plus_two = 2 + 2
$assert(two_plus_two == 4, "Math is broken, expected 4, got %d", two_plus_two)
}
$stop([exit_code])
Stops the simulation with an optional exit code.
module Test(clk: clock) -> () {
const error_condition = false
if error_condition {
$stop(1)
}
}
Primitive Modules
Yodl provides a few essential modules for building sequential digital circuits.
Registers
Registers are fundamental storage elements in digital circuits. They store data between clock cycles.
Basic Register
module Counter(clk: clock) -> (value: uint<8>) {
let counter = Reg<uint<8>>(clk)
counter.d = counter.q + 1'b1
value = counter.q
}
Parameters
Parameter | Type | Description |
---|---|---|
T | type | Data type of the register |
Ports
Port | Direction | Type | Description |
---|---|---|---|
clk | Input | clock | Clock input |
d | Input | T | Data input (next state) |
en | Input | bool | Enable signal (optional) |
rst | Input | bool | Reset signal (optional) |
q | Output | T | Data output (current state) |
Register with Reset
module Counter(clk: clock, rst: bool) -> (value: uint<8>) {
let counter = Reg<uint<8>>(clk, rst)
counter.d = counter.q + 1'b1
value = counter.q
}
When rst
is asserted, the register's value is synchronously reset to 0.
Register with Enable
module Counter(clk: clock, enable: bool) -> (value: uint<8>) {
let counter = Reg<uint<8>>(clk, en: enable)
counter.d = counter.q + 1'b1
value = counter.q
}
The register only updates its value when enable
is asserted.
The same behaviour can be obtained using the d
port only:
module Counter(clk: clock, enable: bool) -> (value: uint<8>) {
let counter = Reg<uint<8>>(clk)
counter.d = enable ? counter.q + 1'b1 : counter.q
value = counter.q
}
Register with Asynchronous Reset
module Test(clk: clock, rst: bool) -> () {
let state = RegAsyncReset<uint<2>>(clk, rst)
}
With RegAsyncReset
, the reset signal is asynchronous and takes effect immediately.
Memory
Memories are arrays of registers that can be read from and written to.
Some configurations may be synthesised as block RAMs in FPGAs.
Parameters
Parameter | Type | Description |
---|---|---|
T | type | Data type of each element |
Depth | uint | Number of entries |
ReadPorts | uint | Number of read ports |
WritePorts | uint | Number of write ports |
ReadLatency | uint | Cycles to read |
WriteLatency | uint | Cycles to write |
Ports
Port | Direction | Type | Description |
---|---|---|---|
read | Input | {clk : clock , en : bool , addr : uint<$clog2(Depth)> }[ReadPorts ] | Read port(s) |
write | Input | {clk : clock , en : bool , addr : uint<$clog2(Depth)> , data : T , mask : MemoryMask<T> }[WritePorts ] | Write port(s) |
q | Output | T [ReadPorts ] | Read data port(s) |
Basic Memory
module RAM(
clk: clock,
addr: uint<10>,
write_data: uint<8>,
write_enable: bool,
) -> (
read_data: uint<8>,
) {
let mem = Memory<
T: uint<8>, // Data type
Depth: 1024, // Number of entries
ReadPorts: 1, // Number of read ports
WritePorts: 1, // Number of write ports
ReadLatency: 1, // Cycles to read
WriteLatency: 1, // Cycles to write
>(
read: [{ clk: clk, en: true, addr: addr }],
write: [{ clk: clk, en: write_enable, addr: addr, data: write_data, mask: true }],
)
read_data = mem.q[0]
}
Memory with Write Masking
Write masking allows selective updates to parts of a memory word:
module ByteAddressableRAM(
clk: clock,
addr: uint<10>,
write_data: uint<8>[4], // 32-bit word as 4 bytes
byte_mask: bool[4], // Which bytes to write
write_enable: bool,
) -> (
read_data: uint<32>,
) {
let mem = Memory<
T: uint<8>[4],
Depth: 1024,
ReadPorts: 1,
WritePorts: 1,
ReadLatency: 1,
WriteLatency: 1,
>(
read: [{ clk: clk, en: true, addr: addr }],
write: [{ clk: clk, en: write_enable, addr: addr, data: write_data, mask: byte_mask }],
)
read_data = uint(mem.q[0])
}
Mask Type
Intuitively, the mask type MemoryMask<T>
of a data type T
matches the structure of T
with each ground type (e.g. uint<N>
, sint<N>
, ..) replaced with bool
.
Examples:
Data Type | Mask Type |
---|---|
bool | bool |
uint<8> | bool |
uint<32>[4] | bool[4] |
{a: uint<8>, b: uint<8>} | {a: bool, b: bool} |
{a: {b: uint<16>[64], c: bool}} | {a: {b: bool[64], c: bool}} |
To learn more about write masks, check out the FIRRTL specification.