011 – dBFS Conversion with Vitis HLS (2)


In this post we will examine in detail the input files required for a Vitis HLS project and will run the C Simulation for our dBFS Converter module.

Input Files

As discussed in the first part of this series, we need at least three input files for a Vitis HLS project: a header file, a source file and a testbench file. Let’s look at each of those in detail.

Header File

The header file contains the definition of the function that performs the linear to dBFS conversion. As a reminder, here’s the formula we want to synthesize:

sample_value_dbfs = 20*log10(abs(sample_value_linear)/max_value_linear)

The header file with the dBFS Converter function declaration is shown below:

#ifndef __DBFS_CONVERTER_HPP__
#define __DBFS_CONVERTER_HPP__

#include "ap_fixed.h"
#include "hls_math.h"

const ap_ufixed<48, 24> MAX_LINEAR_VALUE = 8388608;    // Maximum linear value for a 24-bit audio sample
ap_fixed<48, 24> dbfs_converter(ap_fixed<48, 24> linear_value);

#endif

We start our header file by including the ‘ap_fixed’ and ‘hls_math’ libraries. The ‘ap‘ in ‘ap_fixed’ stands for ‘arbitrary precision‘, and it allows us to define integer signals with an arbitrary bit width, which means we are not limited to the standard integer types defined in C/C++. The ‘hls_math’ library includes common math operations optimized for high-level synthesis, among them the absolute value and log10 functions.

Our formula for converting to dBFS includes division by the maximum value that can be represented in the linear scale, so we declare it here as a constant. The <ap_ufixed 48, 24> type tells Vitis HLS that we want this signal to be a 48-bit wide unsigned fixed-point type with a 24-bit integer part and a 24-bit fractional part.

We then come to our function declaration. Our dBFS Converter function will implement not only the log10 operation, but the entire conversion as defined by the formula presented earlier. We declare our function to receive the value of a sample in the linear scale and return its corresponding value in dBFS. Both the linear value received by the function and the dBFS value returned by the function are 48-bit wide signed fixed-point signals with a 24-bit wide integer part and a 24-bit wide fractional part.

By using 24 bits for the integer part of our signals we can cover the entire dynamic range supported by the audio codec. Furthermore, using 24 bits for the fractional part will allow us to perform all the math operations without any loss of precision. While this is fine for this relatively small module, we must keep in mind that choosing the bit width of the signals for the fixed-point implementation of any non-fixed-point algorithm is a critical step in the design process. A small bit width may lead to a loss of precision that can be unacceptable for our design. A large bit width, as we have chosen here, may guarantee that no precision will be lost, but it can also lead to a significantly higher resource utilization.

Source File

The source code for our dBFS Converter function is shown below.

#include "dbfs_converter.hpp"

ap_fixed<48, 24> dbfs_converter(ap_fixed<48, 24> linear_value) {
    ap_fixed<48, 24> division_result;
    ap_fixed<48, 24> dbfs_value;
    // Theoretical equation: dbfs_value = 20 * log10(abs(linear_value)/MAX_LINEAR_VALUE);
    division_result = hls::abs(linear_value)/MAX_LINEAR_VALUE;
    dbfs_value = 20 * hls::log10(division_result);
    return dbfs_value;
}

The code in the dBFS Converter function definition is the C++ description of the logic that we want Vitis HLS to synthesize. We begin by declaring two signals, division_result and dbfs_value, of the same type and bit width as the function’s argument and return values.

We then perform the conversion to dBFS in two steps. First, we extract the absolute value of the input audio sample and divide it by the maximum linear value. The result of this division is stored in the division_result signal. After that we calculate the log10 of the division_result value, multiply it by 20 and store it in the dbfs_value signal, which will be the return value of our function.

Vitis HLS will synthesize our dbfs_converter function as an RTL module. The function argument (linear_value) will be synthesized as the module input, while the return value will be the module output. Additional control and status signals will be included as well, we will examine those in detail later.

Testbench File

The final input file we need for our project is the testbench, shown below.

#include <iostream>
#include "../sources/dbfs_converter.hpp"

int main() {
    ap_fixed<48, 24> linear_value;
    std::cout<<"Hello World!"<<std::endl;
    linear_value = -8388608;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = 8388608;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = -8388607;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = 8388607;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = 4194304;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = -2097152;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = 1048576;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = -524288;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = 1;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = -1;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    linear_value = 0;
    std::cout<<"Linear value: "<<linear_value<<". dBFS value: "<<dbfs_converter(linear_value)<<"."<<std::endl;
    return 0;
}

The testbench consists of a C++ Main function that checks the results of the dBFS Converter. The testbench calls the dBFS Converter function with a handful of values that will be enough to verify its correctness.

When designing our testbench, we should keep the following guidelines in mind:

  • Depending on the C/C++ description of the function to be synthesized and the checking mechanism in the testbench, it is possible that the output of the function and the checker will not exactly match. In this case the testbench must include certain tolerances when evaluating the correctness of the output.
  • Whenever possible, the testbench should be self-checking. Our dBFS Converter can be easily evaluated by glancing over the results for a handful of inputs, but it is recommended that the testbench automatically checks the output of the function, either by running a reference model or by loading previously generated reference results.
  • The Main function in the testbench should be written in such way that it always returns zero when the results are correct. From a workflow perspective, the return value of the main function is the only thing that Vitis HLS takes into consideration to determine whether a simulation has passed or failed. Thus, the testbench for our dBFS Converter will always pass, regardless of the actual results generated by the function.

‘Solution’ and ‘Directives’ Script Files

When we create a solution for our project, Vitis HLS generates, among others, the script.tcl and directives.tcl files. The script.tcl file contains Tcl commands for regenerating the project and the solution, and can be helpful for workflow automation and revision control. The script.tcl file for our dBFS Converter is shown below.

############################################################
## This file is generated automatically by Vitis HLS.
## Please DO NOT edit it.
## Copyright 1986-2020 Xilinx, Inc. All Rights Reserved.
############################################################
open_project dbfs_converter
set_top dbfs_converter
add_files sources/dbfs_converter.cpp
add_files -tb testbench/dbfs_converter_tb.cpp -cflags \"-Wno-unknown-pragmas\" -csimflags \"-Wno-unknown-pragmas\"
open_solution \"solution1\" -flow_target vivado
set_part {xc7z020-clg484-1}
create_clock -period 10 -name default
source \"./dbfs_converter/solution1/directives.tcl\"
csim_design
csynth_design
cosim_design
export_design -format ip_catalog

When we use directives to drive the C Synthesis process, they are stored in the directives.tcl file, and are called whenever the script.tcl file is run. Because we are not using any directives in our dBFS Converter module, the directives.tcl file is empty.

C Simulation

Now that we have set up our Vitis HLS project and reviewed our input files, we are ready to perform the first step in the Vitis HLS workflow: C Simulation. Here we use our testbench to simulate our dBFS Converter function and check the correctness of its outputs. The output of the C Simulation is shown in the figure below.

C Simulation Results
C Simulation Results

We can confirm that our dBFS Converter function produces the expected outputs. For the maximum or near-maximum linear values, the matching dBFS value is zero or close to zero (on the negative side). With each halving of the input linear value, the output dBFS value is reduced by 6 dB. We can also confirm that the absolute values of the input are extracted correctly, because all the dBFS results are negative values between 0 and ~ -138.

We also see that, while an input value of -1 or 1 in the linear scale produces -138.474 dBFS, an input value of zero in the linear scale produces 0 dBFS at the output. Though this is correct given the mathematical description of the dBFS conversion function, this is not a desirable behaviour for our meter. We will handle this corner case in the RTL logic by ignoring the output of the dBFS Converter module when the input is zero and driving the output LEDs directly for that case.

That’s it! We have now analyzed each of the input files for our Vitis HLS dBFS Converter project and performed the C Simulation to validate our C++ description. In the next part of this series we will Run C Synthesis, Co-Simulation and RTL Export in Vitis HLS before including the dBFS Converter module in our Vivado project.

Cheers,

Isaac


Leave a Reply

Your email address will not be published. Required fields are marked *