我目前正在研究在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");
}
答案 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;
这里有几个问题:
您正在假设关于返回值的含义,而不是文档中的内容。 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
,就不会告诉您在此需要了解的所有内容。
其次,硬编码的127
常量是等待关闭的代码炸弹,如果你缩小line
缓冲区的大小。在这里说sizeof(line)
。
(不,sizeof(line) - 1
:fgets()
在阅读时为空尾字符留出空间。再次,RTFM小心。)
break
也是一个问题,但我们必须在代码中进一步了解原因。
继续前进:
strcat(port, ptr); // append the "ttyACMn" part of the line
这里有两个问题:
你盲目地假设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字符串,所以你可能会更熟悉它们。
评论中的事实断言必须始终为真,否则他们可能会在以后误导您,可能会造成危险。您的特定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很便宜;程序员调试时间很贵。)
下一步:
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
这里错了一堆:
您正在折叠短写和错误案例。单独处理它们:
if (write(cfd, msgOut, 3) < 3) {
logIt(fprintf(lfd, "Sending of output message failed\n"));
close(cfd);
continue;
}
您没有记录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)
)
关闭FD后,您将其设置为有效的FD值,以便在读取一行后在EOF上,您将使用有效但已关闭的FD值保留该功能! (这是上面EINTR
的问题:它可以隐式地将已关闭的FD返回给其调用者。)在每次break
调用后说cfd = -1
。
以上关于close(cfd)
的所有内容也适用于以下write()
来电,但同时也适用:
read()
POSIX中没有任何内容告诉您,如果串行设备发送4个字节,您将获得单个if (read(cfd, msgIn, 4) != 4) {
中的所有4个字节,即使使用阻塞FD也是如此。使用慢速串行端口,每个read()
特别不可能获得超过一个字节,这仅仅是因为与串行端口相比,您的程序闪电般快。您需要在此处循环调用read()
,仅在出错或完成时退出。
以防万一不明显:
read()
如果您切换到上面的remove("usbSerial");
,则不需要。不要在管道所在的文件系统周围分散临时工作文件。