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.
- Introduction
- Why BRAM?
- Getting COE Files from Python
- How Vivado Uses COE to Store Data in BRAM
- How Data Is Accessed Inside BRAM
- Project Structure
- Getting Started
- Implementation Guide
- Troubleshooting
- References
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.
- 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
FPGA has two main memory resources:
- Small, implemented using logic cells
- Limited storage capacity
- Good for small buffers and shift registers
- Dedicated memory blocks inside the FPGA fabric
- Large size and better performance
- Optimized for data tables and waveforms
| 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
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
With an 8-bit DAC (0–255 output levels), the quality of your generated waveform depends on sampling resolution:
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)
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.
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
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.
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)
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.
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
- 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
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!")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)- Open IP Catalog in Vivado
- Search for "Block Memory Generator"
- Click and customize:
- Memory Type: Single Port RAM
- Depth: 256 (or your sample count)
- Width: 8, 10, 12, or 16 bits
- Algorithm: Fixed
- In IP customization dialog, go to "Other Options" tab
- Enable "Load Init File"
- Browse and select your generated
.coefile - Click "OK" to generate
- Right-click generated IP → "Generate Output Products"
- Vivado synthesizes BRAM with pre-loaded data
Add to your Verilog top module:
blk_mem_gen_0 sine_rom (
.clka(clk),
.addra(addr[7:0]),
.douta(data_out),
.ena(1'b1)
);| 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 |
BRAM acts like a digital memory table with address and data lines.
Address Lines → Select Memory Location → Output Data
Example Sequence:
addr = 0x00→ Outputsample[0]addr = 0x01→ Outputsample[1]addr = 0x02→ Outputsample[2]- ...
addr = 0xFF→ Outputsample[255]addr = 0x00→ Outputsample[0](wrap around)
Counter/DDS → Address → BRAM → Sample Output → DAC/PWM
| | | |
↓ ↓ ↓ ↓
Increments Selects Lookup Converted to
address at memory value analog signal
clock rate cell
+----------+--------+
| 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
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.
┌─────────────┐ ┌──────────┐ ┌──────┐ ┌─────┐
│ Counter │───→│ BRAM │───→│ DAC │───→│ Sig │
│ │ │ Lookup │ │ │ │ │
└─────────────┘ └──────────┘ └──────┘ └─────┘
Increments Sample Values 8→Analog Output
Address per Address
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)
- 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)
-
Xilinx Vivado (2020.1 or later)
- Free WebPACK license is sufficient
- Download: Xilinx Vivado Download
-
Python 3.6+
- Required for COE file generation
- Check version:
python --version
Now we create the BRAM IP blocks that will hold your waveform lookup tables.
We will create:
- BRAM for Sine → initialized with
sine_512.coe - BRAM for Square → initialized with
square_512.coe - BRAM for Triangle → initialized with
triangle_512.coe - BRAM for Sawtooth → initialized with
sawtooth_512.coe
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:
8bits - 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
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.coeSymptoms: 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
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*]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
| 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) |
- 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)
-
Xilinx / AMD Block Memory Generator (IP Core Overview / Datasheet)
https://www.ni.com/docs/en-US/bundle/labview-nxg-fpga-module-cdl-api-ref/page/block-memory-generator.html -
Block Memory Generator v1.1 Product Specification (PDF)
http://web.mit.edu/neboat/Public/6.111_final_project/code/blk_mem_gen_ds512.pdf
- ZedBoard Zynq-7000 Development Board Reference Manual (Digilent)
https://digilent.com/reference/programmable-logic/zedboard/reference-manual
-
Python Script for Generating Xilinx .coe Files (General-Purpose COE Generator)
https://github.com/kooltzh/xilinx-coe-generator -
bin2coe Tool (Convert Binary to COE)
https://pypi.org/project/bin2coe/ -
Image-to-COE Example (Map Arbitrary Data into BRAM)
https://github.com/Jesse-Millwood/image-2-coe -
Discussion: Memory Initialization with .coe in Xilinx FPGAs
https://www.edaboard.com/threads/memory-initialization-file-for-xilinx-fpga-boards-using-coe-file.360941/
-
FPGA-Based Function Generator (IRJET Paper)
https://www.irjet.net/archives/V2/i9/IRJET-V2I9289.pdf -
Waveform Generator Implemented in FPGA (Master's Thesis – I/Q Generation with RAM LUTs)
http://www.diva-portal.org/smash/get/diva2:19354/FULLTEXT01.pdf -
Arbitrary Waveform Generator Based on FPGA (Project Report)
http://www.ir.juit.ac.in:8080/jspui/bitstream/123456789/4082/1/Arbitrary%20Waveform%20Generator%20Based%20on%20FPGA.pdf -
Fast FPGA-Based Low-Jitter Waveform Generator (PAC2013 Paper)
https://jacow.org/PAC2013/papers/THPMA01.pdf
-
Digilent Forum: Designing a Waveform Array Generator on FPGA
https://forum.digilent.com/topic/20345-designing-a-waveform-array-generator/ -
DEEDS Tutorial: Digital Waveform Generator on FPGA
https://www.digitalelectronicsdeeds.com/learningmaterials/LM/T120/120080_Digital_WaveForm_Gen_FPGA/Index.htm -
Reddit Thread: Using Xilinx Block Memory Generator
https://www.reddit.com/r/FPGA/comments/hm33q4/how_to_use_xilinx_block_memory_generator_to/
Amarendar Reddy | NRSC ISRO Intern, Electronics & Communication Engineering
Contributions are welcome! Please submit issues and pull requests to improve the project.
- 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]