Linux串口应用编程——GPS

想象一下,你的电脑要通过一根线和一个陌生的硬件(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  就把“分”换算成了“度”的小数部分。

相加:

最后,把整数部分的“度”和小数部分的“度”加起来,就得到了标准的十进制经纬度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值