truncating time in VHDL

A physical literal is comprised of an abstract literal and a unit with at least one separator. The abstract literal, here a decimal literal is either of type universal integer or universal real depending on the presence of a (decimal) point. For an initial value expression of a variable of type TIME that would occur during elaboration.

Note that t1 85.84999999999999 ms got rounded up to 85.84 ms when evaluated. If you trimmed two of the nines off the right end of the fraction part limiting expressing time to an accuracy of femtoseconds there'd be no rounding here.

The mantissa of an IEEE 64 bit floating point value for universal real (supported by all current implementations) is also a limiting factor on accuracy. A 64 bit value of type universal integer (no point) in be more accurate. The standard is carefully worded to make this behavior acceptable.

How can you preserve all the precision possible? Perform truncation textually.

In this answer a modified write procedure is provided capable of specifying the number of digits after the point. It uses the existing write procedure and calls a justify function (introduced in -2008) to provide full features.

It's compatible with the existing write procedure with the extra parameter digits set to 0. Here in it's own library it doesn't share the existing write procedure's name to avoid ambiguity with default values for a different number of parameters when both declarations are made directly visible through use clauses.

use std.textio.all;

package write_time_pkg is
    -- write TIME value to a line at an arbitrary precision
    procedure write_precision ( 
        l:          inout line;
        value:      in    time;
        justified:  in    side := right;
        field:      in    width := 0;
        unit:       in    time := ns;
        digits:     in    natural := 0  -- digits to right of decimal point
    );
end package;

package body write_time_pkg is
    function zeros (num: natural) return string is
        variable rets:  string (1 to num) := (others => '0');
    begin
        return rets;
    end function;
    
    function spaces (num: natural) return string is
        variable rets: string (1 to num) := (others => ' ');
    begin
        return rets;
    end function;
    
    function justify (
        value:      string;
        justified:  side := right;
        field:      width := 0
    ) return string is
      constant len : width :=  value'length;
    begin
      if field <= len then
        return value;
      else
        case justified is
          when right =>
            return spaces (field - len) & value;
          when left =>
            return value & spaces (field - len);
        end case;
      end if;
    end function justify;
    
    procedure write_precision (
            l:          inout line;
            value:      in    time;
            justified:  in    side := right; 
            field:      in    width := 0;
            unit:       in    time := ns; 
            digits:     in    natural := 0 -- to right of decimal point
    ) is
        variable editbuf:   line;
        variable point:     natural;
        variable separator: natural;
    begin
        if digits = 0 then -- No decimal place precision specified
            write (l, value, justified, field, unit); -- package std.textio
            return;
        else
            write (editbuf, VALUE, unit => unit); -- defaults, field = 0
        end if;
        
        for i in 1 to editbuf.all'length loop -- find point and units
            if editbuf.all(i) = '.' then
                point := i;
            elsif editbuf.all(i) = ' ' then
                separator := i;  -- unit immediately following space
                exit;            -- in string representation
            end if;
        end loop;
        
        if point = 0 then  -- No fraction part present
            write (l,
                justify (editbuf.all(1 to separator - 1) & 
                    '.' & zeros(digits) & -- added fraction part
                    editbuf.all(separator to editbuf.all'length), -- unit
                    justified, field
                )
            );
        elsif separator - 1 - point < digits then -- Not enough fraction part
            write (l,
                justify (editbuf.all (1 to separator - 1) &
                    zeros (digits - (separator - 1 - point)) &  -- fill
                    editbuf.all(separator to editbuf.all'length), -- unit
                    justified, field
                )
            );
        else  -- Truncate including to same length
            write (l,
                justify (editbuf.all (1 to point + digits) &
                    editbuf.all(separator to editbuf.all'length), -- unit
                    justified, field
                )
            );
        end if;
        deallocate (editbuf);
    end procedure write_precision;
end package body;

It's functionality can be demonstrated:

use std.TEXTIO.all;
use work.write_time_pkg.all; 

entity truncate is
end entity;

architecture foo of truncate is
    constant t1:    time := 85.849_999_999_999 ms; -- WAS 85.849_999_999_999_99
                         -- ms  us  ns  ps  fs            ms  us  ns  ps  fs ?
    -- trimmed fraction of fs so evaluation of physical literal doesn't round up
    constant t2:    time := 1 ms;
    constant t3:    time := 85.849_999_999_999_99 ms;  -- fractions of femtosec
begin
    process
        variable buf:       line;
    begin
        write_precision (buf, t1, unit => ms, digits => 2);
        report LF & HT & "t1 = " & time'image(t1);
        report LF & HT & "t1 ms, truncated to 1/100 ms = " & string'(buf.all);
        deallocate (buf);
        write_precision (buf, t2, unit => ms, digits => 2);
        report LF & HT & "t2 = " & time'image(t2);
        report LF & HT & "t2 ms, truncated to 1/100 ms = " & string'(buf.all); 
        deallocate (buf);
        write_precision (buf, t3, unit => ms, digits => 4);
        report LF & HT & "t3 = " & time'image(t3);
        report LF & HT & "t3 ms, truncated to 1/10000 ms = " & string'(buf.all); 
        deallocate (buf);
        write_precision (buf, t1, justified => right, field => 24, 
                         unit => ms, digits => 2);
        report LF & HT & "t1 = " & time'image(t1);
        report LF & HT & "t1 ms, to 1/100 ms, justified - " & string'(buf.all); 
        deallocate (buf);
        write_precision (buf, t1, justified => right, field => 20, 
                         unit => ms, digits => 0);
        report LF & HT & "t1 = " & time'image(t1);
        report LF & HT & "t1 ms, std.textio.write justified - " & string'(buf.all); 
        deallocate (buf);
        wait;
    end process; 
end architecture;

This produces:

/usr/local/bin/ghdl -r  truncate
truncate.vhdl:18:9:@0ms:(report note):
    t1 = 85849999999999 fs
truncate.vhdl:19:9:@0ms:(report note):
    t1 ms, truncated to 1/100 ms = 85.84 ms
truncate.vhdl:22:9:@0ms:(report note):
    t2 = 1000000000000 fs
truncate.vhdl:23:9:@0ms:(report note):
    t2 ms, truncated to 1/100 ms = 1.00 ms
truncate.vhdl:26:9:@0ms:(report note):
    t3 = 85850000000000 fs
truncate.vhdl:27:9:@0ms:(report note):
    t3 ms, truncated to 1/10000 ms = 85.8500 ms
truncate.vhdl:31:9:@0ms:(report note):
    t1 = 85849999999999 fs
truncate.vhdl:32:9:@0ms:(report note):
    t1 ms, to 1/100 ms, justified -                 85.84 ms
truncate.vhdl:36:9:@0ms:(report note):
    t1 = 85849999999999 fs
truncate.vhdl:37:9:@0ms:(report note):
    t1 ms, std.textio.write justified -   85.849999999999 ms
%:

All the added arithmetic is character array string index manipulation in one of three cases.