Yodl provides a few essential modules for building sequential digital circuits.
Registers are fundamental storage elements in digital circuits. They store data between clock cycles.
1module Counter(clk: clock) -> (value: u8) {
2 let counter = Reg[u8](clk)
3 counter.d = counter.q + 1'b1
4 value = counter.q
5}
| Parameter | Sort | Description |
T | Type | Data type of the register |
| 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) |
1module Counter(clk: clock, rst: bool) -> (value: u8) {
2 let counter = Reg[u8](clk, rst)
3 counter.d = counter.q + 1'b1
4 value = counter.q
5}
When rst is asserted, the register's value is synchronously reset to 0.
1module Counter(clk: clock, enable: bool) -> (value: u8) {
2 let counter = Reg[u8](clk, en: enable)
3 counter.d = counter.q + 1'b1
4 value = counter.q
5}
The register only updates its value when enable is asserted.
The same behaviour can be obtained using the d port only:
1module Counter(clk: clock, enable: bool) -> (value: u8) {
2 let counter = Reg[u8](clk)
3 counter.d = enable ? counter.q + 1'b1 : counter.q
4 value = counter.q
5}
1let state = RegAsyncReset[u2](clk, rst)
With RegAsyncReset, the reset signal is asynchronous and takes effect immediately.
Memories are arrays of registers that can be read from and written to.
Some configurations may be synthesised as block RAMs in FPGAs.
| Parameter | Sort | Description |
T | Type | Data type of each element |
Depth | Nat | Number of entries |
ReadPorts | Nat | Number of read ports |
WritePorts | Nat | Number of write ports |
ReadLatency | Nat | Cycles to read |
WriteLatency | Nat | Cycles to write |
| Port | Direction | Type | Description |
read | Input | [ReadPorts](clk: clock, en: bool, addr: uint[clog2!(Depth)]) | Read port(s) |
write | Input | [WritePorts](clk: clock, en: bool, addr: uint[clog2!(Depth)], data: T, mask: MemoryMask[T]) | Write port(s) |
q | Output | [ReadPorts]T | Read data port(s) |
1module RAM(
2 clk: clock,
3 addr: u10,
4 write_data: u8,
5 write_enable: bool,
6) -> (
7 read_data: u8,
8) {
9 let mem = Memory[
10 T: u8,
11 Depth: 1024,
12 ReadPorts: 1,
13 WritePorts: 1,
14 ReadLatency: 1,
15 WriteLatency: 1,
16 ](
17 read: [(clk: clk, en: true, addr: addr)],
18 write: [(clk: clk, en: write_enable, addr: addr, data: write_data, mask: true)],
19 )
20
21 read_data = mem.q[0]
22}
Write masking allows selective updates to parts of a memory word:
1module ByteAddressableRAM(
2 clk: clock,
3 addr: u10,
4 write_data: [4]u8,
5 byte_mask: [4]bool,
6 write_enable: bool,
7) -> (
8 read_data: u32,
9) {
10 let mem = Memory[
11 T: [4]u8,
12 Depth: 1024,
13 ReadPorts: 1,
14 WritePorts: 1,
15 ReadLatency: 1,
16 WriteLatency: 1,
17 ](
18 read: [(clk: clk, en: true, addr: addr)],
19 write: [(clk: clk, en: write_enable, addr: addr, data: write_data, mask: byte_mask)],
20 )
21
22 read_data = uint!(mem.q[0])
23}
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 |
u8 | bool |
[4]u32 | [4]bool |
(a: u8, b: u8) | (a: bool, b: bool) |
(a: (b: [64]u16, c: bool)) | (a: (b: [64]bool, 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.