10

I'm developing a receiver for a small hardware project. I'm working on a small board that uses UART to transfer data.
The receiver code is shown in full below, I'll explain the problematic bits separately shortly.

#define TTY "/dev/ttys002"

#include <stdio.h>
#include <string.h>
#include <unistd.h> //Unix standard functions
#include <fcntl.h> //File controls
#include <errno.h> //Error numbers
#include <assert.h>
#include <termios.h> //Posix terminal

int open_port(const char * tty) {
    int fd;
    fd = open(tty, (O_RDWR | O_NOCTTY | O_NDELAY));
    assert("__failed to open port__" && fd != -1);
    //Block until read
    fcntl(fd, F_SETFL, 0);
    return fd;
}

void configure_port(int fd) {
    struct termios options;
    //Get current options
    tcgetattr(fd, &options);

    //9600 Baud
    cfsetispeed(&options, B9600);
    cfsetospeed(&options, B9600);

    //Receive & local mode
    options.c_cflag |= (CLOCAL | CREAD);

    //Raw output
    options.c_oflag &= ~OPOST;

    //No hardware flow control
    options.c_cflag &= ~CRTSCTS;

    //No parity, 1 stop bit
    options.c_cflag &= ~PARENB;
    options.c_cflag &= ~CSTOPB;

    //8 data bits
    options.c_cflag &= ~CSIZE;
    options.c_cflag |= CS8;

    //Write options back, set them immediately
    tcsetattr(fd, TCSANOW, &options);
}

int main(int argc, const char * argv[]) {
    int fd = open_port(TTY);
    const size_t count = 8;
    char buf[count + 1];
    ssize_t n;
    configure_port(fd);
    while (1) {
        n = read(fd, buf, count);
        buf[count] = '\0';
        if (n > 0) {
            printf("%s\n", buf);
        }
    }
    return 0;
}

Since I don't have my hardware at hand currently, I decided to test my receiver over a regular tty (#define TTY "/dev/ttys002"). To test it, I simply compiled and ran the above code and then opened a separate terminal and:

echo "text" >> /dev/ttys002

All this works fine and well and I get all the data I'm echoing into the tty.

However, the problem arises when I input a long message into the tty:

echo "this is a longer test message" >> /dev/ttys002

I receive the whole message as a single string in my program output. Why is this so? I would have expected the text to be split up into blocks of 8 chars (const size_t count = 8;).

If it's of importance, I'm using this guide as my go-to for the configuration.

Edit: Please see the comments for further discussion on the issue.

Etheryte
  • 24,589
  • 11
  • 71
  • 116
  • 1
    It cannot work without having invoked Undefined Behavior at *some* point (possibly more than one). For instance, you don't add a terminating null to the read string before printing it; also, logic dictates that the longer text had to be stored *somewhere*, even if it had to overwrite parts of your program or earlier stored data. So I believe your "it appears to work but why" is just a symptom of this UB. – Jongware Jun 05 '15 at 13:35
  • 1
    .. I changed `printf("%s\n", buf);` to `printf("[%s]\n", buf);` but it printed the original string *only*. Something is wrong – but possibly with my test setup. Please try nevertheless, because if it behaves the same with you then your starting premise is wrong. – Jongware Jun 05 '15 at 14:10
  • @Jongware It works the same for me, to my surprise, which is something I didn't notice before. Thanks a lot, that's valuable information. The next piece of the puzzle is _why_ it works like that. – Etheryte Jun 05 '15 at 14:16
  • 1
    One thing is sure: *your text never appears in* `buf`. I just removed everything inside the `while` loop, leaving only `while (1) {}` ... and it made No Difference Whatsoever. – Jongware Jun 05 '15 at 14:22
  • @Jongware Thanks a lot for your input, it appears my error lies somewhere else entirely than I originally thought. Possibly a configuration issue? – Etheryte Jun 05 '15 at 14:24
  • 1
    I tried with and without Serge's `ICANON` added and got .. surprising results (which, in afterthought, did not relate to the `ICANON` setting). Add a line `printf ("%ld bytes received\n", n);` and restart. Start another terminal and start up `man termios`. Now *some* characters typed into the `man` terminal *will* be echoed to your trap! .. This needs a terminal expert. – Jongware Jun 05 '15 at 14:38
  • 1
    Yes, it seems the whole problem is related to testing this on ttys, but I'm not all that familiar to know how to research it further until I get my hands on actual hardware again. – Etheryte Jun 05 '15 at 15:21
  • @DavidSchwartz That's more or less unrelated to the central issue here if you'd read the comments, I've edited the code to account for this mistake. – Etheryte Jun 05 '15 at 15:49
  • @Nit Are you saying that `read(fd, buf, count)` returns a value greater than `count`? If not, what's the central issue? – David Schwartz Jun 05 '15 at 19:05
  • @Nit: since the purpose of this code was to test your receiver code and it can't do that because of some not-easily found behavior of *native* TTYs, I would suggest asking a new question. Whether that is "how to redirect/catch TTY thru-put" or "how to test abstracted code for an UART receiver" is up to you. – Jongware Jun 06 '15 at 14:11
  • Run your code under `strace`, to see the return value from every `read()` system call. – Peter Cordes Jul 05 '15 at 23:32
  • what value of n are you getting? – mksteve Aug 10 '15 at 06:29
  • Linux? If so should be tagged as such. – Ciro Santilli OurBigBook.com Oct 29 '15 at 22:26

3 Answers3

1

IMHO, your message is split into blocks of eight chars : n = read(fd, buf, count); cannot give more than count bytes at a time.

But as you do not configure the tty line in RAW mode, is is still in line buffered mode. So the underlying driver blocks the first read until it has a full line terminated with a \n (or exceeding the buffer capacity).

Then the read returns with the first 8 bytes, and next reads immediately return with 8 bytes too since data is available in driver buffer.

You should look at non canonical mode in man termios, if you want to use the raw input mode :

options.c_cflags &= ~ICANON;
/* optionally if defaults are not appropriates */
options.c_cc[VMIN] = vmin; /* default is 1 */
options.c_cc[VTIME] = vtime; /* default is 0 */

But anyway, you read chararacters and never add the terminating null, so buf has no reason to be null terminated. You should only print the number of characters actually read:

const size_t count = 8;
char buf[count];
ssize_t n;
configure_port(fd);
while (1) {
    n = read(fd, buf, count);
    if (n > 0) {
        printf("%.*s\n", n, buf); /* only print the n characters */
    }
}
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • 2
    Ok, but `printf("%s\n", buf);` has to print the string divided into different lines 8 chars lenght. Is it correct? Problem is the null termination. – LPs Jun 05 '15 at 13:40
0

It seems that your problem has the same name that this site :)

You're allocating 8 bytes of data on the stack for your buf variable. Then, in read function, you're writing "this is a longer test message" string, which length is much more than 8 bytes. When you're using printf function with "%s", printf will stop printing when it will reach a null character(\0) because it expects your string to be null-terminated. In your case, you should make a check whether your buf variable is null-terminated. If it's not, you should make the last byte of the bufto be a null character

floyd
  • 692
  • 8
  • 23
  • 1
    Not a good solution!! You would throw away the last character if there were exactly 8! – Jongware Jun 05 '15 at 13:45
  • Right, but in fact if you're creating a string with a length of 8, it expects that the actual length of useful data in the string will be 7, and the last byte will be a null-character. Otherwise, if you will write a null-character after the last byte of your string, you will actually corrupt the stack. – floyd Jun 05 '15 at 13:51
  • 1
    The `read` man page says *nothing* on how to use it with strings - because it should not. – Jongware Jun 05 '15 at 13:53
  • 2
    It's not a good practice at all to create a char array with a length of 8 and to write 8 bytes of data to it. If you want to print exactly 8 characters, you should allocate 9 bytes of data on a stack for them - it's just a common rule for handling strings in C. – floyd Jun 05 '15 at 14:01
  • 2
    You can choose whether you'll use `n` byte long array or `n+1` byte long array for storing `n` bytes of text depending on what suits you better. But then in the first case you have to (a) zero out the array in advance (e.g. using `memset()`) or (b) zero the `m`-th byte afterwards where `m` is the return value of `read()` (the number of characters read). And in the second case you must only use `m` bytes not assuming a NUL-terminated string at all. Actually the other answer got the second method right. – Pavel Šimerda Jun 05 '15 at 14:22
  • I've resolved this issue in my code (pardon, it's been a while since I've written C), but the central issue remains. – Etheryte Jun 05 '15 at 16:44
  • @Nit Then could you revise your question so we can understand what the central issue is? Are you claiming `read`'s return value is greater than the number of bytes you request? If so, state that clearly! If not, then what is the issue?! – David Schwartz Jun 05 '15 at 20:32
  • @DavidSchwartz The central issue is the only part in my question that has a question mark. Namely, why do I receive the whole input string as output in my program? – Etheryte Jun 05 '15 at 21:23
  • @Nit **You aren't.** The bug I explained makes you think you are. If you call `read` to read 8 bytes, and `8` is returned from `read`, then you read 8 bytes, not the whole input string. Can you paste code without bugs that demonstrates the problem? – David Schwartz Jun 05 '15 at 21:52
  • @DavidSchwartz As I commented just a few lines above, unless I misunderstood you, it's already in the question. – Etheryte Jun 05 '15 at 21:56
  • @Nit Are you claiming you are getting the whole string back from `read`, or not? If not, what is the problem exactly? I've read this over several times, and from what I can tell, you seem to be incorrectly claiming that `read` is returning more bytes than you asked it for, but that is due to a bug in your code. – David Schwartz Jun 05 '15 at 22:09
  • 1
    @DavidSchwartz As I already said, I've removed the size issue from the code. The issue is that I'm receiving the whole input string as output from my program. You aren't adding anything of value to this discussion. – Etheryte Jun 05 '15 at 22:50
  • @DavidSchwartz Thanks, you saying _"You can't be"_ really helps since another user also reported replicating this issue. – Etheryte Jun 06 '15 at 08:43
  • @Nit What is "the issue"? As you've currently described the issue, it's literally impossible. I'm not doubting that you actually have some issue, you just seem to refuse to clarify what it actually is. – David Schwartz Jun 06 '15 at 19:39
  • @DavidSchwartz The issue, as I described a number of times, is as follows: I run my program, as shown above, with the configured tty, echo into the tty from another shell, and in my program window I get the whole string as output. – Etheryte Jun 06 '15 at 19:42
  • @Nit What does "as output" mean though? Do you mean that that's what you get from `read`? Does it mean that's what you print? – David Schwartz Jun 06 '15 at 19:44
  • Seems that the issue is that the ouput of the program is a single string without '\n' separators. – floyd Jun 06 '15 at 23:24
0

What you are looking for is a pseudo tty (PTY).

A simple way to create a PTY is to use socat(1) like so:

socat pty,link=/tmp/testpty stdio

This will create a PTY and attach the current terminal's stdio to its master side. Whatever data you put in here will be sent to the slave side of the PTY.

Now connect to the slave side from your program and it should work as you expect:

#define TTY "/tmp/testpty"
wkz
  • 2,203
  • 1
  • 15
  • 21