USB串口编程具有“灾难性”结果

时间:2016-07-30 03:07:46

标签: linux usb

我目前正在研究在Raspberry Pi 3(Linux Ubuntu)上运行的C程序,该程序旨在提供用于在嵌入式系统上配置网络的网页界面。

使用带有GDB调试器的Code :: Blocks开发代码。我正在使用microhttpd作为Web服务器,而且各种网页都运行良好。我现在正在使用“POSIX操作系统串行编程指南”中的信息处理嵌入式系统的USB串行链接。

以下代码负责打开目标系统的USB串行链接,似乎工作正常 - 一次。如果我关闭程序并重新启动它(在命令行上或在Code :: Blocks中独立)第二次使用microhttpd - 浏览器窗口将不再连接。此外,在Code :: Blocks中,调试器也会被调用 - 一旦程序启动,它就不会被暂停或停止。唯一的办法就是通过关闭项目来杀死它。

问题显然在函数内部,因为我可以注释掉它的调用,一切都像以前一样工作。不幸的是,一旦问题发生,唯一的解决方案似乎是重新启动Pi。

在使用脚本语言(Tcl)之前我已经完成了这样的事情,但这次我正在寻找一种非解释性语言的性能提升,因为Pi还将运行高带宽数据记录程序类似的USB串行接口。

代码如下所示:

/******************************************************************************/
/* This function scans through the list of USB Serial ports and tries to      */
/* establish communication with the target system.                            */
/******************************************************************************/

void tapCommInit(void) {
    char line[128];
    char port[15];   // this is always of the form "/dev/TTYACMn"
    char *ptr; 
    FILE *ifd;
    struct termios options;
    uint8_t msgOut[3], msgIn[4];

    msgOut[0] = REQ_ID;                       // now prepare the message to send
    msgOut[1] = 0;                                  // no data so length is zero
    msgOut[2] = 0;


    /**************************************************************************/
    /* First, get the list of USB Serial ports.                               */
    /**************************************************************************/

    system("ls -l /dev/serial/by-path > usbSerial\n");  // get current port list
    ifd = fopen("usbSerial", "r");
    logIt(fprintf(lfd, "serial ports: \n"));


    /**************************************************************************/
    /* The main loop iterates through the file looking for lines containing   */
    /* "tty" which should be a valid USB Serial port.  The port is configured */
    /* in raw mode as 8N1 and an ID request command is sent, which has no     */
    /* data.  If a response is received it's checked to see if the returned   */
    /* ID is a match.  If not, the port is closed and we keep looking.  If a  */
    /* match is found, tapState is set to "UP" and the function returns.  If  */
    /* no match is found, tapState is left in the initial "DOWN" state.       */
    /**************************************************************************/

    while(1) {
        if (fgets(line, 127, ifd) == NULL) {                     // end of file?
            break;                                 // yes - break out and return
        }
        ptr = strstr(line, "tty");  // make sure the line contains a valid entry
        if (ptr == NULL) {
            continue;                         // nothing to process on this line
        }
        strcpy(port, "/dev/");                      // create a correct pathname 
        strcat(port, ptr);              // append the "ttyACMn" part of the line
        port[strlen(port)-1] = 0; // the last character is a newline - remove it
        logIt(fprintf(lfd,"  %s\n", port));     // we have a port to process now
        cfd = open(port, O_RDWR | O_NOCTTY | O_NDELAY);   // cfd is a global int
        if (cfd == -1) {
            logIt(fprintf(lfd, "Could not open port: %s\n", port));
            continue;                   // keep going with the next one (if any)
        }
        fcntl(cfd, F_SETFL, 0);                                 // blocking mode
        tcgetattr(cfd, &options);               // get the current port settings
        options.c_cflag |= (CLOCAL | CREAD); // ena receiver, ignore modem lines
        options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);      // raw, no echo
        options.c_oflag &= ~OPOST;               // no special output processing
        options.c_cc[VMIN] = 0;         // minimum number of raw read characters
        options.c_cc[VTIME] = 10;   // timeout in deciseconds (1 second timeout)
        tcsetattr(cfd, TCSANOW, &options);              // set options right now
        cfsetispeed(&options, B115200);                       // input baud rate
        cfsetospeed(&options, B115200);                      // output baud rate
        options.c_cflag &= ~(CSIZE | PARENB |      // clear size bits, no parity
          CSTOPB | CRTSCTS);                   // 1 stop bit, no hw flow control
        options.c_cflag |= CS8;                // now set size: 8-bit characters
        options.c_cflag &= ~(IXON | IXOFF | IXANY);        // no sw flow control

        if (write(cfd, msgOut, 3) < 3) {
            logIt(fprintf(lfd, "Sending of output message failed\n"));
            close(cfd);
            continue; 
        }
        if (read(cfd, msgIn, 4) != 4) {
            logIt(fprintf(lfd, "Didn't get expected amount of return data\n"));
            close(cfd);
            continue; 
        }
        if (msgIn[3] != HOST_ID) {
            logIt(fprintf(lfd, "Got the wrong HOST_ID response\n"));
            close(cfd);
            continue; 
        }
        logIt(fprintf(lfd, "Port found - communication established\n"));
        tapState = UP;
        break;                             // we're done - break out of the loop
    }
    fclose(ifd);                         // close and remove the file we created
    remove("usbSerial");
}

1 个答案:

答案 0 :(得分:2)

  

从Code :: Blocks中调试器也被清除 - 一旦程序启动它就无法暂停或停止

您不了解您的工具的可能性远远超过您创建的不可杀戮程序。

很容易理解这一点:分而治之。你在这里得到了一大堆不相关的组件。开始分离它们,找出哪些部分在隔离状态下工作正常,哪些部分在与其他部分断开连接时仍然表现不佳。然后你就会有罪魁祸首。

具体来说,这意味着尝试在IDE外部运行程序,然后在命令行gdb下运行,而不是通过IDE运行GDB。

此外,应该可以在不启动Web服务器的情况下运行程序,以便您可以单独运行应用程序的串行部分。这不仅有助于通过最小化混杂变量进行调试,还可以鼓励松散耦合的程序设计,这本身就是一件好事。

最后,您可能会发现阻止程序停止的是Web框架,Code :: Blocks,或者GDB在Code :: Blocks下运行Pi的方式,而不是与USB有关到串口适配器。

  

一旦问题发生,唯一的解决方案似乎是重启Pi

如果您的程序仍然在后台运行,那么当然下一个实例尝试打开相同的USB端口时会失败。

不要猜,找出:

$ sudo lsof | grep ttyACM

或:

$ lsof -p $(pidof myprogram)

(如果您的系统没有pgrep,请替换pidof。)

  

在使用脚本语言(Tcl)之前,我已经完成了这样的事情,但这一次我正在寻找非解释性语言的性能提升

您的串口运行速度为115,200 bps。将其除以10以考虑停止和开始位,然后翻转分数以获得每个字节的秒数,并且每个字节达到87微秒。只有当串口运行平稳,每秒发送或接收11,500字节时,才能实现这一点。想猜测Tcl在87微秒内可以解释多少行代码? Tcl并不超快,但即使在Tcl地区,87微秒也是永恒的。

然后在连接的另一端,你有HTTP和一个[W] LAN,可能每个事务再加上100毫秒左右的延迟。

你对速度的需求是一种错觉。

现在回来再次与我交谈,当你需要异步地与其中的100个交谈时,然后也许我们可以开始证明C超过Tcl。

(我说这是一个日常工作涉及维护大型C ++程序,它可以执行大量串行和网络I / O.)

现在让我们解决这段代码的许多问题:

system("ls -l /dev/serial/by-path > usbSerial\n");  // get current port list
ifd = fopen("usbSerial", "r");

不要使用临时管道就足够了;请改用popen()

while(1) {

这是完全错误的。在这里说while (!feof(ifd)) {,否则你将尝试阅读文件的末尾。

这加上下一个错误,可能是您主要症状的关键。

if (fgets(line, 127, ifd) == NULL) { 
    break;

这里有几个问题:

  1. 您正在假设关于返回值的含义,而不是文档中的内容。 The Linux fopen(3) man page对此并不十分清楚; BSD version更好:

      

    fgets()和gets()函数不区分文件结束和错误,调用者必须使用feof(3)和ferror(3)来确定发生了什么。

    因为fgets()是标准C,而不是Linux或BSD特定的,所以通常可以安全地咨询其他系统&#39;手册页。更好的是,请参考一个好的通用C引用,例如Harbison & Steele。 (当我做比C ++更纯粹的C时,我发现它比K&amp; R更有用。)

    一句话,只需检查NULL,就不会告诉您在此需要了解的所有内容。

  2. 其次,硬编码的127常量是等待关闭的代码炸弹,如果你缩小line缓冲区的大小。在这里说sizeof(line)

    (不,sizeof(line) - 1fgets()在阅读时为空尾字符留出空间。再次,RTFM小心。)

  3. break也是一个问题,但我们必须在代码中进一步了解原因。

  4. 继续前进:

    strcat(port, ptr);              // append the "ttyACMn" part of the line
    

    这里有两个问题:

    1. 你盲目地假设strlen(ptr) <= sizeof(port) - 6。请改用strncat(3)

      (前一行&#39; s strcpy()(与strncpy()相对)是合理的,因为您正在复制字符串文字,因此您可以看到您不是超越缓冲区,但你应养成假装旧C字符串函数不检查长度甚至不存在的习惯。一些编译器实际上会在你使用它们时发出警告,如果你曲柄警告级别。)

      或者,更好的是,放弃C字符串,并开始使用std::string。我可以看到你正在努力坚持使用C语言,但C ++中确实有一些值得使用的东西,即使你主要使用C. C ++的自动内存管理工具(不仅仅是{ {1}},string / auto_ptr等等也属于此类。

      另外,C ++字符串的操作更像是Tcl字符串,所以你可能会更熟悉它们。

    2. 评论中的事实断言必须始终为真,否则他们可能会在以后误导您,可能会造成危险。您的特定USB转串口适配器可能使用unique_ptr,但不是全部都可以。还有一些常见的USB device class用于某些串口转USB适配器,导致它们在Linux下显示为/dev/ttyACMx。更一般地说,未来的更改可能会以其他方式更改设备名称;例如,您可能会移植到BSD,现在您的USB转串口设备被称为ttyUSBx,吹掉了15字节/dev/cu.usbserial缓冲区。 不要假设。

      即使抛开BSD案例,您的port缓冲区也不应小于port缓冲区,因为您将后者连接到前者。至少,line应为sizeof(port),以防万一。如果这似乎过多,那只是因为行缓冲区的128字节不必要地大。 (并不是说我试图扭动你的手臂来改变它.RAM很便宜;程序员调试时间很贵。)

    3. 下一步:

      sizeof(line) + strlen("/dev/")

      Unix中默认阻止文件句柄。您必须询问以获取非阻塞文件句柄。无论如何,爆破所有的旗帜是不好的风格;你不知道你在这里改变了哪些其他标志。正确的风格是获取,修改,然后设置,就像你使用fcntl(cfd, F_SETFL, 0); // blocking mode 的方式一样:

      tcsetattr()

      嗯,您正确使用int flags; fcntl(cfd, F_GETFL, &flags); flags &= ~O_NONBLOCK; fcntl(cfd, F_SETFL, flags);

      tcsetattr()

      ...然后对tcsetattr(cfd, TCSANOW, &options); 进行进一步修改,而不再拨打options。糟糕!

      您不会认为对tcsetattr()结构的修改会立即影响串口,不是吗?

      options

      这里错了一堆:

      1. 您正在折叠短写和错误案例。单独处理它们:

        if (write(cfd, msgOut, 3) < 3) {
            logIt(fprintf(lfd, "Sending of output message failed\n"));
            close(cfd);
            continue; 
        }
        
      2. 您没有记录int bytes = write(cfd, msgOut, 3); if (bytes == 0) { // can't happen with USB, but you may later change to a // serial-to-Ethernet bridge (e.g. Digi One SP), and then // it *can* happen under TCP. // // complain, close, etc. } else if (bytes < 0) { // plain failure case; could collapse this with the == 0 case // close, etc } else if (bytes < 3) { // short write case } else { // success case } 或其等效字符串,所以当(!)出现错误时,您不会知道哪个错误:

        errno

        修改品味。只要意识到logIt(fprintf(lfd, "Sending of output message failed: %s (code %d)\n", strerror(errno), errno)); 和其他大多数Unix系统调用一样,都有一大堆可能的错误代码。你可能不想以同样的方式处理所有这些。 (例如write(2)

      3. 关闭FD后,您将其设置为有效的FD值,以便在读取一行后在EOF上,您将使用有效但已关闭的FD值保留该功能! (这是上面EINTR的问题:它可以隐式地将已关闭的FD返回给其调用者。)在每次break调用后说cfd = -1

      4. 以上关于close(cfd)的所有内容也适用于以下write()来电,但同时也适用:

        read()

        POSIX中没有任何内容告诉您,如果串行设备发送4个字节,您将获得单个if (read(cfd, msgIn, 4) != 4) { 中的所有4个字节,即使使用阻塞FD也是如此。使用慢速串行端口,每个read()特别不可能获得超过一个字节,这仅仅是因为与串行端口相比,您的程序闪电般快。您需要在此处循环调用read(),仅在出错或完成时退出。

        以防万一不明显:

        read()

        如果您切换到上面的remove("usbSerial"); ,则不需要。不要在管道所在的文件系统周围分散临时工作文件。