Skip to content

A BRAM-based digital function generator implemented in Verilog on the ZedBoard. Generates sine, square, triangular, and sawtooth waveforms using BRAM lookup tables.

Notifications You must be signed in to change notification settings

amarjaggari/FPGA-Waveform-Generator-Using-BRAM-Verilog-ZedBoard-

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FPGA-Waveform-Generator-Using-BRAM-Verilog-ZedBoard

A BRAM-based digital function generator implemented in Verilog on the ZedBoard. Generates sine, square, triangular, and sawtooth waveforms using BRAM lookup tables with programmable frequency and amplitude control.


Table of Contents

  1. Introduction
  2. Why BRAM?
  3. Getting COE Files from Python
  4. How Vivado Uses COE to Store Data in BRAM
  5. How Data Is Accessed Inside BRAM
  6. Project Structure
  7. Getting Started
  8. Implementation Guide
  9. Troubleshooting
  10. References

Introduction to BRAM-Based Waveform Generation

Waveform generation is a foundational technique used in electronics for producing periodic signals such as sine, square, triangular, and sawtooth waves. Traditional function generators are standalone instruments, but with the rise of FPGA technology, digital waveform generation has become efficient, scalable, and highly customizable.

The BRAM-based waveform generator uses Block RAM inside the FPGA to store digital sample values of a waveform. These samples form a lookup table (LUT) representing one full cycle of the signal. By reading these values sequentially at a controlled clock rate, the FPGA produces repeated periodic waveform output.

This method avoids runtime computation like sin(), cos(), or CORDIC operations. Instead, the waveform is pre-computed and stored in memory, enabling faster and more resource-efficient generation.

Key Advantages

  • Fast Waveform Generation: No runtime computation required
  • Resource Efficient: Minimal DSP slice utilization
  • Scalable: Multiple waveforms can be stored simultaneously
  • Customizable: Easy to modify frequency, amplitude, and phase
  • Deterministic: Precise timing and frequency control

Why BRAM?

FPGA has two main memory resources:

a) Distributed RAM (LUT-based)

  • Small, implemented using logic cells
  • Limited storage capacity
  • Good for small buffers and shift registers

b) BRAM (Block RAM)

  • Dedicated memory blocks inside the FPGA fabric
  • Large size and better performance
  • Optimized for data tables and waveforms

Comparison Table

Feature LUT RAM BRAM
Storage capacity Small (bits–KB) Large (KB–MB)
Speed High Very high
Access latency 1 cycle 1–2 cycles
Best use Logic functions Data tables / waveforms
Resource usage Uses logic fabric Dedicated memory cells
Cost per bit High Low

For the ZedBoard (Zynq-7020):

  • Available BRAM: 120 × 36 Kb (4.86 MB total)
  • Sufficient for storing multiple waveforms with high resolution

Should you store all waveforms in a single large BRAM, or use separate BRAMs for each waveform?

Each approach has distinct advantages and trade-offs. Your choice depends on:

  • Sample resolution required (1024, 4096+ samples)
  • Number of waveforms needed (2, 4, 8+)
  • BRAM availability on your FPGA
  • Frequency control flexibility
  • Power consumption and latency concerns

Why Use Multiple BRAMs?

Reason 1: Independent Waveform Storage

With 4 separate BRAMs, each waveform type gets its own dedicated memory:

BRAM_0 → Sine wave (256/512/1024 samples)
BRAM_1 → Square wave (256/512/1024 samples)
BRAM_2 → Triangle wave (256/512/1024 samples)
BRAM_3 → Sawtooth wave (256/512/1024 samples)

Advantages:

  • Simple multiplexing logic: Just select which BRAM to read from
  • Each BRAM can have different sample counts if needed
  • Independent frequency control per waveform
  • No memory contention between waveforms

Reason 2: Higher Sample Count = Better Signal Purity

With an 8-bit DAC (0–255 output levels), the quality of your generated waveform depends on sampling resolution:

8-bit DAC Output Quality

An 8-bit DAC has 256 discrete levels (0 to 255). The quality of your waveform depends on how many samples you use per cycle:

Samples/Cycle Quality THD Estimate Use Case
64 Poor ~12% Testing only
128 Fair ~6% Basic generation
256 Good ~2% Standard use
512 Very Good ~0.5% Audio-grade
1024 Excellent ~0.1% Precision signals
4096 Ultra-precision <0.01% Lab equipment

THD = Total Harmonic Distortion (lower is better)

Why More Samples Improve Purity

A sine wave with only 64 samples looks like a staircase:

                    *
                  *   *
                *       *
              *           *
            *               *

A sine wave with 1024 samples looks smooth:

                    ***
                  *       *
                *           *
              *               *
            *                   *

Nyquist Theorem: To accurately represent a signal, you need at least 2 samples per cycle of the highest frequency component. Using MORE samples captures harmonic content better and reduces quantization errors.


Reason 3: Parallel Dual-Port Access

With multiple BRAMs, you can implement dual-port or triple-port access:

// Read two different samples simultaneously from different BRAMs
assign sine_out = sine_bram[addr_sine];
assign square_out = square_bram[addr_square];

Single BRAM limitation:

  • Can only read one address per cycle
  • Requires multiplexing if you need multiple waveforms

Reason 4: Frequency Tuning Flexibility

With separate BRAMs and address counters:

BRAM_Sine + Counter_1    → Frequency F1
BRAM_Square + Counter_2  → Frequency F2
BRAM_Triangle + Counter_3→ Frequency F3
BRAM_Sawtooth + Counter_4→ Frequency F4

Each waveform can have independent frequency control without affecting others.


Single BRAM with Partitioning

Concept

Store all 4 waveforms in one large BRAM by partitioning the address space:

Addresses 0000–00FF:    Sine wave (256 samples)
Addresses 0100–01FF:    Square wave (256 samples)
Addresses 0200–02FF:    Triangle wave (256 samples)
Addresses 0300–03FF:    Sawtooth wave (256 samples)

Getting COE Files from Python for BRAM Initialization

What is a COE File?

A COE (Coefficient) file is a text file format used by Xilinx/AMD FPGA tools to initialize Block RAM (BRAM) or ROM contents. It specifies the initial memory values that get loaded into BRAM when the FPGA configuration is loaded.

Basic COE File Format

memory_initialization_radix=16;
memory_initialization_vector=
00,04,0A,10,1F,3A,55,6F,7F,90,A1,AF,BA,C3,CA,CE,D0,CE,CA,C3,BA,AF,A1,90,7F,6F,55,3A,1F,10,0A,04;

Header Fields:

  • memory_initialization_radix=16; → Values in hexadecimal (10 for decimal)
  • memory_initialization_vector= → Start of data values
  • Values are comma-separated, ending with semicolon

Why Generate COE Files from Python?

  • Dynamic data generation: Create sine waves, filters, and custom LUTs
  • Preprocessing complex data: Images, audio samples, sensor calibration tables
  • Algorithmic pattern generation: Mathematical functions and sequences
  • Test vector creation: Automated testing and validation
  • Batch processing: Generate multiple COE files for different configurations

Generating COE Using Python

1. Basic Sine Wave Generator

import math

N = 256                       # number of samples per cycle
bits = 8                      # amplitude resolution (0–255)
filename = "sine256.coe"

file = open(filename, "w")
file.write("memory_initialization_radix=16;\n")
file.write("memory_initialization_vector=\n")

for i in range(N):
    # Convert sine wave (-1 to 1) to unsigned (0 to 255)
    value = int((math.sin(2*math.pi*i/N) + 1) * 127.5)
    file.write(f"{value:02X}")
    if i < N - 1:
        file.write(",")
    
file.write(";")
file.close()

print(f"COE file '{filename}' generated successfully!")

2. Multi-Waveform Generator

import math

def generate_coe(waveform_type, N=256, bits=8, filename=None):
    """
    Generate COE file for different waveforms
    
    Args:
        waveform_type: 'sine', 'square', 'triangle', 'sawtooth'
        N: Number of samples per cycle
        bits: Bit width (8, 10, 12, 16)
        filename: Output COE file name
    """
    if filename is None:
        filename = f"{waveform_type}_{N}.coe"
    
    max_value = (2 ** bits) - 1
    mid_value = max_value // 2
    
    samples = []
    
    if waveform_type == 'sine':
        for i in range(N):
            value = int((math.sin(2 * math.pi * i / N) + 1) * mid_value)
            samples.append(value)
    
    elif waveform_type == 'square':
        for i in range(N):
            value = max_value if i < N // 2 else 0
            samples.append(value)
    
    elif waveform_type == 'triangle':
        for i in range(N):
            if i < N // 2:
                value = int((i / (N // 2)) * max_value)
            else:
                value = int(((N - i) / (N // 2)) * max_value)
            samples.append(value)
    
    elif waveform_type == 'sawtooth':
        for i in range(N):
            value = int((i / N) * max_value)
            samples.append(value)
    
    # Write COE file
    with open(filename, "w") as f:
        f.write("memory_initialization_radix=16;\n")
        f.write("memory_initialization_vector=\n")
        for i, sample in enumerate(samples):
            f.write(f"{sample:0{bits//4}X}")
            if i < len(samples) - 1:
                f.write(",")
        f.write(";")
    
    print(f"Generated {waveform_type} waveform: {filename}")
    return samples

# Generate all waveforms
for wave in ['sine', 'square', 'triangle', 'sawtooth']:
    generate_coe(wave, N=256, bits=12)

How Vivado Uses COE to Store Data in BRAM

Step-by-Step Vivado Workflow

Step 1: Create Block Memory Generator IP

  1. Open IP Catalog in Vivado
  2. Search for "Block Memory Generator"
  3. Click and customize:
    • Memory Type: Single Port RAM
    • Depth: 256 (or your sample count)
    • Width: 8, 10, 12, or 16 bits
    • Algorithm: Fixed

Step 2: Load COE File

  1. In IP customization dialog, go to "Other Options" tab
  2. Enable "Load Init File"
  3. Browse and select your generated .coe file
  4. Click "OK" to generate

Step 3: Generate Output Product

  1. Right-click generated IP → "Generate Output Products"
  2. Vivado synthesizes BRAM with pre-loaded data

Step 4: Instantiate in Your Design

Add to your Verilog top module:

blk_mem_gen_0 sine_rom (
    .clka(clk),
    .addra(addr[7:0]),
    .douta(data_out),
    .ena(1'b1)
);

Memory Layout After Loading COE

Address Stored Value (from COE) Hex Value
0x00 Sample 0 0x00
0x01 Sample 1 0x04
0x02 Sample 2 0x0A
0x03 Sample 3 0x10
... ... ...
0xFF Sample 255 (last) 0x7E

How Data Is Accessed Inside BRAM

BRAM acts like a digital memory table with address and data lines.

Access Mechanism

Address Lines → Select Memory Location → Output Data

Example Sequence:

  • addr = 0x00 → Output sample[0]
  • addr = 0x01 → Output sample[1]
  • addr = 0x02 → Output sample[2]
  • ...
  • addr = 0xFF → Output sample[255]
  • addr = 0x00 → Output sample[0] (wrap around)

Internal Signal Flow

Counter/DDS → Address → BRAM → Sample Output → DAC/PWM
   |              |        |          |
   ↓              ↓        ↓          ↓
Increments    Selects   Lookup    Converted to
address at   memory    value     analog signal
clock rate    cell

ASCII Memory View

+----------+--------+
| Address  | Value  |
+----------+--------+
|   0x00   | 0x00   |  ← First sample
|   0x01   | 0x04   |
|   0x02   | 0x0A   |
|   0x03   | 0x10   |
|   0x04   | 0x1F   |
|   ...    | ...    |
|   0xFE   | 0x7D   |
|   0xFF   | 0x7E   |  ← Last sample
+----------+--------+

Each clock cycle:
1. Counter increments address
2. BRAM outputs data at address
3. Data sent to DAC for conversion
4. Address wraps to 0 after reaching max
5. Cycle repeats → generates periodic waveform

Frequency Control

Output Frequency Formula:

f_out = (f_clk × address_increment) / N

where:
  f_clk = FPGA clock frequency (e.g., 100 MHz)
  address_increment = steps per clock (1, 2, 4, 8, ...)
  N = number of samples in lookup table (256, 512, 1024, ...)

Example:

  • Clock = 100 MHz
  • Samples = 256
  • Increment = 1 sample/cycle
  • Output frequency = (100 MHz × 1) / 256 = 390.625 kHz

To reduce frequency, use fractional increment (DDS technique) or slower address counter.


Signal Flow

┌─────────────┐    ┌──────────┐    ┌──────┐    ┌─────┐
│   Counter   │───→│   BRAM   │───→│  DAC │───→│ Sig │
│             │    │ Lookup   │    │      │    │     │
└─────────────┘    └──────────┘    └──────┘    └─────┘
    Increments     Sample Values   8→Analog   Output
   Address         per Address

Project Structure

FPGA-Waveform-Generator-Using-BRAM-Verilog-ZedBoard/ (Public)
│
├── 📁 BRAM/                    # Block RAM files & COE lookup tables
├── 📁 Constraints/             # XDC constraint files
├── 📁 Simulation/              # Testbenches & simulation files
├── 📁 Sources/                 # Verilog source code
├── 📁 Vivado/                  # Vivado project files
│
├── README.md                   # Main documentation (recently updated)
│
└── 42 Commits (main branch)


Getting Started

Hardware Requirements

  • ZedBoard (Zynq-7020 FPGA)
  • 8-bit DAC (external or onboard)
    • Suggested options: AD5611, MCP4912, MAX5102
    • Connected to FPGA GPIO or PMOD header
  • USB JTAG Cable (for programming)
  • USB Power Supply (5V for ZedBoard)

Software Requirements

  • Xilinx Vivado (2020.1 or later)

  • Python 3.6+

    • Required for COE file generation
    • Check version: python --version

Step 1: Generate COE Files with Python

Step 2: Create Vivado Project

Step 3: Add Verilog Source Files


Implementation Guide

Verilog Implementation

Constraints File


IP Block Generation (Block Memory Generator)

Now we create the BRAM IP blocks that will hold your waveform lookup tables.

Creating Four BRAM IP Cores

We will create:

  1. BRAM for Sine → initialized with sine_512.coe
  2. BRAM for Square → initialized with square_512.coe
  3. BRAM for Triangle → initialized with triangle_512.coe
  4. BRAM for Sawtooth → initialized with sawtooth_512.coe

Configuration

Basic Tab:

  • Memory Type: Single Port ROM
    • This makes BRAM read-only (good for fixed waveforms)
  • Algorithm: Fixed (default)
  • Primitive: Leave default

Port A Configuration:

  • Read Width A: 8 bits
  • Read Depth A: 256

Other Options Tab (or Initialization Tab):

  • Enable ✓ Load Init File
  • Click Browse button
  • Navigate to: coe_files/sine_512.coe
  • Select and click OK

Troubleshooting

Issue 1: COE File Not Loading in Vivado

Symptoms: "File not found" error during IP customization

Solution:

  • Verify file path is absolute or relative to Vivado project
  • Check file format: Ensure file ends with semicolon
  • Verify no special characters in filename
# Validate COE syntax
head -3 sine_256.coe
tail -1 sine_256.coe

Issue 2: Incorrect Waveform Output

Symptoms: Distorted or unexpected waveform shape

Possible Causes:

  • Address counter not incrementing correctly
  • BRAM output width mismatch (12-bit data truncated to 8-bit)
  • DAC resolution incompatibility

Solution:

  • Verify BRAM depth and width in Vivado
  • Check address bus width matches sample count
  • Validate COE file contents with Python script

Issue 3: Memory Timing Issues

Symptoms: Glitches or frequency instability

Solution:

  • Add pipeline registers between BRAM output and DAC
  • Implement registered output in BRAM IP
  • Add timing constraints in XDC file
set_input_delay -clock clk 2.0 [get_ports *]
set_output_delay -clock clk 2.0 [get_ports wave_out*]

Issue 4: Address Counter Overflow

Symptoms: Frequency doesn't change with frequency_ctrl input

Solution:

  • Verify phase accumulator width calculation
  • Ensure frequency tuning word is correctly connected
  • Test with simulation before hardware

Performance Metrics

Parameter Value
FPGA Clock Frequency 100 MHz
Max Output Frequency ~390 kHz (256 samples @ 100MHz)
Resolution 8, 10, 12, or 16-bit DAC
Waveforms Supported 4 (sine, square, tri, saw)
BRAM Utilization ~15% (ZedBoard)
LUT Utilization ~5%
Power Consumption ~1–2 W (estimated)

Future Enhancements

  • Phase modulation control
  • Amplitude envelope generator
  • Real-time frequency sweeping (chirp)
  • Multi-channel waveform generation
  • Integration with Zynq ARM CPU for parameter control
  • Ethernet remote control interface
  • High-speed sampling feedback (ADC integration)

References

Core Concepts: BRAM & Block Memory Generator


ZedBoard Documentation


COE File & Python Generation


FPGA Waveform Generator Examples & Papers


Forum & Tutorial Style References


Authors

Amarendar Reddy | NRSC ISRO Intern, Electronics & Communication Engineering


Contributing

Contributions are welcome! Please submit issues and pull requests to improve the project.


Acknowledgments

  • NRSC (National Remote Sensing Centre), ISRO for internship opportunity
  • Xilinx/AMD for Vivado tools and Block Memory Generator IP
  • Digilent for ZedBoard documentation and resources
  • Open-source FPGA community for inspiration and best practices

Last Updated: 04-December 2025

Status: Active Development 🚀 Contact:[email protected]

About

A BRAM-based digital function generator implemented in Verilog on the ZedBoard. Generates sine, square, triangular, and sawtooth waveforms using BRAM lookup tables.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published