linuxlinux-kerneluarttermiosimx6

How to change Linux kernel to wait longer at UART before returning from read()?


I'm reading data from UART, running Linux 6.1 on i.MX6 ULL embedded device. No canonical mode, just raw data. termios::c_cc[VMIN] = 0, termios::c_cc[VTIME] = 20, so read() returns whenever at least one byte received or when 20 deciseconds timeout expires (I really need this timeout, see below).
One packet is 200 bytes, sent 100 times per second. I pass 200 to read() as count of bytes to expect first, then after read() call returns number of bytes received, next time I pass lesser value, etc. until all 200 bytes received.
When baudrate used is 921600 baud, read() function returns 6-12 times per packet, i.e. every read() call reads roughly 17-33 bytes.
When baudrate used is 460800 baud, read() function returns 12-24 times per packet. So, my conclusion is: there is some nearly constant time interval in Linux code when data accumulated before read() returns. Let's call it "accumulating time".
Not surprisingly, switching process 12-24 times per packet loads CPU ~2 times more than 6-12 times (and that is serious load for i.MX6 ULL - more than 20%). So I get weird situation where I have to choose between reliability (lower baudrate) and CPU load, while intuitively low baudrate should not have any drawbacks.

How to increase that "accumulating time" in Linux code to wait longer before returning from read()? I understand that it would reduce responsiveness, but in this situation I don't need it as much (at least, I don't need to know that something received 24 times per packet).

In fact, I need very easy capability from the kernel: unblock my thread only when specified count of bytes received, or when specified timeout expires; as effective as possible.

I'm experienced in fixing Linux drivers (so, building Linux, readying it's code, configuring and extending DeviceTree etc.), but this time I gave up with searching "accumulating time" value (too much code to read and understand), and I really need help from someone who is already familiar with all that code. I believe "accumulating time" value is in general Linux tty/UART-related code, or in specific i.MX6 ULL UART driver, so my first entry points were:

drivers/tty/n_tty.c
drivers/tty/serial/imx.c

although, without any success with searching.

In case someone would suggest specifying both termios::c_cc[VTIME] and termios::c_cc[VMIN]. When VMIN is not 0, meaning of VTIME actually differs: timeout is counted from first byte received, not from read() call. So if no data received at all, read() never returns.

Some additional notes.
Architecture of our application implies single dedicated thread for every UART port (we use 7 of them, that's why CPU load is so essential). Also, we need to detect stopping of sending data from outside, that's why using termios::c_cc[VTIME] timeout is critical. I understand that the thread loop could be organized different: sleep for, let's say, 100 ms in loop, and check for bytes available at UART every time; that could let achieve both collecting packets and implementing the timeout. But first, there are few nuances of our application (I could explain more in needed), which make this solution reducing the quality of the application and of the codebase quite sensitively. And second, I expect(ed) the kernel could implement the same much more effectively.
I've tested another approach: instead of using read() with termios::c_cc[VTIME] specified, I could wait in poll() (or epoll()) with timeout passed as argument, and call read() after it, with both termios::c_cc[VMIN] = termios::c_cc[VTIME] = 255. The results of using poll() (or epoll()) are the same: my thread unblocks the same number of times per packet as with naked read(). Actually, currently I use approach with poll() in my code because it allows more fine and dynamic control on timeout value; but for asking the question I used read() approach because it demonstrates the problem in simpler way.

May be one would be more pleasured to answer knowing the device we are working on is medical (electrocardiography).


Solution

  • The "some nearly constant time interval in Linux code when data accumulated before read() returns" is simply the processing time between the read() syscalls, You're searching for an "accumulating time value" in the kernel drivers that does not exist.

    How to increase that "accumulating time" in Linux code to wait longer before returning from read()?

    There is nothing in Linux kernel code to change or "increase".
    Your successive read() syscalls are not instantaneous, so there is no need for the kernel "to wait ... before returning".
    Rather the syscall overhead to return to userspace, your program's execution time to process the data and issue another (poll() and then) read(), and the syscall overhead to enter kernel mode add up to the (minimum) time interval to receive/accumulate the bytes that are returned immediately.

    You could extend the time between the read() syscalls in your program though.
    But you really cannot synchronize userspace processing with external I/O.


    You might be asking an XY question.
    IMO you are overloading the termios configuration for a "read with timeout" to perform two purposes:
    (1) fetch data from the serial terminal as fast as possible with low overhead, and
    (2) detect if there's no response or a disconnect.

    Have you tried to separate these two goals?
    For #1, use VMIN=200 and VTIME=1 to try to fetch an entire message.
    For #2, accept the fact that a read() with interbyte timeout could block. Have each reading thread indicate to a monitoring thread that it read a message. So if a read() blocks, then no indication is sent, and the monitor thread takes whatever action.