How to Build a Basic Testbench for SoC Projects

How to Build a Basic Testbench for SoC Projects

How to Build a Basic Testbench for SoC Projects Author:lshj98115

(Compiled from EETOPBBS

The purpose of writing this document is to give everyone a clearer understanding of how to build a SoC project’s Testbench. You can follow this document step by step to set up a basic SoC project’s testbench. This document focuses on guiding everyone in setting up the basic environment and addressing common issues or pitfalls encountered during the setup of the Testbench.

My SoC project’s testbench has some relatively special points:

1) There should be embedded software. This includes two parts: one is the initialization bootloader (usually stored in ROM or in external FLASH), and the other is the application layer program that resides in external volatile storage after booting.

2) After normal startup (the primary boot can switch to the application program), to simplify the process, we will use the ISS environment. — This is a relatively special point.

3) Maintenance and modification of the main environment scripts. This mainly includes single simulation and batch simulation ( regression) core scripts.

4) To optimize simulation and compilation speed, we need to be able to dummy unused modules.

5) Handling of file lists.

6) Handling of a “shared space” accessible by both SoC software and the Testbench.

7) Preparation of common functions, such as directly accessing external DRAM arrays based on the address space seen by the CPU, for initialization writes, data writes, and data read operations.

8) Maintenance of environment variables.

9) Maintenance of Define files.

10) Replacement of DDRC (one is the replacement of AXI_SLV_VIP, and the other is a simple AXI_SLV model replacement).

Sharpening the knife does not delay the work of chopping wood; preparing what is needed in advance makes building the Testbench as simple and quick as building blocks.

Maintenance of Environment Variables

Use module tools to maintain the environment variables for the entire project. The goal is to ensure that all engineers on the project use a unified environment (mainly tool versions and environment variables).

Maintenance of Core Scripts

There are two scripts: run_sim and regress. The run_sim script is responsible for submitting a single simulation task, while regress is responsible for submitting batch simulation tasks. These two scripts have been used in many projects, and I will open a special topic to explain the specifics of the scripts later. Just a reminder here, the run_sim script usually requires minor changes based on different projects.

The run_sim and regress are relatively large Perl scripts, and I will roughly describe their functions.

Function of the run_sim Script

1) Generate a simulation directory for each simulation. The simulation directory should include a file list (hardware and software), compilation and simulation commands (note to include the embedded software’s MakeFile), pre-create the necessary subdirectories, and file links corresponding to the individual simulation (such as maintaining the C test main function, extended random class SV files, links to the primary bootloader file), define files, and reconstruction commands for this simulation (this is easily overlooked; once a simulation fails during regression, and you do not want to re-simulate in the error directory, you can directly submit with this reconstruction command file).

2) Maintenance of various options. For example, different simulations require different defines, compilation and runtime options, dump waveform scopes, and levels.

Function of the regress Script

The regress script is relatively simple; it consumes a command set file consisting of many run_sim simulation commands. The regress script submits these simulation commands to the workstation. It should be noted that sometimes there may be some common options or defines, such as enabling coverage collection or a specific define that needs to be applied to the entire regression. Therefore, the regress script should be able to support adding options to all run_sim commands.

Generating Dummy Files

Use the gen_dummy_file script to generate dummy files. Design engineers may also need to maintain a module_dummy.v file for integration, and verification engineers should ensure that the names of the generated dummy files do not conflict with the files maintained by the design team.

Why not use the files maintained by the design team? Because, after integration, the files maintained by the design team are likely no longer updated; another reason is that the files maintained by the design team might have outputs that are all assigned to 0, but it is better for module outputs such as pready and CEN signals to be assigned to 1, otherwise it may cause issues (for example, if the SRAM enable signal CEN is assigned to 0, it may cause the subsequent SRAM model to think there is read/write activity; if the pready signal is assigned to 0, it may cause the SoC software to hang when performing register operations on that module).

This script is not easy to write. Because the module declaration syntax supported by Verilog has too many variations, it can easily lead to overlooking some details. For example, several complex cases:

Module test #(parameter a = 1,Parameter b = 2,C = 3,D = 4 );

These parameters may need to be used in the declaration of port widths. More troublesome is that parameters may involve functions, which could be written in the code as define. This makes it difficult to resolve using the parse RTL method.

Similarly, handling ifdef else endif type of compile-time macros in port declarations is also quite complicated.

It is also possible to use the user interfaces provided by the simulator or debug tools to write tcl programs to obtain the name, width information of each port. However, different simulations (with different defines) may lead to inconsistencies in port widths and ports, resulting in the need to maintain different dummy files for different defines, which is also quite troublesome.

In short, after generating the dummy files, remember to check them. Dummy files can effectively shorten compilation time.

Maintenance of File List Processing

The above tasks should be prepared in advance, and then we can start compiling the RTL. After integration, the file list should first be compiled. A potential issue is the types of encrypted files; the encrypted files in the file list may not be compatible with the simulator you are using. Then, combined with the previously generated dummy files, we need to process a simplified design mini- file list, which generally includes only the modules necessary for initialization ( Clkrst, PAD, CPU, bus topology, memory controller), effectively dummying out modules like video systems, peripheral interfaces, and storage systems.

Generating Mini- File List

Use a script to maintain a configuration file indicating how to modify the existing file list.

Note that the final file list used should contain absolute paths.

The benefit of using absolute paths is that it allows the run_sim script to specify the simulation to run in any directory (for example, if the regression needs to be submitted to another hard drive, it must use absolute paths).

Note that do not use the$macro structure in absolute paths; others may run simulations or debug using your file list, and their $macro may differ from yours, causing issues.

There is one point to note about generating file lists:

Generally, a file (for example, a.v) is only allowed to appear once in a file list. Otherwise, it may lead to duplicate module compilation errors. However, sometimes integration is special (for example, the FPGA version), and to reduce adjustments to the code during modifications, the ifdef-elsif-else-endif structure may be used to define different module-names for the same file.

For example, the content of the file a.v is as follows:

`ifdef FPGA1module v_fpga1 (

`elsif FPGA2module v_fpga2 (

`elsif FPGA3module v_fpga3 (

`elsemodule v (

`endif

Then the structure in the file list would be as follows:

fpga1_def.v

a.v

fpga1_undef.v

fpga2_def.v

a.v

fpga2_undef.v

……

Please note that the file list processing script may need to have a “deduplication” feature. In this case, the “deduplication” functionality of the file list processing script should be disabled.

After compiling the mini- file list, you can start preparing to write the testbench top module.

Maintenance of Define Files

During the process of building the testbench, the interface, env, svtb etc. may require xmr ( cross module reference) access signals, so maintaining a common define file is very important. This define file should include:

1) The xmr paths of each major module, and remember to distinguish between ASIC/ FPGA/ module-level defines.

2) The paths of model arrays in the address space. For example, the xmr paths of arrays in the dram model, and the xmr paths of memory arrays in the sram.

3) The defines for parts of shared space addresses, such as the defines used in the shared space for our software printing implementation.

4) Basic defines for dram.

Shared Space

The “shared space” in the SoC project Testbench refers to the space visible to both the software (embedded C program) and the Testbench ( SV program). Generally, the Testbench can see everything, while the software can only see the CPU address space (registers, SRAM, ROM, DRAM, external IO space, etc.). The required address range for the shared space is not small (it may require dozens of KB, so it is generally placed in the SRAM and DRAM visible to the CPU), which will vary for the ISS (to be explained later).

Maintenance of Common Functions

The functions that everyone on the project may use are called “common functions”. I personally think the most important are the accesses to the CPU address space (we use xmr_read_mem and xmr_write_mem). Also, functions for file access based on these functions ( task) are important.

When implementing xmr_read_mem and xmr_write_mem task (or function), the width of the main model arrays will affect the addresses accessed based on the model array indices. For example, if the bit width is 128bit, then one memory corresponds to 4 32bit words.—– Each project will be different.

Additionally, handling DRAM is the most complex, especially since DRAM supports bank address and row address remapping, so special attention must be paid to the correspondence between addresses and bank signals, as well as row address signals during remapping.—– This work can inherit from previous projects.

The Xmr functions need to consider the implementation when replacing DDRC with SRAMC or AXI_SLV_VIP.

Briefly discussing the implementation of the vip_slave_write32 function: This function calls the lower-level function:

env.axi_slave_subenv.do_write32(addr, data);

However, this function may not be visible in other components of the tb, but the program can see it. Therefore, create a function in the program to call the lower-level env.axi_slave_subenv.do_write32

Then export this do_write32 from the program using DPI export.

Maintain an xmr.c program on the tb, implementing the vip_slave_write32. In xmr_wr_mem32, this vip_slave_write32 is called.

Using xmr_wr_mem32 and xmr_rd_mem32 can easily implement:

1) Loading the initialization program — The primary bootloader needs to be loaded into ROM, and the application program needs to be loaded into DRAM. Using xmr_wr_mem32 allows you to write programs directly in the form of accessing the CPU address.

2) Injecting stimulus data into DRAM, allowing the module under test to retrieve stimulus data from DRAM. Or reading a frame of data from memory for comparison (for example, after decoding a frame, reading all the decompressed data of the entire frame from memory at once).

Here, you can implement something similar to loading the program. You can also use xmr_rd_mem32 and xmr_wr_mem32 to implement a generic task.

Write the specified file into memory as stimulus data:

Here’s a little trick: use fscanf to read a line of data from a file and then determine the length of the string to get the number of bytes per line in the input file, and then load it into DRAM based on the number of bytes per line.

Testbench Top-Level File

We build the Testbench top-level file based on the mini- file list to speed up compilation.

The most important aspect of the Testbench top level is instantiating the top of the DUT. Emacs users can integrate easily; I am a VI user, which is slightly more complicated, but using the vi replace function can also integrate quickly:

1) Copy the input output inout port declaration part of the top-level module, replace input-output-inout with “ wire“, to declare signals. Individual signals, such as system input signals and system reset signals, need to be changed to reg, and generate the reg signal’s stimulus. It is recommended to use a module to generate the clock to keep the code clean and clear.

2) Copy a portion of the wire declaration and process it into the integration of the DUT.

s/\s*\[\d\+:\d\+\]// Remove bit-width declarations wire [1:0] A; à wire A;

s/wire\s\+// Remove wire declarations wire A; à A;

s/(\w\+)/.\1(\1)/ Generate integration A; à .A(A);

3) Process the module name declaration instantiation and semicolons. .A(A); à .A(A),

Add pullup and pulldown to the top-level signals; generally, all top-level signals should add pulldown, while individual signals may require pullup. Overall, we do not want the TB to introduce X states. If unsure which to add pulldown and pullup, at least ensure that the test mode input pin ( TEST), CPU Jtag port, and the initial state or identification PIN that needs to be read are assigned appropriate pullup or pulldown.

Instantiate interface and program.

Program is usually just a simple instantiation of SV components (like the env under VMM), and includes the different processing parts for each testcase.

In each test.sv, usually implement the extended classes for random variables.

Be aware that if the Program ends, the simulation will also end, so control the end time of the program carefully.

Instantiate the basic simulation model. The most important is the DRAM model. Please note that it is best to handle the instantiation of the DRAM model with defines, as the DRAM may need to handle several situations like 4bit, 8bit, 16bit, etc. Different sizes and different bit widths of DRAM have different address signal widths, and the number of external chips varies, so special attention is needed during integration.

Implementing the mechanism for dumping waveforms:

The principle of dumping waveforms is “whether to dump, modify the start time of the dump, and modify the levels of the dump do not require recompilation.” The first two need to be controlled using simulation runtime parameters, while the latter uses verdi’s pli.

Generally, the top-level file of the Testbench is quite complex, so it is recommended to use include files to maintain it, enhancing code readability. Moreover, the top-level file usually contains many ifdef-elsif-else-endif compile structures, and overly complex code may lead to compilation errors due to typographical mistakes.

Include the previously prepared common function files and common define files.

Program initialization load code. The SoC project requires embedded software code, including the primary boot and the application program in DRAM. Both pieces of code need to be loaded into their respective storage media. This load work can be implemented simply using the file writing function based on the xmr_write_mem function.—— The specific implementation has been shared earlier.

At this point, the top-level testbench is basically complete.

Preliminary Debugging of Design and Environment

After writing the top-level testbench, after successful compilation, dump the overall waveform to check if there are any high-impedance Z states on the ports of each module. If there are, it indicates possible unconnected internal signals, especially connections between various master and slave ports on the main bus.

Check whether the basic modules needed for initializing the CPU, PAD, ROM, SRAM controllers have clocks and resets. If not, it indicates issues with the clocks and resets generated based on external input system clocks and resets.

Primary Bootloader

The primary bootloader is designed for initialization. The bootloader used in the actual system is more complex, involving parameter transfer and configuration from external storage media. The simulation bootloader should be as simple as possible, as it will be used for all simulations, and if this step is slow, it will waste time.—– However, when using ISS, this issue does not exist, but it still requires a quick initialization.

I personally recommend designing the following steps in the simulation bootloader:

1) Initial configuration when the system powers on.

2) Initialize the PLL to the target frequency (if the system’s PLL default frequency is the target frequency, this step can be omitted).

3) Configure the clock frequency of the core module and switch clocks, performing software resets on necessary modules.

4) Initialize the memory controller.

5) Remap to the memory to prepare for executing the application program in memory.—— Generally implemented in assembly.

The most basic functions are the access handling functions for the CPU space:

#define SETREG32(reg,val) (*((volatile unsigned int *) (reg)) = ((unsigned int) (val)))

#define GETREG32(reg)(*((volatile unsigned int *) (reg)))

Where reg is the register address, and val is the value to be configured. The Volatile keyword ensures direct memory access.

Application program code

The application program code also needs to perform some initialization, mainly for clock configurations of non-core modules and software reset operations for non-core modules.

If using ISS, since there is no primary bootloader, the functionality of the primary bootloader must be implemented in the application program initialization.

It is important to note that enabling cache when using ISS may lead to abnormal behavior of ISS. You can use ifdef ISS at the location where cache is enabled.

The assembly code is:

IF( EF: ARM_ISS)

NOP

NOP

ELSE

Cache-operation

ENDIF

In the assembly code, include the define file:

GET define.s (Note: Do not write at the beginning!)

Then construct a very simple application program, generally just accessing ddr, sram, registers, and printing.

endsim() is the function to end the simulation, and if you want the software to control when to end the simulation, you can call this function at the appropriate position in the software. The implementation of the function uses shared space, where the software writes a flag to a specified location in the shared space, and the svtb continuously samples this flag in a while(1) loop.

Implementing Printing of Embedded Code on the Simulation Platform

What is relatively complex in the software code is the implementation of printf. The key is to store the content to be printed in a location visible to both the software and the testbench. Then, in the testbench, based on the “print enable”, “print start”, and “print end” flags, the content can be output using $write.

Both the software and the Testbench can see the SRAM space (the primary bootloader uses it for data storage and stack). Note that the scatter file for the bootloader should not allow the stack-top to overwrite this part of the space.

The implementation mechanism of Printf is similar to the actual C printf, utilizing a “function with a variable number of parameters” (implementation mechanism: since parameters are pushed onto the stack from right to left, the first parameter is closest to the top of the stack, and the compiler can know its position in the stack).

Debugging the Overall Environment

At this point, the entire environment has been basically established. Combining the primary bootloader and simple application program code allows for debugging the system initialization process and overall environment. Typically, there will be some integration and small bugs related to bus access.

Replacing ISS

To speed up compilation and simulation speed, we use ISS to replace CPU-IP. Some C program codes for ISS are pre-compiled into .so files, so during compilation, there is no need to compile the ISS, just link it in.

The advantages of using ISS are:

1) You can dummy the code of CPU-IP.

2) No need for a primary bootloader.

3) The execution of the software is fast.

4) Testcases can still be written based on embedded C programs.

5) Module-level testcases can also be implemented using C.

Wrap ISS with an AXI wrapper and instantiate this module at the top level of the testbench. Force it to the CPU data bus’s AXI port (if it is Arm9, it is the AHB bus). The IO access’s task file needs to be included in the top level of the testbench. For register space IO access, normal timing needs to be generated; for memory space access, you can use the previously introduced xmr_wr_mem32 and xmr_rd_mem32 functions to speed things up.

ISS may not be the same type of CPU as CPU-IP, so be careful to add –cpu distinctions when compiling the software code, as this may even lead to different compilers for the software code. These differences can be reflected in the run_sim script.

Each project’s CPU addressable space may differ, so pay attention to the contents of the ISS space configuration file, which should vary according to the project. The address access in the IO access’s task will also vary.

In ISS, the shared space is different from the actual CPU_IP. For functions like printing, it is not necessary to use CPU address space. This is because the ISS wrapper is part of the testbench, allowing you to directly implement a large array as “shared space” in the testbench, which is simpler and more intuitive.

Replacement of DDRC

Once the system is up, we may need to replace DDRC. Generally, there are two situations:

1) Use SLAVE_VIP to replace DDRC, aiming to randomly control the slave‘s latency, implementing exceptional cases for module memory access.—— This generally needs to be used in conjunction with ISS, as there is no need to initialize application code in the slave-vip. The read/write operations of the slave-vip may be relatively slow, and during large data writes, the simulation can noticeably feel the pause.

Align the internal ports with the slave-vip: It is recommended to use macros for easier reading and simplified code.

2) Use a simpler Slave ( SRAMC) to replace DDRC. The aim is to speed up initialization (no need for register configuration), enhancing compilation and simulation speed.

SRAMC is not a class, but a module. Instantiate it at the top level of the TB, and similarly, it needs to align with the internal ports as mentioned above.

Both SLAVE_VIP and SRAMC are parameterized designs, making it easy to modify data width and other information.

How to Build a Basic Testbench for SoC Projects

How to Build a Basic Testbench for SoC Projects

How to Build a Basic Testbench for SoC Projects

E课网(www.eecourse.com) is a professional integrated circuit education platform under Moer Elite, dedicated to cultivating high-quality integrated circuit professionals in the semiconductor industry for 6 years. The platform provides a practical training environment tailored to the needs of integrated circuit enterprises, quickly training students to meet corporate demands through both online and offline training methods.

E课网 has a mature training platform, a complete curriculum system, and strong faculty strength, planning a boutique course system of 168 courses covering the entire integrated circuit industry chain, and has 4 offline training bases. To date, it has deeply trained a total of 15,367 people and directly supplied 4,476 professionals to the industry. It has established deep cooperative relationships with 143 universities, co-hosting 240 corporate special IC training sessions.

Leave a Comment