要在C语言中接收GPS数据,需要使用串口通信来与GPS设备进行数据交互。一个简单的串口通信代码主要包含了以下几个部分:
1.标准库头文件
stdio.h
:包含输入输出函数,如 printf
string.h
:包含字符串处理函数,如 memset
。
unistd.h
:包含 POSIX 操作系统 API,如 read
和 close
。
fcntl.h
:包含文件控制相关函数,如 open
和 fcntl
。
termios.h
:包含串口配置和控制的结构体和常量。
2.串口控制变量
fd
:用于存储串口文件描述符。
fd主要用于描述状态,比如打开文件是否成功
options
:termios
结构体,用于配置串口的参数。
buffer
:用于存储从串口读取的数据。
termios
结构体说明
termios结构体中,该结构体一般包括如下的成员:
tcflag_t c_iflag;
tcflag_t c_oflag;
tcflag_t c_cflag;
tcflag_t c_lflag;
cc_t c_cc[NCCS];
(1)c_iflag:输入模式标志,控制终端输入方式。
c_iflag参数表
键值说明
IGNBRK 忽略BREAK键输入
BRKINT 如果设置了IGNBRK,BREAK键的输入将被忽略,如果设置了BRKINT ,将产生SIGINT中断
IGNPAR 忽略奇偶校验错误
PARMRK 标识奇偶校验错误
INPCK 允许输入奇偶校验
ISTRIP 去除字符的第8个比特
INLCR 将输入的NL(换行)转换成CR(回车)
IGNCR 忽略输入的回车
ICRNL 将输入的回车转化成换行(如果IGNCR未设置的情况下)
IUCLC 将输入的大写字符转换成小写字符(非POSIX)
IXON 允许输入时对XON/XOFF流进行控制
IXANY 输入任何字符将重启停止的输出
IXOFF 允许输入时对XON/XOFF流进行控制
IMAXBEL 当输入队列满的时候开始响铃,Linux在使用该参数而是认为该参数总是已经设置
(2)c_oflag:输出模式标志,控制终端输出方式。
c_oflag参数
键值说明
OPOST 处理后输出
OLCUC 将输入的小写字符转换成大写字符(非POSIX)
ONLCR 将输入的NL(换行)转换成CR(回车)及NL(换行)
OCRNL 将输入的CR(回车)转换成NL(换行)
ONOCR 第一行不输出回车符
ONLRET 不输出回车符
OFILL 发送填充字符以延迟终端输出
OFDEL 以ASCII码的DEL作为填充字符,如果未设置该参数,填充字符将是NUL(‘/0’)(非POSIX)
NLDLY 换行输出延时,可以取NL0(不延迟)或NL1(延迟0.1s)
CRDLY 回车延迟,取值范围为:CR0、CR1、CR2和 CR3
TABDLY 水平制表符输出延迟,取值范围为:TAB0、TAB1、TAB2和TAB3
BSDLY 空格输出延迟,可以取BS0或BS1
VTDLY 垂直制表符输出延迟,可以取VT0或VT1
FFDLY 换页延迟,可以取FF0或FF1
(3)c_cflag:控制模式标志,指定终端硬件控制信息。
c_oflag参数
键值说明
CBAUD 波特率(4+1位)(非POSIX)
CBAUDEX 附加波特率(1位)(非POSIX)
CSIZE 字符长度,取值范围为CS5、CS6、CS7或CS8
CSTOPB 设置两个停止位
CREAD 使用接收器
PARENB 使用奇偶校验
PARODD 对输入使用奇偶校验,对输出使用偶校验
HUPCL 关闭设备时挂起
CLOCAL 忽略调制解调器线路状态
CRTSCTS 使用RTS/CTS流控制
(4)c_lflag:本地模式标志,控制终端编辑功能。
c_lflag参数
键值说明
ISIG 当输入INTR、QUIT、SUSP或DSUSP时,产生相应的信号
ICANON 使用标准输入模式
XCASE 在ICANON和XCASE同时设置的情况下,终端只使用大写。如果只设置了XCASE,则输入字符将被转换为小写字符,除非字符使用了转义字符(非POSIX,且Linux不支持该参数)
ECHO 显示输入字符
ECHOE 如果ICANON同时设置,ERASE将删除输入的字符,WERASE将删除输入的单词
ECHOK 如果ICANON同时设置,KILL将删除当前行
ECHONL 如果ICANON同时设置,即使ECHO没有设置依然显示换行符
ECHOPRT 如果ECHO和ICANON同时设置,将删除打印出的字符(非POSIX)
TOSTOP 向后台输出发送SIGTTOU信号
3.打开串口设备
fd = open("/dev/pts/7", O_RDWR | O_NOCTTY | O_NDELAY);
open()函数
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open()函数是在学习文件IO中的第一个函数。作用是打开一个文件,或者创建出一个文件。
能实现这种功能的函数有两套,一个是系统IO提供的open(),还有一个是标准的fopen()函数。
flags:标志(本质上是一个数字)
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以可读写方式打开文件.
// 上述三种旗标是互斥的, 也就是不可同时使用,
// 但可与下列的旗标利用 OR(|)运算符组合.
O_CREAT 若欲打开的文件不存在则自动建立该文件.
O_EXCL 如果 O_CREAT 也被设置, 此指令会去检查文件是否存在. 文件若不存在则建立该文件, 否
O_NOCTTY 如果欲打开的文件为终端机设备时, 则不会将该终端机当成进程控制终端机.
O_TRUNC 若文件存在并且以可写的方式打开时, 此旗标会令文件长度清为 0, 而原来存于该文件的资
O_APPEND 当读写文件时会从文件尾开始移动, 也就是所写入的数据会以附加的方式加入到文件后面
O_NONBLOCK 以不可阻断的方式打开文件, 也就是无论有无数据读取或等待, 都会立即返回进程之中
O_NDELAY 同 O_NONBLOCK.
O_SYNC 以同步的方式打开文件.
O_NOFOLLOW 如果参数 pathname 所指的文件为一符号连接, 则会令打开文件失败.
(1)O_RDONLY,O_WRONLY,O_RDWR三个互斥:
不可以同时使用,也不能添加 | (本质上都占用了两个同样的数字位),其余的旗标是可以加 | 的(占用不同的数字位)。
例如:8bit控制位,控制读写的在第1,2bit,xxxxxx00表示读数据,xxxxxx01表示写数据,xxxxxx11表示读写。这时O_RDONLY | O_WRONLY (00 | 01)还是只能得到数字01(O_WRONLY),对这三个旗标用 | 进行操作意义不大。
(2)O_CREAT | O_EXCL 常用组合作用解析
调用 open("abc.c",O_CREAT|O_EXCL);
系统会自动检测当前路径下"abc.c"文件是否存在。
如果存在,就返回 -1 (文件打开错误)。
如果不存在,就自动创建文件并返回文件当前进程ID。
(3)O_TRUNC 与 O_APPEND
写入文件时,系统默认将光标移动到文件开头。
O_TRUNC:作用是在光标移动的同时将文件长度清零(清空文件)。
O_APPEND:作用是在尾部追加内容,即将光标移动到文件末尾。
(4)O_NONBLOCK解析
在文件处于阻塞状态时强行打开文件。
例如我写了个只有从串口接收到数据才能关闭并且保存文件的程序,如果程序迟迟没有从串口中接收到数据,那么文件就会一直处于阻塞状态中等待数据,这个时候文件无法正常打开,需要添加此旗标。
fcntl函数()
int fcntl(int fd, int cmd, ... /* arg */ );// arg表示可变参数,由cmd决定
fcntl()的第三个参数是可选。是否需要此参数由cmd决定。所需的参数类型在每个cmd名称后面的括号中指示(在大多数情况下,所需的类型是int,我们使用名称arg来标识参数),如果不需要参数,则指定void。
以下某些操作仅在特定的Linux内核版本之后才受支持。检查主机内核是否支持特定操作的首选方法是使用所需的cmd值调用fcntl(),然后使用EINVAL测试调用是否失败,这表明内核无法识别该值。
主要介绍下面4个功能:
1、复制文件描述符(F_DUPFD、F_DUPFD_CLOEXEC);
(1)F_DUPFD(int)
F_DUPFD(int) 表示使用 F_DUPFD 作为cmd时,第三个参数需要传入int型数据。
cmd为F_DUPFD表示复制文件描述符fd。调用成功会返回新的描述符。新描述符使用大于或等于arg参数的编号最低的可用文件描述符复制文件描述符fd。新描述符与f似共享同一文件表项。但是,新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除〈这表示该描述符在 exec 时仍保持打开状态)。
(2 )F_DUPFD_CLOEXEC(int)
F_DUPFD_CLOEXEC(int) 表示使用 F_DUPFD_CLOEXEC 作为cmd时,第三个参数需要传入int型数据。
cmd为F_DUPFD_CLOEXEC的功能与F_DUPFD类似,区别在于F_DUPFD_CLOEXEC在复制的同时会设置文件描述符标志FD_CLOEXEC,表示在执行exec系列函数后,该描述符会关闭。
2、获取/设置文件描述符标志(F_GETFD、F_SETFD);
当前只定义了一个文件描述符标志FD_CLOEXEC。用来表示该描述符在执行完fork+exec系列函数创建子进程时会自动关闭,以防止它们被传递给子进程。为什么要这样做呢?
因为当一个进程调用exec系列函数(比如execve)来创建子进程时,所有打开的文件描述符都会被传递给子进程。如果文件描述符没有设置FD_CLOEXEC标志,这些文件将保持打开状态并继续对子进程可见。这可能导致潜在的安全风险或者意外行为。
F_GETFD(void) :表示使用 F_GETFD 作为cmd时,不需要传入第三个参数。
功能:获取文件描述符标志。
返回值:
成功返回文件描述符标志
失败返回 -1.
F_SETFD(int):表示使用 F_SETFD 作为cmd时,传入第三个参数是int型的。
功能:设置文件描述符标志,第三个参数传入新的标志值。
返回值:
成功返回 0
失败返回 -1.
文件描述符的FD_CLOEXEC标志可以通过三个方法得到:
1、调用open函数是,指定 O_CLOEXEC
2、通过fcntl函数使用F_DUPFD_CLOEXEC复制文件描述符,新的描述符就是FD_CLOEXEC
3、通过fcntl函数使用F_SETFD直接设置FD_CLOEXEC。
3、获取/设置文件状态标志(F_GETFL、F_SETFL);
文件状态标志如下表:
F_GETFL(void) :表示使用 F_GETFL 作为cmd时,不需要传入第三个参数。
功能:获取文件状态标志。
返回值:
成功返回文件状态标志
失败返回 -1.
访问方式标志:O_RDONLY 、O_WRONLY、O_RDWR。这3个值是互斥的,因此首先必须用屏蔽O_ACCMODE取得访问方式位,然后将结果与这3个值中的每一个相比较。
F_SETFL(int):表示使用 F_SETFL 作为cmd时,传入第三个参数是int型的。
功能:设置文件状态标志,第三个参数传入新的文件状态标志值。
返回值:
成功返回 0
失败返回 -1.
在Linux上,只能设置这5个文件状态标志:O_APPEND、 O_ASYNC、 O_DIRECT、 O_NOATIME、O_NONBLOCK,其中最常用的是将文件描述符设置成非阻塞(O_NONBLOCK),特别是在网络编程中很常见。
4、获取/设置记录锁(F_GETLK、F_SETLK、F_SETLKW);
Linux实现了POSIX标准化的传统(“进程相关”)UNIX记录锁。
记录锁(record locking)的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。对于UNIX系统而言,“记录”这个词是一种误用,因为UNIX系统内核根本没有使用文件记录这种概念。更适合的术语可能是字节范围锁(byte-rangelocking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。
F_SETLK、F_SETLKW和F_GETLK用于获取、释放和测试记录锁(也称为字节范围、文件段或文件区域锁)的存在。使用记录锁时,第三个参数是指向struct flock
结构的指针。
struct flock
{
...
short l_type;//锁的类型:F_RDLCK(读锁)、F_WRLCK(写锁)、F_UNLCK(解锁)
short l_whence;//偏移的起点:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start; //锁区偏移,从l_whence
off_t l_len; //锁区长度(字节)
pid_t l_pid; //阻塞我们加锁的进程ID
...
}
F_SETLK(struct flock *):表示使用 F_SETLK 作为cmd时,传入第三个参数是struct flock *型的。
在锁的l_where、l_start和l_len字段指定的字节上设置锁(当l_type为F_RDLCK或F_WRLCK时)或释放锁(当l_type为F_UNLOCK时)。如果另一个进程持有冲突锁,则此调用返回-1并将errno设置为EACCES或EAGAIN。(本例中返回的错误因实现而异,因此POSIX需要一个可移植的应用程序来检查这两个错误。)
F_SETLKW(struct flock *):表示使用 F_SETLKW 作为cmd时,传入第三个参数是struct flock *型的。
类似于F_SETLK(命名中的W表示等待(wait)),但如果文件上持有冲突的锁,则等待该锁释放。如果在等待时捕获到信号,则调用将中断,并且(在信号处理程序返回后)立即返回(返回值为-1,errno设置为EINTR;)。
F_GETLK(struct flock *):表示使用 F_GETLK 作为cmd时,传入第三个参数是struct flock *型的。
判断由struct flock *(第三个参数)所描述的锁是否会被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由第三个参数(struct flock *)所描述的锁,则把该现存锁的信息写到第三个参数(struct flock *)指向的结构中。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,第三个参数所指向结构中的其他信息保持不变。
tcgetattr()和tcsetattr()函数
头文件<termios.h>
tcgeattr()
tcgetattr函数,用来获取终端参数,成功返回零;失败则返回非零,发生失败接口将设置errno标识。
tcsetattr()
tcsetattr函数,用来设置终端参数,成功返回零;失败则返回非零,发生失败接口将设置errno标识。
cfsetispeed()和cfsetospeed()、cfsetspeed()函数
头文件<termios.h>和<unistd.h>需要用到termios结构体
cfsetispeed()函数
设置输入波特率。
注:设置波特率有专门的函数,用户不能直接通过位掩码来操作。
cfsetospeed()函数
设置输出波特率。
注:设置波特率有专门的函数,用户不能直接通过位掩码来操作。
cfsetspeed()函数
设置波特率。
注:设置波特率有专门的函数,用户不能直接通过位掩码来操作。
4.配置控制标志c_cflag
options.c_cflag |=(CLOCAL | CREAD);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~CRTSCTS;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_oflag &= ~OPOST;
常见的配置操作
这里讲一下 |= 和| 以及&=和&
|=
和 |
、&=
和 &
是用于位运算的操作符,通常用于修改、设置、或清除变量中的特定位。
|
(按位或运算符)
|
是按位或运算符,对两个数的二进制位逐位进行比较,如果其中一个位为 1,则结果的对应位为 1,否则为 0
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int c = a | b; // 结果:0111,即 7
|=
(按位或赋值运算符)
|=
是按位或赋值运算符,用于将变量与另一个值进行按位或运算,并将结果赋值回该变量。
int a = 5; // 二进制:0101
a |= 3; // 二进制:0011
// a = 0101 | 0011 = 0111,即 7
&
(按位与运算符)
&
是按位与运算符,对两个数的二进制位逐位比较,只有当对应位都为 1 时,结果的该位才为 1,否则为 0。
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int c = a & b; // 结果:0001,即 1
&=
(按位与赋值运算符)
&=
是按位与赋值运算符,将变量与另一个值按位与运算,并将结果赋值回该变量。
int a = 5; // 二进制:0101
a &= 3; // 二进制:0011
// a = 0101 & 0011 = 0001,即 1
|=通常同于设置控制位,&=
通常用于清除(置零)特定位,~FLAG
是取反操作,用于生成 FLAG 的反码,即将 FLAG 位清零。
5.While循环读取串口数据
read()函数
头文件 <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read函数是Linux系统API中的一员,它的作用是:从指定的文件中读取数据。
参数:fd //传入的是一个文件描述符,它是open函数的返回值。可通过open函数得到
参数:buf //传入的是一个数组的名字,是用来暂存已经读取到的数据用的。
参数:count //一次性读取多少个数据到数组中,一般是数组的大小。
如果成功,则返回实际读取到的字节数。(是一个ssize_t类型的一个数)
如果失败,返回-1,并且将errno设置为合适的错误原因。
close()函数
头文件 <unistd.h>
int close(int fd);
返回值:成功返回0,出错返回-1并设置errno
参数fd是要关闭文件描述符。当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。
由open返回的文件描述符一定是该进程尚未使用的最小的描述符。
附上完整串口通信打印数据代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
int main()
{
int fd;
struct termios options;//termios结构体一般包括
char buffer[255];
fd =open("/dev/pts/7",O_RDWR | O_NOCTTY | O_NDELAY);
//O_RDWR:以读写模式打开文件或设备
//O_NOCTTY:不将打开的文件或设备分配为进程的控制终端
//O_NDELAY:非阻塞模式
if (fd ==-1)
{
printf("Error opening serial port!\n");
return -1;
}
fcntl(fd,F_SETFL,0);
//fcntl系统调用可以用来对已打开的恩间描述符进行各种控制操作以改变已打开文件的各种属性,
//F_SRTFL:设置文件状态标志
tcgetattr(fd,&options);
//获取终端相关参数
//设置输入串口波特率
cfsetispeed(&options,B9600);
//设置输出串口波特率
cfsetospeed(&options,B9600);
//|:按为或 |=:按位或赋值
options.c_cflag |=(CLOCAL | CREAD);
//&:按位与 &=:按位与赋值操作
//用于清除 c_cflag 中的PARENB标志位
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag &= ~CRTSCTS;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_oflag &= ~OPOST;
tcsetattr(fd,TCSANOW,&options);
while(1)
{
int bytes_read =read(fd,buffer,sizeof(buffer));
if(bytes_read>0)
{
//如果读取到的数据量 bytes_read 大于 0,则数据被写入 buffer,添加字符串终止符 \0 后输出到终端。
buffer[bytes_read]='\0';
printf("%s",buffer);
}
}
close(fd);
return 0;
}