Long time has passed since my last article. I've taken some time off with my daughter and been occupied with a low-power ASIC project. However, here I am again, back in the flesh. So, let's talk abstraction this time!
Abstraction, what's up now? you might think.
I often run into the discussion about abstraction with collogues and managers. Often people tend to have a different view on benefits, costs, how to approach a certain problem. Added to this, we have a range of IP-providers, silicon manufacturers, tool designers etc etc – that all promises to make life a little bit easier. Not being careful in this jungle, your design might end up being a jungle itself.
The idea of abstraction is to hide details that we currently don't want to care about. Now, why do we want to do that? Well, we might want to simplify the problem and break it into more manageable parts. Sometimes this seems to be an advantageous strategy – sometimes it's the only way to cope with the given task. During our daily work, abstraction can be achieved in different ways. Starting a new design, we tend to focus on the functionally in a bigger picture and add details later on. First we might make a block design sketch of the system. In the next step, we add some details where we try to build a model, looking into interfaces etc. This process is repeated until we have a working implementation that we can hand over to the verification guys (remember to keep the documentation up to date).
Looking at the implementation steps, again we can look at different levels of abstraction. We can choose to implement the system by defining a netlist by hand, writing assembler or machine code… Or we can try to add some abstraction and use a high level language. Looking into the hardware domain, we might think of languages such as Verilog or VHDL. Looking at the software side, we might think about C, C++, ADA, Pascal, JAVA etc… Looking at C, there is a short correspondence between the written code and the actual machine code implementation. This means that the code is easier to write; still a lot of details are left to the designer. A language such as ADA and VHDL addresses the behavior of the implementation hiding even more of the details (adding more reasonability to the tool vendors). The rational of using these tools are of course that the development process becomes faster and less error prone. The qualities of the tools are ensured by the vendor's verification processes and a large group of users.
Again considering the implementation, you can abstract your design by buying IPs (prebuilt building blocks). It is in this article we consider source code reuse as the same thing as using an IP block. An IP block can be a pre-synthesized netlist, a dynamic link library of software, a piece of source code that you reuse. The idea is that someone else has already implemented and verified that part of the design for you. Hence you can focus on using it by addressing (hopefully) a simple interface / API.
The next step of abstraction would be to use graphical tools or tools where the implementation is expressed on a pure functional level, leaving all implementation/ design details to the tools and IPs. Such tools are currently promoted by several silicon providers, however most I've seen up to today only addresses IPs on a structural level. I.e. the graphical tools provide you a simpler way of the structural interconnect and configuration.
So how should you verify your design?
Well, as stated above, the idea of abstraction is to remove details from the problem and hence from the implementation.
Do you always trust the tools to generate the correct result?
Do you always trust the IP vendors to fully verify their IP and that it fit seamless into you design?
Have you used the IP correctly?
Well, the problem is that the tools and IPs fills in all the missing details. An old expression sais, "The Devil's in the details". Looking at a compiler, it interprets the code and generates the sequence of machine code, the objective code that is given to the linker. The same with a synthesize tool, it interprets the VHDL code, tries to figure out what the designer intended with the code and generates a netlist based on that. All these added details, should be correct in a perfect world… But guess what, the world isn't perfect!
Since the tools are software programs them self, IPs are pre-implemented block being verified in previous systems. From a pessimistic view, we should expect these to contain errors as well. Hence there is always a small risk of putting to tools and IPs in a corner condition not previously tested. Most often it will work all fine. But what if not?
What we need to do is to verify the low level outcome of the system. I.e. when all software has been compiled and linked, all you hardware have been properly implemented (in an ASIC project, you probably address the back-annotated netlist from the place and route step before tapeout).
What?! Addressing the lowest detail? The inherited code was correct, wasn't it?!
From this point of view, verification becomes just as important as before. If not even more important. If we tend to trust the tools, the IPs and all reused code too much – errors might be even more likely to slip through. So, are all our efforts to save time, money and increase quality in vain?
No, using abstraction and modular approaches correctly will in the end save you both time and increase quality. Breaking a system into modules where some can be reused as IPs, doing it properly, both design and verification can be boosted at several stages of the working process. Each design component (hard or soft) should be verified using test benches. A carefully implemented test bench should be modular as well, containing trasactors to access the component's interfaces and a test logic implementing the actual tests. A transactor should of course be self checking and responsible for checking each part of the transaction, asserting if any error is detected in the protocol. The transactors and the test logic can later be reused on a higher system level when it comes to addressing the entire system. Hence the effort of implementing the system test rig becomes less and less error prone as the transactors are already implemented and tested. Though we should not trust the models or IPs, but the model will tell us how to test the system. The model will also provide us with a tool for the job, a transactor component.
Using an abstract approach, we can break a large complex task into smaller and more manageable pieces. At the same time using a modular approach, source code can be reused and we can by IPs in order to fill in some of the modules – hence we reuse implementations for shortening the project implementation time. This means that we can spend time on doing careful implementation of the parts we actually need to develop for our product.
Inheriting interfaces and transactors from test benches testing the different modules and IPs, we can save time and effort when building our verification environment. This means that we can use more of the verification time targeting the actual verification, implementing tests that will ensure the system's quality. Nevertheless, the target for the verification shall always be the lowest level output. Since the devil lives in the details (not being considered by our model), there can always be errors introduced when building the final output. Thought our models, components, IPs etc will give us the tools to efficiently test and ensure the quality of our system.
Mattias Almljung
Embedded System's Consultant
Altran Technology Sweden AB