2

Answer, thanks to commenters:

There are some terminal settings that default to values not consistent with raw, unfiltered serial transfer. Adding the code below fixed the problem. I'm sure I'm setting/clearing more things than I need.

struct termios options;
tcgetattr(fd, &options);

cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);

options.c_iflag &= ~(IXOFF | IXANY | BRKINT | ICRNL | INLCR | PARMRK | INPCK | ISTRIP | IXON);
options.c_cflag &= ~(CRTSCTS | CLOCAL | CSIZE | PARENB | CSTOPB);
options.c_cflag |= CREAD | CS8;
options.c_lflag &= ~(ICANON | ISIG | ECHO | ECHOE | ECHONL | IEXTEN);
options.c_oflag &= ~(OPOST);
options.c_cc[VMIN]  = 1;
options.c_cc[VTIME] = 0;

tcsetattr(fd, TCSANOW, &options);

Original question:

I have a device that I want to connect to my Nvidia Jetson Orin Nano, which is running Ubuntu 20.04. It requires TTL-level serial communication, and I'm using an appropriate USB-to-serial cable. I also have my user added to the appropriate group for access to serial devices.

To start with, I can successfully access the serial device using cu. This command works:

cu -f -s 115200 -l /dev/ttyUSB0

So next I'm tying to write my own C program to access the same device.

#include <iostream>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int r;

    int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);
    if (fd == -1) {
        perror("tty ");
        return 1;
    }

    // Get current serial port options
    struct termios options;
    r = tcgetattr(fd, &options);
    if (r < 0) {
        perror("tcgetattr ");
        return 1;
    }

    // Set baud rate to 115200
    cfsetispeed(&options, B115200);
    cfsetospeed(&options, B115200);

    // Disable flow control (CTS and RTS)
    options.c_cflag &= ~CRTSCTS;

    // Set new serial port options
    r = tcsetattr(fd, TCSANOW, &options);
    if (r < 0) {
        perror("tcsetattr ");
        return 1;
    }

    char buf;
    while (1) {
        r = read(fd, &buf, 1);
        if (r < 0) {
            perror("read ");
            return 1;
        }
        printf("%02x\n", 255 & buf);
    }

    close(fd);
    return 0;
}

Unfortunately, this program doesn't work. The open succeeds. The tcgetattr and tcsetattr succeed. But the read just sits there and does nothing.

I ran an strace on cu to see what was different (see paste of part of the trace below), but aside from checking for a lock file (which doesn't exist), I can't tell what could possibly be relevant here.

Anyone have any ideas as to what I'm doing wrong?

Thanks!

[Mostly what precedes this is things like dynamically linking shared object libraries, setting signal handlers, closing already-closed fd's, and trying to access various files that don't exist.]
getpid()                                = 17612
openat(AT_FDCWD, "/var/lock/TMP00000044cc", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 3
write(3, "     17612\n", 11)            = 11
close(3)                                = 0
linkat(AT_FDCWD, "/var/lock/TMP00000044cc", AT_FDCWD, "/var/lock/LCK..ttyUSB0", 0) = 0
unlinkat(AT_FDCWD, "/var/lock/TMP00000044cc", 0) = 0
geteuid()                               = 1000
getuid()                                = 1000
getegid()                               = 1000
getgid()                                = 1000
setregid(1000, 1000)                    = 0
setreuid(1000, 1000)                    = 0
openat(AT_FDCWD, "/dev/ttyUSB0", O_RDWR|O_NONBLOCK) = 3
geteuid()                               = 1000
getuid()                                = 1000
getegid()                               = 1000
getgid()                                = 1000
setregid(1000, 1000)                    = 0
setreuid(1000, 1000)                    = 0
fcntl(3, F_GETFD)                       = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
faccessat(AT_FDCWD, "/dev/ttyUSB0", R_OK|W_OK) = 0
fcntl(3, F_GETFL)                       = 0x20802 (flags O_RDWR|O_NONBLOCK|O_LARGEFILE)
fcntl(3, F_SETFL, O_RDWR|O_LARGEFILE)   = 0
ioctl(3, TCGETS, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, TCFLSH, TCIFLUSH)              = 0
ioctl(3, TCGETS, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, SNDCTL_TMR_START or TCSETS, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, TCGETS, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, TIOCSCTTY, 0)                  = -1 EPERM (Operation not permitted)
ioctl(3, TCGETS, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, SNDCTL_TMR_START or TCSETS, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, TCGETS, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, TCGETS, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, SNDCTL_TMR_STOP or TCSETSW, {B115200 -opost -isig -icanon -echo ...}) = 0
ioctl(3, TCGETS, {B115200 -opost -isig -icanon -echo ...}) = 0
faccessat(AT_FDCWD, "/dev/ttyUSB0", R_OK|W_OK) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
write(1, "\7Connected.\n", 12)          = 12
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_START or TCSETS, {B38400 -opost -isig -icanon -echo ...}) = 0
ioctl(0, TCGETS, {B38400 -opost -isig -icanon -echo ...}) = 0
pipe2([4, 5], 0)                        = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xffffa25c8d00) = 17613
close(5)                                = 0
read(0, 0xffffe67adad6, 1)              = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=17614, si_uid=1000} ---
rt_sigreturn({mask=[]})                 = -1 EINTR (Interrupted system call)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=17613, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
ioctl(0, TCGETS, {B38400 -opost -isig -icanon -echo ...}) = 0
ioctl(0, SNDCTL_TMR_START or TCSETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(2, "cu: Got termination signal\n", 27) = 27
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_START or TCSETS, {B38400 -opost -isig -icanon -echo ...}) = 0
ioctl(0, TCGETS, {B38400 -opost -isig -icanon -echo ...}) = 0
close(4)                                = 0
Timothy Miller
  • 1,527
  • 4
  • 28
  • 48
  • 1
    "*Anyone have any ideas as to what I'm doing wrong?*" -- Your termios configuration is incomplete. You only set the baudrate and HW flow control, and don't even bother to set framing. Hence you reuse existing line and termios settings. So you don't even know if the serial terminal is in *canonical* mode or *raw* mode! All you know is that the serial terminal is blocked waiting for some *unknown* criteria to be met. See https://stackoverflow.com/questions/25996171/linux-blocking-vs-non-blocking-serial-read/26006680#26006680 – sawdust Aug 07 '23 at 18:53
  • 1
    Try to configure character size, parity, flow control, etc. Does remote peer send something periodically or need some trigger to send out data? Try to debug your program with another UART connected to host and opened by minicom/picocom. – dimich Aug 07 '23 at 18:54
  • Thanks for the help. It'd like to make sure y'all get credit for helping, so please post an answer, which you can copy from my update, and then I'll vote up your answers. – Timothy Miller Aug 07 '23 at 19:59

1 Answers1

1

Anyone have any ideas as to what I'm doing wrong?

Your termios configuration is incomplete. You only configure the baudrate and HW flow control, and don't even bother to set framing. Hence you reuse existing line and termios settings.
So you don't even know if the serial terminal is in canonical mode or raw mode!
All you know is that the serial terminal is blocked and waiting for some unknown criteria to be met. See Linux Blocking vs. non Blocking Serial Read for what can unblock a read() based on termios configuration.


There are some terminal settings that default to values not consistent with ...

You simply cannot rely on any "default" termios settings because (a) there is no such thing as a "default configuration", and (b) you don't know what configuration the previous program left that serial terminal in.
If you want your program to be reliable and robust, your program must perform a full termios configuration, and set or clear every attribute that it requires.


Your "answer" has a bug in the statement:

options.c_cflag &= ~(CRTSCTS | CLOCAL | CSIZE | PARENB | CSTOPB);

The CLOCAL attribute should always be enabled because it is supposed to ignore the modem control lines. You would want to "ensure that your program does not become the 'owner' of the port subject to sporatic [sic] job control and hangup signals" (according to the Serial Programming Guide for POSIX Operating Systems).

I'm sure I'm setting/clearing more things than I need.

For a minimal termios configuration for noncanonical mode see this answer.

sawdust
  • 16,103
  • 3
  • 40
  • 50
  • Thanks for the correction. I'm guessing ~CRTSCTS overrode CLOCAL being off, but I fixed it in the code anyway. – Timothy Miller Aug 10 '23 at 00:41
  • The "modem control lines" are signals generated by the modem itself, such as Data Carrier Detect (DCD), rather than a signal from the remote end (such as HW handshake) of the serial link. There's no overlap or "*override*". Since you're not connected to a modem, such input signals should be ignored (since the input pins could be floating). – sawdust Aug 10 '23 at 00:58