3.5.1.21.8 Example Usage of the Driver Functions

When using the SmartHLS-generated API directly, as opposed to the transformed top-level function, users may have more flexibility to exchange data and control over the generated hardware modules. Let's use the following example code to demonstrate a few concepts when using the API:

  • Use of the blocking top-level control driver function.
  • Use of non-blocking calls.
  • Manually write/read arguments.
  • Control of multiple copies of the same SmartHLS-generated module. Note that this not the equivalent to having two different SmartHLS modules.

Below is the example for simple top-level function that adds two numbers and return the result. This function will be converted to Verilog and an HDL+ component for Libero® SmartDesign will be generated.

int add(int a, int b) {
#pragma HLS function top
#pragma HLS interface default type(axi_target)
#pragma HLS interface argument(a) type(axi_target) num_elements(1)
#pragma HLS interface argument(b) type(axi_target) num_elements(1)
 
    return (a + b);
}

Once the module is generated, users can make a copy of the module to have two instances of the same module. Below is the code where the top-level driver functions are used to initialize, start and wait for the completion of both instances of the add(..) module . While the top-level driver function (add_hls_driver(..., SUM0_BASE_ADDR) is called, the processor has to wait for it to complete before starting the second instance. Note that the difference is the base address assigned to each module.

// use the top-level driver
sums[0] = add_hls_driver(tv1[0], tv1[1], SUM0_BASE_ADDR);
sums[1] = add_hls_driver(tv1[2], tv1[3], SUM1_BASE_ADDR);

To avoid the blocking behavior, a program can use the non-blocking API functions for each instance of the module. The following code shows that the inputs are initialized and the SmartHLS modules are started one after the other via <TopFunc>_write_input_and_start( ) and read the outputs via <TopFunc>_join_and_read_output( ). The use of these two functions allow the program to perform other tasks between the calls to start and join while the HLS module are processing in parallel.

// use individual control drivers
add_write_input_and_start(tv2[0], tv2[1], SUM0_BASE_ADDR);
add_write_input_and_start(tv2[2], tv2[3], SUM1_BASE_ADDR);
sums[0] = add_join_and_read_output(SUM0_BASE_ADDR);
sums[1] = add_join_and_read_output(SUM1_BASE_ADDR);

Finally, the following example shows updates to individual input arguments via scalar input driver functions. In this case, the program writes to the arguments a and b prior to calling add_start( ). It also shows that the values written can also be read back. This provides more flexibility when transferring data to and from the SmartHLS modules.

add_write_a(tv3[0], SUM0_BASE_ADDR);
if(tv3[0] != add_read_a(SUM0_BASE_ADDR)){
    PRINTF("Test 3: adder at %#x writing and reading a FAIL\r\n", SUM0_BASE_ADDR);
    err++;
}
add_write_b(tv3[1], SUM0_BASE_ADDR);
if(tv3[1] != add_read_b(SUM0_BASE_ADDR)){
    PRINTF("Test 3: adder at %#x writing and reading a FAIL\r\n", SUM0_BASE_ADDR);
    err++;
}
 
add_write_a(tv3[2], SUM1_BASE_ADDR);
if(tv3[2] != add_read_a(SUM1_BASE_ADDR)){
    PRINTF("Test 3: adder at %#x writing and reading a FAIL\r\n", SUM1_BASE_ADDR);
    err++;
}
add_write_b(tv3[3], SUM1_BASE_ADDR);
if(tv3[3] != add_read_b(SUM1_BASE_ADDR)){
    PRINTF("Test 3: adder at %#x writing and reading a FAIL\r\n", SUM1_BASE_ADDR);
    err++;
}
 
add_start(SUM0_BASE_ADDR);
add_start(SUM1_BASE_ADDR);
 
sums[0] = add_join(SUM0_BASE_ADDR);
sums[1] = add_join(SUM1_BASE_ADDR);
The corresponding Libero SoC design associated to the code snippets above is shown in the following diagram where the two copies of the same add() module are instantiated and accessible by the CPU.
Figure 3-33. Libero SoC Design