0

I noticed something weird yet reproducible.

I first check my serial port settings:

    bash-3.1# stty -F /dev/ttyS0
    speed 0 baud; line = 0;
    intr = <undef>; quit = <undef>; erase = <undef>; kill = <undef>; eof = <undef>; start = <undef>;
    stop = <undef>; susp = <undef>; rprnt = <undef>; werase = <undef>; lnext = <undef>; flush = <undef>;
    min = 1; time = 0;
    -cread
    -brkint -icrnl -imaxbel
    -opost -onlcr
    -isig -icanon -iexten -echo -echoe -echok -echoctl -echoke

Then change speed to 1200bps:

bash-3.1# stty -F /dev/ttyS0 1200

I then execute this fragment of my program in a function to change the baud:

fd=open(dev,O_NOCTTY | O_NONBLOCK | O_RDWR);
struct termios ser[1];
tcflush(fd,TCIFLUSH);
tcflush(fd,TCOFLUSH);
cfmakeraw(ser);
 // I call tcsetattr after each terminal setting to make sure its applied.
if (tcsetattr(fd,TCSANOW,ser) < 0){
    return -1;
}
cfsetspeed(ser,B9600);
if (tcsetattr(fd,TCSANOW,ser) < 0){
  return -2; //returns this after manually setting port via STTY
}

The problem is the baud rate does NOT get changed properly. In fact, I get -2 returned from the function and strerror(errno) returns "input/output error".

After program execution, I check system port settings:

bash-3.1# stty -F /dev/ttyS0
speed 0 baud; line = 0;
intr = <undef>; quit = <undef>; erase = <undef>; kill = <undef>; eof = <undef>; start = <undef>;
stop = <undef>; susp = <undef>; rprnt = <undef>; werase = <undef>; lnext = <undef>; flush = <undef>;
min = 1; time = 0;
-cread
-brkint -icrnl -imaxbel
-opost -onlcr
-isig -icanon -iexten -echo -echoe -echok -echoctl -echoke

And it resets to zero bps even though I specifically asked for 9600bps.

Why does it do that? and how do I force the speed to go to 9600bps programmatically?

Mike -- No longer here
  • 2,064
  • 1
  • 15
  • 37
  • Sorry, why are you using `O_NOCTTY` and `O_NONBLOCK` in the call to open? `O_NOCTTY` is not needed as you are starting the program from a session shell and the process is not a process group leader. And `O_NONBLOCK` makes no sense with a local tty device (you can set it later on, but don't do it on open call) Have you checked the result of `open(2)` for errors. You can get that errno if the open failed. – Luis Colorado May 19 '19 at 15:01

1 Answers1

1

You have many mistakes in your code.

  • You open the tty device with O_NONBLOCK, so when you issue the ioctl calls (tc*attr(3) calls result in ioctl(2) syscalls, depending on the unix flavour you are using) you don't know if the device has been already open to be able to make the tc*attr(3) calls. The same applies to O_NOCTTY flag. You have put those flags without knowledge of what is their function on the open system call. O_NOCTTY is useless in a program that is run from inside a session, and O_NONBLOCK will make your tc*attr(3) calls to return with an error (EAGAIN) if the device is not open yet, when trying to make the parameter adjustments.
  • You don't check the result of the open(2) call. This can make an error if you try to use -1 as a file descriptor (ENODEV, ENOTTY or EBADF, EINVAL, ENXIO, etc)
  • You don't initialize the data of the struct termios structure, so probably that's the reason of the error you get. As you show (your sample code snippet is not complete, one reason to tell you to read How to create a Minimal, Complete, and Verifiable example) the struct termios you use is declared on an automatic variable (because its declaration is embedded into the code) so it's for sure uninitialized and with rubbish data. You need normally to do a tcgetattr() on it to intialize to proper values, and to be able to restore the settings after your program ends.
  • bash(1) makes ioctl(2)s to set and get termios parameters on the standard input descriptor when it is connected to a tty device. If you are working with stdin, you must consider the interference of bash(1). This makes different the values you get from the ones you set with stty.
  • In general unix operating systems (not true in linux, i'm afraid) the last close to a device normally resets the parameters of a tty to standard fixed values, so the flags you set when you change a non stdin device (not the standar input, which is not last closed when stty finishes) with stty, those parameters reset to default once stty terminates (on the last close of the tty). Do a sleep 999999999 </dev/ttyBlaBla & before issuing the stty(1) command, so the port remains open (by the redirection of sleep command) after setting with stty(1).
  • READ termios(3) page, so you can set parameters from your program itself. Only if your program doesn't normally deal with setting parameters, you will not have to do it programmatically. But then there's no sense in changing the terminal parameters, so it is best to learn how to program the device parameters.

A proper way to do should be something (copied from your snippet and edited) like this:

#include <string.h> /* for strerror */
#include <errno.h> /* for errno definition */

/* ... */

fd=open(dev,O_NOCTTY | O_NONBLOCK | O_RDWR);
if (fd < 0) {
    fprintf(stderr, "OPEN: %s (errno = %d)\n",
        strerror(errno), errno);
    return -1;
}
struct termios ser; /* why an array? */
tcflush(fd,TCIFLUSH); /* unneeded, you have not used the tty yet */
tcflush(fd,TCOFLUSH); /* idem. */
/******* THIS IS THE MOST IMPORTANT THING YOU FORGOT ***********/
int res = tcgetattr(fd, &ser); /* *****this initializes the struct termios ser***** */
if (res < 0) {
    fprintf(stderr, "TCGETATTR: %s (errno = %d)\n",
        strerror(errno), errno);
    return -2; /* cannot tcgetattr */
}
/***************************************************************/
cfmakeraw(&ser); /* now it is valid to set it */
cfsetspeed(&ser,B9600);  /* better do all in only one system call */
// I call tcsetattr after each terminal setting to make sure its applied.
/* nope, tcsetattr and tcgetattr are the only calls that make something 
 * on the tty, the rest only manipulate bits on the struct termios
 * structure, but don't do anything to the terminal, you begun with a
 * trashed struct termios, so it's normal you end with an error. */
if ((res = tcsetattr(fd, TCSANOW, &ser)) < 0){
    fprintf(stderr, "ERROR: %s (errno = %d)\n",
        strerror(errno), errno); /* better to know what happened. */
    return -3; /* couldn't tcsetattr */
}

finally, this code (as yours first) has not been tested, mainly because you didn't posted a complete, minimal and verifiable example. So you'll probably need to rework it a little before including it in your code. And please RTFM (the last meaning to read termios(3) completely, and most important: How to create a Minimal, Complete, and Verifiable example) :). Also, don't check tty settings on stdin if you are using bash(1), as it normally restores tty settings after command exit, before issuing the prompt.

Luis Colorado
  • 10,974
  • 1
  • 16
  • 31
  • You raise an interesting point about the termios restoration difference between Unix and Linux. Beware that the OP seems to conflate Unix & Linux (e.g. https://stackoverflow.com/questions/55073784/commands-to-configure-unix-telnet-to-show-and-receive-hex-characters), and that for about 1.5 years (e.g. https://stackoverflow.com/questions/47023330/forcing-raw-serial-mode-in-c-linux) has been trying to get serial terminal code working while ignoring good advice along the way (e.g. https://stackoverflow.com/questions/51828498/reading-serial-data-in-linux-byte-for-byte-at-56k-reliably). – sawdust May 19 '19 at 21:31
  • While most of your remarks are spot on, your advice regarding the **open()** options seem off. O_NOCTTY is often advised for portability. Blocking I/O should certainly be used, but some installations need the O_NONBLOCK to ignore modem control signals. It's simple to revert back to blocking mode; see https://stackoverflow.com/questions/56169472/macos-blocking-serial-i-o/56175513#56175513. The cost of these open options is essentially nil, and should not be the first in the list of things-to-fix (unless you think it's the root cause of the problem). – sawdust May 19 '19 at 21:45
  • @sawdust, `O_NOCTTY` is to avoid the terminal to become a controlling tty, not something to add for portability. This requires also the process to be a process group leader, and the terminal not being a controlling terminal of another group, so it has a very specific use. Don't use options because of _somebody is recommending_, but investigate the actual reasons to use it. Installations that require `O_NONBLOCK` normally have alternate tty devices that ignore on open modem signals. This is true at least (checked) for SCO UNIX, Solaris, hp-ux, AIX, nonstop-ux (tandem) and linux. – Luis Colorado May 20 '19 at 06:25
  • @sawdust, if you are to control a modem signalled tty, then you have better not to ignore the modem controlling signals :) I show reasons to not use both options in the example shown, so you can use the alternate device, or you can force Data Carrier Detect line to on state in hardware. – Luis Colorado May 20 '19 at 06:29
  • Your answer about setting up the termios structure wins. I needed to set up a flag in termios named CLOCAL so that my port can open without having to force the DCD line on the serial port low. Otherwise the program will hang on open(). – Mike -- No longer here May 22 '19 at 02:57
  • @Mike, that's right. One option is to use `O_NONBLOCK` in open, in order to be able to set `CLOCAL`, and then change with `fcntl(2)` the mode of the file. Despite of what I said in the answer, that is not so bad. But the most important thing to do is to read the full (it's large) `termios(3)` page. – Luis Colorado May 22 '19 at 08:16