Timeout bug in GNAT.Serial_Communications

I’m trying to migrate an Ada application that communicates with a piece of hardware over a serial port, from Linux poll() and read() to GNAT.Serial_Communications.

I’m using a GCC 14.2.0 cross-toolchain targeting AArch64 and running my software on a Raspberry Pi 3.

I have found what appears to be a bug, documentation deficiency and/or an annoyance: The Timeout value (of type Duration) seems to be rounded to the nearest 100 milliseconds. The following reduced test program illustrates:

WITH Ada.IO_Exceptions;
WITH Ada.Real_Time;
WITH Ada.Text_IO; USE Ada.Text_IO;
WITH GNAT.Serial_Communications;

USE TYPE Ada.Real_Time.Time;

PROCEDURE test_sercom IS

  PACKAGE sercom RENAMES GNAT.Serial_Communications;

  port : ALIASED sercom.Serial_Port;
  p    : CONSTANT ACCESS sercom.Serial_Port := port'Access;
  c    : Character;
  t0   : Ada.Real_Time.Time;
  t1   : Ada.Real_time.Time;

BEGIN
  sercom.Open(port, "/dev/ttyAMA0");
  sercom.Set(port, sercom.B115200, Block => True, Timeout => 0.051);

  FOR i IN 1 .. 10 LOOP
    t0 := Ada.Real_Time.Clock;

    BEGIN
      Character'Read(p, c);
      Put(c);
    EXCEPTION
      WHEN Ada.IO_Exceptions.End_Error | GNAT.Serial_Communications.Serial_Error =>
        NULL;
    END;

    t1 := Ada.Real_Time.Clock;
    Put_Line(Duration'Image(Ada.Real_Time.To_Duration(t1 - t0)));
  END LOOP;
END test_sercom;

With a Timeout value of 0.51, I get the following output (not exactly what I expected, but close enough):

 0.102885328
 0.102148562
 0.103959852
 0.103983029
 0.103982769
 0.103981623
 0.103985842
 0.103979852
 0.103984279
 0.103985689

With a Timeout value of 0.49, I get the following erroneous output:

 0.001932892
 0.000055260
 0.000049583
 0.000049010
 0.000049895
 0.000049635
 0.000049062
 0.000048958
 0.000049479
 0.000048437

I would like to achieve a loop cycle time of about 1 to 5 ms, which I can tune for responsiveness versus CPU utilization. (My actual code has the loop in a background task that owns the serial port. The main program (environment task) communicates with the background task via a pair of FIFO queues, one for each direction, as is commonly done with FreeRTOS on single chip microcontrollers).

Unfortunately the GNAT.Serial_Communications in GCC 14.2.0 seems to allow loop cycle times of either 48 to 55 microseconds or 103 milliseconds.

I’ve also noticed another anomaly: In my code the Block parameter to GNAT.Serial_Communications.Set has no effect despite what the internal documentation asserts.

For now I’ve implemented a workaround: Replace the NULL statement in the exception handler with DELAY 0.005. I’ve sort of convinced myself that this will be mostly equivalent to the timeout working properly and testing seems to affirm this.

The GNAT.Serial_Communications implementation calls tcsetattr with the VTIME parameter in “non-canonical” mode.

The behavior of libc’s termios implementation is defined by POSIX:

TIME is a timer of 0.1 second granularity that is used to time out bursty and short-term data transmissions.
POSIX 11.1.7 Non-Canonical Mode Input Processing

Timeouts with higher resolution are not possible with the current implementation of GNAT.Serial_Communications. You will need to use a different library or rewrite the implementation to use a different kernel interface. What was wrong with using poll()?

1 Like

poll() and read() worked perfectly on Linux. I was just trying to get my code working on Windows and maybe even MacOS as well.

Out for curiosity, why are you using blocking I/O?
I have used GNAT.Serial_Communications a lot (MODBUS and various proprietary protocols) and always in an asynchronous scenario where Timeout makes sense.

It’s kind of a hack, but one option might be to go full non blocking IO, then use a task to poll the serial port. You can then do a timed select on the task’s entry similar to:

select 
   Serial_Poll.Get_Character(Value : out Character);
   Ada.Text_IO.Put_Line("Found a character");
or delay 0.005;
   Ada.Text_IO.Put_Line("Timed out");
end select;

Then you can rely on the precision of your Ada runtime to get something closer to 1-5ms.

1 Like

It is not a hack, used in production code.
Of course rather than a task entry it is a protected object (input FIFO) entry. The reader task writes FIFO, signals not-empty and timeout events.

Something else I thought of, but I am not experienced enough to know if this is a good idea or not. You can also look into “Asynchronous Transfer of Control”. Again you go full non blocking, then create a loop that checks for characters but inserts a “synchronization point” in the loop so it can be aborted:

    function Get_Character return Character is
	begin
		while not Serial_Character_Available loop
			delay 0.0;  -- Important, need a synchronization point
		end loop;
		return Serial_Get_Character;
	end Get_Character;

Then you can use it in a special select block:

    select
		delay 0.005;
		Ada.Text_IO.Put_Line("Timed Out");
	then abort
		Value := Get_Character;
		Ada.Text_IO.Put_Line("Character Found");
	end select;

In that example, if the delay completes before the bottom part fully finishes, then it will do the stuff after the delay and leave the block. Note that the Put_Line’s probably take longer than the 5ms, so they would need to go, but wanted to highlight where the code goes in each section.

I don’t know if this is good or bad to try, but another option to play with.

I actually wasn’t. I tested various times supplying True or False for the Block argument to Set without any difference in behavior.

I think the following code fragment from g-sercom.adb from GCC 14.2.0 explains:

      if Block and then Timeout = 0.0 then

which suggests that blocking only happens in conjunction with a zero timeout. I never tested with zero timeout until I noticed the above test. Testing with zero timeout then works as expected. The weird thing is that with Timeout < 0.05 you get no blocking and no waiting regardless of the value for Block.

That is what I was originally doing. The code in question is a device driver for the Wio-E5 LoRa Transceiver Module, originally written for Linux. A background task owns the serial port, with a call to poll() followed by a call to read() to receive serial port data. With a poll() timeout of 1 ms, which determines the event loop cycle time, the driver uses about 25% CPU on a Raspberry Pi Zero 2 W, but is fully responsive at the highest RF frame rate (which, for LoRa, is pretty low).

Being an engineer, and never satisfied, after it was working on Linux I wanted it to work on Windows, too…

Timeout is not used with blocking. In Set it says:

   --  The communication port settings. If Block is set then a read call
   --  will wait for the whole buffer to be filed. If Block is not set then
   --  the given Timeout (in seconds) is used.

What is in the source:

      if Block and then Timeout = 0.0 then
         --  MIN > 0, TIME == 0 (blocking read)
         Current.c_cc (VMIN)  := char'Val (1);
         Current.c_cc (VTIME) := char'Val (0);

      else
         --  MIN == 0, TIME > 0  (read with timeout)
         --  MIN == 0, TIME == 0 (polling read)
         Current.c_cc (VMIN)  := char'Val (0);
         Current.c_cc (VTIME) := char'Val (Natural (Timeout * 10));

         Current.c_lflag := Current.c_lflag or (not ICANON);
      end if;

is that Timeout under Linux in connection with blocking (Note, I never did it blocking mode) clears ICANON flag. Timeout = 0.0 does the canonical input, that is with line ends etc. When Timeout > 0.0 it should not wait for line end. See

canonical and noncanonical mode.