Reading serial data in linux byte-for-byte at 56K reliably

假如想象 提交于 2019-12-26 12:23:40

问题


I'm trying to create a function with the most minimal delay possible that checks to see if the serial port has data and if it does, it reads every single byte and prints each byte in hex format until no more bytes are available. If there is no data, the function must return right away.

This is my code:

int fd=open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_SYNC);  

// Trying to set correct options here
struct termios o;
tcgetattr(fd,&o);
cfsetispeed(&o,57600);
cfsetospeed(&o,57600);
/* 8 bits, no parity, 1 stop bit */
o.c_cflag &= ~PARENB;o.c_cflag &= ~CSTOPB;o.c_cflag &= ~CSIZE;o.c_cflag |= CS8;
/* no hardware flow control */
o.c_cflag &= ~CRTSCTS;
/* enable receiver, ignore status lines */
o.c_cflag |= CREAD | CLOCAL;
/* disable input/output flow control, disable restart chars */
o.c_iflag &= ~(IXON | IXOFF | IXANY);
/* disable canonical input, disable echo, disable visually erase chars, disable terminal-generated signals */
o.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
/* disable output processing */
o.c_oflag &= ~OPOST;
o.c_cc[VMIN] = 0; //to prevent delay in read();
o.c_cc[VTIME] = 0;
tcsetattr(fd, TCSANOW, &o);
tcflush(fd, TCIFLUSH);

char sp[1]; //hold 1 byte
int bytes=read(fd,sp,1); //Good news: this function doesn't lock
if (bytes > 0){
    //this is never reached even if a byte is 
    //present on the serial line. why?
    printf("Read: ");
    while(bytes > 0){
        printf("%X ",sp[0]);
        bytes=read(fd,sp,1);
    }
}
fclose(fd);

Is there a way to fix this?

This whole function (minus the serial port options section) will eventually run in an endless loop as I am constantly scanning my port for data then printing it. Then later I'll add some more functionality where I'll write data to the port at predefined times regardless of what data is received.

P.S. not sure if this is of help, but the target device I'm doing I/O with is custom hardware around an 8051 microcontroller and its serial fifo buffers are only 1 byte whereas the PC's buffers I think are 14 or 16 bytes.


回答1:


If you know beforehand the times when you need to write to the device, you can use select() or poll() to wait for input until the next time you wish/intend to write.

A much more simple and robust approach — because your reads and writes are not in a defined sequence, and your hardware is full duplex — is to use separate threads for reading and writing. Basically, you use blocking reads and writes (c_cc[VMIN] = 1, c_cc[VTIME] = 0 for reading, O_NONBLOCK not among file open flags). You should allow larger buffers, though; reads will return all that have been received thus far, but with those settings wake up the reader whenever there is at least one char received. For writing, I do recommend you do a tcdrain(fd); after each write that completes a command/message to the device, to ensure it is sent on the wire by the kernel. (Do remember that writes to the serial port can be short; you need a write loop.)

In all cases, the kernel on the host side will cache data sent and received. Depending on the hardware and driver, even a blocking write() may return earlier than when all of the data is actually on the wire. The hardware and the kernel driver, not the host software, is responsible for correct timing of the serial data.

Using one-byte buffer on the host side will not affect the microcontroller at all, you'll just do more syscalls than is necessary, wasting CPU resources and possibly slow down your program a bit. At 57600 baud, with 8 data bits, no parity, 1 stop bit, and the implicit start bit, the actual data rate is 46080 bits/second (±5% typically allowed), or 5760 bytes per second. The microcontroller will always have about 1 s /5760 ≃ 0.0001736 seconds, or over 173 microseconds, to process each incoming byte. (I'd design my firmware to not allow higher priority interrupts etc. to delay processing for more than 100 microseconds or so even in the worst case, to ensure no chars are dropped. If you receive the chars in an interrupt handler, I'd use a small circular buffer, and an indicator character, \r or \n, so that if such character is received, a flag is raised for the main program to notice that a new complete command has been received. The circular buffer should be long enough to hold two full commands, or more if some commands may take longer to parse/process/handle.)

If the host operating system wakes up the process 1 ms after of receiving the first character, additional four or five characters have arrived in the mean time. Because this latency can be much higher on some systems, I'd use a much larger buffer, say up to 256 chars, to avoid doing superfluous syscalls when the kernel is for some reason delayed in waking up the reader thread. Yes, it will often read just 1 char, and that is fine; but when the system is overloaded, you don't want to add to that load by doing hundreds of superfluous syscalls when one would suffice.

Remember, the termios interface, with VMIN=1, VTIME=0, will cause the blocking read to be woken up as soon as possible, whenever even a single character is received. It is just that you cannot ensure your program is constantly running, unless you waste about 100% of CPU power by spinning in place (and if you do, nobody will want to run your program). Depending on the system, there may be a delay in waking up the blocking read, during which time more data may be received, so using a larger read() buffer is definitely sensible.

Similarly, you can safely use as large writes as you want (up to a limit of about 2 GiB), although most serial drivers can return short counts, so you'll need a loop there anyway. The tcdrain(fd) on the serial port descriptor will block until all written data has been actually transmitted, so you'll probably want to use it. (If you do not, you can just write more data; the kernel driver will take care of the details, and not reorder/mess data up.)

Using two threads, one for reading, one for writing, may sound daunting/odd, but it is actually the simplest way of achieving robust communications. You can even use pthread_setcancelstate(), pthread_setcanceltype() and optionally pthread_testcancel(), to write the thread functions so that you can simply cancel the threads (using pthread_cancel()) to stop them, even if they have critical sections (like adding a received message to some queue protected by a mutex).




回答2:


Your program has a variety of problems, many of which have been discussed in comments:

  • you do not check the return values of your functions
  • you poll the serial port and furthermore perform a system call for every byte read
  • you appear to be setting the serial port transmission speed incorrectly, via raw integers instead of the macros reserved for the purpose
  • you are using the wrong printf format

Here is a modified version of your code that addresses all of those issues. It's unclear whether this will do exactly what you want, and of course I can't test it on your hardware, but it should be a better place to start:

int open_serial(const char *device) {
    int fd = open(device, O_RDWR | O_NOCTTY | O_SYNC);  

    if (fd >= 0) {
        return -1;
    }

    // Set appropriate serial and line-discipline options
    struct termios o;

    if ((tcgetattr(fd, &o) != 0)
            || (cfsetispeed(&o, B57600) != 0)    // note use of MACRO B57600
            || (cfsetospeed(&o, B57600) != 0)) { //
        return -1;
    }

    /* 8 bits, no parity, 1 stop bit, no hardware flow control */
    o.c_cflag &= ~(PARENB | CSTOPB | CSIZE | CRTSCTS);
    o.c_cflag |= CS8;

    /* enable receiver, ignore status lines */
    o.c_cflag |= CREAD | CLOCAL;

    /* no software flow control */
    o.c_iflag &= ~(IXON | IXOFF | IXANY);

    /* Operate in raw mode without echo or signaling */
    o.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

    /* disable output processing */
    o.c_oflag &= ~OPOST;

    /* Perform pure timed reads */
    o.c_cc[VMIN] = 0;
    o.c_cc[VTIME] = 1;

    /* apply the settings, and discard any buffered input */
    if ((tcsetattr(fd, TCSANOW, &o) != 0)
            || (tcflush(fd, TCIFLUSH) != 0)) {
        return -1;
    }

    return fd;
}

int read_serial(int fd) {
    /*
     * 0.1-sec timed reads of a port running at 56K may read up to 720 bytes each,
     * give or take.  Allow for a few more to account for overhead, etc..
     */
    char sp[768];
    int nr;

    puts("Read: ");
    // Reads will normally block for 0.1 seconds:
    while ((nr = read(fd, sp, sizeof(sp))) != -1) {
        // Any data ready to send can be dispatched now ...

        for (int i = 0; i < nr; i++) {            
            printf("%hhX ", (unsigned char) sp[i]);
        }
        fflush(stdout);
    }

    return -1;
}


来源:https://stackoverflow.com/questions/51828498/reading-serial-data-in-linux-byte-for-byte-at-56k-reliably

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!