For me, this is a very exciting post because I think I’ve made some pretty important headway regarding TDD for hardware designers.
My big side project as of late has been a real pilot dedicated to using TDD to write RTL. I’ve blogged about some of the things I’ve learned already but the big eye-opener that I haven’t talked about yet is how TDD helps us with design partitioning and testability.
The design I’m working on is a video processing addition to the Agile2014 demo I built with Soheil a few months ago. To recap: the demo demonstrates how TDD can be used to write embedded applications, firmware and RTL. The good news is that we were successful on the application and firmware. Our RTL, however, was trivial so it failed the proof-of-concept litmus test miserably. This new video processing block I’m working on now is meant to change that.
I blogged about the design I started a couple months ago. Originally, my hand-drawn design sketch looked like this…
In the sketch, you’ll see some ingress logic receiving 1080×1920 HDMI video frames from a streaming AXI4 interface and writing them to a memory. In the middle, there’s a cloud of video processing logic that adds a glow around live cells in our conway’s game-of-life application. Finally, there’s some egress logic that pulls modified frames out of memory and passes them down the line via another streaming AXI4.
When I started, I envisioned all this logic in a single module because it’s neither large nor super complicated. But 2 minutes in I found that in order to write focused unit tests, I’d need access to a lot of the internals which meant I’d either have to a) probe internals by accessing them hierarchically or b) add a lot of new outputs to pull out everything I was interested in.
I didn’t like the idea of probing internals because I’d be risking very tight coupling between my design and unit tests. Even minor changes to the design would also ripple through my test suite and turn into a potential maintenance nightmare. I also didn’t like the idea of creating new outputs for the signals I needed access to because it didn’t seem right to needlessly add outputs solely for the sake of testing. Finding neither choice acceptable, the idea of a single module design broke down immediately.
It was at this point, 2 minutes in, that I started thinking “what would this design look like if it were software?”. If it were software, I figured, I wouldn’t have a single function to do everything. Because there are 3 distinct characteristics in this design (ingress, processing and egress), I’d also have 3 distinct functions. This line of thinking lead to my first partitioning decision: instead of 1 module for everything, I’d have 3 separate modules each with a singular purpose.
By splitting 1 module into 3, the critical nets I required access to became outputs naturally and the interconnections between the 3 modules offered the visibility I needed to write focused unit tests. That was benefit number 1 (aka: the expected benefit). Benefit number 2 however (aka: the unexpected benefit) was that I had also isolated the ingress, processing and egress logic from each other. Coming back to the software analogy, I now had 3 separate functions and unfettered access to all the relevant input and output arguments such that I could build and test each independently.
Major (major) bonus.
I started with the ingress module, verifying that it properly wrote streaming AXI4 frames from the input through to the memory. Also that it would properly signal fill thresholds. I then tested the inverse function for the egress; that the egress module would properly pull frames from memory and send them out the streaming AXI4. When both were done, I put them together and wrote a simple acceptance test to verify they worked when connected. Worked like a charm (save for an issue with how the fill thresholds were created which was easy to clear up with my acceptance test). Referring back to my original design diagram, that acceptance test verified a step 1 milestone of basic memory/ingress/egress interaction.
My first increment was done. The big lesson learned: TDD helps us think of hardware logic as a series of software functions that can be isolated, then built and tested independently.
Step 2, and the addition of the video processing module, would confirm this lesson. A few tests in, I realized the video processing module that I thought performed 1 function actually performed 2. The first function pulls blocks of pixels from memory and organizes them. The 2nd function modifies pixels and writes them back to memory. I split the processing module into 2 and built and tested them independently.
If you’re counting along, this means my original design of 1 untestable module is now 4 independent modules (ingress, egress, proc and calc), all of which are relatively small and highly testable. Here’s a sketch of the updated design diagram…
This idea of isolating hardware logic into single purpose modules in the same way that software developers partition a design between single purpose functions is the most important thing I’ve learned from this RTL proof-of-concept exercise. In my experience, most designers are pretty good at partitioning designs into logical chunks that make sense but I think focusing on testability of each chunk takes things a step further to a finer granularity than people are used to now. The immediate benefit, as I’ve seen, is high quality RTL that works as intended… even when a verification engineer is writing it :).
I think more small modules, each serving a single purpose that can be isolated and tested independently, is absolutely the key to enabling TDD of hardware. After some initial hiccups and learning this key ingredient, the TDD cycle became very natural just as it did when I started using it to build verification IP… even though it’s hardware. In terms of mechanics, each of the steps looks very similar to what’s done in software: write a test, watch the test fail, write the RTL, watch the test pass.
I’ve finally seen the TDD cycle in action on a non-trivial design… and it certainly appears to work very well.