GPS 嵌入式设计核心是 "嵌入式主机(MCU)+ GPS 模块" 的组合,通过接收卫星信号解算位置、时间、速度等信息,广泛应用于车载导航、物流追踪、户外设备、物联网终端等场景。以下是结构化梳理,兼顾理论与实战,适配面试问答和项目落地:
一、核心基础认知(先搞懂关键概念)
- GPS 本质:全球定位系统(Global Positioning System),由美国 24 颗卫星组成的卫星导航网络,终端通过接收至少 4 颗卫星信号,基于三角定位原理计算自身经纬度、海拔、速度、时间(UTC)。
- 嵌入式设计核心目标:让设备实现 "定位数据采集→解析→存储 / 传输→应用",重点解决低功耗、抗干扰、数据稳定性问题。
- 常见替代 / 补充方案 :
- GLONASS(俄罗斯)、北斗(BDS,中国,优先适配国内场景)、Galileo(欧盟),模块多支持多系统联合定位(如 GPS + 北斗,定位更快、精度更高);
- 室内场景可搭配 WiFi / 蓝牙辅助定位(弥补 GPS 信号遮挡问题)。
二、核心硬件架构(最小系统设计)
1. 核心组件选型(嵌入式高频方案)
| 组件 | 选型要点 | 推荐型号 / 方案 |
|---|---|---|
| 嵌入式主机 | 需 UART 接口(与 GPS 模块通信)、GPIO(控制模块电源 / 复位)、可选 SPI/I2C(连接存储芯片),优先低功耗 | STM32F103(通用场景)、ESP32(自带 WiFi / 蓝牙,适合定位 + 数据上传)、STM32L476(低功耗,适配电池供电终端) |
| GPS 模块 | 支持多系统(GPS + 北斗优先)、定位精度(民用 3-5 米)、更新率(1-10Hz)、接口(UART 为主)、功耗 | 经典款:NEO-6M/NEO-7M/NEO-8M(性价比高,入门首选); 进阶款:Ublox MAX-M8Q(多系统,抗干扰强)、ATGM336H(北斗 + GPS,国内信号好) |
| 外围电路 | 电源(模块工作电压 3.3V,需稳定供电)、天线(关键!)、SIM 卡(若需远程传输定位数据) | 天线:外置有源陶瓷天线(增益≥28dB,带 CRC 校验,减少遮挡影响); 电源:LDO 稳压(如 AMS1117-3.3,过滤纹波,避免信号干扰) |
| 辅助组件 | 存储芯片(SD 卡 / Flash,存储历史轨迹)、通信模块(GSM/NB-IoT/LoRa,上传定位数据)、显示屏(实时显示位置) | MicroSD 卡模块(存储轨迹日志)、SIM800C(GPRS 上传)、0.96 寸 OLED 屏(显示经纬度 / 时间) |
2. 硬件设计关键要点(避坑核心)
- 天线布局:必须裸露在设备无遮挡处(远离金属外壳、电源模块),焊接时天线馈线尽量短,避免信号衰减;
- 电源隔离:GPS 模块对电源噪声敏感,需单独供电或加磁珠、滤波电容(如 10μF 电解电容 + 0.1μF 陶瓷电容),防止 MCU 电源干扰;
- GPIO 控制:通过 GPIO 连接模块的 RESET 引脚(复位模块)、PPS 引脚(秒脉冲,用于时间同步,精度达 1μs);
- 抗干扰设计:PCB 布线时,GPS 模块的电源、地、信号线分开,地线加粗,避免与电机、射频模块(如 LoRa)近距离布局。
GPS 嵌入式设计核心代码(STM32 为例,基于 HAL 库)
以下是 GPS 模块数据接收、NMEA 协议解析(核心帧 $GPRMC)、数据存储与显示 的实战代码,适配 STM32F103+NEO-6M/8M/MAX-M8Q 模块,注释详细,可直接移植修改。
一、前提说明
- 硬件连接:GPS 模块 TX → STM32 USART1 RX(PA10),GPS 模块 RX → STM32 USART1 TX(PA9),GPS VCC→3.3V,GND→GND;
- 通信参数:UART 波特率 9600(GPS 模块默认,若为 115200 需修改代码)、8 数据位、1 停止位、无校验;
- 核心功能:接收 NMEA 数据→解析 $GPRMC 帧→提取经纬度、时间、速度→存储到数组(可扩展 SD 卡存储)→OLED 显示(需适配自身 OLED 驱动)。
二、核心代码
1. 头文件定义(gps.h)
#ifndef __GPS_H
#define __GPS_H
#include "stm32f1xx_hal.h"
#include <string.h>
#include <stdio.h>
// GPS数据存储结构体($GPRMC核心字段)
typedef struct {
uint8_t fix_status; // 定位状态:0=未定位,1=定位有效
char time[10]; // UTC时间:格式HHMMSS.SSS(如123456.789)
char latitude[12]; // 纬度:格式DDMM.MMMMM(如3026.12345)
char lat_dir; // 纬度方向:N(北)/S(南)
char longitude[13]; // 经度:格式DDDMM.MMMMM(如12056.78901)
char lon_dir; // 经度方向:E(东)/W(西)
char speed[6]; // 地面速度:单位节(1节≈1.852km/h)
char date[7]; // 日期:格式DDMMYY(如200524=2024年5月20日)
} GPS_DataTypeDef;
// 全局变量(供其他文件调用)
extern GPS_DataTypeDef GPS_Data;
extern uint8_t GPS_Recv_Buf[1024]; // GPS接收缓冲区
extern uint16_t GPS_Recv_Len; // 接收数据长度
// 函数声明
void GPS_Init(void); // GPS初始化(本质是UART初始化)
void GPS_Parse_NMEA(void); // NMEA协议解析(核心函数)
void GPS_Convert_Decimal(void); // 经纬度度分格式→十进制格式(可选)
#endif
2. 源文件实现(gps.c)
#include "gps.h"
#include "usart.h" // 包含STM32 USART1 HAL库配置(需自行生成)
#include "oled.h" // 包含OLED显示驱动(需适配自身OLED)
// 全局变量初始化
GPS_DataTypeDef GPS_Data = {0};
uint8_t GPS_Recv_Buf[1024] = {0};
uint16_t GPS_Recv_Len = 0;
// 串口接收回调函数(HAL库):GPS数据实时存入缓冲区
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) { // 确认是GPS模块连接的USART1
GPS_Recv_Len++;
// 缓冲区溢出保护(超过1023则重置)
if (GPS_Recv_Len >= 1024) GPS_Recv_Len = 0;
// 继续开启中断接收下一个字节
HAL_UART_Receive_IT(&huart1, &GPS_Recv_Buf[GPS_Recv_Len], 1);
}
}
// GPS初始化:配置USART1并开启中断接收
void GPS_Init(void) {
// USART1配置已在STM32CubeMX中生成(见usart.c),此处仅开启中断接收
HAL_UART_Receive_IT(&huart1, &GPS_Recv_Buf[0], 1);
// 初始化GPS数据结构体
memset(&GPS_Data, 0, sizeof(GPS_DataTypeDef));
}
// 字符串分割函数(辅助解析NMEA帧,按逗号分割)
static char* GPS_StrSplit(char *str, const char sep, uint8_t index) {
char *p = str;
uint8_t i = 0;
while (*p != '\0') {
if (*p == sep) {
i++;
if (i == index) {
return p + 1; // 返回第index个分隔符后的字符串起始地址
}
}
p++;
}
return NULL; // 未找到对应索引
}
// NMEA协议解析:重点解析$GPRMC帧
void GPS_Parse_NMEA(void) {
char *ptr = NULL;
// 1. 查找$GPRMC帧的起始位置(NMEA帧以$开头,GPRMC为推荐最小数据帧)
ptr = strstr((char*)GPS_Recv_Buf, "$GPRMC");
if (ptr == NULL) {
GPS_Recv_Len = 0; // 未找到目标帧,清空缓冲区
return;
}
// 2. 按逗号分割字段,提取关键信息($GPRMC字段定义:$GPRMC,HHMMSS.SSS,Status,Lat,LatDir,Lon,LonDir,Speed,Track,Date,MagVar,MagVarDir,*Checksum)
char *field[13] = {0}; // 存储13个字段的指针
for (uint8_t i = 0; i < 13; i++) {
field[i] = GPS_StrSplit((char*)ptr, ',', i);
if (field[i] == NULL) break; // 字段不完整,退出解析
}
// 3. 验证字段完整性,提取有效数据
if (field[0] != NULL && field[1] != NULL && field[2] != NULL &&
field[3] != NULL && field[4] != NULL && field[5] != NULL && field[6] != NULL) {
// 定位状态(field[2]:A=有效定位,V=无效定位)
GPS_Data.fix_status = (field[2][0] == 'A') ? 1 : 0;
if (GPS_Data.fix_status == 1) { // 仅在定位有效时提取数据
// UTC时间(field[1]:HHMMSS.SSS,截取前6位HHMMSS)
strncpy(GPS_Data.time, field[1], 6);
GPS_Data.time[6] = '\0'; // 字符串结束符
// 纬度(field[3]:DDMM.MMMMM)
strncpy(GPS_Data.latitude, field[3], 11);
GPS_Data.latitude[11] = '\0';
// 纬度方向(field[4]:N/S)
GPS_Data.lat_dir = field[4][0];
// 经度(field[5]:DDDMM.MMMMM)
strncpy(GPS_Data.longitude, field[5], 12);
GPS_Data.longitude[12] = '\0';
// 经度方向(field[6]:E/W)
GPS_Data.lon_dir = field[6][0];
// 地面速度(field[7]:节)
strncpy(GPS_Data.speed, field[7], 5);
GPS_Data.speed[5] = '\0';
// 日期(field[9]:DDMMYY)
strncpy(GPS_Data.date, field[9], 6);
GPS_Data.date[6] = '\0';
}
}
// 4. 清空缓冲区,准备接收下一帧数据
memset(GPS_Recv_Buf, 0, sizeof(GPS_Recv_Buf));
GPS_Recv_Len = 0;
}
// 可选功能:经纬度度分格式→十进制格式(便于直接计算距离等)
// 例:纬度3026.12345 → 30 + 26.12345/60 = 30.43539°
void GPS_Convert_Decimal(void) {
float lat_deg, lat_min, lat_dec;
float lon_deg, lon_min, lon_dec;
if (GPS_Data.fix_status == 1) {
// 解析纬度(DDMM.MMMMM → 度+分)
sscanf(GPS_Data.latitude, "%2f%f", &lat_deg, &lat_min);
lat_dec = lat_deg + (lat_min / 60.0f); // 十进制纬度
// 解析经度(DDDMM.MMMMM → 度+分)
sscanf(GPS_Data.longitude, "%3f%f", &lon_deg, &lon_min);
lon_dec = lon_deg + (lon_min / 60.0f); // 十进制经度
// 可将lat_dec、lon_dec存储到新变量,或覆盖原结构体(按需修改)
printf("十进制纬度:%.5f%c\n", lat_dec, GPS_Data.lat_dir);
printf("十进制经度:%.5f%c\n", lon_dec, GPS_Data.lon_dir);
}
}
3. 主函数调用(main.c)
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "gps.h"
#include "oled.h"
void SystemClock_Config(void); // 系统时钟配置(STM32CubeMX生成)
int main(void) {
// 1. 初始化HAL库
HAL_Init();
// 2. 系统时钟配置(按需修改,STM32F103默认72MHz)
SystemClock_Config();
// 3. 外设初始化(USART1、GPIO、OLED)
MX_GPIO_Init();
MX_USART1_UART_Init();
OLED_Init(); // 初始化OLED显示屏
// 4. GPS初始化
GPS_Init();
// 5. 主循环
while (1) {
// 每1秒解析一次GPS数据(GPS模块默认1Hz输出)
HAL_Delay(1000);
// 解析NMEA数据
GPS_Parse_NMEA();
// 在OLED上显示GPS信息(需适配自身OLED的显示函数)
OLED_Clear(); // 清屏
if (GPS_Data.fix_status == 1) {
// 显示定位有效信息
OLED_ShowString(0, 0, "GPS Fix: YES");
OLED_ShowString(0, 2, "Time: ");
OLED_ShowString(48, 2, GPS_Data.time); // 显示时间HHMMSS
OLED_ShowString(0, 4, "Lat: ");
OLED_ShowString(32, 4, GPS_Data.latitude);
OLED_ShowChar(112, 4, GPS_Data.lat_dir);
OLED_ShowString(0, 6, "Lon: ");
OLED_ShowString(32, 6, GPS_Data.longitude);
OLED_ShowChar(112, 6, GPS_Data.lon_dir);
} else {
// 显示未定位
OLED_ShowString(0, 0, "GPS Fix: NO");
OLED_ShowString(0, 2, "Searching...");
}
// 可选:打印十进制经纬度到串口
GPS_Convert_Decimal();
}
}
三、关键说明与适配修改
1. 依赖文件补充
usart.c:需通过 STM32CubeMX 配置 USART1,生成 HAL 库初始化代码(波特率 9600,开启接收中断);oled.c:需适配自身 OLED 模块(如 0.96 寸 I2C/SPI),提供OLED_Init()、OLED_Clear()、OLED_ShowString()等基础显示函数(网上可搜对应驱动)。
2. 常见问题修改
- 波特率不匹配:若 GPS 模块默认波特率为 115200,需修改
MX_USART1_UART_Init()中的huart1.Init.BaudRate = 115200; - 解析失败:检查
GPS_StrSplit函数的字段索引是否正确(不同 GPS 模块的 NMEA 帧字段顺序一致,若缺失字段需增加异常处理); - 定位慢:首次搜星需静置 2-3 分钟,可在代码中增加 "搜星超时处理"(如 3 分钟未定位则复位模块)。
3. 功能扩展
- SD 卡存储:添加 SD 卡驱动(如 SPI 接口),在
GPS_Parse_NMEA()中将 GPS 数据写入 SD 卡日志文件(格式:时间,纬度,经度,速度,日期); - 数据上传:通过 GSM/NB-IoT 模块(如 SIM800C),将解析后的经纬度通过 GPRS/MQTT 协议上传到服务器;
- 低功耗优化:在
main.c中添加模块休眠逻辑(如定位完成后,通过 GPIO 关闭 GPS 模块电源,5 秒后唤醒重新定位)。
四、调试工具
- 串口助手:连接 STM32 的 USART1,查看 GPS 原始 NMEA 数据和解析后的十进制经纬度;
- U-center(Ublox 模块专用):可配置 GPS 模块参数(如更新率、开启多系统定位),辅助排查模块工作状态。