串口通信

串行接口是一种可以将接受来自CPU的并行数据字符转换为连续的串行数据流发送出去,同时可将接受的串行数据流转换为并行的数据字符供给CPU的器件。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通信的端口,这些参数必须匹配。

1. 串口通信原理

1.1 什么是串口?

串口是计算机一种常用的接口,常用串口是RS232接口,RS232事实上有三种(A,B,C),不同类别分别采用不同的电压表示on和off,使用最广泛的是RS-232-C,对应的有RS-232-C标准,也是串行通信的一个物理标准。
Windows操作系统下,串口名为COM1、COM2……
Linux操作系统下,串口设备文件名为/dev/ttyS0、/dev/ttyS1….注:USB/RS-232转换器名为/dev/ttyUSB0

1.2 什么是串行通信?

串行通信是指数据用一根传输线被逐位顺序传送,串行通信分为同步通信和异步通信。
异步通信是指一帧字符用起始位和停止位来完成收发同步,通信的发送与接收设备使用各自的时钟控制数据的接受和发送过程,因此要求收发双方设备的时钟尽可能一致。(各自有各自的时钟)
同步通信要求接收方与发送方的时钟保持完全一致。(同一时钟)
常用的是异步通信。同步通信的传输速率比异步通信更快,但大部分RS-232硬件却不支持同步通信,因为同步通信还需要其他的硬件和软件,而且传输过程中,发送设备和接收设备必须保持完全同步。

1.3 串口异步通信协议

  • 数据格式
    异步传输数据格式:

    一帧字符以起始位开始,然后是数据位、奇偶校验位、停止位。(这些数据在串口编程时,需要自己设置,即收发双方约定一致的协议)
    起始位:起始位必须是持续一个比特时间的逻辑“0”电平,标志传送一个字符的开始;
    数据位:5~8位,从最低位开始传送,数据位的位数有软件或硬件决定;
    奇偶校验位:奇校验,偶校验,无校验三种方式;
    停止位:可以是1位,1.5位,2位,其中,1.5位一般用不到。
  • 传输方向
    单工:数据传输只能沿一个方向,不能反向传输;
    半全功:数据传输可以沿两个方向,但需要分时进行;
    全双工:数据可以同时双向传输。
    RS-232为全双工方式,RS-422 也是全双工方式。
    RS-232-C标准:RS-232-C标准对两个方面作了规定,即信号电平标准和控制信号线的定义。
    RS-232-C采用负逻辑规定逻辑电平,信号电平与通常的TTL电平也不兼容,RS-232-C将-5V~-15V规定为“1”,+5V~+15V规定为“0”。

1.4 串口通信的接收过程

  • 开始通信时,信号线为空闲(逻辑1),当检测到由1到0的跳变时,开始对“接收时钟”计数。 
  • 当计到8个时钟时,对输入信号进行检测,若仍为低电平,则确认这是“起始位”,而不是干扰信号。
  • 接收端检测到起始位后,隔16个接收时钟,对输入信号检测一次,把对应的值作为D0位数据。若为逻辑1, 作为数据位1;若为逻辑0,作为数据位0。
  • 再隔16个接收时钟,对输入信号检测一次,把对应的值作为D1位数据。….,直到全部数据位都输入。
  • 检测校验位P(如果有的话)。
  • 接收到规定的数据位个数和校验位后,通信接口电路希望收到停止位S(逻辑1),若此时未收到逻辑1,说明出现了错误,在状态寄存器中置“帧错误”标志。若没有错误,对全部数据位进行奇偶校验,无校验错时,把数据位从移位寄存器中送数据输入寄存器。若校验错,在状态寄存器中置奇偶错标志。
  • 本幀信息全部接收完,把线路上出现的高电平作为空闲位。
  • 当信号再次变为低时,开始进入下一幀的检测。

2. Linux下串口编程

记住:Linux下皆为文件
串口编程流程:

包含头文件:

1
2
3
4
5
6
7
#include<iostream>
#include<stdlib.h> //标准函数库定义
#include<unistd.h> //Unix标准函数定义
#include<sys/types.h> //基本系统数据类型
#include<sys/stat.h> //文件状态定义
#include<fcntl.h> //文件控制定义
#include<errno.h> //错误号定义

其余头文件根据编程所需,自行添加。

  • 打开串口
    串口设备通过串口终端设备文件访问,文件为:/dev/ttyS0,/dev/ttyS1…;
    Tips:查看当前Linux系统连接的所有串口,可以使用命令行:ls -a /dev/tty*(或ls -a /dev/ttyUSB*)
    以打开文件的方式打开串口设备,调用open()函数:fd=open(port,O_RDWR|O_NOCTTY|O_NDELAY);
    参数O_NOCTTY:表示打开一个终端设备,程序不会成为该终端的控制终端;
    参数O_NDELAY:表示不关心端口另一端是否激活;
  • 设置串口
    最基本的设置串口包括:波特率、数据位、奇偶校验位、停止位
    主要是设置struct termios结构体的各成员值
    1
    2
    3
    4
    5
    6
    7
    8
    struct termio
    { unsigned short c_iflag; /* 输入模式标志 */
    unsigned short c_oflag; /* 输出模式标志 */
    unsigned short c_cflag; /* 控制模式标志*/
    unsigned short c_lflag; /* local mode flags */
    unsigned char c_line; /* line discipline */
    unsigned char c_cc[NCC]; /* control characters */
    };

设置波特率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int   speed_arr[] = { B115200, B19200, B9600, B4800, B2400, B1200, B300};
int name_arr[] = {115200, 19200, 9600, 4800, 2400, 1200, 300};
void set_speed(int fd,int speed)//fd 打开串口的文件句柄(返回值 int型)
{
int i;
struct termios options;
/*tcgetattr(fd,&options)得到与fd指向对象的相关参数,并将它们保存于options,该函数还可以测试配置是否正确,该串口是否可用等。
若调用成功,函数返回值为0,若调用失败,函数返回值为1.*/
if ( tcgetattr( fd,&options) != 0)
{
perror("SetupSerial 1");
return(FALSE);
}
for ( i= 0; i < sizeof(speed_arr) / sizeof(int); i++)
{
if (speed == name_arr[i])
{
cfsetispeed(&options, speed_arr[i]);
cfsetospeed(&options, speed_arr[i]);//设置串口输入输出波特率
}
}
}

设置数据位、校验位、停止位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
int set_Parity(int fd,int databits,int stopbits,int parity)
{
struct termios options;
if ( tcgetattr( fd,&options) != 0) {
perror("SetupSerial 1");
return(FALSE);
}
//设置数据位数
options.c_cflag &= ~CSIZE;
switch (databits)
{
case 5 :
options.c_cflag |= CS5;
break;
case 6 :
options.c_cflag |= CS6;
break;
case 7:
options.c_cflag |= CS7;
break;
case 8:
options.c_cflag |= CS8;
break;
default:
fprintf(stderr,"Unsupported data size\n"); return (FALSE);
}
//设置奇偶校验位
switch (parity)
{
case 'n':
case 'N':
options.c_cflag &= ~PARENB;
options.c_iflag &= ~INPCK; //无奇偶校验
break;
case 'o':
case 'O':
options.c_cflag |= (PARODD | PARENB); /* 设置为奇效验*/
options.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'e':
case 'E':
options.c_cflag |= PARENB; /* Enable parity */
options.c_cflag &= ~PARODD; /* 设置为偶效验*/
options.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'S':
case 's': //设置为空格
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;break;
default:
fprintf(stderr,"Unsupported parity\n");
return (FALSE);
}
//设置停止位
switch (stopbits)
{
case 1:
options.c_cflag &= ~CSTOPB;
break;
case 2:
options.c_cflag |= CSTOPB;
break;
default:
fprintf(stderr,"Unsupported stop bits\n");
return (FALSE);
}
options.c_oflag &= ~OPOST;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);//只是串口数据,不是开发终端,使用原始模式通讯
tcflush(fd,TCIFLUSH);//如果数据溢出,接收数据,但是不在读取,刷新收到的数据但不读取
options.c_cc[VTIME] = 150; //读取一个字符等待时间 为15s
options.c_cc[VMIN] = 0; //读取字符的最少个数
//将修改后的数据设置到串口中
if (tcsetattr(fd,TCSANOW,&options) != 0)
{
perror("SetupSerial 3");
return (FALSE);
}
return (TRUE);
}

  • 读写串口
    写串口,即发送数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int send(int fd,char *send_buf,int data_len)
    {
    int len=0;
    len=write(fd,send_buf,data_len);//返回实际读取的字符数
    if(len==data_len)
    {
    return len;
    }
    else
    {
    tcflush(fd,TCOFLUSH);
    return FALSE;
    }
    }

读串口,即接收数据

1
len=read(fd,rec_buf,data_len);//返回实际读取的字符数

  • 关闭串口
    close(fd);

注:
a.写串口程序时,要先打开串口.然后进行设置,否则设置就不会起作用
b.串口编程会涉及许多文件的I/O操作,一定要记得打开关闭同时出现,有打开,必有关闭!!!

3. Qt串口编程

3.1 方法一:使用第三方类

  • 第三方类的简单介绍
    qextserialbase.cpp和qextserialbase.h文件定义了一个QextSerialBase类;
    win_qextserialport.cpp和win_qextserialport.h文件定义了一个Win_QextSerialPort类;(Windows下)
    posix_qextserialport.cpp和posix_qextserialport.h文件定义了一个Posix_QextSerialPort类;(Linux下)
    qextserialport.cpp和qextserialport.h文件定义了一个QextSerialPort类。
    QextSerialPort类它是所有这些类的子类,是最高的抽象,它屏蔽了平台特征,使得在任何平台上都可以使用它。
    QextSerialBase类继承自QIODevice类,它提供了操作串口所必需的一些变量和函数等,而Win_QextSerialPort和Posix_QextSerialPort均继承自QextSerialBase类,Win_QextSerialPort类添加了Windows平台下操作串口的一些功能,Posix_QextSerialPort类添加了Linux平台下操作串口的一些功能。
    所以,在Windows下使用Win_QextSerialPort类,在Linux下使用Posix_QextSerialPort类。
  • 读写串口的方式
    在QextSerialBase类中定义了枚举变量
    1
    2
    3
    4
    5
    enum QueryMode
    {
    Polling,
    EventDriven
    };

两种方式:Polling(查询方式)和EventDriven(事件驱动方式)
事件驱动方式:使用事件处理串口的读取,一旦有数据到来,就会发出readRead()信号,在事件驱动的方式下,串口的读写是异步的,调用读写函数会立即返回,它们不会冻结调用线程;
查询方式:读写函数是同步执行的,信号不能工作在次模式下,而且有些功能也无法实现,需要建立定时器来读取串口数据
在windows下两种方式都可以,在Linux下只能使用Polling模式

  • 可能会出现的中文乱码问题
    main函数中添加头文件#include<QTextCodec>,添加语句:QTextCodec::setCodecForTr(QTextCodec::codecForLocale());
    更详细的QT中文乱码问题可以参考另一篇文章:《》

3.1.1 Windows下使用Qt串口编程

  • 新建工程,导入第三方类
    将qextserialport.cpp、qextserialport.h、qextserialbase.cpp、qextserialbase.h、win_qextserialport.cpp和win_qextserialport.h六个文件拷贝到新建工程目录下,在Qt Creator中左侧文件列表上,鼠标右击工程文件夹,添加现有文件,将六个文件导入工程,Qt会自动区分头文件和.cpp文件。
  • 设置界面
    界面添加TextBrowser部件,显示接受到的数据,Line Edit部件用来输入要发送的数据,SendButton按钮用来发送数据,OpenButton按钮用来打开串口,CloseButton按钮用来关闭串口,组合框portNameComboBox,设置串口号,条目为COM1、COM2…..,SpeedComboxBox设置波特率,条目为9600、115200….,数据位,奇偶校验位、停止位同样设置。
  • 编程实现串口设置
    在mainwindow.h中添加头文件”win_qextserialport.h“,添加对象和槽函数的声明
    1
    2
    Win_QextSerialPort *myCom;//声明对象
    void readMyCom();//声明读串口槽函数

在mainwindow.cpp实现槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/*打开串口函数*/
void MainWindow::on_OpenBtn_clicked()
{
QString portName=ui->portNamecomboBox->currentText();//获取用户设置的串口号
myCom=new Win_QextSerialPort(portName,QextSerialBase::EventDriven);
myCom->open(QIODevice::ReadWrite);**//先打开串口,再进行串口设置**
//设置波特率
if(ui->baudRatecomboBox->currentText()==tr("9600"))
myCom->setBaudRate(BAUD9600);
else if(ui->baudRatecomboBox->currentText()==tr("115200"))
{
myCom->setBaudRate(BAUD115200);
}
/*这里界面没有设置数据位、停止位、奇偶校验位,这三个数据直接在函数中确定了,如有需求,可以在界面添加组合框,实现程序同波特率设置*/
//设置数据位
myCom->setDataBits(DATA_8);
//设置校验位
myCom->setParity(PAR_NONE);
//设置停止位
myCom->setStopBits(STOP_1);
myCom->setFlowControl(FLOW_OFF); //设置数据流控制,使用无数据流控制的默认设置
myCom->setTimeout(10); //设置延时
connect(myCom,SIGNAL(readyRead()),this,SLOT(readMyCom()));
//信号和槽函数关联,当串口缓冲区有数据时,进行读串口操作

ui->OpenBtn->setEnabled(false); //打开串口后“打开串口”按钮不可用
ui->CloseBtn->setEnabled(true); //打开串口后“关闭串口”按钮可用
ui->baudRatecomboBox->setEnabled(false);
ui->portNamecomboBox->setEnabled(false);
}
/*读串口函数*/
void MainWindow::readMyCom()
{
QByteArray temp=myCom->readAll();
ui->textBrowser->insertPlainText(temp);
//读串口的数据显示在窗口的文本框中
/*保存数据到文件中*/
QFile file("E:\\Com.txt");
if(!file.open(QIODevice::WriteOnly|QIODevice::Append))
{
std::cout<<"Open file failed"<<endl;
}
QTextStream out(&file);
QString t;
t.prepend(temp);
out<<t;
file.close();
}
/*发送数据*/
void MainWindow::on_sendButton_clicked() //发送数据槽函数
{
myCom->write(ui->sendMsgLineEdit->text().toAscii());//以ASCII码形式将行编辑框中的数据写入串口
}
/*关闭串口*/
void MainWindow::on_CloseButton_clicked()
{
myCom->close();
ui->OpenBtn->setEnabled(true);
ui->CloseBtn->setEnabled(false);
ui->baudRatecomboBox->setEnabled(true);
ui->portNamecomboBox->setEnabled(true);
}

需要注意的几个问题

  • 1.在使用查询方式Polling实现串口通信
    Polling方式不能使用readRead()信号,需要自己定义定会器。
    在mainwindow.h中声明定时器对象:添加头文件#include<QTimer>,添加对象声明QTimer *readTimer
    在mainwindow.cpp中定义串口:myCom=new Win_QextSerialPort(portName,QextSerialBase::Polling);
    设置定时器:

    1
    2
    3
    readTimer=new QTimer(this);
    readTimer->start(30);//延时为30ms
    connect(readTimer,SIGNAL(timeout()),this,SLOT(readCom()));
  • 2.时延问题
    数据接收时,要等一会儿才能接收到数据,打印过程中,会有卡顿,可以从以下几个方面考虑原因:

    • a.程序中定时器的时间设置是否过长?定时器时间的设置可以根据数据完整输出到屏幕所需时间进行适当调整
    • b.在构造函数中我们设置过延时:myCom->setTimeout(500),延时500ms,那么这个延时是做什么的?在事件驱动的方式下,一旦有数据就会读取数据到缓冲区,所以这个时延没有什么用处,但是当使用查询方式时,这个延时就真正控制了串口读写数据的时间间隔,这里的读写是指底层的读写,即把读到的数据放到缓冲区,而我们自己设置的定时器是去读缓冲区的数据。

3.1.2 Linux下使用Qt串口编程

  • 将qextserialport.cpp、qextserialport.h、qextserialbase.cpp、qextserialbase.h、posix_qextserialport.cpp和posix_qextserialport.h六个文件拷贝到新建工程目录下,在Qt Creator中左侧文件列表上,鼠标右击工程文件夹,添加现有文件,将六个文件导入工程,Qt会自动区分头文件和.cpp文件。
  • 编写程序
    和windows下串口编程类似,注意更改头文件、对象声明和串口名称:"/dev/ttyS0"

3.2 方法二:在QIODecive基础上,自己写串口类

Qt5加入了串口通信的类,QSerialPort和QSerialPortInfo,也是在QIODevice基础上的,有例子,可以参考。(参考第三方类)

参考资料:
http://blog.csdn.net/liang890319/article/details/7041819
http://www.ibm.com/developerworks/cn/linux/l-serials/
http://blog.csdn.net/yafeilinux/article/details/4717706

写的还不错?那就来个红包吧!
0%