A compiled C test sitting in a file on disk doesn't do anything until its bytes are inside the DUT's memory and its CPU has been pointed at them. Getting from "here's an ELF file" to "the CPU is executing it" is its own small piece of testbench machinery, and there's more than one way to build it.

7 min read | Intermediate Level

What You'll Learn

  • The difference between a backdoor load and a frontdoor load, and when each one is the right choice
  • What a backdoor load sequence actually does, step by step
  • What the reset vector is, and how the reset-to-fetch flow gets the CPU running from it
  • Why a load-then-release sequence is a common source of subtle, hard-to-diagnose failures

Two Ways to Get Code Into Memory

Every SoC testbench needs a way to get a compiled test's bytes into the DUT's memory before it can run. There are two fundamentally different ways to do it, and they trade speed for realism in opposite directions.

A backdoor load writes directly into the memory model through a hierarchical reference or a DPI path, bypassing the DUT's bus entirely. Nothing in the DUT - no bus, no address decode, no memory controller - has to actually work for a backdoor load to succeed, because none of it is involved.

A frontdoor load gets the same bytes into memory by driving them through the DUT's real boot path - a modeled SPI flash the boot ROM reads from, a JTAG interface, whatever mechanism real silicon actually uses to receive its first instructions. Every piece of hardware between the outside world and memory has to work correctly for a frontdoor load to succeed, because a frontdoor load is what exercises it.

Most tests should use a backdoor load. It's fast, and for a test whose purpose is checking a DMA engine or an interrupt controller, the boot mechanism itself isn't what's under test - so there's no reason to pay for exercising it, and no reason to make a DMA test also depend on the flash controller working. A frontdoor load earns its cost specifically when the boot path itself is what needs verifying: a boot ROM, a secure boot flow, or the flash controller a real product actually ships with.

How a Backdoor Load Works

A backdoor load sequence typically leans on a toolchain step first. objcopy - part of the same toolchain used to build the test - converts the ELF into a format simulation tools already know how to load:


riscv32-unknown-elf-objcopy -O verilog test.elf test.hex

The resulting test.hex is address-tagged data $readmemh can consume directly. A load sequence wraps this in a task that runs before reset is released:


class bringup_load_seq extends uvm_sequence;
  `uvm_object_utils(bringup_load_seq)

  string hex_path = "test.hex";

  virtual task body();
    // 1. Backdoor-load .text/.rodata/.data bytes into the memory model
    $readmemh(hex_path, top_tb.dut.mem_model.mem_array);

    // 2. .bss carries no bytes in the file - zero it explicitly,
    //    the same job crt0 does on real hardware
    for (int i = 'h2000; i < 'h2800; i++)
      top_tb.dut.mem_model.mem_array[i] = '0;

    // 3. Release the CPU from reset - it fetches from the reset
    //    vector the instant it comes out
    top_tb.dut.cpu_reset_n = 1'b1;
  endtask
endclass

Step 2 exists only because a backdoor load skips the normal boot path entirely - there's no startup code running yet to zero .bss itself. The sequence does that job on the C test's behalf, over the same address range a linker script would define via _bss_start/_bss_end.

The Reset Vector and the Reset-to-Fetch Flow

The reset vector is the address the CPU is guaranteed to fetch its first instruction from the moment it comes out of reset. On some cores it's hardwired in the RTL; on others it's configurable through a strap pin or a configuration register sampled at reset. Either way, it's a property of the DUT, not of the test - the linker script's entry point only matters because it's supposed to match the DUT's actual reset vector, not because it defines it.

The full sequence - the reset-to-fetch flow - is what turns a memory full of loaded bytes into a running program:

StepWhat happens
1Reset is held asserted; the CPU is not fetching anything yet
2The load sequence writes the test's bytes into memory (backdoor or frontdoor)
3Reset is released
4The CPU fetches its first instruction from the reset vector address

Step 3 only produces a working test if step 2 has actually finished, and if the reset vector address the CPU fetches from in step 4 is where valid code actually landed in step 2. Both of those depend on agreement between the load sequence and the DUT's configuration, not on either one being correct in isolation.

Releasing reset before the load sequence finishes fetches the CPU into memory that's still mid-write. The resulting failure looks exactly like a broken reset vector or a bad address map, when the real bug is a race between two unrelated pieces of the testbench.

Common Mistakes

Mistake: Using a backdoor load for a test that's specifically meant to verify the boot path.

A backdoor load proves nothing about a boot ROM, a flash controller, or a secure boot flow, because it doesn't exercise any of them. A test whose actual purpose is checking one of those needs a frontdoor load, or it isn't testing what it claims to.

Mistake: Hardcoding the reset vector address instead of reading it from the DUT's configuration.

On a testbench that supports multiple DUT configurations, a hardcoded reset vector works for whichever configuration it was written against and silently loads code to the wrong address - or the right address for the wrong core - the moment the configuration changes.

Mistake: Assuming the linker script's entry point and the DUT's reset vector will always agree.

They're two separate settings, maintained in two separate places. Nothing keeps them in sync automatically, and a mismatch between them produces a CPU that fetches successfully - just from the wrong address, executing whatever bytes happen to be there instead of the test.

Summary

In this article, you learned:

  • A backdoor load writes bytes directly into the memory model and is fast but exercises nothing in the DUT; a frontdoor load drives bytes through the real boot path and is slower but is what actually verifies that path
  • A typical backdoor load sequence converts the ELF with objcopy, uses $readmemh to load it, zero-fills .bss itself, and only then releases reset
  • The reset vector is a property of the DUT, and the reset-to-fetch flow only works when the load has finished and the reset vector matches where valid code actually landed
  • A backdoor load is the wrong tool for any test whose actual purpose is verifying the boot path itself
Did you find what you were looking for?

Quiz

No quiz questions available for this article.