串口通信协议和Linux下的串口编程

一、串口通信介绍:
串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节,尽管比按位字节(byte)的并行通信慢,但是串口可以使用一根线发送数据的同时用另一根线接收数据。串口通信属于异步串行通信方式。串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。

二、串口接头:
常见的串口接头有两种,一种是9针串口(简单DB-9),一种是25针串口(简称DB-25)。

以DB9为例为例,如图:
在这里插入图片描述

母头:泛指所有带孔状的接头(5针朝下,从左到右依次是1~9)
公头:泛指所有带针状的接头(5针朝下,从右到左依次是1~9)

各引脚的功能如下图
在这里插入图片描述
三、TTL电平和RS232电平:

1、TTL(Transistor-Transistor Logic),即晶体管-晶体管逻辑的简称,它是计算机处理器控制的设备内部各部分之间通信的标准技术。TTL电平信号应用广泛,是因为其数据表示采用二进制规定,+5V等价于逻辑”1”,0V等价于逻辑”0”。
数字电路中,由TTL电子元器件组成电路的电平是个电压范围,规定:
输出高电平>=2.4V,输出低电平<=0.4V;
输入高电平>=2.0V,输入低电平<=0.8V。

2、RS232电平
RS232电平是串口的一个标准。

在TXD和RXD数据线上:
  (1)逻辑1为-3~-15V的电压
  (2)逻辑0为3~15V的电压
在RTS、CTS、DSR、DTR和DCD等控制线上:
  (1)信号有效(ON状态)为3~15V的电压
  (2)信号无效(OFF状态)为-3~-15V的电压
这是由通信协议RS-232规定的。
RS-232:标准串口,最常用的一种串行通讯接口。有三种类型(A,B和C),它们分别采用不同的电压来表示on和off。最被广泛使用的是RS-232C,它将mark(on)比特的电压定义为-3V到-12V之间,而将space(off)的电压定义到+3V到+12V之间。传送距离最大为约15米,最高速率为20kb/s。RS-232是为点对点(即只用一对收、发设备)通讯而设计的,其驱动器负载为3~7kΩ。所以RS-232适合本地设备之间的通信。

注意:RS-232 是用正负电压来表示逻辑状态,与晶体管-晶体管逻辑集成电路(TTL)以高低电平表示逻辑状态的规定正好相反。而我们 STM32 芯片使用的就是 TTL 电平,所以要实现 STM32 与计算机的串口通信,需要进行 TTL与 RS-232C 电平转换,通常使用的电平转换芯片是 MAX3232

四、串口通信

两台计算机进行通信时,最少可以只要三根线,分别为RXD,TXD GND。

在这里插入图片描述
串口通信属于异步串行通信方式
异步通信是指发送和接收端使用的是各自的时钟,并且它是一种不连续的传输通信方式,一次通信只能传输一个字符数据(字符帧)。字符帧之间的间隙可以是任意的,下图是异步串行通信帧格式:
在这里插入图片描述
起始位:接收方探测, 默认总线空闲时是高电平,一旦来一个位的低电平,说明一帧数据开始了。紧接着就是数据位,一帧数据有多少个数据位,由通信双方定义。

数据位:在起始位之后,发送端发出的就是数据位,数据位的位数没有严格限制(5-8位都可以)。低位在前,高位在后。由低位向高位逐位发送。

校验位:数据位发完了,一般会跟奇偶校验位(奇校验、偶校验、无校验),验证收发双方的数据是否正常。

奇校验: 数据位加上校验位保证1的个数为奇数;

偶校验: 数据位加上校验位保证1的个数为偶数

停止位:数据发完之后,会发一个停止位,停止位的宽度一般是: 1, 1.5, 2

空闲位:空闲位是指从一个字符的停止位结束到下一个字符的起始位开始,表示线路处于空闲状态,必须由高电平来填充。

五、Linux串口编程

在Linux系统中,一切皆文件,所以串口设备也是一类文件,学习过Linux驱动程序的学员都知道,Linux有三类设备:字符设备,块设备,网络设备。那么串口设备属于字符设备。所以串口设备的命名一般为/dev/ttySn(n = 0、1、2…),如果该串口为USB转串口,可能名称为/dev/ttyUSBn(n = 0、1、2…),不同的平台下串口的名称是不同的,且串口的名称也是可以更改的。如何更改?在板卡对应的Linux驱动中更改。

在Linux下操作串口,那么也就是跟操作一个文件一样,既然是文件,也就可以使用标准的文件操作API来操作。

在进行文件操作时,我们先看一下操作串口需要包含的有文件:

#include <stdio.h>   /*标准输入输出的定义*/
#include <errno.h>  /*错误号定义*/
#include <sys/stat.h>
#include <fcntl.h>  /*文件控制定义*/
#include <termios.h>    /*PPSIX 终端控制定义*/
#include <stdlib.h> /*标准函数库定义*/
#include <sys/types.h>
#include <unistd.h> /*UNIX 标准函数定义*/

1、打开串口

fd = open("/dev/ttyUSB0",O_RDWR|O_NOCTTY|O_NDELAY);
open的第二个参数可以设置为如下:

O_RDONLY:以只读方式打开文件

O_WRONLY:以只写方式打开文件

O_RDWR:以读写方式打开文件
O_RDONLY和O_WRONLY和O_RDWR三个中必选一个

O_APPEND:写入数据时添加到文件末尾

O_CREATE:如果文件不存在则产生该文件,使用该标志需要设置访问权限位mode_t

O_EXCL:指定该标志,并且指定了O_CREATE标志,如果打开的文件存在则会产生一个错误

O_TRUNC:如果文件存在并且成功以写或者只写方式打开,则清除文件所有内容,使得文件长度变为0

O_NOCTTY:如果打开的是一个终端设备,这个程序不会成为对应这个端口的控制终端,如果没有该标志,任何一个输入,例如键盘中止信号等,都将影响进程。

O_NONBLOCK:该标志与早期使用的O_NDELAY标志作用差不多。程序不关心DCD信号线的状态,如果指定该标志,进程将一直在休眠状态,直到DCD信号线为0。

2、写串口

write(fd, buf,sizeof(buf) );

和写入其他设备文件的方式相同,write函数也会返回发送数据的字节数或者发生错误的时候返回-1。通常,发送数据最常见的错误就是EIO,当调制解调器或者数据链路将Data Carrier Detect(DCD)信号线弄掉了,就会发生这个错误,而且,直至关闭端口这个情况会一直持续。

3、读串口

read( fd, buf, sieof(buf));

当端口在raw data mode操作模式下,那么read系统调用将返回从串口输入缓冲区中实际得到的字节数,如果没有数据可读,那么该系统调用将会被阻塞(block)直到有数据为止,如果超过一定时间仍然没有数据可读,那么将返回一个错误(读错误)。read函数也可以立即返回(即在没有数据可读的情况下也能立即返回,而不是被阻塞),需要做如下工作:
fcntl(fd, F_SETFL, FNDELAY);

FNDELAY选项的意思是read函数在没有数据可读的情况下立即返回0。需要重新回到阻塞模式(blocking)下,那么在调用fcntl()的时候不要加上FNDELAY这个选项:
fcntl(fd, F_SETFL, 0);

4、关闭串口

关闭串口使用close系统调用,例如:
close(fd);

5、串口属性配置

如果不设置串口的波特率,数据位,停止位,校验位的情况下,Linux下默认设置的属性值为:

波特率:9600

数据位:8

校验位:n(表示无)

停止位:1

在不设置串口属性值的情况下,也可以读写串口值。

但是在实际产品开发过程中,还是会根据不同的应用场景来设置串口的属性。很多系统都支持POSIX终端(串口)接口,程序可以利用这个接口来改变终端的参数,比如波特率,字符大小等,要利用这个端口的话,你必须将<termios.h>头文件包含到你的程序中,这个头文件中定义了终端控制结构体和POSIX控制函数。

设置串口属性先要tcgetattr()用来取得设备终端属性,然后中间设置波特率,设置停止位,校验位,数据位等,最后用tcsetattr()设置终端的属性。调用这两个函数的时候,需要提供一个包含着所有串口选项的tremios结构体:

struct termios
 
      {
 
      tcflag_t  c_iflag;  //输入选项
 
      tcflag_t  c_oflag;  //输出选项
 
      tcflag_t  c_cflag;  //控制选项
 
      tcflag_t  c_lflag;  //行选项
 
      cc_t      c_cc[NCCS]; //控制字符
       
       };

通过termios结构体的c_cflag成员可以控制串口波特率、数据位、校验位、停止位以及硬件流控制等等,位成员有:
在这里插入图片描述在这里插入图片描述
1)设置串口波特率
使用函数cfsetispeed设置输入波特率cfsetospeed设置输出波特率

struct termios options;

tcgetattr(fd, &options);

cfsetispeed(&options, B19200);

cfsetospeed(&options, B19200);

2)通过位掩码的方式激活本地连接和接受使能选项:CLOCAL和CREAD

options.c_cflag | = CLOCAL | CREAD;

3)设置数据位

数据位指的是每字节中实际数据所占的比特数。要修改数据位可以通过修改termios结构体中c_cflag成员来实现。CS5、CS6、CS7和CS8分别表示数据位为5、6、7和8。值得注意的是,在设置数据位时,必须先使用CSIZE做位屏蔽。

options.c_cflag &= ~CSIZE;

options.c_cflag |= CS8;

4)设置校验位

奇偶校验可以选择偶校验、奇校验等方式,也可以不使用校验。如果要设置为偶校验的话,首先要将termios结构体中c_cflag设置PARENB标志,并清除PARODD标志。如果要设置奇校验,要同时设置termios结构体中c_cflag设置PARENB标志和PARODD标志。激活c_iflag中的奇偶效验使能,如果不想使用任何校验的话,清除termios结构体中c_cflag的PARENB位。

设置奇校验

options.c_cflag |= PARENB;

options.c_cflag |= PARODD;

options.c_iflag |= (INPCK | ISTRIP); //INPCK 打开输入奇偶校验,ISTRIP 去掉字符第8位

设置偶校验

options.c_iflag |= (INPCK|ISTRIP);

options.c_cflag |= PARENB;

options.c_cflag |= ~PARODD;

设置无校验位

options.c_cflag &= ~PARENB;

5)设置停止位

用CSTOPB只能设置1位或2位,通过设置c_cflag中的CSTOPB设置停止位。若停止位为1,则清除CSTOPB;若停止位为2,则激活CSTOPB。

一位停止位

options.c_cflag &=~CSTOP;

两位停止位

options.c_cflag |=CSTOPB;

6)设置最少字符和等待时间

在对接收字符和等待时间没有特别要求的情况下,可以将其设置位0.

newtio.c_cc[VTIME] = 0;
newtio.c_cc[VMIN] = 0;

7)调用函数”tcflush(fd,queue_selector)”来处理要写入引用的对象

queue_selector可能的取值有以下几种

TCIFLUSH:刷新收到的数据但是不读

TCOFLUSH:刷新写入的数据但是不传送

TCIOFLUSH:同时刷新收到的数据但是不读,并且刷新写入的数据但是不传送。

7)最后tcsetattr()设置终端的属性

*int tcsetattr(int fd, int optional_actions, const struct termios termios_p);

第一个参数:fd为打开的终端文件描述符

第二个参数:optional_actions用于控制修改起作用的时间。
可以取如下值:
optional_actions可以取如下的值:

TCSANOW:不等数据传输完毕就立即改变属性。

TCSADRAIN:等待所有数据传输结束才改变属性。

TCSAFLUSH:等待所有数据传输结束,清空输入输出缓冲区才改变属性。

第三个参数:结构体termios_p中保存了要修改的参数。

调用该函数出现的错误信息:

EBADF:非法的文件描述符。

EINTR:tcsetattr函数调用被信号中断。

EINVAL:参数optional_actions使用了非法值,或参数termios中使用了非法值。

六、简单的Linux串口编程

读串口程序

#include <sys/time.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h> //标准函数库定义
#include <unistd.h> //unix标准函数定义
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> //文件控制定义
#include <termios.h> //POSIX终端控制定义

int main(int argc, char **argv)
{
        int fd = -1;
        int rv = -1;
        char buf[128];

        struct termios options;//该结构体包含串口的选项
        /*struct termios
         {
            tcflag_t  c_iflag;  //输入选项
            tcflag_t  c_oflag;  //输出选项
            tcflag_t  c_cflag;  //控制选项
            tcflag_t  c_lflag;  //行选项
            cc_t      c_cc[NCCS]; //控制字符
         }*/

        fd_set rset;

        fd = open("/dev/ttyUSB3",O_RDWR|O_NOCTTY|O_NDELAY);//打开串口设备
        if(fd < 0)
        {
                printf("open ttyUSB3 failure:%s\n",strerror(errno));
                close(fd);
                return -1;
        }

        printf("open ttyUSB3 successfuly\n");

        memset(&options, 0, sizeof(options));

        rv = tcgetattr(fd, &options);//获取原有的串口属性的配置

        if(rv != 0 )
        {
                printf("tcgetattr failure:%s\n", strerror(errno));
                return -2;
        }
        options.c_cflag|=(CLOCAL|CREAD);//CREAD开启串行数据接收,CLOCAL并打开本地连接模式
        //options.c_cflag &= ~(ECHO |ICANON |ECHOE);
        options.c_cflag &= ~CSIZE;//先使用CSIZE做位屏蔽
        options.c_cflag |= CS8;//设置8位数据位
        options.c_cflag &= ~PARENB;//无校验位

        /*设置115200波特率*/
        cfsetispeed(&options,B115200);//设置输入波特率
        cfsetospeed(&options,B115200);//设置输出波特率

        options.c_cflag &= ~CSTOPB;//设置一位停止位
        options.c_cc[VTIME] = 0;//非规范模式读取时的超时时间
        options.c_cc[VMIN] = 0;//非规范模式读取时的最小字符数

        tcflush(fd, TCIFLUSH);//tcflush清空终端未完成的输入/输出请求及数据;TCIFLUSH表示清空正收到的数据,且不>
读取出来

        rv=tcsetattr(fd, TCSANOW, &options);//设置终端的属性
        if(rv != 0)
        {
                printf("tcsetattr failure:%s\n",strerror(errno));
                close(fd);
                return -3;
        }


        while(1)
        {
                FD_ZERO(&rset);
                FD_SET(fd, &rset);

                rv = select(fd+1, &rset, NULL, NULL,NULL);
                if(rv < 0)
                {
                        printf("select failure:%s\n",strerror(errno));
                        close(fd);
                }
                if(rv ==0)
                {
                        printf("select timeout\n");
                        close(fd);
                        }

                memset(buf, 0, sizeof(buf));

                rv = read(fd, buf, sizeof(buf));
                if(rv <0)
                {
                        printf("read from buf failure:%s\n",strerror(errno));
                        close (fd);
                }
                printf("read from buf %d  %s bytes\n",rv, buf);
                sleep(3);
        }

        close(fd);
        return 0;
}

写串口程序

#include <sys/time.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h> //标准函数库定义
#include <unistd.h> //unix标准函数定义
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> //文件控制定义
#include <termios.h> //POSIX终端控制定义

#define BUF   "[email protected]"
int main(int argc, char **argv)
{
        int fd = -1;
        int rv = -1;


        struct termios options;//该结构体包含串口的选项
        /*struct termios
         {
            tcflag_t  c_iflag;  //输入选项
            tcflag_t  c_oflag;  //输出选项
            tcflag_t  c_cflag;  //控制选项
            tcflag_t  c_lflag;  //行选项
            cc_t      c_cc[NCCS]; //控制字符
         }*/



        fd = open("/dev/ttyUSB3",O_RDWR|O_NOCTTY|O_NDELAY);//打开串口设备
        if(fd < 0)
        {
                printf("open ttyUSB3 failure:%s\n",strerror(errno));
                close(fd);
                return -1;
        }

        printf("open ttyUSB3 successfuly\n");

        memset(&options, 0, sizeof(options));

        rv = tcgetattr(fd, &options);//获取原有的串口属性的配置

        if(rv != 0 )
        {
                printf("tcgetattr failure:%s\n", strerror(errno));
                return -2;
        }
        options.c_cflag|=(CLOCAL|CREAD);//CREAD开启串行数据接收,CLOCAL并打开本地连接模式
        //options.c_cflag &= ~(ECHO |ICANON |ECHOE); 
        options.c_cflag &= ~CSIZE;//先使用CSIZE做位屏蔽
        options.c_cflag |= CS8;//设置8位数据位
        options.c_cflag &= ~PARENB;//无校验位

        /*设置115200波特率*/
        cfsetispeed(&options,B115200);//设置输入波特率
        cfsetospeed(&options,B115200);//设置输出波特率

        options.c_cflag &= ~CSTOPB;//设置一位停止位

        options.c_cc[VTIME] = 0;//非规范模式读取时的超时时间
         options.c_cc[VMIN] = 0;//非规范模式读取时的最小字符数

        tcflush(fd, TCIFLUSH);//tcflush清空终端未完成的输入/输出请求及数据;TCIFLUSH表示清空正收到的数据,且不>
读取出来

        rv=tcsetattr(fd, TCSANOW, &options);//设置终端的属性
        if(rv != 0)
        {
                printf("tcsetattr failure:%s\n",strerror(errno));
                close(fd);
                return -3;
        }

        while(1)
        {

                rv = write(fd, BUF,strlen(BUF)) ;
                if(rv < 0)
                {
                        printf("write error:%s\n",strerror(errno));
                        close(fd);
                }
                printf("write to read successfuly %d %s \n",rv, BUF);

                sleep(3);
        }

       close(fd);
        return 0;
}

总的来说进行串口通信总有一个是主动方一个是被动方,而且二者传输数据时,会有一定的协商好的数据格式,二者发送接收都按照此数据格式进行。此篇记录自己学习串口编程的学习,更多关于串口的学习,请关注我接下来的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章