最近刚完成GPS+GPRS组合模块的驱动开发,主要是完成GPS定位数据的采集、解析,以及GPRS网络通信、数据远程上报的功能,属于物联网终端设备里很经典的定位+无线传输组合。
硬件选型
主控芯片:stm32f103c8t6
GPS模组:Air530W——国产高性能定位模组,支持GPS、北斗多卫星系统,体积小、功耗低、搜星快,自带NMEA-0183协议输出。
通信模组:EC801E——移远Cat.1通信模组,兼容GPRS网络,相比传统2G模组网速更快、稳定性更强,支持AT指令控制。
Air530W GPS模组驱动开发
Air530W的驱动开发核心是串口通信和NMEA协议解析,模组出厂配置友好,基础调试难度不高,重点在于数据滤波和有效定位判断。
Air530W上电后,会自动输出NMEA-0183标准报文,不需要额外发送指令触发,日常开发中重点解析两条核心报文即可:
- $GNGGA:定位信息报文,包含经纬度、卫星数量、定位状态、海拔高度等核心数据;
- $GNRMC:推荐定位信息,包含定位有效性、时间、速度、方向等参数,判断定位是否有效更便捷。
我这边由于只需要经纬度信息,所以只需要RMC信息即可。格式和说明如下:

一开始我是根据位置有效标志是‘A’还是‘V’来判断当前定位是否有效,但是后面有一次查看服务器上传的数据一直提示数据无效,后面查看完整报文才发现虽然该位是‘V’但是经纬度还是正确,于是后面改变逻辑,通过判断该位的下一个字段是否有数据来断定定位是否成功,一般只要该位有数据就差不多是正确的。
while (1)
{
AIR530W_ReadData(gps_data, &gps_data_len);
// 先判断有没有数据
if (gps_data_len > 0)
{
gnrmc = (char *)gps_data;
// // 判断是否是GNRMC数据
gnrmc = strstr((char *)gps_data, "$GNRMC");
float lat = 0.0f;
// 判断是否成功读取 1 个浮点数
int res = sscanf((char *)gnrmc, "$GNRMC,%*[^,],%*[^,],%f", &lat);
// 判断规则:必须成功读到纬度,且纬度数值合法(GPS纬度不会为0)
if (res == 1 && lat > 0.0f)
{
// 有效数据
printf("GPS data is valid!\r\n");
break;
}
else
{
printf("GPS data is invalid!\r\n");
// 无效数据 上报错误
char error_msg[64];
sprintf(error_msg, "Invalid GPS data: %s", "ERROR");
EC801E_SendJSONData(CLIENT_IDX, 0, 0, 0, gprsParameter.subscribeTopic, strlen(error_msg), (uint8_t *)error_msg);
}
}
Delay_s(1);
}
// 已经获取到数据 开始解析 只需要从$GNRMC开头到*结尾的数据
char gnrmc_data[128] = {0};
sscanf((char *)gnrmc, "%[^*]*", gnrmc_data);
// $GNRMC,064805.000,A,3043.24334,N,11119.93843,E,0.000,120.813,250326,,,A,S*03
// 提取经纬度 转为十进制度格式
uint8_t time[7];
uint8_t date[7];
sscanf(
(char *)gnrmc,
"$GNRMC,%6c%*7c%f,%c,%f,%c,%f,%*f,%6c",
time,
&uploadData.lat,
uploadData.latDir,
&uploadData.lon,
uploadData.lonDir,
&uploadData.speed,
date);
time[6] = '\0';
date[6] = '\0';
// 转为十进制度
uploadData.lat =
(int)(uploadData.lat / 100) +
(uploadData.lat - (int)(uploadData.lat / 100) * 100) / 60;
uploadData.lon =
(int)(uploadData.lon / 100) +
(uploadData.lon - (int)(uploadData.lon / 100) * 100) / 60;
// 将转换后的经纬度信息赋值回去gnrmc_data
sprintf(gnrmc_data, "$GNRMC,%s,A,%.5f,%c,%.5f,%c,%.3f,%s",
time,
uploadData.lat,
uploadData.latDir[0],
uploadData.lon,
uploadData.lonDir[0],
uploadData.speed,
date);
// 已经获取到数据 通过gprs发送数据
EC801E_SendJSONData(CLIENT_IDX, 0, 0, 0, gprsParameter.subscribeTopic, strlen((char *)gnrmc_data),(uint8_t *)gnrmc_data);定位调试技巧
因为卫星信号很微弱,调试时一定要在室外开阔地带无树木高楼遮挡,天线这边我选择的是有源陶瓷天线,调试的时候一定要把陶瓷面向上,并且人手不要触摸,最好站在离设备半米外的地方,等待5-10分钟设备自动搜星。由于我一开始不知道这些小细节,导致我测试的时候耽误了很长时间,甚至一度怀疑设备出问题了...最后通过查阅资料手册才得知这些。
EC801E通信模组驱动开发
EC801E属于AT指令类模组,驱动核心是AT指令收发、网络注册和协议通信,相比GPS模组,需要配置的参数更多,容错机制也更重要。
我采用的是MQTT协议来进行通信,根据官网的示例手册,通信协议如下:
这里需要注意的是,发送数据的时候,需要等待模块返回‘>’,然后在发送要发的数据即可。
另外还有数据长度一定要和数据匹配,多出的数据就自动丢弃,但是如果数据不够会一直阻塞,等待继续发送数据。
// 发送发布命令
sprintf((char *)cmd, "AT+QMTPUBEX=%d,%d,%d,%d,\"%s\",%d\r\n", client_idx, msgID, qos, retain, topic, dataLen);
Serial2_SendData((uint8_t *)cmd, strlen((char *)cmd));
printf("Send CMD: %s", cmd);
// 等待 ">" 提示符
uint32_t wait_cnt = 0;
while (strstr((char *)EC801E_Buffer, ">") == NULL)
{
uint16_t len = Serial2_GetRxData(EC801E_Buffer + EC801E_RxLen, EC801E_BUFFER_SIZE - EC801E_RxLen);
EC801E_RxLen += len;
wait_cnt++;
if (wait_cnt >= 20) // 等待最多10秒
{
printf("Wait > TIMEOUT\r\n");
printf("Buffer: %.*s\r\n", EC801E_RxLen, EC801E_Buffer);
return GPRS_TIMEOUT;
}
Delay_ms(500);
}扩展功能
由于不想每次上电都自动配置GPRS的参数,但是板上又没有设计别的存储芯片,于是考虑存储到单片机的flash里面,上电读取标志位判断是否已经存在配置,否则就提示用户配置,完成之后就存储到flash里面了。
// Flash存储结构体
typedef struct {
uint32_t magic; // 魔术数字标志
GPRS_Parameter params; // GPRS参数
uint16_t checksum; // 数据校验和
} FlashStorageType;
/**
* @brief 检查Flash中是否已保存有效的参数
* @return 1-有有效参数,0-无有效参数
*/
uint8_t FlashStorage_HasSavedParams(void);
/**
* @brief 从Flash读取参数
* @param params 指向存储参数的结构体指针
* @return 0-成功,-1-失败
*/
int8_t FlashStorage_ReadParams(GPRS_Parameter *params);
/**
* @brief 将参数写入Flash
* @param params 指向要保存的参数结构体指针
* @return 0-成功,-1-失败
*/
int8_t FlashStorage_WriteParams(const GPRS_Parameter *params);
/**
* @brief 清除Flash中存储的参数
* @return 0-成功,-1-失败
*/
int8_t FlashStorage_ClearParams(void);在主函数里面
// 检查Flash中是否已保存有效的参数
if (FlashStorage_HasSavedParams())
{
if (FlashStorage_ReadParams(&gprsParameter) == 0)
{
printf("Auto initializing GPRS with saved params...\r\n");
if (EC801E_Init(CLIENT_IDX) == GPRS_OK)
{
printf("GPRS initialized successfully!\r\n");
}
else
{
printf("GPRS initialization failed!\r\n");
}
}
else
{
printf("Failed to load params from Flash, waiting for manual configuration...\r\n");
}
}
else
{
printf("No saved params found, waiting for configuration...\r\n");
}总结
做完这次双模组驱动开发,最深的感触就是:嵌入式驱动开发,硬件永远比软件更重要,八成以上的问题都出在硬件接线、供电、干扰上,软件逻辑反而不难,只要耐心调试,都能解决。
首先,模块化编程、分层设计很重要。把GPS驱动、通信驱动分开封装,底层串口操作、协议解析、业务逻辑分离,后期维护、修改、移植都很方便,哪怕更换模组,只需要修改底层接口,不用改动上层业务代码。
其次,调试要循序渐进。先单独调通GPS模组,确认定位数据无误;再单独调试EC801E,保证联网、上传正常;最后再进行双模组联调,一步步排查问题,不要一开始就全量运行,出问题很难定位根源。
另外,稳定性永远比功能更重要。物联网设备大多是长时间离线运行,容错机制、重连机制、异常复位逻辑一定要做足,不能只实现基础功能就不管稳定性,不然设备投用后会频繁出故障。