13
VHDL'93 - new features
prepared by P. Bakowski
This topic is in preparation.
The VHDL development of the new generation of the language was guided by the following requirements:
The developers were oblidged to preserve the determinism. the enerality and the language scope(from system to gate level). The newly introduced mechanisms should be implemented with a minimal impact on the efficiency. No specific packages should be added.
If compared with VHDL'87 , VHDL'93 integrates several new elements:
In addition to these important features VHDL'93 provides new character set, extended identifiers and aliasing, etc.
Timing : postponed processes and rejection limit
To allow steady state modeling VHDL'93 introduces the mechanism of postponed process. Postponed process is executed at the end of a time step.
Note that in VHDL'87 there is no distinction between a time-in-seconds and a delta-time. The only way to get rid of delta glitches is to introduce minimum physical delay : wait for 1fs.
With VHDL'93 this can be done through postponed process:
PP: postponed process(clk)
begin
assert ((hold_time <= (now - dbus'last_event))
report "hold time violation";
end process;
or through postponed assert:
postponed assert ((hold_time <= (now - dbus'last_event))
report "hold time violation"
With VHDL'87 two delay models were defined: inertial and transport delay. In order to have more control over timing specifications VHDL'93 introduces a new delay model : inertial with pulse rejection window.
The rejection limit allows to model the glitches. For example a combination circuit has a delay of 10 ns but show a glitch if the input changes more than 6 ns later. This situation can not be modeled in VHDL'87.
sout <= reject 6 ns inertial sin after 10 ns;
Instancing and Incremental Binding
The instancing of an entity in a larger structural architecture required in VHDL'87 an explicit declaration of corresponding component.
This constaraint, in some cases cumbersome, is not mandatory in VHDL'93. In VHDL'93 the direct instancing of entities is allowed:
dir_inst: entity memories.ram(first) -- no component declaration is required
generic map(outdel => 5 ns)
port map (dram,rw,sel);
The incremental binding is a new feauture that allows to use configuration declarations to change bindings that were specified in configuration specifications. This feature provides more flexibility and helps the top-design process.
Example:
configuration specification:
for ADD_i1: unite1 use entity adder(first)
generic map(adel => 10 ns) -- specified value
port map(a,b,cin, en=>open);
configuration declaration for this architecture:
for ADD_i1: unite1
generic map(adel => 5 ns) -- actual value : overriding is allowed
port map(en=>'1');
end for;
Shared variables and Monitors
A need to model the digital systems at higher abstraction levels (system level) requires to share data between processes . The shared variables are a modeling convenience in which the shared variables are not part of the physical hardware. They allow to build the models where the entities communicate via external objects such as queues and stacks. Shared variable also allow to model stochastic processes where the hardware operates in an environment containing pseudo-random number generators exploitet representing hardware load.
Shared variables can be declared outside of processes and sub-programs and can be used globally.
shared variable frequency: natural;
sout <= sin after fun_transf(frequency);
frequency := delay_based_computation;
The use of shared variables creates two potential problems:
For example if two processes use the same shared variable shvar :
shared variable shvar: bit_vector;
-- in process one
one: process
shvar := "010101";
wait;
end process one;
-- in process two
two: process
shvar := "101010";
end process two;
What is the resulting value of shvar after the execution of the above assignment ? It depends on the order of execution which can not be determined by the model itself.
These problems require specific mechanisms to assure atomicity of operations on shared data including exclusive assess.
Exclusif access
The first solution enabling atomic access is the use of access flags.
type vsigned is record
signbit: bit;
value: bit_vector(7 downto 0);
end record;
shared variable shvar: vsigned ;
process(...) -- decrement
if shvar.signbit='0' then
shvar.signbit='1'
if shvar.value ="00000000" then
fun_decr_val(shvar.value)
end if;
else
fun_decr_shvar(shvar)
process(...) -- increment
if shvar.signbit='1' then
shvar.signbit='0'
if shvar.value ="11111111" then
fun_incr_val(shvar.value)
fun_incr_shvar(shvar)
The execution of the above processes may lead however to an inconsistent result:
Conclusion:
The nondeterminism of the process activation may generate an inconsistent result !
The above examples show that simple declaration of shared variables is unsufficient to provide atomicity of the operations and consistency of the results.
That is why the working group (SVWG) decided to introduce monitor mechanism to protect the access to shared variables.
The monitors are used to protect the access not only to shared variables (variables) but to a number of language constructs such as functions and procedures. The potected type has been introduced.
The protected type definition may contain declarative items in any order provided that declaration precedes use (as elsewhere in VHDL, consistent with existing visibility rules):
protected_type_definition_declarative_part ::= { protected_type_body_declarative_item } protected_type_definition_declarative_item ::= subprogram_declaration | subprogram_body | type_declaration | subtype_declaration | constant_declaration | variable_declaration | file_declaration | alias_declaration | use_clause | attribute_declaration | attribute_specification | group_template_declaration | group_declaration
For mor details see: shared variable - language change specification (SVWG)
The declarations which can appear in a protected type definition are identical to those which can appear in a subprogram body.
Examples : (these examples are taken from the SVWG document)
for more details see: shared variable - language change specification - (SVWG)
In order to illustrate the protected approach defined in this language change specification, it is useful to consider four examples of increasing complexity: a shared counter protected type, a complex number protected type, a variable size array protected type and a mutual-exclusion semaphore. Each of these examples illustrates a different characteristic of this protected type design.
Shared Counter
The shared counter example illustrates an integer that must be atomically incriminated, decremented and observed. Note that all three of these operations are monadic in nature.
First the shared counter protected type specification and body appear:
type SharedCounter is protected procedure increment (n: integer); procedure decrement (n: integer); function value return integer; end protected SharedCounter; .... type SharedCounter is protected body variable counter_value: integer := 0; procedure increment (n: integer) is begin counter_value := counter_value + n; end procedure increment; procedure decrement (n: integer) is begin counter_value:= counter_value - n; end procedure decrement; function value return integer is begin return counter_value; end procedure value; end protected body SharedCounter;
Then a shared variable is created using the SharedCounter protected type:
shared variable counter : SharedCounter;
Finally, several processes may utilize the shared variable, such as the example process below:
example_process: process is ... counter.increment(5); ... counter.decrement(i); ... v := counter.value; if (v = 0) then ...-- by the time execution gets to here, the counter may ...-- have changed value! end if; end process example_process;
It is important to note that the if condition only insures that the counter was 0 at the instant of the call; by the time comparison occurs, the counter may have increased or decreased in value. If it is important to insure that the semaphore remains zero while the comparison and if condition executes, a more complex CountingSemaphore would be required with methods to conditionally lock and unlock the counting semaphore. The caller (example_process in this case) would try to acquire the explicitly programmed lock. If the CountingSemaphore's lock subprogram granted access via that call, it would return a key by which the owner would be known in a subsequent call and the semaphore would reject any other lock request. The semaphore would only honor increment and decrement requests which contained the appropriate key (belonging to the exclusive owner). When the conditional operation was done, the owner of exclusive access must unlock the semaphore. While illustrating that a semaphore can be embedded in a monitor with simple monadic functionality, the responsibility is on the human to insure that every lock is matched by an unlock on every path. Abstracting high-level function into the protected type can simplify the call down to a single call to the object of protected type. For example, the call to get the semaphore's value followed by a comparison with zero and an assert might be abstracted into a single CountingSemaphore subprogram which triggers a VHDL assert statement if the semaphore_value is zero.
Complex Number
The complex number example illustrates a protected type which could be represented as a real and imaginary part or a phase and magnitude. For purposes of illustration, the example happens to use a real and imaginary representation, although this should not be discernible from the interface. A single operator, addition, serves to illustrate definition of a dyadic operator for the complex number type.
type ComplexNumber is protected procedure extract (variable r, i: out real); procedure add (variable a, b: in ComplexNumber); end protected ComplexNumber; ... type ComplexNumber is protected body variable re, im: real; procedure extract (variable r, i: out real) is begin r := re; i := im; end procedure explode; procedure add (variable a, b: in ComplexNumber) is variable a_real, b_real : real; variable a_imag, b_imag : real; begin a.extract(a_real, a_imag); b.extract(b_real, b_imag); re := a_real + b_real; im := a_imag + b_imag; end procedure add; end protected body ComplexNumber;
Then in some concurrent declarative region, perhaps that of an architecture, three shared variables are declared with default initial values.
shared variable sv1, sv2, result : ComplexNumber;
Sequential statements and concurrent procedure calls may reference these shared variables from many different processes.
Variable Size Array
The variable size array example illustrates a protected type capable of representing an object of varying size. In this case, the varying size refers to the number of bit elements stored:
type VariableSizeBitArray is protected procedure add_bit (index: positive; value: bit); function size return integer; end protected VariableSizeBitArray; type VariableSizeBitArray is protected body type bit_vector_access is access bit_vector; -- In this simple case, element initalization works, however note -- the alternative approach suggested by Peter Ashenden below... variable bit_array: bit_vector_access := NULL; variable bit_array_length : natural := 0; procedure add_bit (index: positive; value : bit) is variable tmp : bit_vector_access; begin if index > bit_array_length then tmp := bit_array; bit_array_length := index; bit_array := new bit_vector(1 to index); if tmp /= null then bit_array(1 to bit_array_length) := tmp.all; deallocate (tmp); end if; end if; bit_array (index) := value; end procedure add_bit; function size return integer is begin return bit_array_length; end function size; impure function initialize return boolean is begin bit_array := null; bit_array_length := 0; return true; end function initialize; constant initialized : boolean := initialize; end protected body VariableSizeBitArray;
Within a sequential declarative region, perhaps a process, we can then declare a variable bit_stack variable bit_stack: VariableSizeBitArray;
The sequential code within the process may then initialize the bit_stack then adds three elements:
bit_stack.add_bit(1,'1'); bit_stack.add_bit(2,'1'); bit_stack.add_bit(3,'0');
The VariableSizeBitArray protected type could equally well have been used to create a shared variable accessible from several processes during the same delta cycle. As long as the array was initialized once, the bit_stack should function equally well. The interested reader is urged to sketch other short examples which illustrate use of protected types for scenarios of interest to the reader. Authors of this LCS would be very interested in examples which imply incorrect results under some parallel processing scenerios or which seem unacceptably awkward.
Semaphore
Monitors are intended to provide a more powerful, higher level mechanism than semaphores, however monitors do not readily replace semaphores. Chuck Swart prepared this example to illustrate the complexity of implementing a semaphore using monitors. In general, semaphores are used in the following way:
p1: process is ... P(s); -- Block until semaphore s is acquired ... -- Critical section c1 V(s); -- Release semaphore s ... p2: process is ... P(s); -- Block until semaphore s is acquired ... -- Critical section c2 V(s); -- Release semaphore s; ...
The P and V procedures are executed atomically. When a process executes a P the process requests control of the semaphore. If another process controls the semaphore, execution of the requesting process is blocked until the semaphore is available. Execution of V releases the semaphore for use by other processes. The above code uses semaphores to guarantee that critical regions c1 and c2 will not execute simultaneously. Here is an example which tries to emulate the above code using protected types:
entity e is end e; architecture foo of e is type Semaphore is protected procedure up ( variable ret_val: out boolean); procedure down; end protected; type Sempahore is protected body variable entry_ok: boolean := true; procedure up ( variable ret_val: out boolean) is begin ret_val := entry_ok; entry_ok := FALSE; end procedure up; procedure down is begin entry_ok := true; -- For simplicity, no error checks end procedure down; end protected; shared variable s: Semaphore; procedure P ( variable s: Semaphore) is variable success: boolean := false; begin loop1: while not (success) loop -- note busy loop s.up(success); end loop loop1; return; end procedure P; procedure V ( variable s: Semaphore) is begin s.down; return; end procedure V; ... p1: process is ... P(s); -- Block until semaphore s is acquired; ... -- Critical section c1 V(s); -- Release semaphore s; ... p2: process is ... P(s); -- Block until semaphore s is acquired; ... -- Critical section c2 V(s); -- Release semaphore s; ...
Unless this code executes in an environment which interleaves execution of p1 and p2, the code will not execute as expected. However, most, if not all, current uniprocessor implementations execute a single process until that process executes a wait statement. Under this common implementation, if p1 executes while p2 is in critical section c2, then process p1 will busy wait forever, since control will never be given to p2 and, thus, the semaphore will never be released. The preferred solution is to reformulate the VHDL code as a monitor. The critical sections (C1 and C2 above) are rewritten as two procedures in a protected type declaration (and body). In this case the protected type body need not have any state (variable declarations). An object of protected type CriticalSection can be created and referenced from two or more processes.
type CriticalSection is protected procedure c1; procedure c2; end protected CriticalSection; ... type CriticalSection is protected body procedure c1 is begin -- Body of critical section c1 end procedure c2; procedure c2 is begin -- Body of critical section c2 end procedure c2; end protected body CriticalSection; shared variable Semaphore : CriticalSection;
Pure and Impure functions
In VHDL'93 there are two kinds of functions : memoryless functions referred to as pure functions and functions with memory refered to as impure functions. A pure function value depends only on the input parameters. Note that VHDL'87 provides only memoryless functions.
One of the obvious application of impure functions is random numbers generation. The pseudo-random sequences are based on a seed value which is updated at each call, and used to calculate the next pseudo-random value. Without persistent memory it is impossible to develop a function generating pseudo-random values.
shared variable seed: integer;
impure function pseudo_rand return real is
generate_new_value; -- operations generating new random value from new seed value
seed_modification ; -- operations modifying the current seed value
return new_random_value;
end;
Files and Text I/O
For the VHDL'93 file primitives such as FILE_OPEN and FILE_CLOSE, the mode of file parameter is inout. This solution allows easier interprocess communication.
file test_vectors: text open READ_MODE is "$vector_file";
The text I/O package has no ENDLINE indicator. The standard input/output can be opened or closed by OPEN and CLOSE primitives.
An attribut F'length allows to know the position in the file; f'length=0 means end_of_file.
while F'length > 0 loop
read(F,value)
end loop;
New attributes
In order to convert the strings into values and values into strings,two new attributes representing the string image ('image) and value and ('value) have been added.
type light_t is (green, orange, red)
constant con1: light_t: light_t'value("green");
...
assert con1 = red)
report "con1 is not red but light_t'image(con1)";
Note: VHDL'87 has already an attribut val
T'val(X) returns a value (of base T) whose position is the universal integer value X.
type COULEUR is (White,Red,Green,Black);
variable T: COULEUR;
T'pos(Red) gives 1
T'val(3) gives Black
'path_name and 'instance_name are new attributes provided to identify the hierarchy of entities.
The 'instance_name atribute reflects the instance name :
pa: process(clk)
variable var: integer;
assert(var'instance_name=":top:insta:pa:var");
end process pa;
The 'path_name atribute informs about the instanced entity and architecture:
pb: process(clk)
assert(var'instance_name=":top(first):insta@ent_b(simple):pb:var");
end process pb;
These attributes offer easier debugging and provide the mechanism for backannotation.
Backannotation - an example
The backannotation is required when we need to update the delay values after the synthesis process.
The backannotation was a hard task for the users of VHDL'87. With the new features such as shared variables, impure functions, 'instance_name and 'value attributes introduced into VHDL'93 the backannotation may be prepared in the source desrcription.
Assume tha annotated value file is constructed such that for each place in the hierarchy a delay value is assigned:
:top:inst1:gate1: 4 ns
:top:inst1:gate2: 6 ns
:top:inst2:gate7: 3 ns
At a model start up and impure function can read the file and hash the values (based on the hierarchy name) to a global hash table using shared variables:
type tItem;
type PtItem is access tItem;
type tItem is record
tag : integer;
value : time
next:PtItem;
type HsTbl is array (1 to Pnumber) of PtItem;
shared variable Global_delay_table: HsTbl;
impure function read_and_hash(file vF: text)
return integer;
constant file_read: integer := read_and_hash(Value_file)
The function read_and_hash computes an integer tag and logs a new entry in the table using the name, and delay from the file.
From each instance another impure function named update_delay is called to get the correct delay value from the table for that instance. This is done at the simulation initialization.
The function update_delay computes a tag from the name, looks it up in the global table and returns the appropriate delay.
impure function update_delay(Hname: in string) return time;
variable Gdelay: time:= update_delay(p'instance_name&":gdelay");
To annotate non-numeric values, it is possible to use text files. Such files may include strings, which are converted to enumerations using attributes
Assume a file has:
:top:gate1:local_delay_kind min
type delay_kind is (min,typ,max);
variable local_dela_kind:= delay_kind'value(read_from_file(p'instance_name & ":local_delay_kind"));
Groups
The group construct allows for the named association of other objects and names. It is useful for synthesis or for evaluation.
Group has no simulation semantics.
group channel is (signal, signal);
group my_channel: channel(sig1,sig2);
attribute length of my_channel: group is 100 m;
Foreign units and subprograms
The attribute foreign allows the denotation of subprograms and architectures as foreign - implemented outside of VHDL.
function call_socket_read(s: in integer) return integer;
attribute foreign of call_socket: function is "$system/calls/sockets/socket_read";
New operators
VHDL'93 integrates predefined shift operators:
architecture newop of shift is
constant shdist: integer:= 4;
process(a,shdist)
b1 <= a sll shdist;
b2 <= a srl shdist;
b3 <= a sla shdist;
b4 <= a sra shdist;
b5 <= a rol shdist;
b6 <= a ror shdist;
end newop;
xnor is added to complete the logic operators
'driving and 'driving_value
The 'driving attribute permits to know if a signal is driven from within a process at a particular time.
This allows to localize the source of the event.
ex1_pr:process(clk)
if (clk'driving) then
-- the value for the driver of clk is not NULL
-- attention: 'Z' is considered driving !
clk <= not clk after 10 ns;
end process ex1_pr;
The 'driving_value attribute allows to test the value of the local driver of a signal:
ex2_pr:process(clk)
assert (clk'driving_value /='Z')
report "ex2_pr is not driving the clk";
end process ex2_pr;
Above , both sources may activate the clk <= not clk after 10 ns assignment.
Only internal (to the process) source may activate the clk <= not clk after 10 ns assignment.
Signature and Aliasing
Extended character set and identifiers
'ascending
Changes:
sensitivity list
report statement
concatenation
staticness
Implementation aspects
Exercises