The goal is to generate two sine waves with different frequencies. We achieve this by using the Block Memory Generator as a lookup table. Initially, we’ll populate the Block Memory with 1, 000 pre-calculated sine wave values. By reading back these values, we’ll be able to generate and visualize two sine waves as output, as shown in the waveform simulation results.
You can find the full Video here:
Key topics covered include:
- Block Memory Generator configuration in standalone native interface mode
- Dual-port ROM setup for independent frequency operation
- Python script implementation for generating optimized sine wave coefficients
- A very easy Simulation setup and waveform analysis
Start by creating a new project for your board, and then add a block design. Within the block design, use the "Add IP" feature to insert a Block Memory Generator. To configure the block memory, double-click on the Block Memory Generator block.
Block Memory SetupIn the Basictab, you’ll find two controller operation modes for the block memory:
Bram controller or AXI4 Bus Interface: This mode enables communication between the block memory and an AXI master, allowing read and write operations through the AXI4 protocol. For this mode we need to use the Bram controller as the interface between the Block memory and AXI Master bus.
Native Interface or Stand-Alone Mode, In this mode, the block memory is controlled directly by the Programmable Logic. The PL can read from and write to the memory without needing a bus interface.
For this tutorial, we will be directly accessing and reading back buffered data from the block memory. Therefore, select the Native Interface, Stand-Alone Mode, option.
For the Memory Type, this tutorial uses Block RAM as a lookup table to store pre-calculated sine wave values. These values are then read from the Block RAM to generate the sine wave. Additionally, we aim to explore how to operate two independent ports with different frequencies. To achieve this, we will use Dual-Port Read-Only Memory (ROM), which allows each port to operate independently, reading data at different clock rates.
Next, we open the Port A and Port B options tabs to configure the appropriate bit width and depth for storing the sine wave coefficients. Storing signal coefficients as double or float in a lookup table is inefficient, as it consumes unnecessary memory and resources. Instead, we will save the coefficients as integer values with a limited bit width, which is both memory-efficient and sufficient for our design requirements.
We choose 11 bits for each port, the Reason is that we are generating sine wave coefficients with 10-bit resolution for amplitude and an additional sign bit, (for positive/negative values). Therefore, the total bit width required to store each sine sample is 11 bits, (10 bits for magnitude + 1 bit for sign).
Port A Depth: 1024, The depth of the memory defines how many different data values can be stored. Since you are generating 1024 sine wave coefficients, you need a depth of 1024 to store all these values. Each memory address will hold one 11-bit sine wave coefficient.
Additionally, we want both ports to remain continuously active, so we set the Enable Port Type to Always Enabled. This ensures that the ports are always ready for read and write operations without requiring external control signals to enable them.
Since we are only focusing on simulating the design, we deselect the Primitive Output Register option. This reduces the Block RAM latency to 1 clock cycle, simplifying the design and making the simulation easier to understand by minimizing delays
Repeat the same setup for Port B as well
Open other options tab to load a memory initialization Coefficient file.
The Coefficient file format is commonly used in Xilinx tools to initialize Block RAM with specific data. Let’s start with simple example of Coefficient file.
Here's a detailed explanation of the provided Coefficient file:
Breakdown of the COE File:
- memory_initialization_radix=10;
- This line defines the radix (number system) in which the initialization values are specified. In this case, the radix is set to 10, meaning the values in the file are in decimal (base 10) format.
- Other possible radix values could be:
- 2: for binary.
- 16 for hexadecimal.
- 10 for decimal, as used here.
- memory_initialization_vector
- This line starts the list of memory initialization values (the vector) for the BRAM.
- The values following this line are the initial values that will be loaded into the BRAM when the design is initialized.
- These values represent the contents of each memory address in the BRAM, starting from address 0.
In the following you can find one example for a memory file that initialize the memory with vector [1, 2, 3,..., 10]. Since all 1000 sine values are too long to display, we only put this example file here. but you can download the.coe file for this project from our GitHub repository.
memory_initialization_radix=10;
memory_initialization_vector=
1
2
3
4
5
6
7
8
9
Now press the brows button and load the COE file. You can validate and then save it.
As previously mentioned, storing signal coefficients as double or float in a lookup table is inefficient, as it consumes unnecessary memory. To optimize this, we can store the coefficients as integers with a limited bit width. The following Python code helps us generate sine wave values with 10-bit resolution for the amplitude and an additional bit for the sign. This ensures efficient memory usage while maintaining the accuracy needed for signal generation. The code generates the suitable coefficients and save them in a text file.
This code efficiently generates and stores sine wave coefficients as integers, reducing memory usage while maintaining precision. It produces values with 10 bits of resolution for amplitude and 1 bit for the sign, making them ideal for use in digital signal processing or FPGA applications.
import numpy as np
# Define the function parameters
bitwidth = 11 # Total resolution: 10 bits for amplitude, 1 bit for sign
number_of_samples = 2**(bitwidth-1) # Number of sine wave samples to generate (1024 samples)
Max_value = 2**(bitwidth-1) -1 # Maximum positive value for the sine wave (1024), since 10 bits are used for magnitude
# Generate an array of x values ranging from 0 to 1024 (sample indices)
x_values = np.arange(0, number_of_samples, 1) # 1024 sample points over one sine wave period
# Calculate the corresponding y values as sine wave coefficients
# The sine wave is scaled to the max amplitude and floored to an integer value
y_values = np.floor(Max_value * np.sin(2 * np.pi / number_of_samples * x_values))
# Save the generated sine wave coefficients into a text file
# The file 'sine_values.coe' will store the integer coefficients in a single column
np.savetxt('sine_values.coe', y_values, fmt='%d') # Save as integers
The code will save the coefficients in a text file, copy the generated values and paste them in coefficients file.
The block memory is now ready, and we will proceed to add additional IPs to the design.
- Clock Generators: Add two simulation clock generators—set one to 100 MHz and the other to 200 MHz.
- Binary Counters: add two binary counters to the design and set the a output bit width of each one to 10.
- Connections:
- Connect the clock pin of Port A to the 100 MHz clock generator and link the output of the first binary counter to the address port for Port A.
- Repeat this process for Port B, connecting it to the second binary counter and the 200 MHz clock generator.
This configuration ensures that Port A operates with a 100 MHz clock while Port B runs at 200 MHz. Consequently, the address for Port A is generated at 100 MHz, and the address for Port B is generated at 200 MHz.
For easier simulation, make both data output ports for Port A and Port B external.
Create top HDL wrapperCreate top HDL wrapper and let the Vivado to manage and auto handle it.
Run simulationNow we can proceed with the simulation.
After simulation completed follow these steps to see the waves clearly.
Increase Simulation Time: Extend the simulation duration to 100 microseconds (µs).
Reset and Rerun: Reset the simulation and then rerun it for the specified time.
Adjust Signal Settings: Right-click on the two simulated signals to change their settings. Set the radix to signed decimal and the waveform style to analog.
After these adjustments, you will observe two sine waves fluctuating at different frequencies.
Let’s add more signals to the waveform window for further analysis.
- Access the Scope Window: Navigate to the scope window.
- Add Signals: Include the counters outputs for both ports in the waveform window.
Upon doing this, you will observe that the address and output data for one port update twice as fast as the other port. The counter here play the role of phase accumulator and generates a Ramp function.
Comments
Please log in or sign up to comment.