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:

  1. Open the command palette (Ctrl | Cmd + Shift + P)
  2. Select 'Developer: Install Extension from Location'
  3. 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:

prefixbaseexample
b211'b11001010101
o811'o3125
d1011'd1621
h1611'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

SequenceDescription
\nNewline
\tTab
\\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 AType BCompatible?
uint<8>uint<8>Yes
uint<1>boolYes
{ 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
AdderAdderYes
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

OperatorDescriptionExample
-Arithmetic negation-x
notBitwise NOTnot x
andrAND reductionandr x
orrOR reductionorr x
xorrXOR reductionxorr 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

OperatorDescriptionExample
+Additiona + b
-Subtractiona - b
*Multiplicationa * b
/Divisiona / b
modModuloa mod b

Bitwise Operators

OperatorDescriptionExample
andBitwise ANDa and b
orBitwise ORa or b
xorBitwise XORa xor b
nandBitwise NANDa nand b
norBitwise NORa nor b
xnorBitwise XNORa xnor b
shlShift Lefta shl b
shrShift Righta 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

OperatorDescriptionExample
==Equala == b
!=Not Equala != b
<:Less Thana <: b
>:Greater Thana >: b
<=Less Than or Equala <= b
>=Greater Than or Equala >= b
Note: The `<:` and `>:` operators are used for comparison instead of `<` and `>` to avoid confusion with type parameter delimiters during parsing.

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:

  1. [high:low] - Extract bits from position high down to low (inclusive)
  2. [start-:width] - Extract width bits starting from position start

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):

  1. Unary operators (not, -, andr, orr, xorr)
  2. Multiplication, division, modulo (*, /, mod)
  3. Addition, subtraction (+, -)
  4. Shift operations (shl, shr)
  5. Comparisons (<:, >:, <=, >=)
  6. Equality operators (==, !=)
  7. Bitwise AND and NAND (and, nand)
  8. Bitwise XOR and XNOR (xor, xnor)
  9. 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:

operationlhs typerhs typeoutput 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>
moduint<A>uint<B>uint<min(A, B)>
modsint<A>sint<B>sint<min(A, B)>
==, !=, <:, >:uint<A>uint<B>uint<1>
==, !=, <:, >:sint<A>sint<B>uint<1>
and, nand, or, nor, xor, xnoruint<A>uint<B>uint<max(A, B)>
and, nand, or, nor, xor, xnorsint<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:

operationlhs typeshift amountoutput type
shluint<A>nuint<A + n>
shlsint<A>nsint<A + n>
shruint<A>nuint<max(A - n, 0)>
shrsint<A>nsint<max(A - n, 1)>

When the shift amount is not known at compile time, the output type is determined as follows:

operationlhs typerhs typeoutput type
shluint<A>uint<B>uint<A + 2^B - 1>
shlsint<A>uint<B>sint<A + 2^B - 1>
shruint<A>uint<B>uint<A>
shrsint<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)
For loops are fully unrolled during compilation, so they must have compile-time constant bounds.

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

ParameterTypeDescription
TtypeData type of the register

Ports

PortDirectionTypeDescription
clkInputclockClock input
dInputTData input (next state)
enInputboolEnable signal (optional)
rstInputboolReset signal (optional)
qOutputTData 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

ParameterTypeDescription
TtypeData type of each element
DepthuintNumber of entries
ReadPortsuintNumber of read ports
WritePortsuintNumber of write ports
ReadLatencyuintCycles to read
WriteLatencyuintCycles to write

Ports

PortDirectionTypeDescription
readInput{clk: clock, en: bool, addr: uint<$clog2(Depth)>}[ReadPorts]Read port(s)
writeInput{clk: clock, en: bool, addr: uint<$clog2(Depth)>, data: T, mask: MemoryMask<T>}[WritePorts]Write port(s)
qOutputT[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 TypeMask Type
boolbool
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}}
Portions of an integer cannot directly be masked in Yodl, just like in Chisel. Instead, the integer can be split into smaller parts which can be masked individually.

To learn more about write masks, check out the FIRRTL specification.