Search code examples
vhdl

Developing multi-use VHDL modules


I've recently just started learning VHDL again after not having touched it for years. The hope is to use it to develop a control system with various sensor interfaces, etc. I'm fairly competent in embedded C, which has been my go-to language for any embedded projects I've had up until this point, which makes learning VHDL all the more frustrating.

Basically, my issue right now, which I see as my biggest barrier in being able to progress with my intended project, is that I don't know how to develop and incorporate a module that I can just pass variables to and call (like a function in C) to carry out some task, i.e. display an integer 0-9999 on a 4 digit 7-segment display. I know there are components in VHDL, but that seems like such a long winded way of performing one task. Is there a better way of doing this?

It seems to me like after you've done all the digital tutorials, there's a huge gap in the info on how to actually develop a full system in VHDL.


Solution

  • EDIT : further explanation re: third comment at the bottom. Apologies for the length of this!

    VHDL has functions and procedures too, just like C. (OK, C calls its procedures "void functions"!) And, just like C, you can call them from sequential code using the same kinds of sequential code constructs (variables, loops, if statements, case-statements which have some similarities to C's switch), and so on.

    So far, all this is synthesisable; some other VHDL features are for simulation only (to let you test the synthesisable code). So in simulation you can - like C - open, read and write files, and use pointers (access types) to handle memory. I hope you can see why these parts of VHDL aren't synthesisable!)

    But what is different in VHDL is that you need a few extra lines to wrap this sequential code in a process. In C, that happens for you; a typical C program is just a single process (you have to resort to libraries or OS functionality like fork or pthreads if you want multiple processes)

    But VHDL can do so much more. You can very easily create multiple processes, interconnect them with signals, wrap them as re-usable components, use "for ... generate" to create multiple processes, and so on. Again, all synthesisable : this imposes some restrictions, for example the size of the hardware (number of processes) cannot be changed while the system is running!

    KEY: Understand the rules for signal assignment as opposed to variable assignment. Variables work very much as in C; signals do not! What they do instead is provide safe, synchronised, inter-process communication with no fuss. To see how, you need to understand "postponed assignment", delta cycles, wait statements, and how a process suspends itself and wakes up again.

    You seem to be asking two questions here:

    (1) - can I use functions as in C? Very much so; you can do better than C by wrapping useful types and related functions, procedures in a package and re-using the package in multiple designs. It's a little like a C++ reusable class with some stronger points, and some weaker.

    (2) can I avoid entities, architectures and components altogether in VHDL? You can avoid components (search for "VHDL direct entity instantiation") but at some point you will need entities and architectures.

    The least you can get away with is to write a single process that does your job, receiving inputs (clk, count) on signals and transmitting to the LEDs on other signals.

    Create an entity with all those signals as ports, and an architecture containing your process, connecting its signals up to the ports. This is easy - it's just boilerplate stuff. On an FPGA, you also need to define the mapping between these ports and the actual pins your LEDs are wired to. Synthesise it and you're done, right? .. not quite.

    Create another "testbench" entity with no external ports. This will contain your entity (directly instantiated), a bunch of signals connecting to its ports, and a new process which drives your entity's input ports and watches its output ports. (Best practice is to make the testbench self-checking, and assert when something bad comes out!) Usually "clk" comes from its own one-liner process, and clocks both testbench and entity.

    Now you can simulate the testbench and watch your design working (or not!) at any level of detail you want. When it works - synthesise.

    EDIT for more information: re: components, procedures, functions.

    Entities/components are the main tool (you can ignore components if you wish, I'll deal with entities later).

    Procedures and functions usually work together in a single process. If they are refactored into a package they can be re-used in other like-minded processes (e.g. operating on the same datatypes). A common abstraction is a datatype, plus all the functions and procedures operating on it, wrapped in a package - this somewhat resembles a C++ class. Functions also have uses in any declaration area, as initialisers (aka "factory" pattern in software terms)

    But the main tool is the entity.

    This is a level that is probably unfamiliar to an embedded C programmer, as C basically stops at the level of the process.

    If you have written a physical block like an SPI master, as a process, you will wrap that process up in an entity. This will communicate with the rest of the world via ports (which, inside the entity, behave like signals). It can be parameterised via generics (e.g. for memory size, if it is a memory). The entity can wrap several processes, other entities, and other logic that didn't fit neatly into the process (e.g. unclocked logic, where the process was clocked)

    To create a system, you will interconnect entities, in "structural HDL code" (useful search term!) - perhaps a whole hierarchy of them - in a top level entity. Which you will typically synthesise into an FPGA.

    To test the system via simulation, you will embed that top level entity (=FPGA) in another entity - the testbench - which has no external ports. Instead, the FPGA's ports connect to signals inside the testbench. These signals connect to ... some of them connect to other entities - perhaps models of memories or SPI slave peripherals, so you can simulate SPI transactions ... some of them are driven by a process in the testbench, which feeds your FPGA stimuli and checks its responses, detecting and reporting errors.

    Best practice involves a testbench for each entity you create - unit testing, in other words. An SPI master might connect to somebody else's SPI slave and a test process, to start SPI transactions and check for the correct responses. This way, you localise and correct problems at the entity level, instead of attempting to diagnose them from the top level testbench.

    A basic example is shown here :

    Note that he shows port mapping by both positional association and (later) named association - you can also use both forms for function arguments, as in Ada, but not C which only allows positional association.

    What "vhdlguru" doesn't say is that named association is MUCH to be preferred, as positional association is a rich source of confusion and bugs.

    Is this starting to help?