DEV Community

Cover image for Learning FPGA programming, key points for a software developer (part 3, code patterns and inferred behavior)
Dmitry Dvoinikov
Dmitry Dvoinikov

Posted on • Updated on

Learning FPGA programming, key points for a software developer (part 3, code patterns and inferred behavior)

The previous parts of this article discussed the time and registered logic. This part is specifically about writing VHDL. It opens with general discussion and then presents code samples and analysis.

VHDL is a declarative language, in which you declare an intent, and let the compiler find the way of implementing it. In a good declarative language it must be easy to express the intent, and the intent must be clear from the code.

You know what other common language is declarative ? SQL. VHDL and SQL are absolutely unrelated of course, but for an opening illustration SQL could be useful.

So, in SQL you write queries that manipulate data. Consider the following simplest SQL query:

Enter fullscreen mode Exit fullscreen mode

The intent here is to find matching records from relation t and return a set of rows with just x in them. But it would be a mistake to assume that the engine would go through table t, check every record for equality of y and extract x from each. Why ?

Because nothing even indicates that t is a table and either x or y are fields. t could be a view, and x and y could be calculated expressions, even constants. In the latter case no data access would be required at all. But even if, we have no idea how it is going to be accessed. SQL optimizer might decide to read an index, rather than the table, and then depending on its coverage, a lookup to the table may or may not be required. Also about indexes, nothing suggests that a suitable index exists, heck, that the engine supports indexes in the first place.

Therefore, to write an efficient SQL query you must take a lot vendor-specific things into account. You may want to read query plans, rearrange the query and even resort to optimizer hints. The query could run either way, but could turn out impractically slow.

Same is true about VHDL.

But VHDL is poorly suited for the job. That it looks like a regular imperative language with conditions, loops, functions and even class-like protected structures, suitable for general programming, is extremely misleading. It doesn't follow a theoretical model the way SQL does, nor does it have first class representation for the objects it actually manipulates.

You should not read nor write VHDL as you would Ada (or Pascal). Instead, you should look at VHDL program the same way the compiler does. And when I say "compiler", I mean the entire chain of tools, or any one in particular.

What's the purpose of the compiler ? To turn your code into a binary configuration file for a particular FPGA device. And every device is different, vendors compete by embedding various reusable blocks. Memory blocks, arithmetic units, all kinds of controllers and even entire CPU's make their way to an FPGA chip. Then where one compiler could use a ready block, another will have to implement it from smaller bricks.

The smallest bricks in FPGA are LUT's (programmable wires) and registers. These are effectively the two kinds of objects we have in a VHDL program. Except for vendor-specific attributes (a-la SQL hints) we never mention anything else. How would a compiler know when to reuse a bigger block, when all it sees is wires and registers ? Besides, even with wires and registers, the best way to arrange them depends on the situation, but how would a compiler even know what the intent was ?

It would help, if the compiler knew what we meant from what we wrote.

The compiler tries to guess the meaning by looking for certain patterns in the code. Once a pattern is detected, an expected behavior is inferred and a known solution is applied, hoping that it is correct and efficient.

As with SQL, it may or may not be, and it is inevitable that we have to read the compilation reports and make sure there is no blunder.

Let's have some examples.

1 . The simplest pattern is this:

c <= a * b
Enter fullscreen mode Exit fullscreen mode

is clearly a multiplication, and an embedded multiplier could be used, or a specific algorithm, depending on how large a and b are.

2 . This here

v: std_logic_vector;
if v = (others => '0') then
Enter fullscreen mode Exit fullscreen mode

infers a zero comparator. Notice that it is different from a general comparator

v1, v2: std_logic_vector;
if v1 = v2 then
Enter fullscreen mode Exit fullscreen mode

and that they produce quite different configurations.

3 . Patterns could span multiple lines, and as such are more like wildcard fragments in the parsing tree. This here

if cnt = 0 then
    cnt <= cnt - 1;
end if;
Enter fullscreen mode Exit fullscreen mode

is a counter pattern. Again, notice how counting up produces a very different configuration

if cnt = max then
    cnt <= cnt + 1;
end if;
Enter fullscreen mode Exit fullscreen mode

and is also somewhat less clear, even when max is a constant.

4 . In some cases there is no way the compiler could correctly guess the intent from the declaration and you have to use a vendor-specific attribute:

type ram_t is array (0 to 63) of std_logic_vector(0 to 7);
signal ram: ram_t;
attribute ramstyle: string;
attribute ramstyle of ram: signal is "MEMORY BLOCK";
Enter fullscreen mode Exit fullscreen mode

With this attribute set, the compiler would utilize the embedded block memory the device might have, and without it, it would have just wasted a lot of precious registers.

Undoubtedly there are a lot more cases, but you get the idea.

Another thing, which may not be a pattern per se, but is similar in concept, and made an important paradigm shift for me was about functions. In software we think of a function as of some kind of generator, something that takes inputs and produces output. As if a new object is instantiated inside the function from the input arguments and returned as a new entity.

VHDL functions should be read differently - all they do is transform input signals to output signals. This way, a VHDL function is a wiring fragment, not a generator. So where in C you had

int add(int a, int b) {
    return a + b; // create a new int and return it
Enter fullscreen mode Exit fullscreen mode

in VHDL you have

function add(a: integer; b: integer) return integer is
    return a + b; -- wire a and b together and stick new wires out
Enter fullscreen mode Exit fullscreen mode

and from this observation there is just one step to looking at VHDL code like this:

all VHDL code is just wire fragments decoupled by registers.

Next, I'd like to give you the most important pattern of all, the component itself.

You see, most of the components that you have look like this:

entity edge_detector_with_passthrough is
        clk:      in std_logic;
        i_input:  in std_logic;
        o_output: out std_logic;
        o_edge:   out std_logic
end edge_detector_with_passthrough;

architecture mixed of edge_detector_with_passthrough is

    signal b_input: std_logic;


    o_output <= i_input;

    process (clk) is

        if rising_edge(clk) then

            b_input <= i_input;
            o_edge <= '0';

            if i_input /= b_input then
                o_edge <= '1';
            end if;

        end if;

    end process;

end mixed;
Enter fullscreen mode Exit fullscreen mode

and the reason for this is, lacking better form of expression, by writing the code in this exact way, we telegraph our intent to the compiler. Specifically, in the above code, when we write

if rising_edge(clk) then
Enter fullscreen mode Exit fullscreen mode

it doesn't mean that we are interested in the actual edge of the clock signal. Instead, we mean that

everything that happens inside that "if" is supposed to be synchronized with the clock signal (i.e. registered)

If you look closely, you see that there are two identically looking signal assignments:

o_output <= i_input;
b_input <= i_input;
Enter fullscreen mode Exit fullscreen mode

but when the first one resides in a combinatorial (i.e. unregistered) area of the component, it gets translated to a direct wire, the second happens to be inside the synchronized block and therefore a register is inferred between b_input and i_input. Similarly o_edge becomes registered just by the fact of being assigned from inside the registered block. Which gives you the following diagram:

Alt Text

Finally, there is a case when it's not even what you wrote that affects the the compiler output, but what you didn't write. I'm talking about latches.

The following fragment

architecture latched of latch is

    if i_enable = '1' then
        o_output <= i_input;
    end if;

end latched;
Enter fullscreen mode Exit fullscreen mode

is hardly worth a second look, but is actually a quite important case of a missing "else". There is no else, you see. From software perspective, it's perfectly normal, if you do nothing, nothing happens.

But as this code resides in the combinatorial area of the component, it describes direct wiring of signals and reads "if enable is high, pass input to output". But what happens to output otherwise ? Unlike in software, nothing cannot happen in hardware. Output must be provided in any case, and if enable is low, we need to keep the previous input somewhere and pass it to output, to create an illusion that "nothing happened". Therefore, in this case a latch is inferred to hold the signal while enable is low.

The moral here is that VHDL makes writing clear programs quite problematic. There is no domain-specific syntax in the language and no first class objects. Therefore, rather than simply declaring the intent, you have to second guess the compiler and write spell-like constructs.

Why it turned out this way, I could guess that back in the days Ada was some kind of a military standard language, and when a committee decided what to use for VHDL, they did what committees do best - stick to the standards. Alas.

Anyway, thank you for reading and I'll be happy to talk to you later.

Top comments (0)