想象一下,你的电脑要通过一根线和一个陌生的硬件(GPS模块)对话,它们必须先约定好“暗号”,比如说话的语速(波特率)、数据的格式等等。我们今天实现读取 GPS 模块的数据。
我将其分成了四个部分来讲解,组合起来就是整个程序
第一部分:准备工作(头文件)
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
* <stdio.h> 和 <stdlib.h>:标准输入输出和标准库,printf、sscanf 等函数都在这里面。
* <string.h>:字符串处理,比如 bzero、strstr、strncmp。
* <sys/types.h>, <sys/stat.h>, <fcntl.h>:这些是文件操作的核心。在 Linux 里,一切皆文件,串口设备(如 /dev/ttySAC1)也被当成文件来操作。open 函数就需要它们。
* <unistd.h>:提供了 read, write, close 等通用的系统调用函数。
* <termios.h>:这是本程序的重中之重! 它定义了 struct termios 结构体,专门用来配置串口的各种参数,比如波特率、数据位、校验位等。
* <errno.h>:当系统调用出错时,它会提供一个错误码,perror 函数会用它来打印出具体的错误信息。
第二部分:配置串口(set_opt 函数)
这个函数是整个程序的“心脏”,它负责和 GPS 模块“对暗号”。如果暗号对不上,就什么也收不到。
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
struct termios newtio,oldtio;
if ( tcgetattr( fd,&oldtio) != 0) {
perror("SetupSerial 1");
return -1;
}
bzero( &newtio, sizeof( newtio ) );
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/
newtio.c_oflag &= ~OPOST; /*Output*/
switch( nBits )
{
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
}
switch( nEvent )
{
case 'O':
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
case 'E':
newtio.c_iflag |= (INPCK | ISTRIP);
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
break;
case 'N':
newtio.c_cflag &= ~PARENB;
break;
}
switch( nSpeed )
{
case 2400:
cfsetispeed(&newtio, B2400);
cfsetospeed(&newtio, B2400);
break;
case 4800:
cfsetispeed(&newtio, B4800);
cfsetospeed(&newtio, B4800);
break;
case 9600:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
case 115200:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
default:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
}
if( nStop == 1 )
newtio.c_cflag &= ~CSTOPB;
else if ( nStop == 2 )
newtio.c_cflag |= CSTOPB;
newtio.c_cc[VMIN] = 1; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间:
* 比如VMIN设为10表示至少读到10个数据才返回,
* 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
* 假设VTIME=1,表示:
* 10秒内一个数据都没有的话就返回
* 如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
*/
tcflush(fd,TCIFLUSH);
if((tcsetattr(fd,TCSANOW,&newtio))!=0)
{
perror("com set error");
return -1;
}
//printf("set done!\n");
return 0;
}
* struct termios newtio, oldtio;:定义两个结构体变量。oldtio 用来备份串口原来的配置,newtio 用来设置我们想要的新配置
* tcgetattr(fd, &oldtio):获取当前串口的配置,并保存到 oldtio 中。
* fd:文件描述符,就是我们打开的串口设备。
* &oldtio:存放配置的结构体地址。
* 如果返回值不是 0,说明获取失败,用 perror 打印错误并返回。
* bzero(&newtio, sizeof(newtio)):把 newtio 这块内存全部清零。这是个好习惯,确保结构体里没有随机的垃圾数据
* newtio.c_cflag:这是控制模式标志(Control modes flag)。
* CLOCAL:忽略调制解调器(Modem)的状态线。简单说,就是不管有没有“载波检测”信号,都强制打开串口。对于直接连接的设备(如GPS),必须设置这个。
* CREAD:允许接收数据。如果不设置,串口就只能发不能收。
* CSIZE:这是一个位掩码,用来设置数据位的长度。
* &= ~CSIZE:这一步是先清零。因为 CSIZE 可能包含 CS5, CS6, CS7, CS8 等值,我们必须先把它清空,才能设置我们想要的值。
* newtio.c_lflag:这是本地模式标志(Local modes flag),主要控制终端的输入输出行为。
* ICANON:关闭“规范模式”。规范模式下,输入会经过处理(比如退格键会删除前一个字符),并且要等到回车才一次性返回。我们读取 GPS 数据需要“原始模式”,收到一个字节就处理一个,所以要关闭它。
* ECHO, ECHOE:关闭回显。我们输入的数据不需要在屏幕上显示出来。
* ISIG:关闭信号字符。比如 Ctrl+C 通常会产生中断信号,在这里我们不需要,所以要关闭。
* newtio.c_oflag:这是输出模式标志(Output modes flag)。
* OPOST:关闭输出处理。比如自动把换行符 n 转换成回车换行 rn。我们不需要任何处理,直接输出原始数据
* 设置数据位:GPS 模块通常使用 8 个数据位。
* CS7:7个数据位。
* CS8:8个数据位。我们通过 |= 操作符来设置。
* 设置校验位:用于检查数据传输是否出错。
* PARENB:启用校验。
* PARODD:设置为奇校验。如果清除这个位(&= ~PARODD),就是偶校验。
* INPCK:启用输入校验。
* ISTRIP:剥离第8位。如果数据是7位,这个标志会把第8位去掉。
* GPS 通常使用 无校验 ('N'),所以 newtio.c_cflag &= ~PARENB; 这行最关键。
* 设置波特率:也就是通信速度。收发双方必须一致。
* cfsetispeed:设置输入波特率。
* cfsetospeed:设置输出波特率。
* B9600 等是系统定义的宏,代表具体的速率值。
* 设置停止位:用于分隔数据包。
* CSTOPB:使用 2 个停止位。
* &= ~CSTOPB:清除 CSTOPB,表示使用 1 个停止位。
* GPS 通常使用 1 个停止位。
* 设置读取行为:这是控制 read 函数何时返回的关键
* VMIN = 1:表示 read 函数至少要读到 1 个字节的数据才会返回。
* VTIME = 0:表示不使用定时器。
* 组合起来的意思是:read 函数会一直阻塞(等待),直到至少读到 1 个字节的数据为止
* tcflush:清空串口缓冲区。
* TCIFLUSH:清空接收到但尚未读取的数据。防止读到旧的、不完整的数据。
* tcsetattr:应用我们设置的新配置。
* TCSANOW:表示更改立即生效。
* 如果设置成功,函数返回 0。
第三部分:打开串口(open_port 函数)
int open_port(char *com)
{
int fd;
//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
fd = open(com, O_RDWR|O_NOCTTY);
if (-1 == fd){
return(-1);
}
if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/
{
printf("fcntl failed!\n");
return -1;
}
return fd;
}
* open(com, O_RDWR | O_NOCTTY):打开串口设备文件。
* com:设备文件路径,如 /dev/ttySAC1。
* O_RDWR:以读写方式打开。
* O_NOCTTY:告诉系统,这个程序不希望成为这个串口的“控制终端”。如果不加这个,来自串口的 Ctrl+C 等信号可能会终止你的程序。
* fcntl(fd, F_SETFL, 0):设置文件状态标志。
* F_SETFL:设置文件描述符 fd 的标志。
* 0:表示清除所有标志,将串口设置为阻塞模式。这意味着 read 函数会一直等待,直到有数据到来。
第四部分:读取和解析数据
int read_gps_raw_data(int fd, char *buf)
{
int i = 0;
int iRet;
char c;
int start = 0;
while (1)
{
iRet = read(fd, &c, 1);
if (iRet == 1)
{
if (c == '$')
start = 1;
if (start)
{
buf[i++] = c;
}
if (c == '\n' || c == '\r')
{
buf[i] = '\0';
return 0;
}
}
else
{
return -1;
}
}
}
GPS 数据是以 NMEA-0183 协议发送的,每条语句都以 开头,以 <CR><LF>(回车换行)结尾。
1. read(fd, &c, 1):每次只读 1 个字节。
2. if (c == '') start = 1;:寻找数据包的开头。
3. if (start) { buf[i++] = c; }:一旦找到 ,就开始把数据存入缓冲区 buf。
4. if (c == 'n' || c == 'r'):如果遇到换行或回车,说明一条完整的数据接收完毕。
5. buf[i] = '0';:在末尾加上字符串结束符,方便后续处理。
1. strncmp(buf+3, "GGA", 3) != 0:我们只关心 GPGGA 语句,它包含了时间、位置、定位质量等核心信息。buf+3 跳过了 GP。
2. strstr(buf, ",,,,,")):检查是否有连续的 5 个逗号。在 GGA 语句中,这通常意味着没有定位成功(数据为空)。
3. sscanf(buf, "%[^,],...", ...):这是 C 语言中非常强大的字符串解析函数。
* %[^,]:这是一个扫描集。它的意思是“读取一串字符,直到遇到逗号 , 为止”。
* 通过这种方式,我们可以轻松地把用逗号分隔的字符串,分别存入 time, lat (纬度), ns (南北纬), lng (经度), ew (东西经) 这些变量中。
第五部分:主函数(main)
int main(int argc, char **argv)
{
int fd;
int iRet;
char c;
char buf[1000];
char time[100];
char Lat[100];
char ns[100];
char Lng[100];
char ew[100];
float fLat, fLng;
/* 1. open */
/* 2. setup
* 115200,8N1
* RAW mode
* return data immediately
*/
/* 3. write and read */
if (argc != 2)
{
printf("Usage: \n");
printf("%s </dev/ttySAC1 or other>\n", argv[0]);
return -1;
}
fd = open_port(argv[1]);
if (fd < 0)
{
printf("open %s err!\n", argv[1]);
return -1;
}
iRet = set_opt(fd, 9600, 8, 'N', 1);
if (iRet)
{
printf("set port err!\n");
return -1;
}
while (1)
{
/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"<CR><LF>*/
/* read line */
iRet = read_gps_raw_data(fd, buf);
/* parse line */
if (iRet == 0)
{
printf("GPS raw data: %s\n", buf);
iRet = parse_gps_raw_data(buf, time, Lat, ns, Lng, ew);
}
/* printf */
if (iRet == 0)
{
printf("Time : %s\n", time);
printf("ns : %s\n", ns);
printf("ew : %s\n", ew);
printf("Lat : %s\n", Lat);
printf("Lng : %s\n", Lng);
/* 纬度格式: ddmm.mmmm */
sscanf(Lat+2, "%f", &fLat);
fLat = fLat / 60;
fLat += (Lat[0] - '0')*10 + (Lat[1] - '0');
/* 经度格式: dddmm.mmmm */
sscanf(Lng+3, "%f", &fLng);
fLng = fLng / 60;
fLng += (Lng[0] - '0')*100 + (Lng[1] - '0')*10 + (Lng[2] - '0');
printf("Lng,Lat: %.06f,%.06f\n", fLng, fLat);
}
}
return 0;
}
GPS 模块输出的经纬度格式是 ddmm.mmmm (度分格式),而不是我们常用的十进制度数。
例如,纬度 4005.22599 代表 40度 05.22599分 。
转换思路:
分离“度”和“分”:
对于纬度 Lat (如 "4005.22"), Lat+2 指向 "05.22"。 sscanf(Lat+2, "%f", &fLat) 就把“分”的部分读出来了。
(Lat[0] - '0')*10 + (Lat[1] - '0') 这部分代码,是把字符 '4' 和 '0' 转换成数字 40,得到“度”的部分。
将“分”转换为“度”:
我们知道 1 度 = 60 分。所以, fLat / 60 就把“分”换算成了“度”的小数部分。
相加:
最后,把整数部分的“度”和小数部分的“度”加起来,就得到了标准的十进制经纬度。

3658

被折叠的 条评论
为什么被折叠?



