5

I have two computers with two serial ports: ttyS0 and ttyUSB0. (Actually they are on the same computer but that's just for testing). The ports are connected via null modem cable. I want to be able to simply send bytes into one end and come out the other end, and vice versa. Why doesn't the below work?:

# set both serial ports to 9600 8n1
# `-parenb` means no parity,
# `-cstopb` means 1 stop bit
# cs8 means 8 bits at a time 
stty -F /dev/ttyUSB0 cs8 -cstopb -parenb 9600
stty -F /dev/ttyS0 cs8 -cstopb -parenb 9600

# in one terminal:
echo "asdf" > /dev/ttyUSB0

# in another terminal, this hangs and does nothing
cat < /dev/ttyS0

I can do similar things with netcat and pipes very easily (below), so I feel something like the above should be possible as well.

mkfifo mypipe

# in one terminal
cat < mypipe

# in another. works as expected
echo "asdf" > mypipe
slm
  • 369,824

2 Answers2

7

Why doesn't the below work?

# in one terminal:
echo "asdf" > /dev/ttyUSB0

# in another terminal, this hangs and does nothing
cat < /dev/ttyS0

Because, as a rule, serial ports don't buffer data. If there's no client app to receive the bytes landing on the serial port, they will simply be discarded.

As an experiment, try launching minicom or cu or another serial terminal program on the receiving computer, then run the echo command again on the transmitting computer. Assuming the baud rate and framings line up, you should see "asdf" appear at the destination.

ewhac
  • 1,003
  • 2
  • 10
  • 24
  • so if you were to run cat /dev/ttyS0 & or cat /dev/ttyS0 > somefile & and then echo/pipe to the device, you will see the data come out the other side. I suspect one could also use mkfifo, pipe from the serial port to the fifo buffer then read off the fifo as desired. – thom_nic Aug 22 '17 at 17:58
  • 1
    This answer is wrong, and doesn't answer the question. The Linux kernel driver for the UART does buffer all received data for the termios subsystem. See Linux serial drivers. Additionally, the command cat < /dev/ttyS0 would create a "client app to receive the bytes landing on the serial port". A real reason for the hang could be misuse of canonical mode. – sawdust Feb 16 '22 at 00:47
  • @sawdust, I added an answer here: https://unix.stackexchange.com/a/720397/114401 – Gabriel Staples Oct 10 '22 at 04:57
3

I want to be able to simply send bytes into one end and come out the other end, and vice versa. Why doesn't the below work?

I think it's because you simply need to switch the order: start the listener first and then send the data (just like you did in your own pipe example--you started listening first):

# Absolutely first, configure each port using `stty`, as you already
# do:

set both serial ports to 9600 8n1

-parenb means no parity,

-cstopb means 1 stop bit

cs8 means 8 bits at a time

stty -F /dev/ttyUSB0 cs8 -cstopb -parenb 9600 stty -F /dev/ttyS0 cs8 -cstopb -parenb 9600

THEN, do in this order

first, in the receiving terminal, start listening

cat < /dev/ttyS0

then, in a separate terminal for sending, send the data

echo "asdf" > /dev/ttyUSB0

I am pretty confident my answer is procedurally correct, meaning: if you blindly follow it, it will work.

But, the question still remains: why? Why do you have to start listening first? Doesn't the Linux kernel buffer for you? If so, shouldn't the data just be sitting there in the buffer ready to be read? Clearly it isn't, else writing first and reading second would work. But, it doesn't.

I'm guessing here, but I think the answer here is that when the driver receives data, it first checks to see if the receiving pseudo-file at /dev/ttyUSB0 is in an opened state, held open by another process. If not, the driver discards the data, since no one is there to receive it. If the file is open, it allows the process which has the file open to read the buffered data.

Key differences between sending over a serial port and sending over an inter-process-communication (IPC) pipe

This is different from your pipe example where you make a pipe with mkfifo. I just checked, and in the pipe example, you can either start listening in one terminal first or send to the pipe in the other terminal first. It doesn't matter. It works in both cases.

If you write first, the write blocks and waits until a process reads from the pipe. If you read first, the read blocks and waits until a process writes to the pipe. This is confirmed by the Linux documentation for C's mkfifo() function, which the bash mkfifo is almost certainly based on. See man 3 mkfifo (emphasis added):

Once you have created a FIFO special file in this way, any process can open it for reading or writing, in the same way as an ordinary file. However, it has to be open at both ends simultaneously before you can proceed to do any input or output operations on it. Opening a FIFO for reading normally blocks until some other process opens the same FIFO for writing, and vice versa.

This is different from how serial ports behave, however, probably because the serial ports on each end are expected to be controlled from different machines, unlike FIFO pipes, whose read and write ends are expected to be controlled from the same machine. In the latter case, the kernel can easily track when one process tries to read and another tries to write, since it controls both ends of the pipe...hence, it allows either to block.

In the serial port case, however, the kernel has no idea if another device is listening or going to send, so it won't block on sending. To be clear: when writing to a pipe, the write is blocked until a listener is present. But, when writing to a serial port, the computer has no means of checking if a listener is present, so it never blocks! It just sends. Instead of blocking if no listener is present, data sent over serial while no-one is listening is just lost. Again, this is different from pipes, where my experiment and the documentation above both confirm that a process trying to send data over a pipe is blocked until a listener is present to read that data.

References and "see also":

  1. I'm actively learning as I write this, documenting various findings in my eRCaGuy_dotfiles repo here, in case you're interested: serial_terminals_README.md.
  2. My demo code: eRCaGuy_hello_world/bash/ipc_pipe_fifo.sh
  3. man 3 mkfifo: https://man7.org/linux/man-pages/man3/mkfifo.3.html