Formal verification isn’t scary — you’re just using it wrong

 DESIGN VERIFICATION  ·  ARTICLE 05 OF 06

Formal verification isn’t scary — you’re just using it wrong

Most teams treat formal like a fire extinguisher — only when there’s a fire, and usually too late. Here’s how to bake it into your flow from day one.

 

Formal verification has a reputation problem.

Among DV engineers who haven’t used it, formal is the thing that requires a PhD, takes weeks to set up, and either proves your design is correct (unlikely) or times out after 24 hours (common). Among engineers who have used it badly — usually by throwing a formal tool at a large, unconstrained design and waiting for magic — the reputation is similar: powerful in theory, impractical in practice.

Both characterizations are wrong in the same way. They describe formal applied incorrectly, to the wrong problems, without the setup work that makes it useful. Formal verification, used well, is one of the few techniques in the DV toolkit that can give you a mathematical proof of correctness for a bounded set of properties. That’s not a feature of any simulation-based approach. No amount of constrained-random testing can give you that.

This article is about how to use formal well. It’s aimed at DV engineers who work primarily in simulation-based flows and want to add formal to their repertoire without a multi-month onboarding curve. We’ll cover what formal actually does, where it beats simulation decisively, how to write your first meaningful property, and how to integrate formal into a UVM-centric flow without reorganizing your entire methodology.

What formal actually does

At its core, formal verification answers a specific question: given a set of constraints on the inputs to a design, does a specified property hold for all possible input sequences, across all reachable states?

That’s it. No test vectors. No random seeds. No coverage closure. The tool exhaustively explores the state space — either proving the property holds everywhere, or finding a counterexample that violates it.

There are two primary formal engines you’ll encounter:

        Model checking (bounded and unbounded): searches for counterexamples up to a specified depth (bounded) or across all reachable states (unbounded). Unbounded proofs are expensive; bounded proofs give you “no bug exists within N cycles” — which is still far stronger than simulation coverage.

        Property checking with SAT/BDD solvers: the engine under most commercial formal tools (Cadence JasperGold, Synopsys VC Formal, Mentor Questa Formal). You write SystemVerilog Assertions (SVA), the tool converts them to a SAT problem, and either proves them or produces a waveform counterexample.

 

What formal cannot do: verify a design end-to-end at full complexity. A modern SoC has a state space that no formal tool can exhaust in any reasonable time. Formal is not a replacement for simulation. It is a complement — and a remarkably powerful one, applied to the right problems.

Formal doesn’t replace simulation. It proves the things simulation can only approximate — and it does so exhaustively, across every possible input sequence.

Where formal beats simulation, every time

The most useful thing to understand about formal is not how it works internally but where it has a decisive advantage over simulation. There are four categories where I reach for formal without hesitation:

Control logic and arbitration

Arbiter correctness — mutual exclusion, starvation-freedom, priority ordering — is extremely hard to verify by simulation. A constrained-random testbench can generate millions of arbitration scenarios and still miss the specific sequence of requests that causes a lower-priority agent to wait forever under a heavy-load condition. A formal tool, given a property that says “if agent A requests and holds its request, it must eventually receive a grant,” will either prove it across all possible request sequences or hand you the exact counterexample.

// Liveness property: a requesting agent must eventually be granted

// This is trivially hard to close with simulation alone

property arb_liveness;

  @(posedge clk) disable iff (!rst_n)

  req[0] |-> ##[1:MAX_WAIT] gnt[0];

endproperty

 

arb_liveness_chk: assert property (arb_liveness)

  else $error("Agent 0 starved: request not granted within %0d                               cycles", MAX_WAIT);

 

FIFO and buffer behavior

FIFOs are deceptively simple to get wrong. Off-by-one errors in depth calculations, incorrect full/empty flag timing, and pointer wrap-around bugs at power-of-two boundaries are all failure modes that simulation can miss for a long time — they only appear at specific fill levels that random stimulus may never reach in a short regression.

Formal, given properties about full/empty flag correctness and no-overflow/no-underflow guarantees, will explore every fill level exhaustively. The state space for a 16-entry FIFO is tractable. The proof is fast. And once proven, it’s proven — not just “we ran a lot of tests and didn’t see this fail.”

// No-overflow: write must not occur when FIFO is full

property fifo_no_overflow;

  @(posedge clk) disable iff (!rst_n)

  (fifo_full && wr_en) |-> $stable(wr_data);  // write is ignored when full

endproperty

 

// Flag correctness: full flag asserted iff count == DEPTH

property fifo_full_correct;

  @(posedge clk) disable iff (!rst_n)

  fifo_full == (count == DEPTH);

endproperty

 

// Empty flag correctness

property fifo_empty_correct;

  @(posedge clk) disable iff (!rst_n)

  fifo_empty == (count == 0);

endproperty

 

Reset and initialization sequences

Reset behavior is one of the most consistently under-verified areas of RTL design. Questions like “what state is the DUT in immediately after deasserting reset?”, “can reset be asserted mid-transaction without corrupting state?”, and “does a second reset after partial initialization produce the same result as a clean reset?” are all properties that formal handles cleanly and simulation handles poorly.

Simulation can run a reset sequence, but it can only run the reset sequences you think of generating. Formal can prove that reset produces a known-good state regardless of what was happening in the design when reset was asserted.

Protocol compliance checking

AXI, AHB, APB, CHI — bus protocols define legal signal orderings as sequences of events over time. These are naturally expressed as SVA sequences and properties. A formal tool can prove that the DUT never violates a protocol rule across all possible transaction sequences, not just the ones your testbench happened to generate.

This is especially valuable for interface logic that bridges two protocols. The bridging logic must simultaneously produce legal output on the downstream interface regardless of what it receives on the upstream one. Simulation can explore the combinations your testbench generates. Formal can prove the property holds for all of them.

Formal vs. simulation for protocol compliance

A common workflow: use formal to prove the DUT never violates protocol rules (the safety properties), and use simulation to verify that the DUT achieves its functional goals (correct data, correct ordering, correct response). The two techniques are complementary, not competing.


Writing your first meaningful property

The single biggest barrier to adopting formal is the blank page problem: where do I start? The answer is almost always the same: start with something you believe to be true about your design, that you would currently verify by simulation, and that simulation can’t prove exhaustively.

Here is a worked example for a simple write-enable register block. We’ll build up from a basic assertion to a formally meaningful property with constraints.

Step 1: Identify what you believe is true

The register block has a write-enable input. When write-enable is deasserted, no register should change value, regardless of what the data or address inputs do. This is a safety property: “registers are read-only when write-enable is low.”

// The claim: registers only change when write-enable is asserted

// We want to prove this for ALL possible addr and data inputs

property reg_stable_when_we_low;

  @(posedge clk) disable iff (!rst_n)

  !we |-> ##1 $stable(reg_file);  // all registers unchanged next cycle

endproperty

reg_stable_chk: assert property (reg_stable_when_we_low);

 

Step 2: Add assumptions to constrain the environment

A property without assumptions will be checked against all possible input sequences, including ones that could never occur in a real system. This leads to spurious counterexamples — the tool finds a violation that your protocol would never produce. You need assumptions to tell the tool what inputs are legal.

// Assumptions: what the environment is allowed to do

// These constrain the formal tool’s input space

 

// Address must be within the valid register range

assume property (@(posedge clk) addr < NUM_REGS);

 

// Write-enable must be a single-cycle pulse (per protocol)

// This prevents the tool from holding we high indefinitely

assume property (@(posedge clk)

  $rose(we) |-> ##1 !we);

 

// Now the assertion is checked only against legal input sequences

reg_stable_chk: assert property (reg_stable_when_we_low);

 

Step 3: Add a cover property to confirm reachability

One of the most common formal pitfalls: an assertion passes because the assumed constraints make it vacuously true. The property holds because the scenario it describes is unreachable. Cover properties let you check that the interesting scenario actually occurs under your assumptions.

// Cover: a write actually happens under these constraints

// If this never fires, your assumptions may be over-constraining

cov_write_occurs: cover property (

  @(posedge clk) disable iff (!rst_n)

  we && (addr < NUM_REGS)  // a write is legal and occurs

);

 

// Cover: consecutive writes to the same address

cov_back_to_back: cover property (

  @(posedge clk) disable iff (!rst_n)

  we ##1 we && ($past(addr) == addr)  // same address, two cycles

);

 

// If both cover properties are reachable, your assumptions are not

// over-constraining the interesting behavior.

 

Step 4: Understand the output

When you run the formal tool, you get one of three results for each property:

        Proven: the property holds for all reachable states within the proof depth. For bounded proofs, this means “no counterexample exists within N cycles.” For unbounded proofs, it’s a full mathematical guarantee.

        Counterexample (CEX): the tool found an input sequence that violates the property. It will give you a waveform. This is not a failure of formal — it’s exactly the output you want. Analyze the waveform, determine whether it’s a real bug or an assumption gap, and fix accordingly.

        Inconclusive / timeout: the proof didn’t complete within the resource budget. This usually means the property is too complex for the current decomposition, or the design is too large without abstraction. It’s not a result you can act on — see the section on bounded proofs below.

 

A counterexample is the tool doing its job

First-time formal users sometimes treat a CEX as a sign that formal “failed.” It didn’t. It found a case you missed. Your job is to determine whether the CEX represents a real design bug, a gap in your assumptions, or an underspecified property. All three are useful discoveries.

Integrating formal into a UVM-centric flow

The most common objection to formal from simulation engineers: “my entire testbench is UVM. I’d have to build a separate formal environment from scratch.” This is partly true and mostly unnecessary.

The practical integration approach is to treat formal as a parallel lane, not a replacement lane. Your UVM simulation environment continues to exist. You add a lightweight formal environment alongside it, targeting the specific properties that formal is better suited to prove.

The formal app model

Modern commercial formal tools support “formal applications” — pre-packaged verification tasks with their own setup and methodology. The most useful for a first-time formal user:

        Connectivity / X-propagation checks: prove that specific outputs can never go X under normal operating conditions. Zero setup beyond enabling the app against your RTL.

        Reset checking: prove that reset drives all registers to their defined initial values, and that no register retains a value across an active reset. Usually a one-day setup.

        Unreachability checks: prove that specific states or signal combinations defined as illegal in the spec can never be reached. Complements your illegal-bin functional coverage.

 

These apps are explicitly designed for engineers who are not formal specialists. They have constrained scope, fast setup, and interpretable results. Starting here — rather than with a blank SVA file — is the right entry point for most DV engineers.

Binding assertions to RTL without modifying it

A common concern: if I write SVA assertions, do I have to add them to the RTL? That would require RTL sign-off, coordination with designers, and introduces code that’s verification-only into production files.

The answer is no. SystemVerilog bind declarations let you attach an assertion module to any RTL hierarchy point without modifying the RTL itself. The assertions live in your verification environment, not in the design.

// Assertion module — lives in your tb/formal/ directory, not in RTL

module my_block_assertions (

  input logic clk,

  input logic rst_n,

  input logic we,

  input logic [ADDR_W-1:0] addr,

  input logic [DATA_W-1:0] reg_file [NUM_REGS]

);

 

  property reg_stable_when_we_low;

    @(posedge clk) disable iff (!rst_n)

    !we |-> ##1 $stable(reg_file);

  endproperty

  reg_stable_chk: assert property (reg_stable_when_we_low);

 

endmodule

 

// Bind statement — in your testbench or formal plan file

// Attaches the assertion module to the RTL instance

bind my_reg_block my_block_assertions u_assertions (

  .clk      (clk),

  .rst_n    (rst_n),

  .we       (we),

  .addr     (addr),

  .reg_file (reg_file)

);

 

// The RTL is untouched. Assertions are co-simulated in UVM

// and proven formally — same SVA, two flows.

 

Shared SVA between simulation and formal

The bind approach has a second benefit: the same assertion module runs in both your UVM simulation and your formal tool. In simulation, the assertions fire as runtime checkers and flag violations immediately. In formal, the same properties are proven exhaustively.

This means you write the SVA once. You get runtime checking in simulation for free, and you get formal proof coverage for the properties that matter most. The maintenance cost is one set of assertions, not two.

// Makefile / build script excerpt

# UVM simulation: assertions fire as runtime checkers

$ vcs -sverilog +define+BIND_ASSERTIONS \

      tb/formal/my_block_assertions.sv \

      rtl/my_reg_block.sv \

      tb/top.sv

 

# Formal: same assertions proven exhaustively

$ jg -formal \

     -define BIND_ASSERTIONS \

     tb/formal/my_block_assertions.sv \

     rtl/my_reg_block.sv \

     tb/formal/my_block_formal_plan.tcl

 

Handling the bounded proof problem

Unbounded proofs — proofs that hold for all reachable states, not just up to a cycle depth — are the gold standard. They are also computationally expensive and often infeasible for complex designs without significant abstraction effort.

Most practical formal verification for DV engineers working outside a dedicated formal team is bounded. A bounded proof says: no counterexample exists within N cycles. This is weaker than a full proof, but it’s still far stronger than simulation, and it’s achievable in a daily regression context.

The right way to use bounded proofs:

        Set the bound based on the latency of the property you’re checking. An arbitration liveness property with a 4-cycle maximum grant latency needs a bound of at least 8–12 cycles (round-trip plus pipeline depth). A FIFO overflow property needs a bound that covers at least DEPTH+2 write cycles.

        Treat an unreached bound as a scope decision, not a proof. If your tool runs for 6 hours and doesn’t find a counterexample at depth 50, you haven’t proven the property — you’ve determined that no bug manifests within 50 cycles. Document this scope explicitly.

        Use k-induction when available. K-induction extends bounded model checking to produce unbounded proofs for many practical properties by proving both the base case (no CEX in k steps) and the inductive step (if no CEX in k steps, then no CEX ever). Most commercial tools support this.

 

A bounded proof that covers the relevant latency range is not a compromise. It’s a precise, documented claim: no bug of this class exists within this scope. That’s more than most simulation campaigns can say.

Common objections, honest answers

Every time I recommend formal to a team that hasn’t used it, I hear variations of the same objections. Here they are, with honest answers.

“We don’t have a formal specialist on the team”

You don’t need one to start. The formal app model — reset checking, X-propagation, connectivity — requires no SVA expertise. Writing basic assertions for control logic or FIFO behavior requires a day of learning SVA syntax, which is a tractable investment for any experienced DV engineer. The PhD-level formal work — full unbounded proofs of complex protocols, abstraction refinement, deep equivalence checking — does require specialization. The work described in this article does not.

“The tools are too expensive”

Commercial formal tools (JasperGold, VC Formal, Questa Formal) are licensed separately and can be expensive. However, most EDA vendor agreements at companies that already use their simulation tools include some formal entitlement. Check your license before assuming it’s unavailable. Additionally, open-source tools (SymbiYosys with the SMTBMC backend) handle a meaningful subset of SVA properties and are genuinely useful for FPGAs and smaller ASIC blocks.

“Formal always times out on our design”

This is the result of applying formal to the wrong scope. If you point a formal tool at a full SoC subsystem with dozens of interfaces and expect it to prove complex end-to-end properties, it will time out. Formal works on bounded, well-defined subproblems: a single arbiter, a FIFO, a CSR block, a reset network. Scope the problem to match the tool’s capabilities, not the other way around.

“Simulation gives us enough confidence”

For many properties, this is true. For control logic, arbitration, and reset behavior — properties where the failure space is large and the failure conditions are hard to generate randomly — it is not. The question is not whether simulation is good enough on average; it’s whether simulation is good enough for the specific class of bugs that formal excels at catching. Those bugs have shipped in production silicon from teams with mature simulation-based verification flows. Formal catches them cheaply, early, and exhaustively.

Where to start this week

If you have never run a formal tool and want to start with something concrete, here is a one-week path:

        Day 1: identify a FIFO, arbiter, or CSR block in your current design that has well-defined properties. Write down three things you believe are true about it — in English first, then in SVA syntax.

        Day 2: write the SVA properties and assumptions. Use the bind approach so the RTL is untouched. Simulate with your UVM testbench and confirm the assertions don’t fire on legal stimulus.

        Day 3: run the properties through your formal tool. If you hit a counterexample, determine whether it’s a real bug, an assumption gap, or a property specification error. All three are useful.

        Day 4: add cover properties to confirm that the scenarios you care about are reachable under your assumptions. Adjust assumptions if needed.

        Day 5: document the scope and result. “No counterexample for these three properties within 20 cycles.” Add this to your sign-off evidence.

 

That’s a week of work, not a six-month project. The output is a set of properties that run in your regression, catch a class of bugs that simulation handles poorly, and produce documented proof evidence for your sign-off package.

Formal isn’t scary. It’s a power tool with a specific set of applications where it’s decisively better than anything else in the DV toolkit. Learn those applications. Apply the tool to them. The state space it exhausts in an afternoon would take your simulation regression years to approximate.

 

Next in this series:

Article 06 — What AI tools actually change about verification — and what they don’t

Comments

Popular posts from this blog

Why verification is the hardest engineering job nobody talks about

UVM anti-patterns I see in almost every new project

Coverage closure isn’t done when the number hits 100%