Implementing a build counter in VHDL

Posted on September 16, 2021 in Hardware

When using an FPGA in a system, it may be necessary to know the version of the bitstream loaded in the FPGA.

This may be necessary when the FPGA loads its configuration from a memory, for example.
A new software version will then have to determine if the bitstream stored in the memory needs to be updated.

For this, a BuildCounter can be used. We will need to be able to read it of course, but this is not the purpose of this article.
To be useful, it has to be up to date.
Who hasn't waited 1 day (or more) of compilation to realize that this famous identifier has not been updated?

I propose two codes to manage a build counter automatically.

The first one, which I have been using for years, is in VHDL. The second one, which I wrote recently, is in VHDL-2008.



Here is the first code, in VHDL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use ieee.numeric_std.all;
use std.textio.all;
use IEEE.STD_LOGIC_TEXTIO.all;

entity BuildCounter is
Generic
(
    BUILD_COUNTER_SIZE : positive := 16
);
Port
(
    BuildCounter : out std_logic_vector(BUILD_COUNTER_SIZE-1 downto 0)
);
end BuildCounter;


architecture Behavioral of BuildCounter is

constant FILE_NAME : string := "../../Sources/BuildCounter.txt"; -- Path relative to working directory

impure function BuildCounterFromFile (BCFileName : in string) return std_logic_vector is
FILE BCFile : text is in BCFileName;
variable BCFileLine : line;
variable BCounter : STD_LOGIC_VECTOR(BUILD_COUNTER_SIZE-1 downto 0);

begin
    readline (BCFile, BCFileLine);
    read (BCFileLine, BCounter);
    report "File name : " & BCFileName;
    report "Build counter value : " & integer'Image(to_integer(unsigned(BCounter)));
    return BCounter;
end function;


constant BC : STD_LOGIC_VECTOR (BUILD_COUNTER_SIZE-1 downto 0) := BuildCounterFromFile(FILE_NAME);

file  BCFile : text;

begin

    process
    variable file_status : file_open_status;
    variable BCFileLine  : line;
    variable run : natural := 0;

    begin
        file_open(file_status, BCFile, FILE_NAME, write_mode);
        write(BCFileLine, std_logic_vector(unsigned(BC)+1));
        writeline(BCFile, BCFileLine);
        file_close(BCFile);
        wait until run=1;
    end process;

    BuildCounter <= BC;

end Behavioral;
The entity has a single port, the BuildCounter output, which contains the build number.
The generic BUILD_COUNTER_SIZE parameter is used to set the size of BuildCounter.
The constant FILE_NAME contains the path to the file containing the BuildCounter value.
The function BuildCounterFromFile reads the file whose name is passed as a parameter and returns the value read. The report contained in this function allows useful information to be inserted into the logfile of the synthesizer.
The constant BC is initialized with the current content of the file using the BuildCounterFromFile function.
Finally, the process contained in the body of the entity (line 43) opens the file for writing, writes the new BuildCounter to the file, closes the file and waits for the run variable to go to 1; that is, waits indefinitely.

This code is sensitive. I had to tweak it as versions of ISE/Vivado were released. The run variable, for example, should not be needed, and may not be needed anymore (I haven't tried). It was introduced to work around a synthesis error.
The content of the file is also sensitive. The first line of the file must contain a binary number whose number of bits is exactly equal to BUILD_COUNTER_SIZE.

The number contained in the file at the time of synthesis is the one that will be used in the design.
After synthesis, the number in the file is incremented by 1... If all went well ;)

Warning: This entity must be instantiated only once in the design. Otherwise, the behavior might not be consistent from one synthesis to another. Especially when multi-tasking synthesis is enabled. By passing the file name in a generic parameter, it becomes possible to instantiate this entity several times, provided you pass a different file name to each instance.

With this method, you should be aware that the build number is incremented each time the entity is synthesized. This means that during a development phase, even if the synthesis of the design ends in error, the BuildCounter will still be incremented if the build counter has been synthesized. The BuildCounter is not only incremented when the design is synthesized without error.


Here is now the VHDL-2008 version.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use ieee.numeric_std.all;
use std.textio.all;
use IEEE.STD_LOGIC_TEXTIO.all;

entity BuildCounter is
Port
(
    Build_Counter : out std_logic_vector
);
end BuildCounter;


architecture Behavioral of BuildCounter is

constant FILE_NAME : string := "../../Sources/BuildCounter.txt"; -- Path relative to working directory

pure function str_to_natural (Str : in String) return Natural is
variable Val : Natural := 0;
variable FirstCharFound : Boolean := False;
begin
    for i in Str'Range loop
        case Str(i) is
            when '0' => Val := Val * 10; Val := Val + 0; FirstCharFound := True;
            when '1' => Val := Val * 10; Val := Val + 1; FirstCharFound := True;
            when '2' => Val := Val * 10; Val := Val + 2; FirstCharFound := True;
            when '3' => Val := Val * 10; Val := Val + 3; FirstCharFound := True;
            when '4' => Val := Val * 10; Val := Val + 4; FirstCharFound := True;
            when '5' => Val := Val * 10; Val := Val + 5; FirstCharFound := True;
            when '6' => Val := Val * 10; Val := Val + 6; FirstCharFound := True;
            when '7' => Val := Val * 10; Val := Val + 7; FirstCharFound := True;
            when '8' => Val := Val * 10; Val := Val + 8; FirstCharFound := True;
            when '9' => Val := Val * 10; Val := Val + 9; FirstCharFound := True;
            when ' ' => if FirstCharFound then return Val; end if;
            when others => return Val;
        end case;
    end loop;
    return Val;
end function;

impure function BuildCounterFromFile (BC_FileName : in string) return Natural is
File BC_File : text open read_mode is BC_FileName;
variable FileLine : line;
variable Counter     : Natural;
variable CounterStr  : String (1 to 32);
variable LineLength  : Natural;
begin
    report "Build counter file name : " & BC_FileName;
    readline (BC_File, FileLine);
    --file_close(BC_File);
    LineLength := FileLine'Length;
    CounterStr := (others => ' ');
    read (FileLine, CounterStr(1 to LineLength));
    --Counter := Integer'Value(CounterStr(1 to LineLength));
    Counter := str_to_natural(CounterStr(1 to LineLength));
    report "Build counter value : " & Natural'Image(Counter);
    return Counter;
end function;


constant Counter    : Natural := BuildCounterFromFile(FILE_NAME);
constant CounterStr : String := Natural'Image(Counter + 1);

File BC_File : text;

begin

    process
    variable file_status : file_open_status;  -- @suppress "variable file_status is never read"
    variable FileLine : line;

    begin
        file_open(file_status, BC_File, FILE_NAME, write_mode);
        write(FileLine, CounterStr);
        writeline(BC_File, FileLine);
        file_close(BC_File);
        wait;
    end process;

    Build_Counter <= std_logic_vector(to_unsigned(Counter, Build_Counter'Length));

end Behavioral;
Overall, the operation is the same as the previous code but with the advantages of VHDL-2008.

Among other things, there are no more generic parameter and the build counter port is no longer constrained. Its size is then determined when instantiated.
Note that the declaration of the BC_File file in the BuildCounterFromFile function (line 43) has changed to be adapted to VHDL-2008.
The run variable has disappeared.
Line 51 is commented but could be active. As the file is automatically closed at the end of the function, this line is not necessary. Making it active would protect against unexpected use of the file later in the code.

I took advantage of this version to make the content of the file containing the BuildCounter more readable. The content is now in decimal.
In this respect, line 55 should work but it doesn't. So I created the function str_to_natural to get around the problem. This function eliminates the non-digit characters before the number read and stops parsing as soon as a non-digit character is detected.

The restrictions and usage recommendations of the VHDL version also apply to this version in VHDL-2008.

The Vivado synthesizer's support for VHDL-2008 is imperfect, but is improving with each release. The code proposed here has been tested with Vivado 2021.1.



Note

These source codes have only been tested with Xilinx tools. They might not be usable with other development toolchains. The main reason is that in VHDL, the reading/writing of files is present, basically, to be used during simulation. Reading test vectors, writing simulation report files... The Xilinx synthesizer allows to read and write files during the synthesis, with restrictions.



Translated with the help of www.DeepL.com/Translator (free version)