termio equivalent of stty command? - c++

I am writing a program to communicate over a serial port to a piece of hardware that utilizes XON. To make it work before my code executes I have to execute this on the command line (debian)
stty ixon -F /dev/ttyUSB0 sane raw pass8 -echo -hupcl clocal 115200
How can I do the equivalent in c++ so that I don't have to manually execute the command?
Here is what I have so far, which gets close. Writes seem to work fine. Reads are mostly fine but have a few garbled characters here and there plus data in seems to get mirrored over to the data out sometimes causing horrific loops with the hardware.
//fd is a file descriptor pointing to /dev/ttyUSB0
struct termios terminalAttributes;
memset(&terminalAttributes, 0, sizeof(struct termios));
if(tcgetattr(fd, &terminalAttributes) != 0) cout << "ERROR: Error getting terminalAttributes\n";
terminalAttributes.c_cflag = B115200 | CS8 | CREAD;
terminalAttributes.c_iflag = IGNPAR | ONLCR | IXON | IXOFF | IXANY;
terminalAttributes.c_oflag = OPOST;
terminalAttributes.c_cc[VTIME] = 0;
terminalAttributes.c_cc[VMIN] = 1;
tcsetattr(fd, TCSANOW, &terminalAttributes);

Related

Methodology to debug tcdrain() blocking indefinitely

I'm sending packets from an Ubuntu machine to an STM32 autopilot through a USB cable using write() and tcdrain():
int Serial_port::_write_port(char* buff, unsigned int len) {
const std::lock_guard<std::mutex> lock(_mutex_write_on_port);
const int bytes_written = static_cast<int>(write(_fd,buff, len));
if(bytes_written<0) {
printf("write to port failed with errno: %s\n", std::strerror(errno));
}
//wait until all data has been written to the port
tcdrain(_fd);
return bytes_written;
}
After a few thousands packets successfully sent, tcdrain() randomly blocks. I'm trying to understand why.
Any recommendations on how to debug this?
I've done some research but couldn't come across a good debug methodology.
Note: I've noticed if I run a simple python command in another terminal ser = serial.Serial('/dev/ttySAC0') this "unfreezes" my code about 75% of the time but I'm kind of at a loss to understand why.
If that helps, here is how I configure the port:
int Serial_port::_config_port() {
//Config based on following sources:
//https://github.com/mavlink/c_uart_interface_example
//https://blog.mbedded.ninja/programming/operating-systems/linux/linux-serial-ports-using-c-cpp/
struct termios config;
if(tcgetattr(_fd, &config) < 0) {
return -1;
}
// Input flags - Turn off input processing
// convert break to null byte, no CR to NL translation,
// no NL to CR translation, don't mark parity errors or breaks
// no input parity check, don't strip high bit off,
// no XON/XOFF software flow control
config.c_iflag &= ~(IGNBRK | BRKINT | ICRNL | INLCR | PARMRK | INPCK | ISTRIP | IXON);
// Output flags - Turn off output processing
// no CR to NL translation, no NL to CR-NL translation,
// no NL to CR translation, no column 0 CR suppression,
// no Ctrl-D suppression, no fill characters, no case mapping,
// no local output processing
config.c_oflag &= ~(OCRNL | ONLCR | ONLRET | ONOCR | OFILL | OPOST);
// No line processing:
// echo off, echo newline off, canonical mode off,
// extended input processing off, signal chars off
config.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);
// Turn off character processing
// clear current char size mask, no parity checking,
// no output processing, force 8 bit input
config.c_cflag &= ~(CSIZE | PARENB | CSTOPB); //~CSTOPB to set only 1 stop bit for multiflex
config.c_cflag |= CS8;
// One input byte is enough to return from read()
// Inter-character timer off
config.c_cc[VMIN] = 0; //DEBUG: this is different from mavlink recommended setup. Confirm this is ok
config.c_cc[VTIME] = 10;
//baudrate
if(cfsetispeed(&config,_baudrate)<0) return -1; //input baudrate
if(cfsetospeed(&config,_baudrate)<0) return -1; //output baudrate
//write config to port
if(tcsetattr(_fd,TCSAFLUSH,&config) < 0) {
return -1;
}
std::cout<<"port "<<_uart_name<< " configured successfully"<<std::endl;
return 0;
}

Linux serial read blocks minicom

I'm attempting to read from a serial port (/dev/ttyS4) on a BeagleBone Black, but I think(?) this should apply to all Linux devices in general.
Currently, I can set up minicom with a baud rate of 9600 and 8N1 data to read from the serial port correctly. However, if I attempt to directly cat /dev/ttyS4, nothing shows up in my terminal. My code also does this, and returns a Resource temporarily unavailable error, which I suspect is what is happening with the cat command.
If I run stty -F /dev/ttyS4, I get the following output (which, as far as I can tell, is consistent with my minicom settings):
speed 9600 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>;
-brkint -imaxbel
-opost -onclr
-isig -iexten -echo -echoe -echok -echoctl -echoke
An interesting note is that when I have minicom open, if I start my program, minicom will stop printing anything, and stay that way even if I stop my program. I need to open the serial settings again (Ctrl-A, P) and close it for minicom to resume working (it appears that nothing was changed).
My code is as follows:
int main() {
std::cout << "Starting..." << std::endl;
std::cout << "Connecting..." << std::endl;
int tty4 = open("/dev/ttyS4", O_RDWR | O_NOCTTY | O_NDELAY);
if (tty4 < 0) {
std::cout << "Error opening serial terminal." << std::endl;
}
std::cout << "Configuring..." << std::endl;
struct termios oldtio, newtio;
tcgetattr(tty4, &oldtio); // save current serial port settings
bzero(&newtio, sizeof(newtio)); // clear struct for new settings
newtio.c_cflag = B9600 | CS8 | CREAD | CLOCAL;
newtio.c_iflag = IGNPAR | ICRNL;
newtio.c_oflag = 0;
newtio.c_lflag = ICANON;
tcflush(tty4, TCIFLUSH);
tcsetattr(tty4, TCSANOW, &newtio);
std::cout << "Reading..." << std::endl;
while (true) {
uint8_t byte;
int status = read(tty4, &byte, 1);
if (status > 0) {
std::cout << (char)byte;
} else if (status == -1) {
std::cout << "\tERROR: " << strerror(errno) << std::endl;
}
}
tcsetattr(tty4, TCSANOW, &oldtio);
close(tty4);
}
Edit: I've gotten the serial port to work correctly (in python) by following Adafruit's tutorial for using python with the BeagleBone. At this point I'm certain that I'm doing something wrong; the question is what. I would much prefer using C++ over python, so it'd be great to get that working.
Your program opens the serial terminal in nonblocking mode.
int tty4 = open("/dev/ttyS4", O_RDWR | O_NOCTTY | O_NDELAY);
Nonblocking I/O, especially read operations, requires additional, special handling in a program.
Since you neglect to mention this mode, and your program has no capability to properly process this mode, this could be considered a bug.
Either remove O_NDELAY option from the open() call, or insert a fcntl(tty4, F_SETFL, 0) statement to revert back to blocking mode.
My code also does this, and returns a Resource temporarily unavailable error,
That's an EAGAIN error, which is consistent with a nonblocking read().
The man page describes this error will occur when "the file descriptor ... has been marked nonblocking (O_NONBLOCK), and the read would block".
The read() syscall "would block" because there is no data to satisfy the read request.
If you insist on using nonblocking mode, then your program must be able to cope with this condition, which is not an error but a temporary/transient status.
But blocking mode is the simpler and preferred mode of operation for typical programs in a multitasking system.
Your program should be modified as previously mentioned.
There are numerous issues with the initialization of the serial terminal.
tcgetattr(tty4, &oldtio); // save current serial port settings
The return values from the tcgetattr() and tcsetattr() syscalls are never checked for errors.
bzero(&newtio, sizeof(newtio)); // clear struct for new settings
Starting with an empty termios structure is almost always a bad idea. It may appear to work on some systems, but it is not portable code.
The proper method for initializing a termios structure is to use values from tcgetattr().
See Setting Terminal Modes Properly.
Since it is already called, all you need is newtio = oldtio to copy the structure.
newtio.c_cflag = B9600 | CS8 | CREAD | CLOCAL;
newtio.c_iflag = IGNPAR | ICRNL;
newtio.c_oflag = 0;
newtio.c_lflag = ICANON;
Rather than assignment of constants, the proper method of changing these flags is to enable or disable the individual attributes.
The following should suffice for canonical mode:
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
newtio.c_cflag |= CS8; /* 8-bit characters */
newtio.c_cflag &= ~PARENB; /* no parity bit */
newtio.c_cflag &= ~CSTOPB; /* only need 1 stop bit */
newtio.c_cflag &= ~CRTSCTS; /* no hardware flowcontrol */
newtio.c_lflag |= ICANON | ISIG; /* canonical input */
newtio.c_lflag &= ~(ECHO | ECHOE | ECHONL | IEXTEN);
newtio.c_iflag &= ~INPCK;
newtio.c_iflag |= ICRNL;
newtio.c_iflag &= ~(INLCR | IGNCR | IUCLC | IMAXBEL);
newtio.c_iflag &= ~(IXON | IXOFF | IXANY); /* no SW flowcontrol */
newtio.c_oflag &= ~OPOST;
The following is the preferred method for setting the baudrate:
cfsetospeed(&newtio, B9600);
cfsetispeed(&newtio, B9600);
If any salient attributes are left unspecified, the existing settings are used.
This can lead to erratic program behavior, e.g. sometimes it works, sometimes it doesn't.
An interesting note is that when I have minicom open, if I start my program, minicom will stop printing anything, and stay that way even if I stop my program. I need to open the serial settings again (Ctrl-A, P) and close it for minicom to resume working (it appears that nothing was changed).
The serial terminal is not intended for sharing among more than one process.
Some of the termios attributes have to be implemented in the serial device driver, which has no concept of sharing the port. The most recent termios attributes are in effect for the device.
When you execute your program after minicom has started, you are clobbering the termios attributes that minicom expects to execute with.
You are restoring the termios attributes to minicom's requirements by using its menu.

Hardware flow control with termios (CRTSCTS) for UART Device

right now I'm communicating with a device over UART in C++ on a Udoo Neo. For this I'm using termios to set up the connection and write data to the device.
For this purpose I want to use hardware flow control and have set the flag (CRTSCTS) with termios.
For the hardaware flow control I've connected the device RTS line to the boards CTS and I've also checked via oscilloscope, that the device is giving me an active high, if it is not ready to read.
The problem is that I still loose bytes in the following example of just spamming the device with numbers, but the boards output says that everything was written correctly.
I thought the UART would be blocked when using HW flow control, so that no information is lost. Am I not understanding this correctly - or is there an error in the code?
Thanks for the help
const char dev[] = "/dev/ttymxc4";
int main(int argc, char **argv) {
int fd;
struct termios t; ///< control structure for a general asynchronous interface
// edited code
tcgetattr(fd, &t);
t.c_iflag &= ~(IGNBRK | BRKINT | ICRNL |
INLCR | PARMRK | INPCK | ISTRIP | IXON);
t.c_oflag = 0;
t.c_cflag &= ~(CSIZE | PARENB);
t.c_cflag |= (CS8 | CRTSCTS);
// edited code
t.c_cflag |= (CLOCAL | CREAD);
t.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);
t.c_cc[VMIN] = 0;
t.c_cc[VTIME] = 0;
cfsetispeed(&t,B57600); /* normal shall be: B115200 Baud */
fd = ::open(dev, O_RDWR);
if (fd==-1) {
printf("UART: cannot open file: %s\n",dev);
return -1;
}
tcsetattr(fd,TCSANOW, &t);
// edited code
fcntl(fd, F_SETFL, 0);
int count = 0;
while (true) {
count++;
std::stringstream output;
output << count << ",";
::write(fd, output.str().c_str(), output.str().length());
printf("%d, writing: %s\n", fd, output.str().c_str());
usleep(10000);
}
return 0;
}
Referring to the links by #sawdust, the HW flow control is manipulated via
CCTS_OFLOW and CRTS_IFLOW via libc-doc
Macro: tcflag_t CCTS_OFLOW
If this bit is set, enable flow control of output based on the CTS wire (RS232 protocol).
Macro: tcflag_t CRTS_IFLOW
If this bit is set, enable flow control of input based on the RTS wire (RS232 protocol).
CNEW_RTSCTS and CRTSCTS via SerProgGuide
Some versions of UNIX support hardware flow control using the CTS
(Clear To Send) and RTS (Request To Send) signal lines. If the
CNEW_RTSCTS or CRTSCTS constants are defined on your system then
hardware flow control is probably supported. Do the following to
enable hardware flow control:
options.c_cflag |= CNEW_RTSCTS; /* Also called CRTSCTS */
Note the "Some versions..." and "...is probably supported."
On my particular cross compilation toolchain (Linaro GCC 6.5-2018.12) if I grep for these values, CRTSCTS is not documented, but defined, CCTS_OFLOW is in a lot of info files, but in no header files...
libc/usr/include/bits/termios.h:
174:# define CRTSCTS 020000000000 /* flow control */
As you have said in your comment
... I just thought this would be handled by the kernel?
I am seeing the phenomenon, that even if I add the relevant rts/cts properties in the device-tree ({rts,cts}-gpios or uart-has-rtscts), the command stty -a -F /dev/ttyS still reports back -crtscts meaning that the RTS/CTS handshake is disabled, so even without the userspace application this doesn't seem to be a trivial config. (Kernel 5.4)

How to change the baudrate in c/c++

Hello I need advice of how correctly to change the baud rate of the UART port in the middle of the program.
To establish communication with my desired slave I need to send the so called "break field" which consists of 13-26 (MIN 11) LOW BITS, on UART as I think it's not achievable as before and after every 8 bits UART adds start & stop bit. I had an idea of achieving it by manipulating the baud-rates send break field at 9600Bauds followed by the 19200Baud speed.
But I'm unable to see difference between baudrates on the oscilloscope. So I suppose I do it in the wrong way.
#include <iostream>
#include <termios.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/signal.h>
#include <time.h>
#include <stdlib.h>
#define BAUDRATE9600 B9600
#define BAUDRATE19200 B19200
#define PORT "/dev/ttyO4"
#define _POSIX_SOURCE 1
main()
{
int fd, c;
struct termios oldtio, newtio, midtio;
fd = open(PORT, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd <0) { perror(PORT); exit(-1); }
tcgetattr(fd, &oldtio); /* save current port settings */
/* set new port settings for canonical input processing */
midtio.c_cflag = BAUDRATE9600 | CS8 | CLOCAL | CSTOPB ;
midtio.c_iflag = IGNPAR | ICRNL;
midtio.c_oflag = 0;
midtio.c_lflag = ICANON;
newtio.c_cflag = BAUDRATE19200 | CRTSCTS | CS8 | CLOCAL | CREAD;
newtio.c_iflag = IGNPAR | ICRNL;
newtio.c_oflag = 0;
newtio.c_lflag = ICANON;
tcsetattr(fd, TCSANOW, &midtio);
char break[] = {0x00}; // break field (0000 0000)
c = write (fd, break, sizeof(break));
tcsetattr(fd, TCSANOW, &newtio);
char sync [] = {0x55};
char PID [] = {0xE2};
char data [] = {0xAA};
char data1 [] = {0xA5};
char check [] = {0xAF};
c = write (fd, sync, sizeof(sync));
c = write (fd, PID, sizeof(PID));
c = write (fd, data, sizeof(data));
c = write (fd, data1, sizeof(data1));
c = write (fd, check, sizeof(check));
/* restore old port settings */
tcsetattr(fd,TCSANOW,&oldtio);
}
I have tried it also by setting baudrates in different way as well:
cfsetispeed(&midtio, B9600)
cfset0speed(&midtio, B9600)
cfsetispeed(&newtio, B19200)
cfset0speed(&newtio, B19200)
I need to send the so called "break field"
If the goal is to send a "break", then you've asked an XY question with "How to change the baudrate in c/c++". You cannot generate a break condition by manipulating the baud rate. You would need a baud rate of less than 32 baud to achieve a quarter-second break.
A break condition can be sent on the serial link by using the TCSBRK command in an ioctl().
BTW your serial port initialization code does not conform to best programming practices. You should always be testing the return codes from system calls. The elements of the termios structure should not be directly assigned, but rather just the individual bits should be modified. Refer to a guide such as this.

TIOCEXCL in Solaris

I have a little problem. I work with serial port, for example /dev/term/0 and I need to lock multiple access to this device. For that I use this code:
int hComm;
hComm = open(portName, O_RDWR | O_NOCTTY | O_NDELAY);
if(hComm != -1){
ioctl(hComm, TIOCEXCL, NULL);
int flags = fcntl(hComm, F_GETFL, 0);
flags &= ~O_NDELAY;
fcntl(hComm, F_SETFL, flags);
}
All works fine. Then I run another application and try to open this port, I have error EBUSY - and it's OK, but in this moment my first application stop working. I can't read/write and close this port, I always have only one error ENXIO (No such device or address).
I have tested this code in Linux and Mac OS X and all works without any problems, but in Solaris...
I don't know what to do.