IMX6ULL Linux GPS串口读取实验
GPS协议:采用NMEA-0183协议,核心定位帧为GGA帧,以$开头、\n/r结尾。串口参数:GPS模块默认9600波特率、8N1(8数据位、无校验、1停止位),不可随意修改。阻塞/超时模式:通过VMIN和VTIME配置,本文优化后避免程序卡死。经纬度转换:GPS输出格式为ddmm.mmmm(纬度)、dddmm.mmmm(经度),需转换为标准十进制度数。关键注意点:串口交叉连接、拔掉短接线、GP
一、实验概述
1.1 实验目的
-
掌握Linux下串口设备的打开、配置、读写操作,理解串口阻塞/超时模式的应用
-
学会读取GPS模块输出的NMEA-0183协议数据,解析经纬度、时间等核心信息
-
解决GPS实验中常见的程序卡死、设备名错误、无信号等问题
-
适配百问网IMX6ULL开发板,实现“无GPS信号也不卡死”的稳定运行
1.2 适用环境
-
硬件:IMX6ULL开发板、GPS模块、杜邦线、串口调试工具
-
软件:Ubuntu虚拟机(已配置arm-buildroot-linux-gnueabihf交叉编译工具链)
-
开发板系统:百问网Buildroot系统(默认NFS共享目录:/home/book/nfs_rootfs)
1.3 实验核心功能
通过开发板UART5串口读取GPS模块数据,解析NMEA-0183协议中的GGA定位帧,提取时间、纬度、经度,并转换为标准十进制度数格式;添加超时机制,未接GPS、无信号时不卡死,且给出明确中文提示。
二、硬件准备与接线
2.1 硬件清单
-
IMX6ULL开发板(百问网)
-
GPS模块(支持NMEA-0183协议,默认波特率9600)
-
杜邦线4根(供电2根、串口2根)
-
串口调试工具(用于登录开发板)
2.2 关键接线规则(必严格遵守)
GPS模块与开发板UART5(实验专用串口)交叉连接,禁止接反;同时拔掉之前串口自发自收实验的短接线(否则GPS数据无法输入)。
|
GPS模块引脚 |
IMX6ULL开发板引脚 |
注意事项 |
|---|---|---|
|
VCC |
3.3V |
禁止接5V,否则烧毁GPS模块 |
|
GND |
GND |
必须共地,否则数据传输异常 |
|
TX(发送) |
UART5_RXD(接收) |
交叉连接,GPS发送→开发板接收 |
|
RX(接收) |
UART5_TXD(发送) |
交叉连接,开发板发送→GPS接收 |
2.3 禁止操作
-
禁止使用UART0(/dev/ttymxc0):该串口是开发板调试登录口,使用会导致系统卡死
-
禁止GPS模块接5V供电:3.3V为GPS标准供电电压
-
禁止TX接TX、RX接RX:必须交叉连接,否则无法接收数据
-
禁止保留UART5的短接线:自发自收实验的短接线需拔掉,避免数据冲突
三、带超详细注释的完整代码
文件名:gps_read.c(核心优化:无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>
// 串口配置函数:配置GPS模块默认参数(9600波特率、8数据位、无校验、1停止位)
// 参数说明:fd-串口文件描述符;nSpeed-波特率;nBits-数据位;nEvent-校验位;nStop-停止位
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
struct termios newtio,oldtio;
// 读取当前串口的原始配置,保存到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);
newtio.c_oflag &= ~OPOST; // 关闭输出数据处理(不添加换行、回车等转换)
// 配置数据位:支持7位或8位,GPS默认8位数据位
switch( nBits )
{
case 7:
newtio.c_cflag |= CS7; // 7位数据位
break;
case 8:
newtio.c_cflag |= CS8; // 8位数据位(GPS默认,实验使用)
break;
}
// 配置校验位:支持奇校验(O)、偶校验(E)、无校验(N),GPS默认无校验
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': // 无校验(GPS默认,实验使用)
newtio.c_cflag &= ~PARENB;
break;
}
// 配置波特率:GPS模块默认9600,支持2400、4800、115200等常用波特率
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); // 接收波特率(GPS→开发板)
cfsetospeed(&newtio, B9600); // 发送波特率(开发板→GPS)
break;
case 115200:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
default: // 默认波特率9600
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
}
// 配置停止位:GPS默认1位停止位,支持1位或2位
if( nStop == 1 )
newtio.c_cflag &= ~CSTOPB; // 1位停止位(GPS默认,实验使用)
else if ( nStop == 2 )
newtio.c_cflag |= CSTOPB; // 2位停止位
// ===================== 核心优化:解决程序卡死 =====================
newtio.c_cc[VMIN] = 0; // 不强制等待数据(无数据也不阻塞)
newtio.c_cc[VTIME] = 10; // 超时时间:1秒(10×0.1秒,单位为1/10秒)
// 说明:超时后read函数返回,程序不会无限卡死,而是给出提示
// ==============================================================
tcflush(fd,TCIFLUSH); // 清空串口接收缓冲区,避免残留数据干扰
// 立即生效新的串口配置,失败则打印错误信息
if((tcsetattr(fd,TCSANOW,&newtio))!=0)
{
perror("com set error");
return -1;
}
return 0; // 配置成功,返回0
}
// 打开串口函数:打开指定串口设备,设置为阻塞模式(适配GPS数据读取)
// 参数:com-串口设备名(如/dev/ttymxc5)
int open_port(char *com)
{
int fd;
// 打开串口:读写模式(O_RDWR) + 不将串口作为控制终端(O_NOCTTY)
fd = open(com, O_RDWR|O_NOCTTY);
if (-1 == fd){ // 打开失败,返回-1
return(-1);
}
// 设置串口为阻塞模式:read函数会等待数据,直到超时或读到数据
if(fcntl(fd, F_SETFL, 0)<0)
{
printf("fcntl failed!\n");
return -1;
}
return fd; // 打开成功,返回串口文件描述符(后续操作的唯一标识)
}
// 读取GPS原始数据函数:读取一帧完整的GPS数据($开头,\n或\r结尾)
// 参数:fd-串口文件描述符;buf-存储GPS原始数据的缓冲区
// 返回值:0-读取成功;-2-未收到数据(超时);-1-读取失败
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); // 逐字节读取串口数据
// 超时或无数据(iRet≤0),返回-2,提示未收到GPS数据
if (iRet <= 0)
{
return -2;
}
// 读到数据(iRet==1),开始处理
if (iRet == 1)
{
if (c == '$') // 检测到GPS数据帧开头($是NMEA协议帧的起始标志)
start = 1;
if (start) // 开始接收数据后,将字节存入缓冲区
{
buf[i++] = c;
}
// 检测到数据帧结尾(\n或\r),说明一帧数据接收完成
if (c == '\n' || c == '\r')
{
buf[i] = '\0'; // 添加字符串结束符,避免乱码
return 0; // 读取成功,返回0
}
}
}
}
// 解析GPS原始数据函数:解析NMEA协议中的GGA帧,提取时间、纬度、经度等信息
// 参数:buf-原始GPS数据;time-存储时间;lat-存储纬度;ns-存储南北纬;lng-存储经度;ew-存储东西经
// 返回值:0-解析成功;-1-解析失败(非GGA帧/无定位信号)
int parse_gps_raw_data(char *buf, char *time, char *lat, char *ns, char *lng, char *ew)
{
char tmp[10]; // 临时缓存,用于拆分数据
if (buf[0] != '$') // 不是GPS数据帧(GPS帧以$开头)
return -1;
else if (strncmp(buf+3, "GGA", 3) != 0) // 不是GGA定位帧(GGA是核心定位帧)
return -1;
else if (strstr(buf, ",,,,,")) // 检测到无定位信号(数据字段为空)
{
printf("⚠️ GPS无定位信号,请放到室外空旷处!\n");
return -1;
}
else {
// 拆分GGA帧数据:格式为$GPGGA,时间,纬度,南北纬,经度,东西经,...
sscanf(buf, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]", tmp, time, lat, ns, lng, ew);
return 0; // 解析成功,返回0
}
}
// 主函数:程序入口,实现串口打开、配置、GPS数据读取+解析+打印
// 运行格式:./gps_read <串口设备名>(如./gps_read /dev/ttymxc5)
int main(int argc, char **argv)
{
int fd; // 串口文件描述符
int iRet; // 函数返回值,用于判断操作是否成功
char buf[1000]; // 存储GPS原始数据的缓冲区(足够容纳一帧数据)
char time[100]; // 存储解析后的时间(格式:HHMMSS.00)
char Lat[100], ns[100]; // 存储纬度(格式:ddmm.mmmm)和南北纬(N/S)
char Lng[100], ew[100]; // 存储经度(格式:dddmm.mmmm)和东西经(E/W)
float fLat, fLng; // 存储转换后的标准十进制度数(便于后续使用)
// 1. 参数校验:必须传入1个串口设备名参数,否则提示用法
if (argc != 2)
{
printf("用法: ./gps_read /dev/ttymxc5\n");
return -1;
}
// 2. 打开串口设备(传入用户指定的串口名,如/dev/ttymxc5)
fd = open_port(argv[1]);
if (fd < 0)
{
printf("打开串口失败: %s\n", argv[1]);
return -1;
}
// 3. 配置串口:GPS固定参数(9600波特率、8数据位、无校验、1停止位)
iRet = set_opt(fd, 9600, 8, 'N', 1);
if (iRet)
{
printf("串口配置失败!\n");
return -1;
}
// 提示用户程序已正常运行,等待GPS数据
printf("✅ GPS读取程序运行中,等待数据...\n");
printf("========================================\n");
// 4. 循环读取+解析GPS数据(无限循环,按Ctrl+C退出)
while (1)
{
// 读取一帧GPS原始数据
iRet = read_gps_raw_data(fd, buf);
// ============== 未收到GPS数据(超时)处理 ==============
if (iRet == -2)
{
printf("⌛ 未收到GPS数据,请检查:\n");
printf(" 1. GPS模块是否正确接线(TX→UART5_RXD)\n");
printf(" 2. GPS模块是否正常供电(3.3V)\n");
printf(" 3. 是否拔掉UART5的自发自收短接线\n");
sleep(1); // 每隔1秒提示一次,避免刷屏
continue;
}
// 解析GPS原始数据(iRet==0表示读取到数据,开始解析)
if (iRet == 0)
{
iRet = parse_gps_raw_data(buf, time, Lat, ns, Lng, ew);
}
// 解析成功(iRet==0),打印解析结果和标准经纬度
if (iRet == 0)
{
printf("📡 原始数据: %s\n", buf);
printf("⏰ 时间 : %s\n", time);
printf("🌍 纬度 : %s %s\n", Lat, ns);
printf("🌍 经度 : %s %s\n", Lng, ew);
// 纬度格式转换:ddmm.mmmm → 标准十进制度数(如4005.1234 → 40.0854)
sscanf(Lat+2, "%f", &fLat); // 提取mm.mmmm部分
fLat = fLat / 60; // 分转换为度
fLat += (Lat[0] - '0')*10 + (Lat[1] - '0'); // 加上度的部分
// 经度格式转换:dddmm.mmmm → 标准十进制度数(如11632.5678 → 116.5428)
sscanf(Lng+3, "%f", &fLng); // 提取mm.mmmm部分
fLng = fLng / 60; // 分转换为度
fLng += (Lng[0] - '0')*100 + (Lng[1] - '0')*10 + (Lng[2] - '0'); // 加上度的部分
printf("📍 标准经纬度: %.6f , %.6f\n\n", fLng, fLat); // 保留6位小数,精度足够
}
}
close(fd); // 关闭串口(循环中不会执行,按Ctrl+C退出)
return 0;
}
3.1 串口自发自收测试代码(验证串口硬件)
文件名:serial_send_recv.c(核心功能:验证开发板串口是否完好,用于排查GPS无数据问题,需短接串口TX/RX)
#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>
// 串口配置函数:与GPS程序参数一致(9600波特率、8N1),通用可复用
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
struct termios newtio,oldtio;
// 读取当前串口配置,失败返回-1
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);
newtio.c_oflag &= ~OPOST;
// 数据位:8位(与GPS一致)
switch( nBits )
{
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
}
// 校验位:无校验(与GPS一致)
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;
}
// 波特率:9600(与GPS一致)
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;
}
// 停止位:1位(与GPS一致)
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;
tcflush(fd,TCIFLUSH);
// 生效配置
if((tcsetattr(fd,TCSANOW,&newtio))!=0)
{
perror("com set error");
return -1;
}
return 0;
}
// 打开串口函数:与GPS程序完全一致,通用
int open_port(char *com)
{
int fd;
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;
}
// 主函数:自发自收测试,短接串口TX/RX后,输入字符会自动接收并打印
int main(int argc, char **argv)
{
int fd;
int iRet;
char buf[100];
int len;
// 参数校验:必须传入串口设备名
if (argc != 2)
{
printf("用法: ./serial_send_recv <串口设备名>(如./serial_send_recv /dev/ttymxc5)\n");
return -1;
}
// 打开串口
fd = open_port(argv[1]);
if (fd < 0)
{
printf("打开串口失败: %s\n", argv[1]);
return -1;
}
// 配置串口(9600 8N1,与GPS一致)
iRet = set_opt(fd, 9600, 8, 'N', 1);
if (iRet)
{
printf("串口配置失败!\n");
return -1;
}
printf("✅ 串口自发自收测试程序运行中\n");
printf("📌 说明:需短接串口TX和RX,输入任意字符,会自动接收并打印\n");
printf("📌 退出:按Ctrl+C终止程序\n");
printf("========================================\n");
// 循环:读取输入的字符,再通过串口发送,然后接收并打印
while (1)
{
memset(buf, 0, sizeof(buf));
// 读取用户输入的字符
len = read(STDIN_FILENO, buf, sizeof(buf)-1);
if (len <= 0)
{
perror("read stdin error");
continue;
}
// 将输入的字符通过串口发送
iRet = write(fd, buf, len);
if (iRet <= 0)
{
perror("write serial error");
continue;
}
// 读取串口接收的数据(自发自收,发送的数据会被自身接收)
memset(buf, 0, sizeof(buf));
iRet = read(fd, buf, sizeof(buf)-1);
if (iRet <= 0)
{
perror("read serial error");
continue;
}
// 打印接收的数据
printf("📥 串口接收数据:%s", buf);
}
close(fd);
return 0;
}
四、编译与运行步骤(新手零失败)
4.1 Ubuntu端(交叉编译)
操作环境:远程连接的Ubuntu虚拟机,代码目录为 ~/Desktop/study/14_UART/01_app_send_recv
-
进入代码目录(复制命令直接执行):
cd ~/Desktop/study/14_UART/01_app_send_recv -
创建代码文件并粘贴上述完整代码:
vim gps_read.c打开vim后,按i进入编辑模式,粘贴代码,按Esc,输入:wq保存退出。 -
交叉编译(生成开发板可执行文件):
arm-buildroot-linux-gnueabihf-gcc -o gps_read gps_read.c编译成功后,目录下会生成gps_read可执行文件。 -
将可执行文件复制到NFS共享目录(开发板可直接访问):
cp gps_read /home/book/nfs_rootfs/
4.2 开发板端(运行程序)
操作环境:串口登录的IMX6ULL开发板(root账号),NFS共享目录为 /home/book/nfs_rootfs
-
进入NFS共享目录:
cd /home/book/nfs_rootfs -
验证程序是否存在(确保复制成功):
ls输出中包含gps_read即成功。 -
运行GPS读取程序(核心命令,必须使用正确串口):
./gps_read /dev/ttymxc5 -
退出程序:按
Ctrl+C即可终止程序运行。
五、常见问题与报错排查(新手必看)
5.1 报错1:open /dev/ttyms5 err!
-
错误原因:串口设备名拼写错误,将
/dev/ttymxc5误写为/dev/ttyms5(把c写成了s)。 -
解决方法:运行命令时使用正确设备名
/dev/ttymxc5,命令:./gps_read /dev/ttymxc5。
5.2 问题2:程序运行后卡住不动,无任何输出
-
错误原因:未使用优化后的代码,原代码为阻塞模式(VMIN=1,VTIME=0),无数据时无限等待,看起来卡死。
-
解决方法:使用本文提供的优化代码(VMIN=0,VTIME=10),重新编译运行,程序会超时提示,不再卡死。
5.3 问题3:打印内核日志:alloc_contig_range: [8c0a0, 8c0b4) PFNs busy
-
错误原因:这是IMX6ULL开发板的内核内存分配警告,属于系统正常日志,与程序、GPS无关。
-
解决方法:直接忽略,不影响程序运行和GPS数据读取。
5.4 问题4:提示“未收到GPS数据,请检查...”
-
错误原因(按优先级排查):
-
UART5的自发自收短接线未拔掉,导致GPS数据无法输入;
-
GPS模块接线错误(TX未接UART5_RXD、RX未接UART5_TXD);
-
GPS模块未供电或供电异常(未接3.3V);
-
GPS模块损坏。
-
-
解决方法:按提示检查接线、供电,拔掉短接线,重新连接后重试。
5.5 问题5:提示“GPS无定位信号,请放到室外空旷处!”
-
错误原因:GPS模块已正常发送数据,但处于室内/遮挡环境,未搜索到卫星,无法定位。
-
解决方法:将GPS模块放到室外空旷处,等待1~3分钟搜星,定位成功后会自动打印经纬度。
5.6 问题6:程序打印乱码
-
错误原因:串口波特率配置与GPS模块不一致(GPS默认9600,代码已配置,若修改会导致乱码)。
-
解决方法:确保代码中
set_opt函数的波特率参数为9600,不修改该参数。
六、运行现象说明(3种常见场景)
6.1 场景1:未接GPS模块 / 接线错误 / 未拔短接线
✅ GPS读取程序运行中,等待数据... ======================================== ⌛ 未收到GPS数据,请检查: 1. GPS模块是否正确接线(TX→UART5_RXD) 2. GPS模块是否正常供电(3.3V) 3. 是否拔掉UART5的自发自收短接线
说明:程序正常运行,未收到GPS数据,按提示检查硬件即可。
6.2 场景2:GPS模块已接好,但在室内/无卫星信号
✅ GPS读取程序运行中,等待数据... ======================================== ⚠️ GPS无定位信号,请放到室外空旷处!
说明:已收到GPS数据,但未定位成功,将GPS放到室外即可。
6.3 场景3:GPS接好、室外有信号(运行成功)
✅ GPS读取程序运行中,等待数据... ======================================== 📡 原始数据: $GPGGA,125643.00,4002.12345,N,11630.56789,E,1,05,2.8,50.0,M,,,*70 ⏰ 时间 : 125643.00 🌍 纬度 : 4002.12345 N 🌍 经度 : 11630.56789 E 📍 标准经纬度: 116.509465 , 40.035391
说明:GPS定位成功,程序正常读取并解析数据,实验完成。
七、核心知识点总结(新手必记)
-
GPS协议:采用NMEA-0183协议,核心定位帧为GGA帧,以$开头、\n/r结尾。
-
串口参数:GPS模块默认9600波特率、8N1(8数据位、无校验、1停止位),不可随意修改。
-
阻塞/超时模式:通过
VMIN和VTIME配置,本文优化后避免程序卡死。 -
经纬度转换:GPS输出格式为ddmm.mmmm(纬度)、dddmm.mmmm(经度),需转换为标准十进制度数。
-
关键注意点:串口交叉连接、拔掉短接线、GPS室外搜星、3.3V供电,这四点是实验成功的核心。
更多推荐



所有评论(0)