【问题标题】:how to read multiple serial ports in realtime in C or Python如何在 C 或 Python 中实时读取多个串口
【发布时间】:2019-12-05 15:03:26
【问题描述】:

我需要读取多个(至少 2 个)串行端口(目前通过 USB 连接的 FT2232H 模块上有两个端口)。

我正在使用它来监控串行连接,因此两个端口的 RX 并联连接到我需要监控的串行的 RX 和 TX。

设置与this非常相似。

我正在设置这样的端口:

#define waitTime   0

int start_dev(const int speed, const char *dev) {
    int fd = open(dev, O_RDWR | O_NOCTTY |O_NONBLOCK| O_NDELAY);
    int isBlockingMode, parity = 0;
    struct termios tty;

    isBlockingMode = 0;
    if (waitTime < 0 || waitTime > 255)
        isBlockingMode = 1;

    memset (&tty, 0, sizeof tty);
    if (tcgetattr (fd, &tty) != 0) {
        /* save current serial port settings */
        printf("__LINE__ = %d, error %s\n", __LINE__, strerror(errno));
        exit(1);
    }

    cfsetospeed (&tty, speed);
    cfsetispeed (&tty, speed);

    tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;     // 8-bit chars
    // disable IGNBRK for mismatched speed tests; otherwise receive break
    // as \000 chars
    tty.c_iflag &= ~IGNBRK;         // disable break processing
    tty.c_lflag = 0;                // no signaling chars, no echo,
                                    // no canonical processing
    tty.c_oflag = 0;                // no remapping, no delays
    tty.c_cc[VMIN]  = (1 == isBlockingMode) ? 1 : 0;            // read doesn't block
    tty.c_cc[VTIME] = (1 == isBlockingMode) ? 0 : waitTime;     // in unit of 100 milli-sec for set timeout value

    tty.c_iflag &= ~(IXON | IXOFF | IXANY); // shut off xon/xoff ctrl

    tty.c_cflag |= (CLOCAL | CREAD);        // ignore modem controls,
                                            // enable reading
    tty.c_cflag &= ~(PARENB | PARODD);      // shut off parity
    tty.c_cflag |= parity;
    tty.c_cflag &= ~CSTOPB;
    tty.c_cflag &= ~CRTSCTS;

    if (tcsetattr (fd, TCSANOW, &tty) != 0) {
        printf("__LINE__ = %d, error %s\n", __LINE__, strerror(errno));
        exit(1);
    }
    return fd;
}

... 目前我有这段代码可供阅读(我也尝试过使用select()):

...
    for (running=1; running;) {
        for (int*p=devs; p<end; p++) {
            char b[256];
            int n = read(*p, b, sizeof(b));
            if (n > 0) {
                for (int i=0; i<n; i++) {
                    ...
                }
            }
        }
    }
...

这显然不是最理想的,因为它不会暂停等待字符。

问题是我遇到了某种缓冲,因为当两个进程在一个紧密的循环中交换数据时,我经常会看到一些请求一起出现,然后是相应的答案(1b6f 是请求,19 是空答案):

1b6f
19
1b6f
19
1b6f
19
1b6f
191919
1b6f1b6f1b6f
19191919
1b6f1b6f1b6f1b6f
1b6f1b6f1b6f
191919

我也尝试过使用 python (pyserial),但得到了类似的结果。

我应该如何确保执行正确的时间安排?

注意:我对精确的时间不是很感兴趣,但是应该保留序列(即:我希望避免在请求之前看到答案)。

【问题讨论】:

  • 我建议您在问题中选择一种语言。如果您对两种语言的解决方案感兴趣,请作为两个单独的问题。
  • 购买这些产品不是比花时间解决问题更快吗? List of the best RS232 Sniffers
  • @kunif:如前所述,我没有找到合适的 Linux 版本,否则我会非常乐意不打扰社区。硬件解决方案不适用,因为它们依赖于我没有的“直通”连接(我只能使用测试点在印刷电路上分接线路)。非常欢迎任何具体的建议。
  • 如果您不能使用外部硬件,您可以做的最好的事情是为主设备创建一个专用的设备驱动程序,在C中具有跟踪功能,正如其他人已经回答的那样。如果端口接口芯片上的固件可以改写,那就更好了,但是几率很小。敲一行或编写应用程序级程序没有多大意义。

标签: python c serial-port real-time


【解决方案1】:

在我看来,您正在尝试做的事情是,如果我理解正确的话,一种端口嗅探器来识别在串行链路上交换的事务对于 USB 到串行转换器和传统操作系统是不可行的,除非您正在以缓慢的波特率运行。

USB 端口总是会引入一定的延迟(可能是几十毫秒),并且您必须将操作系统的不可预测性置于此之上。

由于您有两个端口,您可以尝试运行两个单独的线程并为收到的每个数据块添加时间戳。这可能有助于改善事情,但我不确定它是否能让您清楚地遵循顺序。

如果您有真正的(旧版)串行端口,并且操作系统不是很负载,也许您可​​以通过某种方式做到这一点。

但如果你想要一个便宜的串口嗅探器,你可以试试this solution。如果您在端口上进行转发,您将始终知道来自哪里的内容。当然,您需要访问通信的任一方。

如果你没有那么奢侈,我想几乎任何类型的微控制器都可以很容易地得到你想要的东西。

编辑: 另一个想法可能是使用双串口转 USB 转换器。由于两个端口都由同一个芯片提供服务,因此我认为您很可能可以按照其中一个端口的顺序进行操作。我可以访问this one,如果您发布代码的完整工作 sn-p,如果您想知道,我可以在下周对其进行测试。

【讨论】:

  • 我打开了一个四端口设备,有 4 个带有集线器芯片的离散 FTDI USB 转串口芯片。我怀疑您的 “使用双串口转 USB 转换器” 的想法是否可行。
  • 我的双端口设备有一个FT2232H。我没有深入研究细节,但我觉得这款芯片可能比两个单独的 USB 端口工作得更好。虽然我不愿意打赌,但这将是一个很好的练习......
【解决方案2】:

两个串行端口将具有缓冲 - 单个字符的到达顺序无法在应用程序级别确定。这可能需要编写您自己的驱动程序或将任何缓冲减少到 1 个字符 - 有溢出的风险。

即便如此,它也只有在你有一个真正的 UART 并直接控制它并且它没有硬件 FIFO 的情况下才能工作。使用作为 USB CDC/ACM 类驱动程序实现的虚拟 UART 在任何情况下都是不可能的,因为实时 UART 事务在主从 USB 传输中丢失,这与真正的 UART 工作方式完全不同。除此之外,FT2232H 还具有您无法控制的内部缓冲。

简而言之,由于多种因素,您无法在实现中的两个独立端口上对单个字符进行实时排序,其中大部分因素无法缓解。

您必须了解 FT2232 有两个真正的 UARTS 和 USB 设备接口,表现为两个 CDC/ACM 设备。它具有在 UART 和 USB 之间缓冲和交换数据的固件,并且 USB 交换由主机轮询 - 以它自己的最佳时间、速率和顺序。数据以数据包而不是单个字符的形式异步传输,并且不可能恢复任何单个字符的原始到达时间。您所知道的只是字符到达单个端口的顺序 - 您无法确定 个端口之间的到达顺序。而这一切甚至在数据被主机操作系统设备驱动程序缓冲之前。

可能需要硬件解决方案,使用在 UART 级别工作的微控制器将时间戳记并记录每个字符到达两个端口的每个端口,然后将带时间戳的日志数据传输到您的主机(可能通过 USB),其中然后,您可以根据时间戳重建到达顺序。

【讨论】:

    【解决方案3】:

    我正在设置这样的端口:
    ...
    这显然不是最理想的,因为它不会暂停等待字符。

    尽管有这种意识,您仍使用并发布此代码?
    我怀疑这个 “次优” 代码会在浪费 CPU 周期并消耗进程的时间片的同时轮询系统数据,这是问题的一部分。您尚未发布该问题的完整且最小的示例,我只能部分复制该问题。

    在具有两个 USART 的 SBC 上,我有一个程序在串行端口上生成“请求”和“响应”数据。生成程序为:

    #include <errno.h>
    #include <fcntl.h> 
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <termios.h>
    #include <unistd.h>
    
    int set_interface_attribs(int fd, int speed)
    {
        struct termios tty;
    
        if (tcgetattr(fd, &tty) < 0) {
            printf("Error from tcgetattr: %s\n", strerror(errno));
            return -1;
        }
    
        cfsetospeed(&tty, (speed_t)speed);
        cfsetispeed(&tty, (speed_t)speed);
    
        tty.c_cflag |= (CLOCAL | CREAD);    /* ignore modem controls */
        tty.c_cflag &= ~CSIZE;
        tty.c_cflag |= CS8;         /* 8-bit characters */
        tty.c_cflag &= ~PARENB;     /* no parity bit */
        tty.c_cflag &= ~CSTOPB;     /* only need 1 stop bit */
        tty.c_cflag &= ~CRTSCTS;    /* no hardware flowcontrol */
    
        /* setup for non-canonical mode */
        tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
        tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
        tty.c_oflag &= ~OPOST;
    
        /* fetch bytes as they become available */
        tty.c_cc[VMIN] = 1;
        tty.c_cc[VTIME] = 1;
    
        if (tcsetattr(fd, TCSANOW, &tty) != 0) {
            printf("Error from tcsetattr: %s\n", strerror(errno));
            return -1;
        }
        return 0;
    }
    
    
    int main(void)
    {
        char *masterport = "/dev/ttyS0";
        char *slaveport  = "/dev/ttyS2";
        int mfd;
        int sfd;
        int wlen;
    
        /* open request generator */
        mfd = open(masterport, O_RDWR | O_NOCTTY | O_SYNC);
        if (mfd < 0) {
            printf("Error opening %s: %s\n", masterport, strerror(errno));
            return -1;
        }
        /*baudrate 115200, 8 bits, no parity, 1 stop bit */
        set_interface_attribs(mfd, B115200);
    
        /* open response generator */
        sfd = open(slaveport, O_RDWR | O_NOCTTY | O_SYNC);
        if (sfd < 0) {
            printf("Error opening %s: %s\n", slaveport, strerror(errno));
            return -1;
        }
        /*baudrate 115200, 8 bits, no parity, 1 stop bit */
        set_interface_attribs(sfd, B115200);
    
        /* simple output loop */
        do {
            wlen = write(mfd, "ABCD", 4);
            if (wlen != 4) {
                printf("Error from write cmd: %d, %d\n", wlen, errno);
            }
            tcdrain(mfd);    /* delay for output */
    
            wlen = write(sfd, "xy", 2);
            if (wlen != 2) {
                printf("Error from write resp: %d, %d\n", wlen, errno);
            }
            tcdrain(sfd);    /* delay for output */
    
        } while (1);
    }
    

    问题是我遇到了某种缓冲,因为当两个进程在一个紧密的循环中交换数据时,我经常会看到一些请求一起出现,然后是相应的答案

    您没有明确您所谓的“紧密循环”,但上述程序将在“请求”后 30 毫秒生成“响应”(由双通道示波器测量) .

    顺便说一句,串行终端接口是高度分层的。即使没有 USB 使用的外部总线的开销,至少还有 termios 缓冲区和 tty 翻转缓冲区,以及 DMA 缓冲区。见Linux serial drivers

    SBC 的每个 USART 都连接到一个 FTDI USB 到 RS232 转换器(它是旧的四端口转换器的一部分)。请注意,USB 端口速度仅为 USB 1.1。用于串行捕获的主机 PC 是 10 年前运行旧 Ubuntu 发行版的硬件。


    尝试复制您的结果:

    ABCD
    x
    y
    A
    BCD
    xy
    ABCD
    xy
    ABCD
    xy
    A
    BCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABC
    D
    xy
    ABCD
    xy
    ABCD
    xy
    ABC
    D
    xy
    ABCD
    xy
    ABCD
    xy
    ABC
    D
    xy
    ABCD
    xy
    ABCD
    xy
    ABC
    D
    xy
    ABCD
    xy
    ABCD
    xy
    ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD
    xyxyxyxyxyxyxyxyxyxyxyxyxy
    ABCD
    xy
    ABCD
    xy
    AB
    CD
    xy
    ABCD
    xy
    ABCD
    xy
    AB
    CD
    xy
    ABCD
    xy
    ABCD
    x
    y
    A
    BCD
    xy
    ABCD
    xy
    ABCD
    x
    y
    AB
    CD
    xy
    ABCD
    xy
    ABCD
    x
    y
    

    只有一次(捕获程序启动后大约 1.5 秒)有一次多写捕获。 (在这种情况发生之前,输出中甚至会出现明显的暂停。)否则,每个读取/捕获都是部分或单个/完整的请求/响应。


    使用使用阻塞 I/O 的捕获程序,对于 4 字节的请求消息和 2 字节的响应消息,结果始终是“完美的”。

    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    

    通过将请求的 VMIN=4 和响应的 VMIN=2 更改为所有内容的 VMIN=1 来调整程序,会稍微改变捕获的质量:

    ABCD
    xy
    ABCD
    x
    ABCD
    y
    ABCD
    xy
    ABC
    xy
    D
    x
    ABCD
    y
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABCD
    xy
    ABC
    xy
    D
    x
    ABCD
    y
    

    虽然会发生部分捕获,但每次读取永远不会有多个“消息”。输出流畅一致,与非阻塞程序一样没有任何停顿。



    使用阻塞读取的捕获程序是:

    #include <errno.h>
    #include <fcntl.h> 
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <termios.h>
    #include <unistd.h>
    
    int set_interface_attribs(int fd, int speed, int rlen)
    {
        struct termios tty;
    
        if (tcgetattr(fd, &tty) < 0) {
            printf("Error from tcgetattr: %s\n", strerror(errno));
            return -1;
        }
    
        cfsetospeed(&tty, (speed_t)speed);
        cfsetispeed(&tty, (speed_t)speed);
    
        tty.c_cflag |= (CLOCAL | CREAD);    /* ignore modem controls */
        tty.c_cflag &= ~CSIZE;
        tty.c_cflag |= CS8;         /* 8-bit characters */
        tty.c_cflag &= ~PARENB;     /* no parity bit */
        tty.c_cflag &= ~CSTOPB;     /* only need 1 stop bit */
        tty.c_cflag &= ~CRTSCTS;    /* no hardware flowcontrol */
    
        /* setup for non-canonical mode */
        tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
        tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
        tty.c_oflag &= ~OPOST;
    
        /* fetch bytes as they become available */
        tty.c_cc[VMIN] = rlen;
        tty.c_cc[VTIME] = 1;
    
        if (tcsetattr(fd, TCSANOW, &tty) != 0) {
            printf("Error from tcsetattr: %s\n", strerror(errno));
            return -1;
        }
        return 0;
    }
    
    
    int main(void)
    {
        char *masterport = "/dev/ttyUSB2";
        char *slaveport  = "/dev/ttyUSB3";
        int mfd;
        int sfd;
    
        /* open request reader */
        mfd = open(masterport, O_RDWR | O_NOCTTY | O_SYNC);
        if (mfd < 0) {
            printf("Error opening %s: %s\n", masterport, strerror(errno));
            return -1;
        }
        /*baudrate 115200, 8 bits, no parity, 1 stop bit */
        set_interface_attribs(mfd, B115200, 4);
    
        /* open response reader */
        sfd = open(slaveport, O_RDWR | O_NOCTTY | O_SYNC);
        if (sfd < 0) {
            printf("Error opening %s: %s\n", slaveport, strerror(errno));
            return -1;
        }
        /*baudrate 115200, 8 bits, no parity, 1 stop bit */
        set_interface_attribs(sfd, B115200, 2);
    
        tcflush(mfd, TCIOFLUSH);
        tcflush(sfd, TCIOFLUSH);
    
        /* simple noncanonical input loop */
        do {
            unsigned char buffer[80];
            int rdlen;
    
            rdlen = read(mfd, buffer, sizeof(buffer) - 1);
            if (rdlen > 0) {
                buffer[rdlen] = 0;
                printf("%s\n", buffer);
            } else if (rdlen < 0) {
                printf("Error from read: %d: %s\n", rdlen, strerror(errno));
            } else {  /* rdlen == 0 */
                printf("Timeout from read\n");
            }               
    
            rdlen = read(sfd, buffer, sizeof(buffer) - 1);
            if (rdlen > 0) {
                buffer[rdlen] = 0;
                printf("%s\n", buffer);
            } else if (rdlen < 0) {
                printf("Error from read: %d: %s\n", rdlen, strerror(errno));
            } else {  /* rdlen == 0 */
                printf("Timeout from read\n");
            }               
        } while (1);
    }
    

    这实质上是每个串行终端上的双半双工捕获,用于请求-响应对话框。无法准确捕获/显示实际的全双工对话框。


    这些使用阻塞读取的结果似乎与其他答案相矛盾,即 USB 串行转换器会将串行数据缓冲并打包成无法识别的字节段。
    只有当我使用非阻塞读取时,我才会遇到您报告的“缓冲”

    【讨论】:

    • 伟大的贡献。我将用你的代码和我的硬件做一些测试。您是否更改了 FTDI 驱动程序的默认延迟? (/sys/bus/usb-serial/devices/ttyUSBx/latency_timer)。或者,也许您正在使用旧驱动程序
    • @MarcosG。 -- 没有调整驱动程序。
    • 感谢您的回答。 VMIN Tweaking 在我的情况下实际上并不可用,因为那是“不需要工作”的消息;如果我需要实际交换的东西,我的数据包可能长达 520 字节。我会尽快在我的环境中测试。我发布了“次优”版本,因为我(可能是错误地)认为 I7-7700(什么都不做)应该能够跟上 MT7628 与 PIC18F67K40 的通信。如前所述,我尝试了其他几种解决方案,包括使用 select() 和 pyserial(这是首选)。
    【解决方案4】:

    您对VMINVTIME c_cc 单元格的使用不当。如果您仔细阅读termios(3) 手册页,在VMIN &gt; 0 &amp;&amp; VTIME &gt; 0 的基础上,驱动程序不会将数据发送到应用程序直到检测到VTIME 持续时间超时。在这种情况下,VTIME 参数是一个字符间超时(但它会阻塞直到接收到第一个字符)。我想你误解了那个案子。这是在驱动程序中引入的,用于处理可变长度的数据包输入设备,例如鼠标或网络,它们可以按顺序传递多个数据包,以确保缓冲区与数据包的开始同步(同时处理数据包丢失)。但是该模式下的操作是无限期地等待第一个字符,然后等待 VTIME 十分之一秒以查看是否收到另一个字符,一旦达到 VMIN 计数 ,在这种情况下,驱动程序会缓冲 char 并等待另一个超时。这适用于具有可变长度和标头的数据包,您通常将VMIN 设置为标头的大小,然后使用字符间超时来处理超时后丢失的字符。这不是您在问题中所说的。

    要创建一个读取多个端口并在获得单个字符后立即接收它们的场景,您必须使用VMIN == 1, VTIME == 0,以便在收到每个字符后立即获得它们。要接收你得到的第一个,不管你从哪个端口接收它,你需要使用select(2)系统调用,它会阻止你,直到在几个端口之一上有一些输入可用,然后看看它是哪个端口,然后对该端口执行read(2)。如果您想要精细的时间戳,请在从select(2) 系统调用返回后立即执行clock_gettime(2)(您还没有read(2) char,但是您知道它就在那里,稍后,一旦您阅读它,您可以将时间戳与正确的字符和端口相关联。

    正如我在您的问题中看到的那样,您已经与 termios(3) 进行了斗争,并且您知道自己想要什么,请阅读 select(2) 手册页并准备处理该问题的代码。如果你遇到麻烦,请在下面给我留言,我会为你写一些代码。请记住:VMIN 是您希望接收的最小字符数,而不是最大值(您将其放入参数中的最大值为read(2)),VTIME 只是一个绝对超时,当VMIN == 0(但你可以在select(2) 中处理超时,比在驱动程序中更好)

    这种错误很常见,我也遇到过:)

    编辑

    我使用此处指示的方法开发了a simple example to monitor several tty lines(不一定是两个)。只是说它允许将树莓派 2B+ 用作串行协议分析器,通过逐个字符读取并使用最佳时间粒度方法。

    【讨论】:

    • OP 的程序以非阻塞模式打开串行终端,即同时使用 O_NONBLOCK 和 O_NDELAY 选项。没有 fcntl() 可以清除这些选项。因此忽略 VMIN 和 VTIME 参数。 read() 永远不会在非阻塞模式下等待(并浪费 CPU 周期和进程的时间片轮询 termios 缓冲区以获取数据)。
    • 这是一个穷人的解决方案,因为它不断消耗cpu资源。如果他在其他地方延迟,那么他将无法获得准确的时间戳。 VMIN 和 VTIME 参数控制驱动程序如何使执行 read() 的程序可以使用字符,因此它们永远不会被忽略。读取永远不会等待,但它会根据驱动程序缓冲它们的方式获取字符。如果他不想缓冲字符(正如他的问题中所述),他最好执行VMIN == 1VTIME == 0,否则在内核缓冲它们之前将永远不会读取字节。
    • 我会在周末提供一个完整的答案......现在,你必须等到那时
    • (1) “VMIN 和 VTIME ... 永远不会被忽略。” -- termios 手册页另有说明:“如果设置了 O_NONBLOCK,则读取 (2)在非规范模式下可能会立即返回,而不管 MIN 或 TIME 的设置如何。" (2) " ...在内核缓冲它们之前" -- 仅供参考,有几层缓冲终端数据。没有办法避免这种缓冲。
    • 据说当O_NONBLOCK被设置时,read(2)会立即返回,即使驱动程序由于VMIN和/或VTIME处理而有一些字符。相信我,与 SysV 和 BSD tty 驱动程序斗争已经有很多时间(现在已经超过 30 年了)。如果您将 VMIN 设置为 3 并且驱动程序缓冲区中有两个字符,则 O_NONBLOCK 将使 read(2) 返回时完全没有字符,因为 VMIN 不会被忽略我>。只需再获取一个字符,read(2) 将返回缓冲区中的三个字符(立即,如O_NONBLOCK 所述)
    猜你喜欢
    • 2023-04-03
    • 2011-01-22
    • 2012-05-18
    • 1970-01-01
    • 1970-01-01
    • 2020-05-16
    • 1970-01-01
    • 1970-01-01
    • 2022-01-11
    相关资源
    最近更新 更多