Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/lessons/content-alignment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,25 @@ describe('browser-audit lesson alignment', () => {
expect(read('sva/sequence-basics/grant_check.sol.sv')).not.toContain('llhd.process');
expect(read('sva/sequence-basics/grant_check.sol.sv')).not.toContain('$display("PASS at');
});

it('keeps the SystemVerilog Basics copy technically accurate', () => {
const welcome = read('sv/welcome/description.html');
const events = read('sv/events/description.html');
const parameters = read('sv/parameters/description.html');
const enums = read('sv/enums/description.html');

expect(welcome).not.toContain('One tight paragraph on printf/display notation');
expect(welcome).toContain('Calling <code>$finish</code> explicitly ends the simulation');
expect(events).not.toContain('events are not stateful (latching)');
expect(events).toContain('wait(event_name.triggered)');
expect(events).toContain('IEEE 1800-2023 §15.5');
expect(events).toContain('This event is generated automatically');
expect(parameters).not.toContain('$bits(16) = 4');
expect(parameters).toContain('IEEE 1800-2023 §20.6.2');
expect(parameters).toContain('IEEE 1800-2023 §20.8.1');
expect(parameters).toContain('replace the hardcoded dimensions');
expect(enums).not.toContain('remove <code>state_bits</code>');
expect(enums).toContain('do not add a separate <code>state_bits</code> port');
expect(enums).toContain('IEEE 1800-2023 §6.19');
});
});
4 changes: 2 additions & 2 deletions src/lessons/sv/enums/description.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<p>An <strong><dfn data-card="An enum (enumeration) gives symbolic names to a set of integer constants. The compiler assigns values automatically (0, 1, 2, …) or you can override them. In synthesis, the tool chooses the encoding (binary, one-hot, gray code) unless you specify the base type. Using enum names instead of raw numbers makes RTL self-documenting and prevents invalid state assignments.">enum</dfn></strong> gives meaningful names to a set of integer constants. For a memory controller, the states are naturally expressed as an enum rather than raw bit-vectors:</p>
<p>An <strong><dfn data-card="An enum declares a set of named integral constants and can define a strongly typed enum type. Unassigned names receive consecutive values; you can also assign explicit values. An explicit base type sets the underlying integral type and width, while any synthesis encoding is a tool choice rather than SystemVerilog language semantics (IEEE 1800-2023 §6.19).">enum</dfn></strong> gives meaningful names to a set of integer constants. For a memory controller, the states are naturally expressed as an enum rather than raw bit-vectors:</p>
<pre>typedef enum logic [1:0] { IDLE, CMD, READ, WRITE } ctrl_state_t;</pre>
<p>The compiler assigns <code>IDLE=0</code>, <code>CMD=1</code>, <code>READ=2</code>, <code>WRITE=3</code> automatically. Once defined, you use the type just like any other port type and use the constant names directly in <code>case</code> branches — no raw numbers needed.</p>
<p>Open <code>status.sv</code>. There are three things to do:</p>
<ol>
<li>Add the <code><dfn data-card="typedef creates a type alias — a new name for an existing type. typedef enum {...} name_t lets you use the type name directly in port declarations and variables instead of repeating the full enum syntax. The _t suffix is a convention for type names. typedef is purely a compile-time construct with no hardware cost.">typedef</dfn> enum</code> declaration above the module</li>
<li>Change the port to <code>input ctrl_state_t state</code> (remove <code>state_bits</code>)</li>
<li>Change the existing <code>state</code> port from <code>logic [1:0]</code> to <code>ctrl_state_t</code> — do not add a separate <code>state_bits</code> port</li>
<li>Update the <code>case</code> branches to match on <code>READ</code> and <code>WRITE</code> by name</li>
</ol>
<blockquote><p>In the next lesson we'll build the full memory access controller FSM that transitions between these states. The enum you define here becomes the state register type.</p></blockquote>
10 changes: 5 additions & 5 deletions src/lessons/sv/events/description.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
</p>
<p>
We can coordinate concurrent processes using <dfn data-card="Events are similar to constructions in many other programming languages. Such as threading.Event in Python, std::condition_variable in C++, sync.Cond in Go, and so on.
The main gotcha is that System Verilog events are not stateful (latching).
If you wait for an event that has already been posted, you will wait forever.">events</dfn>.
The trigger itself is instantaneous, but the event's <code>triggered</code> state persists for the remainder of the current simulation time slot (IEEE 1800-2023 §15.5).
A process waiting with <code>@</code> must start waiting before the trigger; use <code>wait(event_name.triggered)</code> when the wait and trigger may occur in either order.">events</dfn>.
An event is a named synchronization point that processes can wait for or post.
The basic operations on events are: </p>
<ul>
Expand All @@ -20,7 +20,7 @@
<li><code>@(write_done)</code>: wait for the event — suspends until posted</li>
</ul>
<p>
<blockquote><p>Note: You could also just write <code>@ write_done</code>, but using brackets around the event is a universal convention</p></blockquote>
<blockquote><p>Note: You can write either <code>@write_done</code> or <code>@(write_done)</code>. The parenthesized form is common and makes the event control especially easy to spot.</p></blockquote>

<p>The post is instantaneous: every process suspended at <code>@(write_done)</code> resumes in the same simulation time step. Events carry no data and cannot be synthesised — they live only in testbenches and simulation models.</p>

Expand Down Expand Up @@ -63,14 +63,14 @@ <h2>A two-event pipeline</h2>

<p>Add the four TODO lines — one <code>-&gt;</code> and one <code>@</code> per handshake. Without them, the reader and checker start at t=0, read uninitialised <code>x</code> values, and <code>PASS</code> never prints.</p>

<blockquote><p>Warning: If you add <code>@(write_done)</code> but forget <code>-> write_done</code>, the reader waits forever and the simulation never finishes. Each handshake requires both sides: a sender and at least one receiver.</p></blockquote>
<blockquote><p>Warning: If you add <code>@(write_done)</code> but forget <code>-> write_done</code>, the reader remains blocked. If no other process ends the test or schedules more work, the simulation can then stop with the reader still waiting. Each handshake requires both sides: a sender and at least one receiver.</p></blockquote>
<p>
Hint: You can follow the events status in the simulator waveform viewer, just like any other signal.
</p>

<h2>Built-in vs named events</h2>
<p>
A very important built-in event is <code>posedge clk</code>.
This even is generated automatically by the simulator whenever the signal <code>clk</code> transitions from 0 to 1.
This event is generated automatically by the simulator whenever the signal <code>clk</code> transitions from 0 to 1.
You don't post it yourself, but you can wait for it with <code>@(posedge clk)</code>.
<p>In the next lesson, <code>always_ff @(posedge clk)</code> uses exactly this: the block suspends at the rising-edge event and latches new values into the flip-flop array. Every register in our SRAM waits for that one recurring event.</p>
14 changes: 6 additions & 8 deletions src/lessons/sv/parameters/description.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<p>
So far we've been writing arrays and vectors with fixed widths, like <code>[7:0]</code>.
Later we will discuss include dynamic arrays, <code>arr[]</code>, associative arrays, <code>arr[string]</code>, and queues, <code>int q[$]</code>,
which add more flexibility.
However, these are not synthesizable.
Later we will discuss dynamic arrays, <code>arr[]</code>, associative arrays, <code>arr[string]</code>, and queues, <code>int q[$]</code>,
which add more flexibility. These testbench-oriented containers are not the focus of this synthesizable RTL lesson.
Instead we will use compile-time <strong>parameters</strong> to create flexible, reusable modules:
<pre>module sram #(parameter int DEPTH = 16, ...) (
// ... ports ...
Expand All @@ -14,12 +13,11 @@
</p>
<p>
We can apply some system functions, like
<code><dfn data-card="$clog2(n) computes the ceiling base-2 logarithm: the smallest integer k such that 2^k ≥ n. $clog2(16) = 4, $clog2(256) = 8, $clog2(10) = 4. It is evaluated at compile time, so it is safe inside port declarations and parameter expressions. It is the standard way to derive address width from a depth parameter — if depth changes from 16 to 1024, the address port automatically grows from 4 to 10 bits with no manual update.">$clog2</dfn></code>
and <code><dfn data-card="$bits(n) computes the number of bits needed to represent n distinct values: the smallest integer k such that 2^k > n. $bits(16) = 4, $bits(256) = 8, $bits(10) = 4. It is evaluated at compile time, so it is safe inside port declarations and parameter expressions. It is the standard way to derive address width from a depth parameter — if depth changes from 16 to 1024, the address port automatically grows from 4 to 10 bits with no manual update.">$bits</dfn></code>
at compile time.
We can use this to compute the minimum number of address bits needed for a given depth.</p>
<code><dfn data-card="$clog2(n) returns the ceiling of log base 2 of n. The argument is treated as unsigned, and $clog2(0) returns 0. It is evaluated in constant expressions, so it is the standard way to derive a minimum address width from a depth parameter (IEEE 1800-2023 §20.8.1).">$clog2</dfn></code>
and <code><dfn data-card="$bits(expr) returns the number of bits required to hold an expression or fixed-size data type (IEEE 1800-2023 §20.6.2). It does not calculate the minimum width for a numeric value: an unsized decimal literal such as 16 is at least 32 bits wide (IEEE 1800-2023 §5.7.1).">$bits</dfn></code>
at compile time. For this lesson, use <code>$clog2(DEPTH)</code> for the minimum number of address bits.</p>
<p>Task: Add parametrization to <code>sram.sv</code>.
Add the parameter block to the module header, then replace the three hardcoded widths with their parameter-derived equivalents: use <code>$clog2(DEPTH)</code> for the address port and <code>WIDTH</code> for the data ports and memory array.</p>
Add the parameter block to the module header, then replace the hardcoded dimensions with parameter-derived equivalents: use <code>$clog2(DEPTH)</code> for the address port, <code>WIDTH</code> for both data ports and each memory element, and <code>DEPTH</code> for the memory range.</p>
<blockquote><p>Always use <code>$clog2</code> for address widths derived from depth parameters — it automatically adjusts when the depth changes, so the address bus is always exactly the right size.</p></blockquote>
<h2>Testbench</h2>
<p>The solution testbench instantiates the same module <em>twice</em> with different parameters to demonstrate the power of parameterization: an 8-deep × 4-bit SRAM and a 256-deep × 16-bit SRAM, from a single RTL source.</p>
15 changes: 5 additions & 10 deletions src/lessons/sv/welcome/description.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
</svg>
<ul>
<li><strong>Part 1</strong> — SystemVerilog Basics: build the full parameterized SRAM in RTL</li>
<li><strong>Part 2</strong> — SystemVerilog Assertions: formally verify its correctness</li>
<li><strong>Part 3</strong> — UVM: stress-test it with a complete verification environment</li>
<li><strong>Part 2</strong> — SystemVerilog Assertions: check its behavior with properties</li>
<li><strong>Part 3</strong> — Verification environments: learn the structure of a complete testbench</li>
</ul>
<p>By the end you'll have a chip that's been hand-coded, formally proved, and exhaustively randomized — the same flow used in industry-grade ASIC design.</p>
<p>The tutorial introduces RTL design, assertions, and verification environments step by step, using the SRAM as a running example.</p>
<h2>Hello World</h2>
<p>Before we build anything, let's make sure the simulator is working. Open <code>top.sv</code>. The <code><dfn data-card="An initial block is a procedural block that runs once at the start of simulation (time 0) and then stops — it doesn't repeat. Statements inside execute sequentially, top to bottom, like a program. Initial blocks cannot be synthesized to hardware; they exist only in testbenches. They're the standard way to write test sequences: drive signals, wait for clock edges, check results, then call $finish.">initial</dfn></code> block runs once at time 0. Use <code>
<dfn data-card='
Expand All @@ -29,15 +29,10 @@ <h2>Hello World</h2>
'>
$display</code>
</dfn>
to print a message to the log (the syntax is identical to C's <code>printf</code>.)

One tight paragraph on printf/display notation:


For example, you can write
to print a message to the log. For example, you can write
<pre>$display("HELLO, %s WORLD", "SRAM");</pre>
</p>
<blockquote><p>Every SystemVerilog simulation ends when <code>$finish</code> is called. Without it, the simulator would run forever waiting for events that never come.</p></blockquote>
<blockquote><p>Calling <code>$finish</code> explicitly ends the simulation. A simulation can also reach its natural end when no process or scheduled event remains; a clock or other recurring process can keep it active, so testbenches commonly call <code>$finish</code> when their checks are complete.</p></blockquote>
<blockquote><p><strong>Simulation vs. synthesis:</strong> <code>initial</code> blocks, <code>$display</code>, and <code>#n</code> delay controls exist only in simulation — they have no hardware equivalent and synthesis tools ignore them. Only structural constructs (<code>always_ff</code>, <code>always_comb</code>, module ports, <code>assign</code>) describe real hardware. You will use both worlds throughout this tutorial: RTL for the chip, simulation-only constructs for testing it.</p></blockquote>
Hint: Install <a href="https://chromewebstore.google.com/detail/claude/fcoeoabgfenejglbffodgkkbkcdhcgfn" target="_blank">Claude Chrome Extension</a> to ask follow up questions on the lessons right in your browser. If any lesson can be improved, please send a <dfn data-card="
A pull request (PR) is a way to propose changes to a codebase. You create a PR by forking the repository, making your changes in a new branch, and then submitting the PR for review. The maintainers can discuss, request changes, and eventually merge your contributions into the main codebase.
Expand Down
Loading