In this (very late) post, I attempt a look at why RTL designers should use Test Driven Development (TDD) to create each of their modules.
For this topic espeically, I’d like to hear your experiences with TDD, whether you think TDD is an appropriate methodology for RTL, and if you’ve been following along with our TDD-month(-and-a-half), whether you agree with Neil’s and my position that TDD is a great addition to our ASIC/FPGA development toolbox.
Why RTL Designers Should Use TDD
While a Verification Engineer should see the immediate benefits of using Test Driven Design, the idea of an RTL designer using it is perhaps a little more difficult to grasp. The difficulty is when you start to think about the amount of infrastructure required to create a unit test — it can be large for a reasonable sized block. Just providing the required stimulus across the tens, or sometimes hundreds of pins can seem impractical.
However, that presumes that you want to create a unit test from a fully completed RTL block. Instead the TDD methodology encourages the incremental creation of tests and functionality. In the case of RTL, this would begin with the creation of simple stimulus to test the minimal interface — ideally built as the RTL designer creates the block. Starting from a simple core with clock and reset and minimal functionality the unit test is straightforward. As the designer continues to add more features, the signals and stimulus required in the unit test build gradually and incrementally. In other words, TDD for RTL is very similar to what was described in previous posts regarding adding TDD to the verification process — you build the tests and functionality incrementally.
First, let’s go through some of the benefits of using TDD for RTL development.
Every RTL designer I have ever had the pleasure of working with has always wanted a simple testbench that would allow them to provide some simple stimulus to their block. Using TDD with a standard framework such as SVUnit, the goal is to create every module with very simple stimulus since it is at the module level, and then gradually add the extra functionality and signals required until the module is complete. The unit test will cover all possible stimulus, many of which would be difficult to provide at a block-level test consisting of several interacting modules.
As with Verification Engineers, the unit tests demonstrate the block’s interface expectations. A block that must interface with that block could potentially use the unit test’s functionality as the driver or receiver of data from the block depending on the direction of the data.
Easier SVA Adoption Curve
It’s easy to see from an RTL perspective why adding SystemVerilog Assertions into a module/block is beneficial. One issue to adoption is the complexity of building assertions on a reasonably complex block. That is, many assertions require complicated system-level test stimulus in order to activate the correct sequence of signals to trigger an assertion. Creating unit tests is a complimentary technique, and can in some cases provide some of the same value of assertions — the ability to pinpoint where logic is incorrect; identify conditions where the underlying assumptions are thoroughly tested. It is a complimentary in that the stimulus provided starts out simple and grows incrementally more complex — the checking of the DUTs responses can be done in either the SystemVerilog functionality in the unit test, or with an SV Assertion. Any assertions created also begin very simply and can grow gradually more complex. Again, building both confidence in using writing assertions, and confidence that any new assertion builds on previously validated assertions.
TDD for RTL Workflow
How would I create an RTL block using TDD? Here are my thoughts; I’d welcome your opinions.
Let’s assume that we’re creating a simple round-robin arbiter block. Briefly, the design has a simple interface:
- Clock and reset,
- Input request signal (one bit per client),
- Output grant signal (one bit per client). ≈
- Round-robin arbitration across all asserted requests. One per clock cycle.
As discussed in previous posts, the typical TDD workflow creates a unit test first; by design this test fails because the associated functionality does not exist. The first unit test can be as simple as instantiating an empty module with pins for clock and reset and then hooking up the clock and reset generation logic to these pings. The ‘test’ is defined to succeed when the reset signal in the block is deasserted at the same time as the external reset generator is deasserted. Following the TDD flow, you’d attempt to compile this unit test and it should fail spectacularly as the block does not even exist.
The RTL designer creates the block with the clock and reset signals to make the reset test pass. The test passes when the internal block comes out of reset. A simple assertion could be used to test this condition.
The next test to create is the resetting of the internal pointers that maintain the updating of the request and grant logic, and the current client state. An assertion to check these values after reset should be sufficient. Again, the test fails as the logic to reset these values does not exist. Add the logic until the assertion no longer fires and the test passes.
As you can see you gradually build the functionality of an arbiter e.g., adding the independent requests and generation of the grants, keeping state of which client has been served last, meanwhile adding increasingly meaningful assertions and functional checks into the unit test as you continuously add the functional components.
Of course, most modules are more complicated than a round-robin arbiter, but my intent is to show that TDD encourages an incremental approach to design. Each incremental step building on the strength of knowledge that the previous step is solidly grounded in working code. Each incremental test and assertion with a corresponding functional addition contributes to increasing confidence that the code is correct by construction. Truly the whole (unit test + module) is greater than the sum of its parts.
You’re also secure in the knowledge that if the interface changes with a dependent block, a simple run of these unit tests confirms whether your block has been affected by the change. Similarly, any additional functionality that is added later in the design cycle, you’ll have a solid foundation from which to build.
My goal here is to demonstrate how one could create an RTL module using TDD, with the end-result of a working module, and a set of unit tests that ensure the block works as expected.
If you’ve been using TDD-like ideas in your RTL development, please share them.
The Last Post… on TDD… for now
I’ll be pushing out my last post on the TDD topic in a couple of days. This will define my ideas on some possible future direction for SVUnit. Neil has pushed the source code into GitHub and we’re looking for some early adopters to test it out before a general release. Neil also posted a video demonstrating the svunit in action. If you’re keen and serious to take svunit for a spin, please let Neil or I know.