A Crib For Formatting Strings in VHDL
There's no printf() equivalent in VHDL that works with all types, and I think it would be quite hard to create as it is a strongly typed language. Here are some examples of what can be done in VHDL 2008, and a couple of extensions.
- Real Numbers
- Engineering Format
- Complex Numbers
- Fixed Point Numbers
- Time Values
- Justification
- Conclusions
- References
For reference the print() function definitions I am using is:
procedure print(s : string) is
variable l : line;
begin
write(l, s);
writeline(output, l);
end procedure;
Of course you could use VHDL report statement instead and include the simulation time.
Real Numbers
Okay, so there's a printf() like formatting string here, but that only applies to real numbers.
function to_string (value: real) return string;
function to_string (
value : real;
digits : natural
) return string;
function to_string (
value : real;
format : string
) return string;
Some quick examples for illustration:
process
variable r : real := 3.141592653589793238e-4;
begin
print(to_string(r));
-- # 3.141593e-04
print(to_string(r, 5));
-- # 0.00031
-- If the 'digits' parameter is zero, the conversion behaves as if it were absent.
print(to_string(r, 0));
-- # 3.141593e-04
print(to_string(r, "%.2g"));
-- # 0.00031
std.env.stop;
wait;
end process;
Engineering Format
Two of these three functions rely on using a normalised scientific format. This is not helpful when printing reals in an engineering format. I've had a go at creating an alternative printing function that is more helpful with scientific units, i.e. exponents where the power of 10 is a multiple of 3.
-- Create a string from a real in non-normalised scientific form. That means the significand or
-- mantissa does not need to be a value between 1 and 10. This is convenient when wanting to display
-- real values as strings in an ISO unit, typically where the exponent is -9, -6, -3, 0, 3, 6 etc,
-- i.e. in engineering notation. But this function is more general in that it works for any exponent.
--
function to_scientific(
value : real;
exp : integer; -- e.g. -6 for micro
digits : positive := 1
) return string is
-- For a real printed as x.xxxe+04, extract the part starting with 'e' to the end of the string.
-- i.e. e+04" in this example.
function exponent(
str : string
) return string is
begin
for i in str'range loop
if str(i) = 'e' then
return str(i to str'high);
end if;
end loop;
return "";
end function;
constant scale : real := 10.0**exp;
begin
return to_string(value / scale, digits) & exponent(to_string(scale));
end function;
Examples to illustrate, using powers of 10 not limited to multiples of 3 since this function is more general.
for i in -4 to 4 loop
print(
"scientific exp=" & justify(to_string(i), field => 2) & ":",
to_scientific(1.23456789e+0, i, 4)
);
end loop;
-- # scientific exp=-4: 12345.6789e-04
-- # scientific exp=-3: 1234.5679e-03
-- # scientific exp=-2: 123.4568e-02
-- # scientific exp=-1: 12.3457e-01
-- # scientific exp= 0: 1.2346e+00
-- # scientific exp= 1: 0.1235e+01
-- # scientific exp= 2: 0.0123e+02
-- # scientific exp= 3: 0.0012e+03
-- # scientific exp= 4: 0.0001e+04
Complex Numbers
There's nothing specific for converting complex numbers to strings even though they are part of an IEEE standard library. They are just a pair of real values, but for convenience I offer these functions to mirror the to_string(real) ones.
function to_string(
c : complex;
digits : natural := 0
) return string is
begin
return to_string(c.re, digits) & " " & to_string(c.im, digits) & "i";
end function;
function to_string(
c : complex;
format : string
) return string is
begin
return to_string(c.re, format) & " " & to_string(c.im, format) & "i";
end function;
Some quick examples for illustration:
process
variable c : complex := (1.2345, -1.2345);
begin
print(to_string(c, 2));
-- # 1.23 -1.23i
print(to_string(c, "%.3f"));
-- # 1.234 -1.234i
std.env.stop;
wait;
end process;
Fixed Point Numbers
A couple of examples for ufixed and sfixed which fall back to using to_string(real). The point here is that by converting to the real type you have the printing options already, so the fixed point functions concentrate on printing the bit patterns. Note the discrepancy in the conversion of real to u/sfixed back to real caused by the limited resolution.
process
variable uf : ufixed(15 downto -16) := to_ufixed( 4.1, 15, -16);
variable sf : sfixed(15 downto -16) := to_sfixed(-4.1, 15, -16);
begin
print("ufixed 1:", to_string(uf));
-- # ufixed 1: 0000000000000100.0001100110011010
print("ufixed 2:", to_string(to_real(uf), "%.2g"));
-- # ufixed 2: 4.1
print("ufixed 3:", to_string(to_real(uf), 7));
-- # ufixed 3: 4.1000061
print("sfixed 1:", to_string(sf));
-- # sfixed 1: 1111111111111011.1110011001100110
print("sfixed 2:", to_string(to_real(sf), "%.2g"));
-- # sfixed 2: -4.1
print("sfixed 3:", to_string(to_real(sf), 7));
-- # sfixed 3: -4.1000061
std.env.stop;
wait;
end process;
Time Values
function to_string (
value : time;
unit : time
) return string;
Some quick examples for illustration:
print(to_string(12.5 ns, ps));
-- # 12500 ps
print(to_string(321.5 ps, fs));
-- # 322 ps
Justification
Justifying text is limit to left or right, there is no center option. The justification function defaults to right justified, as quite simply there's limited point using it for right justification unless you want the padding of spaces to the right. Justification is also build into the write() functions too.
function justify (
value : string;
justified : side := right;
field : width := 0
) return string;
procedure write (
L : inout line;
value : in AType;
justified : in side := right;
field : in width := 0
);
An example used in the Github source code is given here. The function right justifies the first string argument and left justifies the second.
procedure print(s1, s2 : string) is
variable l : line;
begin
write(l, s1, right, 18);
swrite(l, " ");
write(l, s2);
writeline(output, l);
end procedure;
This could also be written using the justify() function to concatenate strings to a single write() call:
procedure print(s1, s2 : string) is
variable l : line;
begin
write(
l,
justify(
value => s1,
justified => right,
field => 10
) & " " & s2
);
writeline(output, l);
end procedure;
The former feels more succinct in this case, but the justify() function has more general utility as it is not tied to writing to a line type. Examples to illustrate:
print(
"time:",
justify(
value => to_string(321.5 ps, fs),
field => 20
)
);
-- You would most likely use 'justify()' for a right justification (the default for the 'justified' parameter),
-- otherwise you are just adding spaces to pad to the right.
print(
"vector:",
justify(
value => to_string(std_ulogic_vector'("00010110")),
-- Pointless justification specification
justified => left, -- 'right' or 'left' only.
field => 20
)
);
print(
"int:",
justify(
value => to_string(20),
field => 20
)
);
-- # time: 322 ps
-- # vector: 00010110
-- # int: 20
Conclusions
VHDL 2008 has improved the options available for formatting strings by providing for more types and more options for their formatting. Real values remain a pain for any sensible formatting as they are the ones requiring the most control, and the complex type was omitted. The justify() function is a welcome addition.