E6 Simple Runtime Environment

You have already implemented the minirv NPC in RTL, but it can only run some very simple programs. To run more complex programs, we need to compile programs that can run on minirv. For this purpose, we provide a project named abstract-machine, or AM for short, for building programs. This project provides a bare-metal runtime environment, offering the necessary support for programs to run directly on the processor, that is, in a bare-metal environment.

Experience AM

Let's try a classic game first. Before that, you need to install python:

apt install python python-is-python3

Run an NES Game

Read the beginning of Before embarking on an enjoyable PA journey -> What is NEMU? in PA1, follow the instructions in the handout to try running an NES game, and complete the checks for graphics, keyboard input, and sound.

You might be wondering how an NES game actually runs. In fact, the game is also an instruction sequence, but its instructions belong to an instruction set called 6502. This instruction set came out in the 1980s and is rarely used today; the corresponding processors are also not that common anymore. But just now, you did not run the game on a real 6502 processor. Then how exactly did the game run?

What happened just now was that a 6502 instruction set emulator program, fceux, was first executed. This program behaves in a very special way: it can use software behavior to simulate the execution process of 6502 instructions, thereby simulating the execution process of the entire game! Therefore, in essence, the working process of fceux is the same as that of the sEMU and minirvEMU you developed earlier!

In fact, AM is designed in a very clever way: by providing different ARCH parameters, we can compile the same program to different platforms. For example, ARCH=native means compiling the program to run locally on Linux. We can also compile fceux to minirv, and thus run an NES game on the minirv NPC you designed!

However, in the AM project, the runtime environment related to minirv is not yet complete. Let's improve it first. For now, you only need to follow the operations below to compile programs; you do not need to understand the details inside AM yet. We will explain AM in more detail in Stage D.

Run the First Compiled Program

We will first compile and run a simple C program. This simple C program is on am-kernels/tests/cpu-tests/tests/dummy.c. After reading its code, you will find that it does nothing and returns immediately.

To compile this dummy program, you first need to install the cross-compilation toolchain:

apt install g++-riscv64-linux-gnu

Then try compiling it under the am-kernels/tests/cpu-tests/ directory:

make ARCH=riscv32-nemu ALL=dummy

If a compilation error occurs, you need to refer to RTFM -> Run the first C program -> Fix riscv32 compilation errors in PA2, try to fix the related issues, and then enter the make command above again to compile. If compilation succeeds, you can use the ls build/ command to see the following three files:

dummy-riscv32-nemu.bin
dummy-riscv32-nemu.elf
dummy-riscv32-nemu.txt

The operations above are only for confirming that the RISC-V toolchain can run successfully. Now let's compile the dummy program to minirv. First, clean the build results generated just now:

make clean

Then enter the following command:

make ARCH=minirv-npc ALL=dummy

If compilation succeeds, you can use the ls build/ command to see the following three files:

dummy-minirv-npc.bin
dummy-minirv-npc.elf
dummy-minirv-npc.txt

Among them:

  • dummy-minirv-npc.bin is the binary representation of the program.
  • dummy-minirv-npc.txt is the disassembly result of the program, from which you can view the assembly representation of the instructions. We mentioned just now that the dummy program is essentially an empty function. But if you read the disassembly result, you will find that the compiled program also contains many other instructions. In fact, these instructions are all part of the runtime environment and are used to support program execution.
  • dummy-minirv-npc.elf is the ELF form of the program. You do not need to care about it for now.

Before running a program on NPC, we used to manually write the instruction sequence of the program into NPC's memory array as initialization. As the compiled programs become more and more complex, manually initializing the memory array will become increasingly inefficient. To improve the efficiency of loading programs into the simulation environment, we can let the simulation environment read the binary file of a program into NPC's memory array through a file.

Different from the programs previously run on NPC, the minirv-npc runtime environment in AM assumes that programs start from 0x80000000. Although this is not mandatory, RISC-V processors usually reserve the address space below 0x80000000 for devices. We will continue to discuss devices later. In short, to run the dummy program on the minirv NPC, you also need to:

  1. Modify the reset value of NPC's PC to 0x80000000.
  2. Modify pmem_read() and pmem_write() so that they access the memory array according to the offset between the parameter addr and 0x80000000.

Run the dummy Program on the minirv NPC

Modify NPC and the simulation environment according to the requirements above, let the simulation environment load the dummy program into memory, and then let NPC start executing the dummy program. We recommend changing the memory array in the simulation environment to 128MB. This size is large enough to hold various test programs in the future.

If your implementation is correct, the dummy program will fall into an infinite loop near the halt() function.

Build a Flow for Batch Running Programs

Soon you will need to run many programs to test NPC more thoroughly. If you have to manually specify the program to run every time and manually judge whether the result is correct, the process will be very inefficient. Therefore, you need to build a flow that supports automatically running programs in batches, so that you can run various programs efficiently.

We previously agreed that a program ends after executing the ebreak instruction. However, programs compiled on AM do not contain the ebreak instruction, and manually inserting an ebreak instruction into every program in the simulation environment is inefficient. In fact, we can insert this ebreak instruction into the minirv runtime environment. After doing so, all programs compiled to minirv through AM will automatically contain the ebreak instruction.

But the halt() function in AM is C code. To insert the instruction we want into it, we can use an inline assembly statementopen in new window:

--- abstract-machine/am/src/riscv/npc/trm.c
+++ abstract-machine/am/src/riscv/npc/trm.c
@@ -17,3 +17,4 @@
 void halt(int code) {
+  asm volatile("ebreak");
   while (1);
 }

Compile a Program Containing the ebreak Instruction

After modifying the code, recompile and run the dummy program. If your implementation is correct, the dummy program will terminate automatically instead of falling into an infinite loop.

Next, let's consider how to automatically determine whether the program has executed correctly. For this purpose, we define the following convention: before a program terminates, it first writes an integer representing the termination status into the a0 register, and then executes the ebreak instruction. If this integer is 0, it means the program terminated correctly; if it is not 0, it means an error occurred in the program.

To implement this convention, on the one hand, we need to modify the instruction sequence at program termination to ensure that the a0 register holds the program's termination status before the ebreak instruction is executed. We only need to slightly modify the inline assembly instruction above: asm volatile("mv a0, %0; ebreak" : :"r"(code));. After this modification, before executing the ebreak instruction, the inline assembly first moves the value of the variable code, which represents the program's termination status, into the a0 register.

On the other hand, after NPC executes ebreak, the simulation environment can check the value of the a0 register to see whether the program ended successfully. For example, if a0 is 0, it outputs HIT GOOD TRAP, indicating that the program ended successfully; if a0 is not 0, it outputs HIT BAD TRAP, indicating that an error occurred in the program.

Let the Simulation Environment Output Program Termination Information

Modify the inline assembly statement and the simulation environment according to the requirements above.

After modifying the code, recompile and run the dummy program. You will see the simulation environment output HIT GOOD TRAP. Try compiling and running the wrong program in the same directory, and you will see the simulation environment output HIT BAD TRAP.

To run the wrong program, you may need to manually change the program file path specified in the simulation environment. If you need to switch programs frequently, manually changing the file path is very inefficient. For this reason, we can pass the program file path as a command-line argument when starting the simulation. From then on, by providing different file paths through command-line arguments, we can run different programs during simulation.

Furthermore, we can connect the command that starts the simulation with the Makefile, so that running a command such as make run will automatically perform simulation. Specifically, you need to implement a run rule in abstract-machine/scripts/platform/npc.mk, so that after entering make ARCH=minirv-npc ALL=dummy run, the dummy program can be compiled and run on NPC automatically. In addition, as long as you change ALL=dummy to ALL=wrong, you can compile and run the wrong program. Next, you will repeatedly run various programs to test whether your NPC remains correct after new features are added. This ability to run programs in batches can greatly improve testing efficiency.

Let the run Rule Automatically Perform Simulation

Add the related code according to the requirements above, so that the Makefile can start simulation through the run rule.

Hint: In the Makefile, you can use $(IMAGE).bin to refer to the path of the generated binary file.

However, when running the wrong program with make ARCH=native ALL=wrong run, the result shows FAIL; but when running the wrong program with make ARCH=minirv-npc ALL=wrong run, the result instead shows PASS. According to the convention for program termination status mentioned above, the wrong program should always terminate with an error status, so showing PASS after running on minirv-npc is not expected.

In fact, this happens because when the simulation environment finds that the program executes the ebreak instruction with a0 not equal to 0, it does not return the information that "the program ran incorrectly" to the Makefile that started the simulation. As a result, the Makefile thinks the simulation result is correct and therefore shows PASS. To fix this issue, we need to let the simulation environment return different values from the main() function according to the program's execution result: return 0 when the program runs successfully, and return a non-zero value when the program runs incorrectly.

Let the Makefile Know Whether the Simulation Result Is Correct

Modify the code of the simulation environment so that it returns different values according to the program's execution result. After the modification, run make ARCH=minirv-npc ALL=wrong run again. You should see the result show FAIL.

Run More Programs

After building the batch-running flow, we can use it to conveniently run more programs and test your NPC.

First, riscv-tests:

cd ysyx-workbench
bash init.sh riscv-tests
cd riscv-tests
make ARCH=minirv-npc run TEST_ISA=i # run all RV32I tests
make ARCH=minirv-npc run ALL=addi   # run only the addi test

Although the tests above are related to RV32I, and the tested targets include many instructions that do not belong to minirv, AM's build process guarantees that the final generated binary files contain only minirv instructions. Therefore, riscv-tests can also be used for the current minirv NPC.

Run riscv-tests

Run riscv-tests to test whether your NPC is correct.

Compared with the programs you ran before, these test programs are larger in scale. When a test fails, you will find that debugging becomes more difficult. To deal with this problem, we can add the DiffTest mechanism introduced earlier to the simulation environment.

Add the DiffTest Mechanism

Add the DiffTest mechanism, and compare NPC against minirvEMU so that it can help you quickly locate the incorrect instruction. When DiffTest finds that the execution result of an instruction is different, you need to let the simulation environment return a non-zero value, so that the Makefile can catch the error reported by DiffTest.

After implementation, try injecting some errors into NPC and observe whether DiffTest and the Makefile report errors as expected.

Next are the cpu-tests. However, some tests in it depend on the implementation of klib library functions. For now, we do not require you to implement these library functions. We have prepared a precompiled klib libraryopen in new window. After downloading and extracting it, place klib-minirv-npc.a into the abstract-machine/klib/build/ directory. If this directory does not exist, you need to create it manually first; if the file already exists, you need to overwrite it.

Finally, you also need to update its modification time to prevent make from considering it out of date and overwriting it:

cd abstract-machine/klib/build/
touch klib-minirv-npc.a

After preparing klib, you can compile and run cpu-tests:

cd am-kernels/tests/cpu-tests
make ARCH=minirv-npc run          # run all tests
make ARCH=minirv-npc run ALL=fib  # run only the fib test

Run cpu-tests

Run cpu-tests to test whether your NPC is correct.

Add UART Support for Character Output

In Stage F, you have learned that users interact with the processor through input and output devices. In Stage F, sISA provides a dedicated io instruction for input and output. In RISC-V, however, input and output are performed through "Memory-mapped I/O". Specifically, RISC-V load and store instructions can access either memory or external devices. Which one is accessed is determined by the address range. minirv also adopts this approach.

For example, suppose address 0x10000000 corresponds to the output data register of a serial port. Then the following instruction sequence can output the character A through the serial port:

lui t0, 0x10000    # t0 = 0x10000000
addi t1, zero, 65  # 65 is the ASCII code of 'A'
sb t1, 0(t0)

As you can see, the instruction sequence above is essentially no different from "writing the character A into memory". In C code, we can use a pointer to implement similar functionality:

void putch(char c) {
  volatile char *p = (volatile char *)0x10000000ul;
  *p = c;
}

Here, volatile is a keyword in C. It tells the compiler not to optimize accesses through the corresponding pointer. Optimizing accesses to external devices usually leads to unexpected problems. We will discuss more details about volatile in later stages. For now, you only need to understand that when accessing external devices, you should add the volatile keyword in C code.

To allow programs to use the serial port for output, we also need to add a serial port to NPC. For now, however, we only need to add a simple behavior model of the serial port in the simulation environment:

extern "C" void pmem_write(int waddr, int wdata, char wmask) {
  if (waddr == 0x10000000) {  // write to UART
    fputc(wdata & 0xff, stderr);   // defined in stdio.h
    return;
  }
  // write to the memory array
}

This behavior model is simple. During a memory write operation, if the target address is 0x10000000, it outputs the character represented by the low 8 bits of wdata through fputc(), thereby imitating UART output behavior. This simple behavior model helps you understand the essence of memory-mapped I/O. We will introduce a real serial port controller when connecting to the SoC.

Add a UART Behavior Model

Implement the putch() function in abstract-machine/am/src/riscv/npc/trm.c, then add the UART behavior model in the simulation environment, and then run the hello program:

cd am-kernels/kernels/hello
make ARCH=minirv-npc run

You can first run it on native to see the expected output.

For DiffTest, you can refer to the approach above: identify store instructions that write to the serial port in minirvEMU, and then ignore them.

With UART, we can run some programs that need to output characters.

Run benchmark

Try running the benchmarks under the am-kernels/benchmarks/ directory. You can first run them on native to see the expected output. Since minirv is not very fast, we recommend shortening the simulation time as follows:

  • For dhrystone and coremark, you can modify the code and change the number of iterations to 2.
  • For microbench, specify a small-scale input through mainargs=test.
make ARCH=minirv-npc run mainargs=test

Since we cannot measure time in NPC yet, you can ignore the time or score printed by the programs for now.

We can even run the LLaMa on NPC! We have ported LLaMa to another benchmark suite. You need to obtain the related code first:

cd ysyx-workbench
bash init.sh archbench

Then you can first run it on native:

cd archbench/bench/202.llama2
make ARCH=native run mainargs=test

archbench provides four input scales, from small to large: test, train, ref, and huge. For the LLaMa program, the test scale uses a very small model file with very low accuracy, so it is hard for it to output a logically coherent sentence; it only outputs one word, Once. You can try other input scales: the larger the input scale, the larger the model file used by LLaMa, and the more logical the generated sentence will be. But the running time will also be longer. Since processor simulation is several orders of magnitude slower than running on real hardware, running the test and train scales on NPC is usually enough. The test scale is mainly used to check functional correctness, while the train scale is mainly used for some performance evaluation. We will continue to discuss performance evaluation in Stage B.

Run the LLaMa

Try running the LLaMa on NPC and check whether the result is correct. If you are patient, you can try the train scale, which takes about 5 minutes to finish.

Run benchmark (2)

Run the other programs in archbench and check whether the results are correct. archbench provides a script for parallel execution. You need to install some tools first:

apt install time parallel python3-numpy

Then run the script:

cd archbench/scripts
bash run.sh ARCH=minirv-npc mainargs=test

When using a real UART, the program needs to check whether the transmitter is ready before sending a character. If a character is sent before the transmitter is ready, the character may be lost. Whether the UART is ready is queried by reading the UART status register. Similarly, we can add the corresponding functionality to the UART behavior model:

extern "C" int pmem_read(int raddr) {
  if (raddr == 0x10000004) {  // read UART status
    return (rand() & 0x7) == 0 ? 1 : 0; // ready probability is 12.5%
  }
  // read the memory array
}

The behavior model above specifies that the UART status can be read from address 0x10000004. The behavior of the status register is simulated by random numbers: there is a 12.5% probability of reading 1, meaning ready.

Add a Status Register for UART

According to the description above, add a status register to the UART behavior model. Then modify the putch() function so that it queries the UART status before outputting a character. If the status is ready, output the character and return; if the status is not ready, query again until it becomes ready.

In particular, for DiffTest, you also need to identify load instructions used to query the UART status and copy the status value read into minirvEMU, so that NPC and minirvEMU read the same data after executing the same load instruction.

Add Timer Support for Timing

Similar to UART, we can also add a simple timer behavior model in the simulation environment:

unsigned long long get_time() {
  // return the time elapsed since simulation started, in microseconds
  // can be implemented with gettimeofday()
  return 0;
}

extern "C" int pmem_read(int raddr) {
  if (raddr == 0x10000004) { /* ... */ } // read UART status
  // read the low 32 bits of the timer
  else if (raddr == 0x20000000) { return get_time() & 0xffffffff; }
  // read the high 32 bits of the timer
  else if (raddr == 0x20000004) { return get_time() >> 32; }
  // read the memory array
}

Add a Timer

According to the description above, add the timer behavior model in the simulation environment. Then implement the __am_timer_uptime() function in abstract-machine/am/src/riscv/npc/timer.c, and return the number of microseconds by accessing the timer behavior model.

After implementation, try running the timer test:

cd am-kernels/tests/am-tests
make ARCH=minirv-npc run mainargs=t

If your implementation is correct, you will see the program output a line every second.

For DiffTest, you can handle the load instructions that access the timer behavior model in a way similar to the UART status register.

Run benchmark (3)

Run the previous benchmarks again. You will see the programs output meaningful time or scores.

Run an NES Game

Now you can also try running an NES game on NPC! However, since NPC is not connected to a keyboard or display yet, we can only watch the screen changes in character mode. Specifically, modify the FCEUX configuration as follows:

--- fceux-am/src/config.h
+++ fceux-am/src/config.h
@@ -1,5 +1,5 @@
 #ifndef __CONFIG_H__
 #define __CONFIG_H__

-#define HAS_GUI
+//#define HAS_GUI
 #define SIZE_OPT

After the modification, compile and run it on NPC. You can reduce the terminal font size to get a better visual effect.

At present, the efficiency of running programs on NPC in the simulation environment is not high. Even if a keyboard is connected, the game experience will not be good. Therefore, for now, we treat the NES game as a functional test for NPC. We will improve the NES game experience in later stages.

Performance Evaluation: Getting Started

Evaluate NPC Performance

Try running the train-scale program in archbench through the script. The script will automatically calculate the score of each test, as well as their arithmetic mean and geometric mean. The geometric mean is usually used as the final result for performance evaluation.

In the future, we will use the train scale of archbench as the default test programs for performance evaluation. You can organize the script results into a table to observe the quantitative impact of system changes on performance evaluation results.

Can the Running Result of the test Scale Be Used as a Performance Evaluation Result?

Usually, a computer's job is to repeatedly process tasks. In this sense, a computer's performance is its performance when repeatedly processing these tasks under a certain workload. Therefore, each program used for performance evaluation should have some representativeness: it should represent tasks that the computer may need to repeatedly process in the future. Performance evaluation is essentially testing the performance of the target system under these representative scenarios.

However, the test scale is usually intended for quickly testing functional correctness. The corresponding input scale is usually very small, the number of loop iterations is also small, and the workload is weak. Therefore, from the perspective of performance evaluation, it is not representative.

Time Elapsing Inside NPC

If you think carefully, you will find that although the programs now output scores, these scores are not meaningful references. This is because we expect benchmarks to evaluate NPC's performance, and therefore timing should use the clock elapsing inside NPC. However, gettimeofday() returns the host system time, which reflects the time elapsed in the real world.

To fix this problem, you can modify the implementation of the get_time() function described above. Specifically, you need to record how many cycles have been simulated in the simulation environment, and then convert the cycle count into microseconds according to NPC's frequency. However, NPC's frequency is related to the timing behavior of the circuit, and it can be obtained through evaluation by EDA tools. For simplicity, you can assume for now that NPC runs at 100MHz.

Evaluate NPC Performance (2)

Modify the implementation of the get_time() function, and then run archbench again. At this point, the scores you see will represent NPC's performance when running at 100MHz.

Evaluate NPC Performance (3)

Assume that NPC runs at 1000MHz. Modify the implementation of the get_time() function, and then run archbench again. What score do you expect to see? Try comparing your expectation with the actual output to verify whether your idea is correct.

Obtain NPC's Frequency Through EDA Tools

The frequency used for the NPC earlier was just an assumption. In practice, we can use EDA tools to evaluate the maximum operating frequency of the NPC.

We will use the ECC (ECOS Chip Compiler)open in new window toolchain for synthesis. You need to obtain the following tools:

  1. ECC v0.1.0-alpha.8open in new window. After clicking the link, find the Assets list at the bottom of the page and download ecc-cli-linux-x86_64.tar.gz. After downloading, extract it:

    mkdir ecc
    cd ecc
    mv path-to-ecc-cli-linux-x86_64.tar.gz .
    tar xvf ecc-cli-linux-x86_64.tar.gz
    
  2. ICsprout55 PDKopen in new window. You can obtain it with the following commands:

    git clone -b unconfirmed git@github.com:openecos-projects/icsprout55-pdk
    cd icsprout55-pdk/patch
    bash patch.sh
    
  3. Open-Source CAD Tool Suiteopen in new window. After clicking the link, find the Assets list at the bottom of the page and download the appropriate package. After extracting it, add path-to-oss-cad-suite/bin to the environment variable PATH.

    This suite contains the open-source RTL synthesis tool Yosysopen in new window, the SystemVerilog plugins used by Yosys, and some other tools that will be used in later stages.

We will first use ECC to evaluate the previous running-light example:

cd ecc
./ecc init light

Create the file ecc/light/rtl/light.v and copy the running-light code from the previous example into it. Then edit ecc/light/ecc.toml and change it to the following:

[design]
name = "light"
top = "light"
rtl = ["rtl/light.v"]
clock_port = "clk"
frequency_mhz = 100.0

[pdk]
name = "ics55"
root = "path-to-icsprout55-pdk"

[flow]
# preset: rtl2gds | rcx | harden | syn_sta
preset = "syn_sta"
run = "default"

[pdk.overrides]
libs = [
  "path-to-icsprout55-pdk/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CH/liberty/ics55_LLSC_H7CH_typ_tt_1p2_25_nldm.lib",
  "path-to-icsprout55-pdk/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/liberty/ics55_LLSC_H7CR_typ_tt_1p2_25_nldm.lib",
  "path-to-icsprout55-pdk/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/liberty/ics55_LLSC_H7CL_typ_tt_1p2_25_nldm.lib",
]
lefs = [
  "path-to-icsprout55-pdk/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CH/lef/ics55_LLSC_H7CH_ecos.lef",
  "path-to-icsprout55-pdk/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CR/lef/ics55_LLSC_H7CR_ecos.lef",
  "path-to-icsprout55-pdk/IP/STD_cell/ics55_LLSC_H7C_V1p10C100/ics55_LLSC_H7CL/lef/ics55_LLSC_H7CL_ecos.lef",
]
dont_use = [
  "*AO222*",
  "*2BB2*",
  "*AOI222*",
  "*AOI33*",
  "*OA222*",
  "*OAI222*",
  "*OAI33*",
  "*NOR4*",
]

The changes are explained below:

  • Replace path-to-icsprout55-pdk with the absolute path to the ICsprout55 PDK.

  • Set the root property in the [pdk] section to the path of the ICsprout55 PDK.

  • Change the preset property in the [flow] section to syn_sta, which means that only synthesis and timing analysis will be performed.

  • In the [pdk.overrides] section:

    • The libs property lists the .lib files to be used. Here, the tt process corner is selected.
    • The lefs property lists the .lef files to be used.
    • The dont_use property lists the standard cells that are disabled. These cells are disabled because they have a relatively large number of ports, which would put additional pressure on the routing stage of the back-end flow.

Once the configuration is complete, you can use ECC to synthesize and evaluate the running-light design:

cd ecc
./ecc run --project light

ECC will invoke Yosys to synthesize the RTL code and map the synthesis results to the ICsprout55 PDK.

After synthesis is complete, you can inspect the following files to obtain the synthesis results:

  • light/runs/default/Synthesis_yosys/report/post_synthesis/qor_summary.rpt — Summary report. It contains information such as frequency and area. The area is given in the CELLA column, in units of .
  • light/runs/default/Synthesis_yosys/report/post_synthesis/power.rpt — Power report.
  • light/runs/default/Synthesis_yosys/log/Synthesis.log — Synthesis log.
  • light/runs/default/Synthesis_yosys/output/light_Synthesis.v.gz — Netlist generated by synthesis.
  • light/runs/default/Synthesis_yosys/output/light_Synthesis_sim.v.gz — Netlist intended for gate-level simulation.

The synthesis script used by the flow is located at:

ecc/_internal/chipcompiler/tools/yosys/scripts/yosys_synthesis.tcl

The synthesis script contains multiple synthesis strategies, which are divided into three optimization objectives: delay optimization (DELAY), area optimization (AREA), and balanced delay-and-area optimization (BALANCE). Each optimization objective contains several specific strategies. A particular strategy is selected using the format optimization_objective index. For example, the synthesis flow uses the BALANCE 3 strategy by default. You can verify which strategy was used by checking the information printed at the beginning of the synthesis log. The number of strategies available under each optimization objective can be determined by simply inspecting the synthesis script mentioned above. You can also use the YOSYS_SYNTH_STRATEGY environment variable to specify the synthesis strategy. For example:

YOSYS_SYNTH_STRATEGY="DELAY 0" ./ecc run --overwrite --project light

The --overwrite option forces ECC to overwrite existing synthesis results. For more information about ECC, refer to the relevant documentation on GitHubopen in new window. To synthesize the NPC, you will need to make some adjustments to the code.

First, since the memory is conceptually not part of the NPC, we do not want to synthesize the memory together with the processor. Therefore, if you currently perform DPI-C accesses to the memory inside the processor, such as in the IFU and LSU, you need to move these accesses outside the processor and expose the memory-related signals at the top level of the processor.

This allows the processor module itself to be used as the top-level module during synthesis.

In addition, ECC cannot process non-synthesizable code, including declarations and calls to DPI-C functions, $display(), and other system tasks.

You can wrap such non-synthesizable code with `ifndef SYNTHESIS and `endif. The Yosys synthesis script automatically defines the SYNTHESIS macro when reading the source code, causing these non-synthesizable sections to be skipped.

In particular, if you are using Chisel, you can use configuration variables to control whether the DPI-C-related BlackBox modules are generated.

Evaluate the NPC's synthesis frequency

After adjusting the NPC code, synthesize the NPC according to the ECC workflow described above. Then inspect the synthesis report to determine the synthesized frequency.

If your NPC project contains multiple source files, you can change the rtl property in the [design] section of ecc.toml to a file list. For example:

rtl = ["rtl/filelist.f"]

The rtl/filelist.f file can contain:

IFU.v
IDU.v
EXU.v
LSU.v
WBU.v

Evaluate NPC Performance (4)

Based on the frequency reported by the synthesis flow, modify the implementation of the get_time() function and then run archbench again. At this point, the score you obtain will represent the NPC's performance at its maximum operating frequency at the circuit level.

It is worth noting that the frequency reported by synthesis is not necessarily the final operating frequency of the chip, because it does not take the effects of the back-end physical design into account. After completing the back-end design flow, the maximum operating frequency of the chip will generally decrease further. Nevertheless, for the front-end design iteration process, the frequency reported by synthesis is still a useful reference.

yyz