Co-simulation Run-Time Options
Introduction
When writing a C or C++ program the entry point for execution is a main function which has a prototype definition as shown below:
int main (int argc, char *argv[]);
The parameters are used to pass in command line inputs from the user, with the first argument indicating how many arguments have been specified, including the name of the executable, and a pointer to an array of pointers to zero terminated character strings which are the arguments specified by the user on the command line.
This means, when a user runs an executable, its behaviour can be modified by specifying some parameters that the code can read from the passed in arguments to main. Utility functions like getopt() can formalise the format for these arguments with the familiar command line options of a dash followed by a letter and an optional following argument.
For OSVVM co-simulation programs, running in a logic simulation things need to be slightly different. The simulator itself is an executable and has the main function within it, so this is unavailable to a co-simulation program to use as an entry point function and the command lines are for the simulator. In addition, multiple co-simulation programs can be run in a simulation, and these have associated “nodes” which uniquely identify them, both in the C/C++ domains and in the VHDL (see this blog post). Each co-simulation node has a corresponding unique node number to identify the channel between the VHDL and corresponding running software. Each node’s software has a unique entry point that stands in for a main function. This has the form of:
void VUserMain<n>(int node);
where <n> specifies the node number: e.g. VUserMain0 is the entry point for software running on node 0.
Just as for a program that’s to be an executable with a main function, it would be great if we could pass in user command options to modify the behaviour of the program, but there are some things to solve:
- The co-simulation entry does not have user command parameters like main
- We’d, optionally, want to specify different command options for each node’s program
- We’d like the parameters to look like that which would be used with a normal executable.
In this blog I want to address these issues and show how this can be done. As an example, I will be using the parse_args method found in the rv32_cosim_utils class code found in CoSim/tests/rv32demo.
The Requirement
To solve the first problem of VUserMain<n> having no user input command parameters we will use a file to specify the “command line” which can be read in at run time for parsing. In the example code, this is called vusermain.cfg.
To solve the second problem of different command lines for each node, each line in the file starts with a node identifier. For the example code, this has the form of vusermain0, for node 0.
The last problem is solved by being able to specify a set of command line options after the vusermain<n> and using getopt to parse these in the software. The vusermain.cfg in the example code is used to configure the rv32 RISC-V instruction set simulator and has a usage message as shown below:
Usage: vusermain0 -t <test executable> [-hHeEbdrgcxTaCBRc][-n <num instructions>]
[-L <load addr>][-S <start addr>][-s <sp addr>[-A <brk addr>]
[-m <num of mem dump words>][-M <mem dump start addr>]
[-D <debug o/p filename>][-p <port num>][-s <addr>]
-t specify test executable (default test.exe)
-B specify to load a raw binary file (default load ELF executable)
-L specify address to load binary, if -B specified (default 0x00000000)
-n specify number of instructions to run (default 0, i.e. run until unimp)
-d Enable disassemble mode (default off)
-r Enable run-time disassemble mode (default off. Overridden by -d)
-C Use cycle count for internal mtime timer (default real-time)
-a display ABI register names when disassembling (default x names)
-T Use external memory mapped timer model (default internal)
-H Halt on unimplemented instructions (default trap)
-e Halt on ecall instruction (default trap)
-E Halt on ebreak instruction (default trap)
-b Halt at a specific address (default off)
-A Specify halt address if -b active (default 0x00000040)
-D Specify file for debug output (default stdout)
-i Specify a software interrupt address (default = 0xaffffff8)
-x Dump x0 to x31 on exit (default no dump)
-c Dump CSR registers on exit (default no dump)
-m Dump specified number of words from memory (default no dump)
-M Start byte address of memory dump (default 0x1000)
-g Enable remote gdb mode (default disabled)
-p Specify remote GDB port number (default 49152)
-S Specify start address (default 0)
-s Specify stack pointer address (default 0)
-h display this help message
An example vusermain.cfg file for this test might be as shown:
# -e Exit on ecall # -H Exit on unimplemented instruction # -t Specify executable to run # -g Run in debug mode vusermain0 -g -eH -t ../../OsvvmLibraries/CoSim/tests/rv32demo/riscv/test.exe
This specifies the test is to be run in debug mode, with the RISC-V program to halt on encountering an ecall instruction or an unimplemented instruction exception. Then the RISC-V program to be run is specified. If there were multiple nodes in this test, then other vusermain<n> entries can be added to the file with their own set of command line options. This could be for a completely different program and have a completely different command line option selection.
The Code
As mentioned before, in the example test, there is an rv32_cosim_utils class defined, and one of its methods is parse_args which has the following prototype definition:
int parse_args (rv32i_cfg_s &cfg, const int node);
The first argument is a configuration structure that’s passed into the rv32’s run method to configure it. This is defined in CoSim/include/rv32_cpu_hdr.h as:
struct rv32i_cfg_s {
const char* exec_fname;
bool load_binary;
uint32_t load_bin_addr;
bool user_fname;
unsigned num_instr;
bool rt_dis;
bool dis_en;
bool abi_en;
bool hlt_on_inst_err;
bool hlt_on_ecall;
bool hlt_on_ebreak;
bool en_brk_on_addr;
bool dump_regs;
bool dump_csrs;
bool use_cycles_for_mtime;
bool use_external_timer;
uint32_t num_mem_dump_words;
uint32_t mem_dump_start;
bool gdb_mode;
uint32_t gdb_ip_portnum;
uint32_t brk_addr;
bool update_rst_vec;
uint32_t new_rst_vec;
bool update_sp;
uint32_t new_sp;
FILE* dbg_fp;
rv32i_cfg_s()
{
// initialise fields
}
}
In the actual definition the rv32i_cfg_s constructor sets default values for the structure’s fields. By passing the command line options we can alter the values of the fields from their defaults to configure the rv32 model.
The second argument is the node number that the rv32 ISS is running on, which is used to select the entry for the node in the configuration file. We now have enough to parse a configuration file and update the fields of the configuration structure.
First we must open up the configuration file, which must be in the directory from which the simulation was run:
FILE *fp = fopen("vusermain.cfg", "r");
Note that the actual code makes use of constant definitions, but literals are used here for clarity and brevity. Also, error checking is done but left out for the same reason. If opening the file was successful we need to read in the file’s lines, one at a time, searching for a vusermain entry at the beginning of the line that matches the node number passed in, such as vusermain0.
strcpy(delim, " ");
sprintf(vusermainname, "%s%c", "vusermain", '0' + node);
int argc = 0;
char* argvBuf[MAXARGS];
while (fgets(argstr, STRBUFSIZE, fp) != NULL) {
char* name = strtok(argstr, delim);
if (strcmp(name, vusermainname) == 0) {
argvBuf[argc++] = name;
break;
}
}
fclose(fp);
The code fragment uses a set of char buffers for delim, vusermainname and argstr. The delim buffer simply holds a space to use as an argument to strtok. The vusermainname buffer holds the string for the entry search; e.g. vusermain0. We then have a loop that starts reading in lines from the file into the argstr buffer using the fgets function. Using strtok to break the line string into individual entries, delimited by the space in delim, it returns a pointer to the first token. This is then compared with the string in vusermainname and, if matching, an argvBuf, which is an array of char pointers, is updated to copy the pointer to the retrieved name into its 0th entry. The loop is broken to exit so that the rest of the arguments can be parsed, and the configuration file is closed.
A new loop will extract the pointers to the individual options which are added to the argvBuf array. Note that subsequent calls to strtok, with the first argument NULL, will return pointers to each delimited token in sequence and will return NULL when none left.
while ((argvBuf[argc] = strtok(NULL, delim)) != NULL && argc < MAXARGS) {
unsigned lastchar = argvBuf[argc][strlen(argvBuf[argc])-1];
// If last character is CR or LF, delete it
if (lastchar == '\r' || lastchar == '\n') {
argvBuf[argc][strlen(argvBuf[argc])-1] = 0;
}
argc++;
}
We now have an array of pointers with each argument string pointed to in the successive entries, just like an argv argument to a main call. Since the argc integer variable has been keeping tally of the argument count, we now have main type arguments that can be used by getopt:
int c;
while ((c = getopt(argc, argvBuf, "<arg specification string>")) != EOF) {
switch (c) {
// Normal getopt parsing
}
}
return 0;
There’s no need to go into detail of the getopt parsing to update the rv32 configuration structure, but a typical case entry might look like the following:
case 'A':
cfg.brk_addr = strtol(optarg, NULL, 0);
break;
This updates the break on address value when -A is used in the configuration file and the other entries are similar to implement the command options as shown earlier.
Conclusions
In this blog we looked at how we can have command line options for co-simulated programs just as for an ordinary program as passed into the main function. Since VUserMain<n> entry points don’t have argc and argv parameters, an alternative method was required. By having a configuration file, vusermain.cfg, that can have multiple entries, one for each node, with a command line of the form that an executable would have, we can achieve the same things and set options at each run of a simulation.
We saw code that can implement this that reads the configuration file and looks for a vusermain<n> entry at the beginning of a line, where <n> matches the node number being parsed. The parameters following this were extracted into a char pointer buffer array, and the number of parameters tallied. At this point we had variables that look like argc and argv arguments passed into main, and so getopt can be used to process these, just as for command line options for an executable program.
A full example can be found in CoSim/tests/rv32demo/rv32_cosim_utils.cpp, and the parse_args method of the rv32_cosim_utils class can be called to read a configuration file and update the rv32 configuration structure, which is then passed into the run method of the rv32 ISS.
Of course, getopt does not have to be used, but the first part of the code discussed creates a pointer to a char buffer array and a count of the number of options, that is the same as if passed into main, and so any method that parses main arguments can be used from this point on.