Linux serial read blocks minicom - c++

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.

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;
}

How do i set up termios parameters in C++ for a Raspberry PI USB UART connection?

I have a RPI 3 and a LoRa USB module and I'm writing some code in C++ for the port connection to the device. I am able to connect to the port (which is assigned as ttyUSBPort1 in a udev rule). However when I am sending data to the port, I'm getting an error. I just don't know enough about termios and port communications to determine if that's the problem (yes, I've read the manpage).
The LoRa module is a RN2903 device and the following is the UART Interface instructions on the reference sheet:
All of the RN2903 module’s settings and commands are transmitted over UART using
the ASCII interface.
All commands need to be terminated with < CR >< LF > (spaces added for formatting) and any replies they generate will
also be terminated by the same sequence.
The default settings for the UART interface are 57600 bps, 8 bits, no parity, 1 Stop bit,
no flow control.
When sending commands, I can see that the device is responding with "invalid_parameter" by monitoring the port with
sudo cat /dev/ttyUSBPort1
I am assuming either I have some of the termios flags set incorrectly, or the write command set up incorrectly. Here's the code I have that sets up the port:
int openPort(void) {
struct termios tty;
memset(&tty, 0, sizeof tty);
if ((usb_port = open(device, O_RDWR))>=0) {// | O_NOCTTY | O_SYNC
std::cout << "DEVICE OPENED: " << device << " handle number: " << usb_port << std::endl;
} else {
fprintf(stderr, "unable to open serial device");
return -1;
}
if(tcgetattr(usb_port, &tty) != 0) {
printf("Error %i \n", errno);
}
cfsetispeed(&tty, B57600);
cfsetospeed(&tty, B57600);
tty.c_cflag &= ~PARENB; // Make 8n1
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag &= ~CRTSCTS; // no flow control
tty.c_cc[VMIN] = 0; // read doesn't block
tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout
tcflush( usb_port, TCIFLUSH );
if (tcsetattr(usb_port, TCSANOW, &tty) != 0) {
printf("Error %i\n", errno);
}
return usb_port;
}
And here's the call command to get the version information from the device:
void radioCMD(string tmp) {
string tmp2 = tmp + "\r\n";
int n = tmp2.length();
char cmd[n];
strcpy(cmd, tmp2.c_str());
std::cout << write(usb_port, cmd, sizeof(cmd)) << " " << cmd << "Writing to " << usb_port << " Delay: " << delay << " Command Size: " << sizeof(cmd) << std::endl;
}
void setupRadio() {
radioCMD("sys get ver");
usleep(delay);
}
When writing to the console std::cout, I am seeing this:
13 sys get ver
Writing to 3 Delay: 200000 Command Size: 13
showing that the message is indeed being correctly written.
The cat output from the device should respond with something like this (from the datasheet):
2.3.6.1
sys get ver
Response: RN2903 X.Y.Z MMM DD YYYY HH:MM:SS, where X.Y.Z is the firmware
version, MMM is month, DD is day, HH:MM:SS is hour, minutes, seconds (format: [HW]
[FW] [Date] [Time]). [Date] and [Time] refer to the release of the firmware.
This command returns the information related to the hardware platform, firmware
version, release date and time-stamp on firmware creation.
Example: sys get ver
What I actually get is "invalid_param\r\n", which is the appropriate response from the device if something in the call is not correct.
Any ideas where I might be going wrong here?
EDIT
thanks to Ted for pointing me in the right direction and simplifying my code. There were two missing termios flags. Once I set these (last two), it works fine.
tty.c_cflag &= ~PARENB; // Make 8n1
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
tty.c_cflag &= ~CRTSCTS; // no flow control
tty.c_cc[VMIN] = 0; // read doesn't block
tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout
***ADDITIONAL TWO FLAGS THAT FIXED IT****
tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
tty.c_oflag &= ~OCRNL; // Prevent conversion of newline to carriage return/line feed
New Write Call Function:
void radioCMD(string cmd) {
cmd += "\r\n";
write(usb_port, cmd.c_str(), cmd.size());
}
This creates a VLA that is one char too short to fit the null terminator of the C string:
void radioCMD(string tmp) {
string tmp2 = tmp + "\r\n";
int n = tmp2.length();
char cmd[n]; // should be n+1 for strcpy to work
strcpy(cmd, tmp2.c_str()); // undefined behavior \0 is written out of bounds
write(usb_port, cmd, sizeof(cmd)); // sizeof(cmd) should be replaced by n
}
A better alternative would be to use std::memcpy instead of std::strcpy to copy the C string without the null terminator - and to avoid VLA:s.
An even better alternative would be to use the std::string you get as a parameter directly:
void radioCMD(string cmd) {
cmd += "\r\n";
write(usb_port, cmd.c_str(), cmd.size());
}
It may not be the only problem, but as the std::strcpy currently makes your program have undefined behavior, it's a good place to start.

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)

termio equivalent of stty command?

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);

Unix: How to clear the serial port I/O buffer?

I am working on a "high level" C++ interface for the standard PC serial port. When I open the port, I would like to clear the input and output buffers in order not to receive or send data from previous usage of the port. To do that, I use the tcflush function. However, it does not work. How can that be? My "port opening" code can be seen below. Yes I use C++ exceptions but none are getting thrown. That indicates that tcflush returns 0 but it does not clear the buffer.
The only way I can clear the input buffer is to read bytes from it until there is none left. This usually takes a couple of seconds and I do not think of it as a solution.
Thanks in advance :-)
fd = ::open(port.c_str(), O_RDWR | O_NOCTTY);
if (fd < 0)
{
throw OpenPortException(port);
return;
}
// Get options
tcgetattr(fd, &options);
// Set default baud rate 9600, 1 stop bit, 8 bit data length, no parity
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
// Default timeout (1000 ms)
options.c_cc[VMIN] = 0;
options.c_cc[VTIME] = 10;
// Additional options
options.c_cflag |= (CLOCAL | CREAD);
this->port = port;
// Apply the settings now
if (tcsetattr(fd, TCSANOW, &options) != 0)
{
throw PortSettingsException();
}
// Flush the port
if (tcflush(fd, TCIOFLUSH) != 0)
{
throw IOException();
}
This is the correct way (as below):
usleep(1000);
ioctl(fd, TCFLSH, 0); // flush receive
ioctl(fd, TCFLSH, 1); // flush transmit
ioctl(fd, TCFLSH, 2); // flush both
User can choose both of the first 2 lines OR last line alone based on requirement. Please check if sleep may be required.
Try
ioctl(fd, TCFLUSH, dir)
with dir equal to 0 for receive, 1 for transmit, 2 for both.