CSCE 611 Fall 2026 Lecture 7: SystemVerilog 4
Watch on YouTube →
Overview
Jason D. Bakos completes the synthesizable SystemVerilog material for CSCE 611, explaining combinational and clocked `always` blocks, resets, enables, blocking versus nonblocking assignments, and test-bench techniques. He then applies those concepts to RAMs and the RISC-V register file, including asynchronous reads, synchronous writes, two read ports, one write port, register-zero behavior, and a bypass path.
Key takeaways
- Use `always_comb` with complete assignments for combinational logic; missing a branch assignment can infer a latch, while `always_comb` is designed to flag that mistake.
- For the FPGA designs discussed, asynchronous resets can map to native flip-flop reset circuitry and dedicated routing; Bakos’s example contrasted roughly 200 MHz with about 55 MHz for a synchronous-reset implementation.
- Nonblocking assignments model simultaneous clocked state updates: `n1 <= d; q <= n1;` creates two sequential stages because `q` receives the previous value of `n1`.
- A CPU GPIO output needs a register enable so a value remains visible on the seven-segment display until a later instruction changes it.
- The RISC-V register file needs two read ports and one write port to supply two instruction operands and update a destination within a cycle; bypassing handles simultaneous reads and writes to the same nonzero register.
- An asynchronous RAM read can return newly written data as soon as the address selects that location, while a synchronous read adds a clocked operation and a cycle of latency.
Chapters
0:00
SystemVerilog `always` Blocks for Logic and Test Benches
- `always_comb` describes combinational logic and infers its sensitivity to signals used in the block.
- `always_ff` describes edge-triggered state such as counters, registers, and state machines, typically with `posedge clock`.
- Legacy `always @(...)` syntax is introduced for recognition; `always` without a sensitivity list is reserved for simulation tasks such as generating a test-bench clock.
3:45
Avoiding Latches in `always_comb` Logic
- An `if` statement or `case` statement used for combinational behavior belongs inside an `always_comb` block.
- Assign every output on every control path; a missing assignment can imply a latch that holds its previous value.
- `always_comb` helps catch incomplete assignments, while a conditional assignment can also be written as a ternary `assign`.
6:11
Registers as Edge-Triggered, One-Cycle Delays
- A four-bit `always_ff` assignment from `D` to `Q` implements a multi-bit register that captures data on a clock edge.
- With no enable, the register captures `D` every cycle, so `Q` reflects the value captured one cycle earlier.
- A hardware register is a bank of flip-flops, distinct from an architectural RISC-V register such as `x1`, which is stored in a register file.
9:17
Synchronous and Asynchronous Reset Choices on FPGAs
- A synchronous reset is checked inside the clocked block and takes effect only at the next active clock edge; an asynchronous reset is also included in the sensitivity list and can act without a clock edge.
- The shown `if (reset)` condition is active-high; using `if (!reset)` would make the reset active-low.
- Bakos describes an FPGA timing comparison: a CPU using synchronous resets ran at about 55 MHz, versus about 200 MHz for another design using the FPGA flip-flops’ native asynchronous reset.
- The synchronous-reset implementation required extra input logic and ordinary routing, while asynchronous reset could use dedicated high-fan-out FPGA routing.
15:59
Register Enables, Program Counters, and Counter Logic
- An enable lets a register hold its value across cycles instead of capturing new data on every clock edge.
- The CPU’s GPIO output register needs an enable so a display value persists until an instruction updates it, rather than appearing for only one 50 MHz cycle.
- A clocked `if`/`else` structure can implement an up/down counter with reset, increment, decrement, and hold behaviors.
18:51
Blocking and Nonblocking Assignments in Sequential Logic
- `<=` is a nonblocking assignment: updates are deferred until the clocked block completes, so later reads in the same block see the old value.
- `=` is a blocking assignment: later statements in the same block see the value assigned earlier.
- With nonblocking assignments, `n1 <= d; q <= n1;` creates two register stages and a two-cycle delay; replacing them with blocking assignments can collapse the intended pipeline behavior.
- Bakos’s course convention is to use `=` for combinational logic and `<=` for clocked `always_ff` logic.
28:49
Test-Bench Timing and File-Based Test Vectors
- Simulation needs time to advance between driving inputs and checking outputs; a test bench can use `#` delays or drive on a rising clock edge and check on a falling edge.
- A test-vector file separates test cases from the test-bench engine, with each row holding three binary inputs and one expected output.
- The example test bench loads vectors using `$readmemb`, applies reset, drives inputs on `posedge clock`, and checks results on `negedge clock`.
32:35
SystemVerilog Test-Vector Arrays and Four-State Comparisons
- The vector array stores test cases by depth and signal bits by width; SystemVerilog declaration syntax places the packed width before the name and the array depth after it.
- Concatenation on the left side of an assignment lets the test bench load several signals, such as `{a, b, c, y_expected}`, from one vector.
- `===` and `!==` compare four-state values including `X` and `Z`, which is useful in test benches; ordinary `==` and `!=` are the hardware-oriented comparisons.
- If a test-vector file has eight rows but the declared array has 10,000 entries, later entries can be `X`; the example uses that condition to detect the end of the vectors.
44:16
Test-Bench Utilities and a One-Cycle Rising-Edge Detector
- Bakos reviews `always_ff`, `always_comb`, and an unclocked `always` loop as the principal block styles needed for the course.
- Simulation tasks include `$readmemh`, `$readmemb`, `$display`, and `$monitor`; `$time` can attach simulation time to diagnostic output.
- A rising-edge detector stores the previous input in `dn_old` and asserts `dout` when the current input is 1 and the delayed value is 0.
- Because clocked nonblocking assignments read old values at the active edge, the detected pulse appears one cycle after the input transition.
49:27
Tracking the Second-Largest Value with Clocked Registers
- The example maintains `largest` and `second_largest` as values arrive, resetting both to zero when reset is asserted.
- When `dn > largest`, the new input becomes the largest and the old largest becomes second-largest; nonblocking assignments preserve access to that old value.
- When the input is below the largest but greater than `second_largest`, it replaces the second-largest value; repeated values count as separate candidates.
- Comparisons on an unsigned `logic` value are treated as unsigned unless the signal is declared as signed.
54:23
RAM Capacity, Addressing, and the Memory Wall
- A RAM is organized as addressable rows of fixed-width data; a 1,024-by-32 memory contains 32,768 bits and needs a 10-bit address.
- The CPU uses RAM for both its instruction storage and its RISC-V architectural register file.
- A RAM port accesses only selected entries rather than exposing every stored bit simultaneously, creating the memory-bandwidth bottleneck known as the memory wall.
57:37
Declaring and Accessing an 8,192-by-32 SystemVerilog RAM
- The example declares an 8,192-entry memory with 32-bit words, requiring a 13-bit address because `log2(8192) = 13`.
- SystemVerilog places the word width before the memory name and the depth after it, unlike the conventional rows-by-columns description.
- `$readmemh` or `$readmemb` can initialize the memory from a text file before the design accesses it.
1:02:20
Asynchronous RAM Reads and Synchronous Writes
- An `assign` from `mem[address]` makes read data respond to address changes without waiting for a clock edge.
- A clocked `always_ff` block with `write_enable` writes `write_data` into `mem[address]` on the active edge.
- Moving the read into a clocked block changes the memory to a synchronous-read design, adding a clocked read operation.
1:06:34
RAM Ports and Read-After-Write Timing
- A RAM port combines address and data access; separate read and write addresses allow operations at different addresses in the same cycle.
- The timing examples write `32'hDEADBEEF` to address 6 and show that an asynchronous read reflects it immediately when address 6 is selected.
- With a synchronous read, the same value appears after the read clock edge, adding roughly one cycle compared with the asynchronous-read example.
- At a clock edge, a clocked block evaluates the old input values, which determines which address and write-enable state the example uses.
1:10:22
RISC-V Register File: Two Reads, One Write, and Bypass
- The 32-register RISC-V register file is a 32-by-32-bit RAM, with five-bit addresses because `log2(32) = 5`.
- Two read ports supply the two source operands of an instruction, while one write port updates its destination register.
- Reads are asynchronous and writes are clocked with a write-enable input; instructions that do not update a register leave the write port disabled.
- Reading register `x0` always returns zero, and a bypass path forwards write data when a read and write target the same nonzero register.
Summary, takeaways, and chapters were generated by AI from the video's transcript and may contain errors. The video belongs to its creator, Jason D. Bakos.