C++读取GPS传感器串口数据的两个技术问题求助
解决你的C++ GPS读取问题
作为C++初学者,你遇到的这两个问题确实是串口读取GPS数据时的常见痛点,我来一步步帮你解决:
问题1:自动识别USB串口设备
每次插拔后设备名变化的问题,有两种实用的解决思路:
方法1:用udev规则固定设备名(推荐)
你可以给GPS设备设置一个永久别名,比如/dev/gps0,这样不管插拔多少次,都不用改代码里的设备路径。
步骤如下:
先查GPS设备的硬件信息:
lsusb找到设备的Vendor ID和Product ID(格式类似
1234:5678)。创建udev规则文件:
sudo nano /etc/udev/rules.d/99-gps.rules写入以下内容(替换成你的设备ID):
SUBSYSTEM=="tty", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", SYMLINK+="gps0"重新加载规则生效:
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; }
关键修改说明:
- 新增
serialBuffer全局变量,缓存串口接收的字节流,确保只有完整的一行NMEA语句才会被解析。 - 拆分出
configureSerial和parseGNGLL函数,让代码逻辑更清晰,便于维护。 - 实现了NMEA经纬度格式到十进制格式的转换,让输出的位置信息更直观。
- 在主循环中加入短暂休眠,避免程序占用过高CPU。
内容的提问来源于stack exchange,提问作者Niklas Lyszio
相关产品推荐
相关产品推荐

