You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++读取GPS传感器串口数据的两个技术问题求助

解决你的C++ GPS读取问题

作为C++初学者,你遇到的这两个问题确实是串口读取GPS数据时的常见痛点,我来一步步帮你解决:


问题1:自动识别USB串口设备

每次插拔后设备名变化的问题,有两种实用的解决思路:

方法1:用udev规则固定设备名(推荐)

你可以给GPS设备设置一个永久别名,比如/dev/gps0,这样不管插拔多少次,都不用改代码里的设备路径。

步骤如下:

  1. 先查GPS设备的硬件信息:

    lsusb
    

    找到设备的Vendor ID和Product ID(格式类似1234:5678)。

  2. 创建udev规则文件:

    sudo nano /etc/udev/rules.d/99-gps.rules
    

    写入以下内容(替换成你的设备ID):

    SUBSYSTEM=="tty", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", SYMLINK+="gps0"
    
  3. 重新加载规则生效:

    sudo udevadm control --reload-rules
    sudo udevadm trigger
    

    之后你的GPS设备就会固定映射到/dev/gps0,代码里直接用这个路径就行。

方法2:在代码中自动扫描设备

如果不想修改系统配置,也可以让代码主动扫描/dev目录下的串口设备,尝试读取NMEA数据来判断是不是GPS设备:

#include <dirent.h>
#include <vector>
#include <string>

std::string findGPSDevice() {
    DIR* dir = opendir("/dev");
    struct dirent* entry;
    std::vector<std::string> candidates;

    // 收集所有可能的串口设备
    while ((entry = readdir(dir)) != nullptr) {
        std::string name = entry->d_name;
        if (name.substr(0, 7) == "ttyUSB" || name.substr(0, 12) == "tty.usbmodem") {
            candidates.push_back("/dev/" + name);
        }
    }
    closedir(dir);

    // 逐个尝试打开设备,检查是否有NMEA数据
    for (const auto& dev : candidates) {
        int fd = open(dev.c_str(), O_RDONLY | O_NOCTTY | O_NONBLOCK);
        if (fd >= 0) {
            // 快速配置串口(和你原代码的配置一致)
            struct termios tty;
            memset(&tty, 0, sizeof(tty));
            tcgetattr(fd, &tty);
            cfsetospeed(&tty, B9600);
            cfsetispeed(&tty, B9600);
            tty.c_cflag &= ~PARENB;
            tty.c_cflag &= ~CSTOPB;
            tty.c_cflag &= ~CSIZE;
            tty.c_cflag |= CS8;
            tty.c_cflag &= ~CRTSCTS;
            tty.c_lflag = 0;
            tty.c_oflag = 0;
            tty.c_cc[VMIN] = 0;
            tty.c_cc[VTIME] = 1;
            tty.c_cflag |= CREAD | CLOCAL;
            tty.c_iflag &= ~(IXON | IXOFF | IXANY);
            tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
            tty.c_oflag &= ~OPOST;
            tcsetattr(fd, TCSANOW, &tty);

            // 尝试读取数据,判断是否是GPS设备
            char buf[256];
            int n = read(fd, buf, sizeof(buf));
            if (n > 0 && (strstr(buf, "$GNGLL") != nullptr || strstr(buf, "$GPGGA") != nullptr)) {
                close(fd);
                return dev;
            }
            close(fd);
        }
    }
    return ""; // 未找到设备
}

在main函数里调用这个函数获取设备路径:

int main() {
    std::string gpsDev = findGPSDevice();
    if (gpsDev.empty()) {
        std::cout << "找不到GPS设备!" << std::endl;
        return 1;
    }
    int USB = open(gpsDev.c_str(), O_RDONLY | O_NOCTTY | O_NONBLOCK);
    // ... 后续代码
}

问题2:正确按行读取并解析NMEA语句

原代码里read直接读取缓冲区,会导致NMEA语句被截断(比如一行数据拆成两次读取),所以需要维护一个全局缓冲区,先把读取到的字节流缓存起来,直到遇到换行符,再提取完整的一行进行解析。

修改后的完整代码如下:

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <string>
#include <sstream>
#include <vector>
#include <dirent.h>

// 全局缓冲区,用于缓存串口接收的字节流
std::string serialBuffer;

// 配置串口参数
bool configureSerial(int fd) {
    struct termios tty;
    memset(&tty, 0, sizeof(tty));
    if (tcgetattr(fd, &tty) != 0) {
        std::cout << "Error " << errno << " from tcgetattr: " << strerror(errno) << std::endl;
        return false;
    }

    // 设置波特率为9600
    cfsetospeed(&tty, B9600);
    cfsetispeed(&tty, B9600);

    // 配置8N1格式(无校验、1停止位、8数据位)
    tty.c_cflag &= ~PARENB;
    tty.c_cflag &= ~CSTOPB;
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;

    // 禁用流控制,启用读取功能
    tty.c_cflag &= ~CRTSCTS;
    tty.c_cflag |= CREAD | CLOCAL;

    // 关闭软件流控制,设置为raw模式
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);
    tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    tty.c_oflag &= ~OPOST;

    // 设置读取超时
    tty.c_cc[VMIN] = 0;
    tty.c_cc[VTIME] = 1; // 0.1秒超时

    // 刷新缓冲区并应用配置
    tcflush(fd, TCIFLUSH);
    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
        std::cout << "Error " << errno << " from tcsetattr: " << strerror(errno) << std::endl;
        return false;
    }
    return true;
}

// 查找GPS设备(和之前的函数一致)
std::string findGPSDevice() {
    DIR* dir = opendir("/dev");
    struct dirent* entry;
    std::vector<std::string> candidates;

    while ((entry = readdir(dir)) != nullptr) {
        std::string name = entry->d_name;
        if (name.substr(0, 7) == "ttyUSB" || name.substr(0, 12) == "tty.usbmodem") {
            candidates.push_back("/dev/" + name);
        }
    }
    closedir(dir);

    for (const auto& dev : candidates) {
        int fd = open(dev.c_str(), O_RDONLY | O_NOCTTY | O_NONBLOCK);
        if (fd >= 0) {
            if (configureSerial(fd)) {
                char buf[256];
                int n = read(fd, buf, sizeof(buf));
                if (n > 0 && (strstr(buf, "$GNGLL") != nullptr || strstr(buf, "$GPGGA") != nullptr)) {
                    close(fd);
                    return dev;
                }
            }
            close(fd);
        }
    }
    return "";
}

// 解析$GNGLL格式的NMEA语句
void parseGNGLL(const std::string& line) {
    std::stringstream ss(line);
    std::string segment;
    std::vector<std::string> fields;

    // 按逗号分割语句字段
    while (std::getline(ss, segment, ',')) {
        fields.push_back(segment);
    }

    // 检查是否是有效的GNGLL语句
    if (fields.size() < 6) {
        return;
    }

    std::cout << "原始语句: " << line << std::endl;
    if (fields[1].empty() || fields[3].empty()) {
        std::cout << "Keine Position erkannt" << std::endl;
    } else {
        // 把NMEA格式的经纬度转换为十进制格式
        double lat = std::stod(fields[1].substr(0, 2)) + std::stod(fields[1].substr(2)) / 60.0;
        if (fields[2] == "S") lat = -lat;

        double lon = std::stod(fields[3].substr(0, 3)) + std::stod(fields[3].substr(3)) / 60.0;
        if (fields[4] == "W") lon = -lon;

        std::cout << "Lat: " << lat << " " << fields[2] << std::endl;
        std::cout << "Lon: " << lon << " " << fields[4] << std::endl;
        std::cout << "UTC Time: " << fields[5] << std::endl;
    }
}

int main() {
    std::string gpsDev = findGPSDevice();
    if (gpsDev.empty()) {
        std::cout << "找不到GPS设备!" << std::endl;
        return 1;
    }
    std::cout << "找到GPS设备: " << gpsDev << std::endl;

    int USB = open(gpsDev.c_str(), O_RDONLY | O_NOCTTY | O_NONBLOCK);
    if (USB < 0) {
        std::cout << "Error " << errno << " opening " << gpsDev << ": " << strerror(errno) << std::endl;
        return 1;
    }

    if (!configureSerial(USB)) {
        close(USB);
        return 1;
    }

    while (true) {
        char buf[256];
        int n = read(USB, buf, sizeof(buf));
        if (n > 0) {
            // 将读取到的数据追加到缓冲区
            serialBuffer.append(buf, n);

            // 提取所有完整的行(以换行符分割)
            size_t pos;
            while ((pos = serialBuffer.find('\n')) != std::string::npos) {
                std::string line = serialBuffer.substr(0, pos);
                // 移除可能的回车符
                if (!line.empty() && line.back() == '\r') {
                    line.pop_back();
                }
                // 只处理$GNGLL语句
                if (line.substr(0, 6) == "$GNGLL") {
                    parseGNGLL(line);
                }
                // 删除已处理的行
                serialBuffer = serialBuffer.substr(pos + 1);
            }
        }
        // 短暂休眠,避免占用过多CPU资源
        usleep(10000);
    }

    close(USB);
    return 0;
}

关键修改说明:

  1. 新增serialBuffer全局变量,缓存串口接收的字节流,确保只有完整的一行NMEA语句才会被解析。
  2. 拆分出configureSerial和parseGNGLL函数,让代码逻辑更清晰,便于维护。
  3. 实现了NMEA经纬度格式到十进制格式的转换,让输出的位置信息更直观。
  4. 在主循环中加入短暂休眠,避免程序占用过高CPU。

内容的提问来源于stack exchange,提问作者Niklas Lyszio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:45:32