Makefile vs YAML: Modernizing Verification Simulation Flows

Introduction

Automation has become the backbone of modern SystemVerilog/UVM verification environments. As designs scale from block-level modules to full SoCs, engineers rely heavily on scripts to orchestrate compilation, simulation, and regression. The effectiveness of these automation flows directly impacts verification quality, turnaround time, and team productivity.

For many years, the Makefile has been the tool of choice for managing these tasks. With its rule-based structure and wide availability, Makefiles offered a straightforward way to compile RTL, run simulations, and execute regressions. This approach served well when testbenches were relatively small and configurations were simple.

However, as verification complexity has exploded, the limitations of Makefiles have become increasingly apparent. Mixing execution rules with hardcoded test configurations leads to fragile scripts that are difficult to scale or reuse across projects. Debugging syntax-heavy Makefiles often takes more effort than writing new tests, diverting attention from coverage and functional goals.

These challenges point toward the need for a more modular and human-readable alternative. YAML, a structured configuration language, addresses many of these shortcomings when paired with Python for execution. Before diving into this solution, it is important to first examine how today’s flows operate and where they struggle.

Current Scenario and Challenges

In most verification environments today, Makefiles remain the default choice for controlling compilation, simulation, and regression. A single Makefile often governs the entire flow—compiling RTL and testbench sources, invoking the simulator with tool-specific options, and managing regressions across multiple testcases. While this approach has been serviceable for smaller projects, it shows clear limitations as complexity increases.

  • Configuration Management: Testlists are commonly hardcoded in text or CSV files, with seeds, defines, and tool flags scattered across multiple scripts. Updating or reusing these settings across projects is cumbersome.
  • Readability and Debugging: Makefile syntax is compact but cryptic, which makes debugging errors non-trivial. Even small changes can cascade into build failures, demanding significant engineering time.
  • Scalability: As testbenches grow, adding new test cases or regression suites quickly bloats the Makefile. Managing hundreds of tests or regression campaigns becomes unwieldy.
  • Tool Dependence: Each Makefile is typically tied to a specific simulator (e.g., VCS, Questa, Xcelium). Porting the flow to a different tool requires major rewrites.
  • Limited Reusability: Teams often reinvent similar flows for different projects, with little opportunity to share or reuse scripts.

These challenges shift the engineer’s focus away from verification quality and coverage goals toward the mechanics of scripting and tool debugging. As a result, the industry needs a cleaner, modular, and more portable way to manage verification flows.

Makefile-Based Flow

A traditional Makefile-based verification flow centers around a single file containing multiple targets that handle compilation, simulation, and regression tasks. Consider this representative structure:

compile:
                vcs +define+UVM_NO_DPI -timescale=1ns/1ps \
                    -sverilog +incdir+./include \
                    ./rtl/*.sv ./tb/*.sv -o simv

simulate:


                ./simv +UVM_TESTNAME=basic_test +UVM_VERBOSITY=LOW \
                       +seed=12345 +ntb_random_seed=67890

regress: compile
                @for test in basic_test advanced_test stress_test; do \
                    ./simv +UVM_TESTNAME=$$test +seed=`date +%s`; \
                done

This approach offers clear strengths: immediate familiarity to software engineers, no additional tool requirements, and straightforward dependency management. For small teams with stable tool chains, this simplicity remains compelling.

However, significant challenges emerge with scale. Cryptic syntax becomes problematic—escaped backslashes, shell expansions, and dependencies create arcane scripting rather than readable configuration. Debug cycles lengthen with cryptic error messages, and modifications require deep Make expertise.

Tool coupling is evident above—compilation flags, executable names, and runtime arguments are VCS-specific. Supporting Questa requires duplicating rules with different syntax, creating synchronization challenges.

Maintenance overhead grows exponentially. Adding tests requires multiple modifications, parameter changes demand careful shell escaping, and regression management quickly outgrows Make's capabilities, forcing hybrid scripting solutions.

These drawbacks motivate the search for a more human-readable, reusable configuration approach—which is where YAML's structured, declarative format offers compelling advantages for modern verification flows.

YAML-Based Flow

YAML (YAML Ain't Markup Language) provides a human-readable data serialization format that transforms verification flow management through structured configuration files. Unlike Makefiles' imperative commands, YAML uses declarative key-value pairs with intuitive indentation-based hierarchy.

Consider this YAML configuration structure that replaces complex Makefile logic:

# build.yml - Compilation Configuration
project:
  name: "soc_verification"
  tool: "vcs"

compile:
  files:
    - "includes.svh"
    - "design.sv"
    - "testbench.sv"
  defines:
    - "UVM_NO_DPI"
  flags:
    - "-timescale=1ns/1ps"
    - "-sverilog"
  includes:
    - "./include"

 

# sim.yml - Simulation Configuration 
simulation:
  testname: "basic_test"
  verbosity: "LOW"
  seed: 12345
  waves: true
  coverage: true
  plusargs:
    - "+UVM_TESTNAME=${testname}"
    - "+UVM_VERBOSITY=${verbosity}"

The modular structure becomes immediately apparent through organized directory hierarchies. As shown in Figure 1, a well-structured YAML-based verification environment separates configurations by function and scope, enabling different team members to modify their respective domains without conflicts. Block-level engineers manage component-specific test configurations (IP1, IP2),[GU1] [MS2]  while integration teams focus on pipeline and regression management. Instead of monolithic Makefiles, teams can organize configurations across focused files: build.yml for compilation settings, sim.yml for simulation parameters, and various test-specific YAML files grouped by functionality.

Figure 1: YAML-Based Verification Directory Structure

Advanced YAML features like anchors and aliases eliminate configuration duplication using the DRY (Don't Repeat Yourself) principle:

# Common configuration anchor
common_flags: &default_flags
  - "-timescale=1ns/1ps"
  - "-sverilog"

# Reuse across multiple builds
debug_build:
  flags:
    - *default_flags
    - "+define+DEBUG"

release_build: 
  flags:
    - *default_flags
    - "+define+SYNTHESIS"

Tool independence emerges naturally since YAML contains only configuration data, not tool-specific commands. The same YAML files can drive VCS, Questa, or XSIM simulations through appropriate Python parsing scripts, eliminating the need for multiple Makefiles per tool.

Of course, YAML alone doesn't execute simulations—it needs a bridge to EDA tools. This is achieved by pairing YAML with lightweight Python scripts that parse configurations and generate appropriate tool commands.

Implementation of YAML-Based Flow

The transition from YAML configuration to actual EDA tool execution follows a systematic four-stage process, as illustrated in Figure 2[GU3] [MS4] . This implementation addresses the traditional verification challenge where engineers spend excessive time writing complex Makefiles and managing tool commands instead of focusing on verification quality.

Figure 2: YAML to EDA Tool Bridge

YAML files serve as comprehensive configuration containers supporting diverse verification needs:

  • Project Metadata: Project name, descriptions, version control
  • Tool Configuration: EDA tool selection, licenses, version specifications
  • Compilation Settings: Source files, include directories, defines, timescale, tool-specific flags
  • Simulation Parameters: Tool flags, snapshot paths, log directory structures
  • Test Specifications: Test names, seeds, plusargs, coverage options
  • Regression Management: Test lists, reporting formats, parallel execution settings

Figure 3: Python YAML parsing workflow phases

The Python implementation demonstrates the complete flow pipeline. Starting with a simple YAML configuration:

# Build and sim YAML


project:
  name: "soc_verification"
  tool: "vcs"

compile:
  files:
    - "rtl/design.sv"
    - "tb/testbench.sv"
  defines: ["UVM_NO_DPI"]
  flags:
    - ["-timescale=1ns/1ps", "-sverilog"]


simulation:
  testname: "basic_test"
  seed: 12345

  coverage: true

The Python script loads and processes this configuration:

import yaml

 

def load_yaml_config(config_file):

    """Load and parse YAML configuration"""

    with open(config_file, 'r') as f:

        config = yaml.safe_load(f)

    return config

 

def build_compile_command(config):

    """Build compilation command from YAML config"""

    tool = config['project']['tool']

    files = ' '.join(config['compile']['files'])

    defines = ' '.join([f'+define+{d}' for d in config['compile']['defines']])

    flags = ' '.join(config['compile']['flags'])

   

    cmd = f"{tool} {flags} {defines} {files} -o simv"

    return cmd

 

def build_simulation_command(config):

    """Build simulation command from YAML config"""

    testname = config['simulation']['testname']

    seed = config['simulation']['seed']

    coverage = "+coverage" if config['simulation']['coverage'] else ""

   

    cmd = f"./simv +UVM_TESTNAME={testname} +seed={seed} {coverage}"

    return cmd

 

# Main execution flow

if __name__ == "__main__":

    config = load_yaml_config('build.yml')

    print("=== YAML to Command Translation ===")

    print(f"Project: {config['project']['name']}")

    print(f"Tool: {config['project']['tool']}")

   

    # Display compile command

    compile_cmd = build_compile_command(config)

    print("Generated Commands:")

    print(f"  {compile_cmd}")

   

    # Display simulation command

    sim_cmd = build_simulation_command(config)

    print(f"  {sim_cmd}")

When executed, the Python script produces clear output showing the command translation:

$ python yaml_flow.py

 

=== YAML to Command Translation ===

Project: soc_verification

Tool: vcs

 

Generated Commands:

  vcs -timescale=1ns/1ps -sverilog +define+UVM_NO_DPI rtl/design.sv tb/testbench.sv -o simv

 

  ./simv +UVM_TESTNAME=basic_test +seed=12345 +coverage

The complete processing workflow operates in four systematic phases, as detailed in Figure 3:

   1. Load & Parse: The PyYAML library converts YAML file content into native Python dictionaries and lists, making configuration data accessible through standard Python operations.
   2. Extract: The script accesses configuration values using dictionary keys, retrieving tool names, file lists, compilation flags, and simulation parameters from the structured data.
   3. Build Commands: The parser intelligently constructs tool-specific shell commands by combining extracted values with appropriate syntax for the target simulator (VCS or Xcelium).
   4. Display/Execute: Generated commands are shown for verification or directly executed through subprocess calls, launching the actual EDA tool operations.

This implementation creates true tool-agnostic operation. The same YAML configuration generates VCS, Questa, or XSIM commands by simply updating the tool specification. The Python translation layer handles all syntax differences, making flows portable across EDA environments without configuration changes.

The complete pipeline—from human-readable YAML to executable simulation commands—demonstrates how modern verification flows can prioritize engineering productivity over infrastructure complexity, enabling teams to focus on test quality rather than tool mechanics.

Comparison: Makefile vs YAML

Both approaches have clear strengths and weaknesses that teams should evaluate based on their specific needs and constraints. Table 1 provides a systematic comparison across key evaluation criteria.

Table 1: Makefile vs YAML Flow Comparison

Aspect

Makefile-Based Flow

YAML-Based Flow

Readability

Complex syntax with shell commands and escaping

Clean, human-readable key-value format

Maintainability

Difficult - mixed logic and configuration

Easy - clear separation of config and logic

Ease of Modification

Requires scripting knowledge for changes

Simple text edits in configuration files

Reusability

Hard to port across projects/tools

Highly reusable across different environments

Scalability

Becomes unwieldy with large test suites

Scales naturally with structured hierarchies

Tool Independence

Tool-specific rules and commands

Abstract configs work with multiple tools

Setup Complexity

No additional dependencies required

Requires Python and parsing infrastructure

Where Makefiles Work Better:

  • Simple projects with stable, unchanging requirements
  • Small teams already familiar with Make syntax
  • Legacy environments where changing infrastructure is risky
  • Direct execution needs for quick debugging without intermediate layers
  • Incremental builds where dependency tracking is crucial

Where YAML Excels:

  • Growing complexity with multiple test configurations
  • Multi-tool environments supporting different simulators
  • Team collaboration where readability matters
  • Frequent modifications to test parameters and configurations
  • Long-term maintenance across multiple projects

The Reality: Most teams start with Makefiles for simplicity but eventually hit scalability walls. YAML approaches require more initial setup but pay dividends as projects grow. The decision often comes down to whether you're optimizing for immediate simplicity or long-term scalability.

For established teams managing complex verification environments, YAML-based flows typically provide better returns on investment. However, teams should consider practical factors like migration effort and existing tool integration before making the transition.

Conclusion

The challenges with traditional Makefile flows are clear: cryptic syntax that's hard to read and modify, tool-specific configurations that don't port between projects, and maintenance overhead that grows with complexity. As verification environments become more sophisticated, these limitations consume valuable engineering time that should focus on actual test development and coverage goals.

YAM_L-based flows address these fundamental issues through human-readable configurations, tool-independent designs, and modular structures that scale naturally. Teams can describe verification intent simply—"run 100 iterations with coverage"—while the flow engine handles all tool complexity automatically. The same approach works from block-level testing to full-chip regression suites.

Key Benefits Realized:

  • Faster onboarding: New team members understand YAML configurations immediately
  • Reduced maintenance: Configuration changes require simple text edits, not scripting
  • Better collaboration: Clear syntax eliminates the "Makefile expert" bottleneck
  • Tool flexibility: Switch between VCS, Questa, or XSIM without rewriting flows
  • Project portability: YAML configurations move cleanly between different projects

The choice between Makefile and YAML approaches ultimately depends on project complexity and team goals. Simple, stable projects may continue benefiting from Makefile simplicity. However, teams managing growing test suites, multiple tools, or frequent configuration changes will find YAML-based flows provide better long-term returns on their infrastructure investment.

blog__img