使用C语言初始化、读写Linux串口设备

4
我正在开发一个新项目,希望与连接到我的 Debian 机器的 FTDI 进行连接。我打算使用 C 编写代码,而不是 C++。这里出现了问题,我找到的所有示例都不完整,或者是为 c++ 编译器编写的,而不是 GCC 编译器。
目标是与连接到 FTDI 的微控制器进行通信。为了调试,我想开始构建一个 Linux 应用程序,该应用程序能够:
- 在启动时使用 ttyUSB1 初始化串行连接 - 发送字符字符串 - 当计算机接收到字符字符串时显示它们 - 将通信保存到 .txt 文件中
是否有任何示例代码或教程可以实现此功能?
如果我成功了,我肯定会将代码放在这里,以便新观众可以使用它!
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

#define MODEM "/dev/ttyUSB0"
#define BAUDRATE B115200    

int main(int argc,char** argv)
{   
    struct termios tio;
    struct termios stdio;
    struct termios old_stdio;
    int tty_fd, flags;
    unsigned char c='D';
    tcgetattr(STDOUT_FILENO,&old_stdio);
    printf("Please start with %s /dev/ttyS1 (for example)\n",argv[0]);
    memset(&stdio,0,sizeof(stdio));
    stdio.c_iflag=0;
    stdio.c_oflag=0;
    stdio.c_cflag=0;
    stdio.c_lflag=0;
    stdio.c_cc[VMIN]=1;
    stdio.c_cc[VTIME]=0;
    tcsetattr(STDOUT_FILENO,TCSANOW,&stdio);
    tcsetattr(STDOUT_FILENO,TCSAFLUSH,&stdio);
    fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK);       // make the reads non-blocking
    memset(&tio,0,sizeof(tio));
    tio.c_iflag=0;
    tio.c_oflag=0;
    tio.c_cflag=CS8|CREAD|CLOCAL;           // 8n1, see termios.h for more information
    tio.c_lflag=0;
    tio.c_cc[VMIN]=1;
    tio.c_cc[VTIME]=5;
    if((tty_fd = open(MODEM , O_RDWR | O_NONBLOCK)) == -1){
        printf("Error while opening\n"); // Just if you want user interface error control
        return -1;
    }
    cfsetospeed(&tio,BAUDRATE);    
    cfsetispeed(&tio,BAUDRATE);            // baudrate is declarated above
    tcsetattr(tty_fd,TCSANOW,&tio);
    while (c!='q'){
        if (read(tty_fd,&c,1)>0){
            write(STDOUT_FILENO,&c,1); // if new data is available on the serial port, print it out
            printf("\n");
        }
        if (read(STDIN_FILENO,&c,1)>0){
            write(tty_fd,&c,1);//if new data is available on the console, send it to serial port
            printf("\n");
        }
    }
    close(tty_fd);
    tcsetattr(STDOUT_FILENO,TCSANOW,&old_stdio);
    return EXIT_SUCCESS;
}

大部分代码来自于http://en.wikibooks.org/wiki/Serial_Programming/Serial_Linux,但我也使用了下面发布的一些代码片段。

1
甚至有一个专门介绍这个主题的不错的教程:http://tldp.org/HOWTO/Serial-Programming-HOWTO/。 - fvu
2
你有检查过这个问题吗?https://dev59.com/vU7Sa4cB1Zd3GeqP10Dv?rq=1 - 999k
这篇文章 https://dev59.com/vU7Sa4cB1Zd3GeqP10Dv?rq=1 只展示了一些初始化。 - Embed
@KBart确实是一个可靠的来源,但它不像OP需要/想要的那样容易阅读/重用,因为它做了很多很多其他的东西。 - fvu
显示剩余7条评论
2个回答

3

处理串口(针对Linux操作系统):
- 要打开通信,您需要一个描述符,它将成为您的串口句柄。
- 设置标志以控制通信方式。
- 将命令写入此句柄(确保正确格式化输入)。
- 获取答案。(确保读取所需信息的数量)
- 关闭句柄。 具体步骤如下:

int fd; // file descriptor
int flags; // communication flags
int rsl_len; // result size
char message[128]; // message to send, you can even dinamically alocate.
char result[128]; // result to read, same from above, thanks to @Lu

flags = O_RDWR | O_NOCTTY; // Read and write, and make the job control for portability
if ((fd = open("/dev/ttyUSB1", flags)) == -1 ) {
  printf("Error while opening\n"); // Just if you want user interface error control
  return -1;
}
// In this point your communication is already estabilished, lets send out something
strcpy(message, "Hello");
if (rsl_len = write(fd, message, strlen(message)) < 0 ) {
  printf("Error while sending message\n"); // Again just in case
  return -2;
}
if (rsl_len = read(fd, &result, sizeof(result)) < 0 ) {
  printf("Error while reading return\n");
  return -3;
}
close(fd);

请注意需要关注写入和读取的内容。在进行奇偶校验、停止位、波特率等控制时,还可以使用更多标志。

当然,这段代码没有处理E_INTR,也没有使用O_CLOEXEC打开文件句柄,也没有处理信号...这不是你想要告诉新手编写良好的Unix代码的方式。一旦你把它变成良好的Unix代码,它会变得非常冗长,对于它所做的那么少,甚至有一个问题,是否还值得在纯C中继续进行而不失去理智... - Kuba hasn't forgotten Monica
在另一方面,可以说Qt非常庞大且包罗万象,以至于是否值得使用都存在疑问。我曾看到Qt被引入项目依赖中,但实际上只用了其中很小一部分的功能(例如:基本数据结构,这些结构使用STL实现更为合理),而你所付出的代价就是膨胀。我认为这种方式更容易保持理智……这就像从头开始建造汽车,而不是被制造商束缚在替换零件的限制下。 - Steven Lu
我可能选择了一个糟糕的比喻,因为如果没有非凡的资源,你最终得到的汽车将无法使用。不过,POSIX则稍微容易一些,主要是因为它有很好的文档记录。 - Steven Lu
@StevenLu 即使您不引入整个模块,只是其中的部分,Qt 也是非常可用的。一般来说,如果您静态链接 Qt 库到可执行文件中,则会自动完成此操作。额外的好处是,您肯定可以仅在项目中包含某些 Qt 文件,就像 Qt 在引导自身时所做的那样。在 Windows 上,一个完全静态编译的 HelloWorld 程序,使用 cstdio 上的 QString、QTextStream 输出到 stdout,没有线程支持,没有任何运行时依赖项,大小约为 800KB,而不需要太多努力。 - Kuba hasn't forgotten Monica
1
@rfermi 我相当确定 messageresult 需要足够大的 char[] - Steven Lu
显示剩余2条评论

0

由于gcc是C/C++编译器,因此您不需要局限于纯C。

如果您喜欢编写大量样板代码,并且确实知道自己在做什么,那么坚持使用纯C是可以的。许多人错误地使用Unix API,而且很多示例代码都过于简单化。编写正确的Unix C代码有点烦人,至少可以这么说。

个人建议不仅使用C++,还要使用像Qt这样的更高级别的应用程序开发框架。Qt 5附带了一个QtSerialPort模块,使得枚举串行端口、配置它们以及将数据输入和输出变得容易。Qt不强制您使用GUI模块,它可以是命令行应用程序或非交互式服务器/守护程序。

QtSerialPort也可从Qt 4中使用,但它不随Qt 4捆绑,您必须将其添加到项目中。我建议从Qt 5开始入手,因为它支持C++11,使用起来更加舒适。

使用Qt编写的代码可以非常简单,不会比您的普通英语描述更长。以下是一个使用Qt 5和C++11的Qt控制台应用程序。它使用coreserialport模块。它还处理SIGINT信号,以便在进程由于^C而终止之前刷新输出文件。我使用QLocalSocket代替原始Unix API来从Unix信号处理程序进行通信,功能相同。

只有main内部的代码是严格要求的,其余部分仅是为了在按下^C时正确包装它们。

#include <QCoreApplication>
#include <QSerialPort>
#include <QFile>
#include <QTextStream>
#include <QLocalServer>
#include <QLocalSocket>
#include <cstdio>
#include <csignal>

QLocalSocket * xmit;

static void signalHandler(int)
{
    xmit->write(" ");
    xmit->flush();
}

static bool setupSignalHandler()
{
    QLocalServer srv;
    srv.listen("foobarbaz");
    xmit = new QLocalSocket(qApp);
    xmit->connectToServer(srv.serverName(), QIODevice::WriteOnly);
    srv.waitForNewConnection();
    QLocalSocket * receive = srv.nextPendingConnection();
    receive->setParent(qApp);
    qApp->connect(receive, &QLocalSocket::readyRead, &QCoreApplication::quit);
    struct sigaction sig;
    sig.sa_handler = signalHandler;
    sigemptyset(&sig.sa_mask);
    sig.sa_flags = SA_RESTART;
    return ! sigaction(SIGINT, &sig, NULL);
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    setupSignalHandler();

    QSerialPort port("ttyUSB1");
    QFile file("file.txt");
    QTextStream err(stderr, QIODevice::WriteOnly);
    QTextStream out(stdout, QIODevice::WriteOnly);
    if (!file.open(QIODevice::WriteOnly)) {
        err << "Couldn't open the output file" << endl;
        return 1;
    }
    if (!port.open(QIODevice::ReadWrite)) {
        err << "Couldn't open the port" << endl;
        return 2;
    }
    port.setBaudRate(9600);
    QObject::connect(&port, &QSerialPort::readyRead, [&](){
        QByteArray data = port.readAll();
        out << data;
        file.write(data);
    });
    out << "Use ^C to quit" << endl;
    return a.exec();
}

听起来很有前途,但我该如何将Qt库添加到GCC中呢?我刚刚安装了QT5,但在编译时仍然出现以下错误:"gcc -o qtSerial qtSerial.cpp qtSerial.cpp:1:28: fatal error: QCoreApplication: No such file or directory"。 - Embed
你不能手动完成它而不了解其过程。使用QT Creator创建一个项目,将您的文件添加到该项目中,将serialport模块添加到.pro文件中(QT += serialport),然后它应该可以工作。 - Kuba hasn't forgotten Monica
需要使用C语言编程,并且操作系统必须是Unix。由于这两个要求没有得到遵守,因此被投下了反对票。 - Lesto
@lesto 我提供了一种替代方案,这肯定适用于任何你可以编译Qt(基本上是有价值的任何Unix系统)的系统 :) - Kuba hasn't forgotten Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接