Project 14: Simple RAM (8 words x 8 bits)

Infer a small synchronous RAM and test read/write behavior.

Quick Reference

Attribute Value
Difficulty Intermediate
Time Estimate 3-4 hours
Main Programming Language Verilog (Alternatives: VHDL, SystemVerilog)
Alternative Programming Languages VHDL, SystemVerilog
Coolness Level Medium
Business Potential Medium
Prerequisites Registers, Synchronous logic
Key Topics Memory inference, Read latency

1. Learning Objectives

  1. Infer a RAM with correct read/write semantics
  2. Handle read-during-write cases
  3. Document read latency

2. All Theory Needed (Per-Concept Breakdown)

Memory Inference and RAM Behavior

Description/Expanded Explanation of the concept

FPGAs contain block RAMs. You can infer them by writing the right Verilog pattern. Synchronous read and write behavior is common, and read-during-write behavior must be defined.

Definitions & Key Terms
  • Block RAM -> dedicated on-chip memory
  • Synchronous read -> data available after a clock edge
  • Read-during-write -> behavior when reading and writing same address
Mental Model Diagram (ASCII)
addr -> [RAM] -> dout
         ^
        din (we)

RAM block diagram

How It Works (Step-by-Step)
  1. Define a memory array in Verilog.
  2. Use a clocked block for writes.
  3. Decide and document read behavior.
  4. Test read-during-write cases.
Minimal Concrete Example
reg [7:0] mem [0:7];
always @(posedge clk) begin
  if (we) mem[addr] <= din;
  dout <= mem[addr];
end
Common Misconceptions
  • “Reads are always combinational.” -> Many FPGA RAMs are synchronous.
  • “Read-during-write is obvious.” -> Tools may infer different modes.
Check-Your-Understanding Questions
  1. What is the read latency of your RAM?
  2. What happens when read and write share the same address?
  3. How do you initialize memory for simulation?
Where You’ll Apply It
  • This project: used in Section 3.2 and Section 4
  • Also used in: Final CPU

Flip-Flops and Registers

Description/Expanded Explanation of the concept

Flip-flops are the fundamental storage elements in synchronous digital logic. A register is a collection of flip-flops that store multi-bit values. Correct usage requires edge-triggered logic and non-blocking assignments.

Definitions & Key Terms
  • Flip-flop -> edge-triggered storage element
  • Register -> N-bit storage built from flip-flops
  • Reset -> initializes state to known values
Mental Model Diagram (ASCII)
clk edge -> [FF] -> q

Flip-flop clocked output

How It Works (Step-by-Step)
  1. On each clock edge, the flip-flop samples D.
  2. The output Q holds the sampled value until the next edge.
  3. Reset forces Q to a known value.
Minimal Concrete Example
always @(posedge clk) begin
  if (reset) q <= 0;
  else q <= d;
end
Common Misconceptions
  • “reg always means storage.” -> It is a variable that may infer storage.
  • “Blocking assignments are fine for registers.” -> Use non-blocking.
Check-Your-Understanding Questions
  1. Why is non-blocking used for sequential logic?
  2. What is the difference between a latch and a flip-flop?
  3. What is the role of reset?
Where You’ll Apply It
  • This project: used in Section 3.2 and Section 4
  • Also used in: P07-4-bit-updown-counter-with-load.md, P08-shift-register-led-chaser.md

Verification with Testbenches and Waveforms

Description/Expanded Explanation of the concept

Testbenches are simulation-only modules that apply stimulus and check outputs. Waveforms (VCD) are the hardware engineer’s microscope; they reveal timing, glitches, and ordering problems. A good testbench is deterministic and covers edge cases.

Definitions & Key Terms
  • Testbench -> a non-synthesizable module that drives a DUT
  • VCD -> Value Change Dump waveform file
  • Deterministic test -> same inputs produce same outputs every run
Mental Model Diagram (ASCII)
[Testbench] -> [DUT] -> [VCD] -> [GTKWave]

Testbench DUT VCD GTKWave flow

How It Works (Step-by-Step)
  1. Initialize inputs to known values.
  2. Apply stimulus over time.
  3. Dump waveforms and check outputs.
  4. Add assertions or PASS/FAIL messages.
Minimal Concrete Example
initial begin
  $dumpfile("wave.vcd");
  $dumpvars(0, tb);
  a = 0; b = 1; #10;
  $finish;
end
Common Misconceptions
  • “If it simulates once, it’s correct.” -> Cover all relevant cases.
  • “Waveforms are optional.” -> They are often the only way to debug timing.
Check-Your-Understanding Questions
  1. Why keep testbench and DUT separate?
  2. What is the purpose of $dumpvars?
  3. How do you make a testbench deterministic?
Where You’ll Apply It
  • This project: used throughout Section 6 (testing)
  • Also used in: all other projects in this folder

3. Project Specification

3.1 What You Will Build

An 8x8 RAM with synchronous write and read.

3.2 Functional Requirements

  1. Requirement 1: Write on clock edge when we=1
  2. Requirement 2: Read data with defined latency
  3. Requirement 3: Support all addresses 0-7

3.3 Non-Functional Requirements

  • Performance: Stable operation at the target clock and interfaces.
  • Reliability: Deterministic outputs on all defined inputs.
  • Usability: Clear ports and documented behavior.

3.4 Example Usage / Output

{p['example_usage']}

3.5 Data Formats / Schemas / Protocols

{p[‘data_format’]}

3.6 Edge Cases

  • Read during write
  • Uninitialized memory

3.7 Real World Outcome

3.7.1 How to Run (Copy/Paste)

vvp ram_tb

3.7.2 Golden Path Demo (Deterministic)

Run the demo command above with the provided testbench and confirm the outputs match the golden transcript.

3.7.3 CLI Transcript

write addr=3 data=0xAA
read addr=3 -> 0xAA

3.7.4 Failure Demo (Expected)

# Example failure case
ERROR: Output mismatch at vector 3
Expected: 0x0A, Got: 0x0B
EXIT CODE: 1

Notes:

  • Exit code 0 indicates all tests passed
  • Exit code 1 indicates a test failure

4. Solution Architecture

4.1 High-Level Design

[inputs] -> [core logic] -> [outputs]

Core logic flow

4.2 Key Components

Component Responsibility
ram Memory array and control logic

4.3 Data Structures (No Full Code)

// Example signals (adapt to your design)
reg [7:0] state_reg;
reg [7:0] data_reg;

4.4 Algorithm Overview

Key Algorithm: Core control flow

  1. Initialize state/reset conditions.
  2. Apply inputs and compute outputs.
  3. Update state on clock edges (if sequential).

Complexity Analysis:

  • Time: O(1) per cycle
  • Space: O(N) for registers and logic

5. Implementation Guide

5.1 Development Environment Setup

iverilog -v
# Ensure GTKWave is installed for waveform viewing

5.2 Project Structure

project-root/
|-- src/
|   |-- top.v
|   |-- core.v
|-- tb/
|   |-- tb.v
|-- Makefile
|-- README.md

Project folder structure

5.3 The Core Question You’re Answering

“How do you model memory so synthesis infers real RAM blocks?”

5.4 Concepts You Must Understand First

  • Registers
  • Synchronous logic

5.5 Questions to Guide Your Design

  • Will read be synchronous or async?
  • How will you handle read-during-write?

5.6 Thinking Exercise

Draw timing for write followed by read in consecutive cycles.

5.7 The Interview Questions They’ll Ask

  • Why do FPGA RAMs prefer synchronous reads?
  • How do you infer BRAM?

5.8 Hints in Layers

  • Use a reg array with clocked write.
  • Assign dout in the clocked block for sync read.

5.9 Books That Will Help

Topic Book Chapter
Memory Digital Design and Computer Architecture Ch. 5

5.10 Implementation Phases

Phase 1: Foundation

Goals:

  • Establish core module structure
  • Implement minimal behavior

Tasks:

  1. Scaffold module ports and internal signals
  2. Write a minimal testbench that compiles

Checkpoint: Simulation runs without errors

Phase 2: Core Functionality

Goals:

  • Implement full logic
  • Verify edge cases

Tasks:

  1. Complete core logic
  2. Add directed tests for edge cases

Checkpoint: All tests pass and waveforms match expectations

Phase 3: Polish & Edge Cases

Goals:

  • Improve readability
  • Document behavior

Tasks:

  1. Add comments and README notes
  2. Expand tests for unusual inputs

Checkpoint: Design is deterministic and documented

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Reset strategy Sync / Async Sync Simpler timing closure
Test coverage Directed / Exhaustive Exhaustive for small logic Prevents missed cases

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Test core logic Small vectors
Integration Tests Test modules together Full system
Edge Case Tests Boundary conditions Max/min values

6.2 Critical Test Cases

  1. Test 1: Write/read all addresses
  2. Test 2: Read-during-write behavior documented

6.3 Test Data

Use deterministic vectors and document expected outputs.

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Unexpected latency Data appears one cycle later Document sync read latency

7.2 Debugging Strategies

  • Inspect waveforms at key internal signals
  • Add temporary debug outputs to verify state
  • Reduce testcases to the smallest failing case

7.3 Performance Traps

  • Overly wide counters or combinational paths can reduce max clock

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add parameterization for widths
  • Add optional features (enable, reset)

8.2 Intermediate Extensions

  • Add configuration registers
  • Build a simple driver or demo program

8.3 Advanced Extensions

  • Integrate with another project in this series
  • Implement a hardware demo on FPGA

9. Real-World Connections

9.1 Industry Applications

  • Digital control systems and embedded peripherals
  • FPGA prototyping and validation
  • Yosys / nextpnr toolchain for open-source FPGA flow
  • Example HDL projects in the FPGA community

9.3 Interview Relevance

  • Demonstrates RTL thinking and verification skills

10. Resources

10.1 Essential Reading

  • Digital Design and Computer Architecture - Focus on Ch. 5

10.2 Video Resources

  • Search for project-specific HDL walkthroughs and waveforms

10.3 Tools & Documentation

  • Icarus Verilog
  • GTKWave
  • See adjacent projects in VERILOG_FROM_ZERO_PROJECTS/

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain the core concept without notes
  • I can predict waveform behavior for basic inputs

11.2 Implementation

  • All functional requirements are met
  • All tests pass
  • Edge cases are documented

11.3 Growth

  • I can explain this project in an interview
  • I documented at least one lesson learned

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Functional requirements implemented
  • Testbench passes
  • Waveforms inspected

Full Completion:

  • All minimum criteria plus
  • Edge cases covered and documented

Excellence (Going Above & Beyond):

  • Hardware demo on FPGA
  • Clear write-up of lessons learned

    Appendix A: Deep Dive Walkthrough

A.1 Signal Map and Invariants

  • Inputs: clk, we, addr[2:0], din[7:0]
  • Output: dout[7:0]

Invariant: Writes occur on clock edge when we=1. Reads are synchronous (one-cycle latency).

A.2 Read-During-Write Policy

Define and document one of:

  • Write-first: dout returns new data.
  • Read-first: dout returns old data.

Pick one and assert it in the testbench. The recommended choice is write-first for simplicity.

A.3 Deterministic Test Sequence

  1. Write 0xAA to addr 3.
  2. Write 0x55 to addr 4.
  3. Read addr 3 -> expect 0xAA after 1 cycle.
  4. Write 0xF0 to addr 3 while reading addr 3 -> verify policy.

A.4 Waveform Debug Tips

  • Probe addr, we, din, dout together.
  • Check that dout updates only on clock edges.

13. Deep Dive Appendix

13.1 Timing and Resource Budget

  • With synchronous RAM, read data appears one clock after address.
  • Write timing must meet setup/hold for addr and data around the write edge.
  • On FPGA, this maps to block RAM if coded correctly.

13.2 Waveform Interpretation Guide

  • Verify write enable and addr are stable at the clock edge.
  • Confirm that dout updates one cycle after read address.

Example sequence:

cycle 0: we=1 addr=3 din=0xAA
cycle 1: we=0 addr=3 -> dout=0xAA

13.3 Hardware Bring-Up Notes

  • Use switches to set address/data and a button to pulse we.
  • Display dout on LEDs or 7-seg.
  • If inference fails, check that your memory is declared as reg [7:0] mem [0:7];.

13.4 Alternate Implementations and Trade-offs

  • Sync read: realistic for FPGA block RAM.
  • Async read: easier for simulation, but not always synthesizable to BRAM.
  • Dual-port: add separate read/write ports for higher throughput.

13.5 Additional Exercises

  • Add byte enable bits for partial writes.
  • Initialize memory from a hex file using $readmemh.
  • Add a memory-mapped register at a specific address.