一、实验概述

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

  1. 进入代码目录(复制命令直接执行): cd ~/Desktop/study/14_UART/01_app_send_recv

  2. 创建代码文件并粘贴上述完整代码: vim gps_read.c打开vim后,按i进入编辑模式,粘贴代码,按Esc,输入:wq保存退出。

  3. 交叉编译(生成开发板可执行文件): arm-buildroot-linux-gnueabihf-gcc -o gps_read gps_read.c编译成功后,目录下会生成 gps_read 可执行文件。

  4. 将可执行文件复制到NFS共享目录(开发板可直接访问): cp gps_read /home/book/nfs_rootfs/

4.2 开发板端(运行程序)

操作环境:串口登录的IMX6ULL开发板(root账号),NFS共享目录为 /home/book/nfs_rootfs

  1. 进入NFS共享目录: cd /home/book/nfs_rootfs

  2. 验证程序是否存在(确保复制成功): ls输出中包含 gps_read 即成功。

  3. 运行GPS读取程序(核心命令,必须使用正确串口): ./gps_read /dev/ttymxc5

  4. 退出程序:按 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停止位),不可随意修改。

  • 阻塞/超时模式:通过 VMINVTIME 配置,本文优化后避免程序卡死。

  • 经纬度转换:GPS输出格式为ddmm.mmmm(纬度)、dddmm.mmmm(经度),需转换为标准十进制度数。

  • 关键注意点:串口交叉连接、拔掉短接线、GPS室外搜星、3.3V供电,这四点是实验成功的核心。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐