Writing good, portable, reusable HDL code, that enables the synthesized design to implement at the desired frequency can definitely be a challenge. To do this, many (if not most) professional organizations have coding standards. Coding rules can become a battleground though. Some companies have very intricate coding rules, which control not only code structures but also formatting and naming, while others are a little more relaxed.
However, if you are developing FPGAs professionally or even as a hobby in the maker space, you should have some basic coding rules. Adherence to these basic rules can save a lot of time and effort when you come to the back end of a project and timing closure must be achieved. A recent conversation got me thinking about what my top 10 rules for HDL development would be.
State Machines – These should be single process only. Forget what school and the courses teach about a combinatorial section and a sequential section. All state machines should be single process. This aids debugging, prevents the implementation of latches, and ensures that all outputs are registered, which in turn reduces glitches and helps us achieve timing closure.
State Machines – Decouple as much functionality from the state machine as possible. This means ensuring that counters etc. sit in their own process (external to the FSM process) and are controlled by signals to / from the FSM. This decoupling enables the implementation of the device to be more optimal for both FSM and counter -- resulting in a better quality of result for example.
State Machines – Ideally state machines should be in their own file separate from other elements of code. I would allow the decoupled timers and other elements in the same file though.
Inference – Whenever possible, write your code in such a way that the synthesis tool is able to infer the logic structure to which is it bounds (e.g., DSP48, BRAM etc.). This makes your code portable between families as architectural features change (e.g., DSP48 to DSP58).
Instantiation – When inference cannot be performed and the component must be instantiated, it should be in its own file at the top level of the architecture. This makes replacing or updating the instantiated IP block easier.
Entity / Module – Every signal which enters or leaves a module should be registered. This helps address timing closure, especially when we start running at faster frequencies.
Use Standard Interfaces – Use AXI4, AXIS, AXI4-Lite or other standards if a pure HDL design e.g., wishbone to communicate between modules. This simplifies the need for architectural elements to have lots of vector ports and associated handshaking which need to be connected, place and routed, and achieve timing closure. These interfaces are often also more flexible if pipelining needs to be implemented to achieve timing closure.
Do not mix structural and functional code – This keeps reuse nice and simple because the functional code is in separate files and can be easily reused. Similarly the structural code files are much simpler to understand because they just define the configuration of the solution.
Naming – Use sensible variable names across the design which are descriptive to aid readability. Consider using prefixes such as i_ o_ s_ v_ etc. to identify the type of variable you are accessing or working on. Prefixes work better than post fixes because variable names change in size. Using a prefix eases bulk editing if you wish to make changes.
Documentation – Read the documentation for the IP you are using to understand how it works. Don’t assume you know how it works. It’s also worthwhile reading the UltraFast Design Methodology Reference Guide to ensure our coding structures are correct (e.g., reset and control signals). Similarly, it is a good idea to include documentation of your own on the design. There are several open-source tools which can be used such as WaveDrom for timing diagrams, Symbolator for block diagrams, bitfield for register definitions etc. Being able to include these in the source code is ideal because it prevents documentation from getting lost. In this case, the TerosHDL editor might be of interest to you.
Following these rules, you should be able to create functional FPGA designs which are not only implementable at the speed required but also flexible and enable significant reuse. I am, however, going to add in two additional rules:
Simulate your design – Before you even think about implementation, ensure your modules and top level have a test bench which demonstrates that the design works as intended. On-device debugging is intended only for integration type issues, not to verify if the design works.
Use a version control system - GIT or SVN for example and use the issue tracking and release capabilities seriously. It can really help you and get you out of trouble.
These are my top 10 (well ok, 12 rules) for HDL development. I am curious to hear what additional rules you might think to add?