干货 | 分享一种有趣的数据解析方法

原创 嵌入式大杂烩 2021-06-10 22:30

点击上方「嵌入式大杂烩」,选择「置顶公众号」第一时间查看嵌入式笔记!

本片笔记是一篇开发小结,总结GPS数据的接收、解析示例,以实例为基础分享一些思考过程:

GPS数据协议

常用的GPS模块大多采用NMEA-0183 协议,目前业已成了GPS导航设备统一的RTCM(Radio Technical Commission for Maritime services)标准协议。

NMEA-0183 是美国国家海洋电子协会(National Marine Electronics Association)所指定的标准规格,这一标准制订所有航海电子仪器间的通讯标准,其中包含传输资料的格式以及传输资料的通讯协议。

协议采用 ASCII 码来传递 GPS 定位信息,我们称之为

帧格式形如:

$aaccc,ddd,ddd,…,ddd*hh(CR)(LF)

GPS帧数据种类大致如下:

实际应用中,并不是所有数据都完全用得上,我们可以根据需要选择所需要的数据。

下面我们以$GPGGA数据为例分享接收、解析方法。

$GPGGA 语句的基本格式如下:

$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>,<13>,<14>*hh<CR><LF>

举例如下:

$GPGGA,082006.000,3852.9276,N,11527.4283,E,1,08,1.0,20.6,M,,,,0000*35

GPS数据接收

GPS模块使用串口通信,在解析之前当然需要先接收数据。我这里是在嵌入式Linux平台下做的接收,读串口的接口如:

int uart_read(void *data, int data_len, long time_out);

下面分享我在实际应用中的三种接收方法:

方法一:粗略法

为了能快速验证数据解析、跑通整个过程,可以先使用粗略的方法获取数据。粗略法我们可以先不用考虑一帧数据的实际字节数,我们先大致设置一个用于解析的缓冲数组,如:

char rx_gps_data[512];

uart_read每次读到的字节数与线程挂起时间有关,粗略法我们大致设置一个串口接收缓冲数组,如:

char uart_rx_buf[64];

这时候需要把每次收到的uart_rx_buf里的内容自己拼接一下,存放到rx_gps_data中,再去做解析。

粗略法可以用于快速验证数据解析、跑通整个过程,缺点就是uart_rx_buf、rx_gps_data设置得不够合理的话可能会破坏掉大量的数据帧。

一般我都比较习惯地先快速调通整个流程,再慢慢做优化。

方法二:状态机法

上面地粗略法可能会破坏掉一些数据帧,另外,代码结构可能不够清晰。针对这些问题做改进,使用状态机来接收。一字节一字节地接收,接收完完整一帧数据之后再去做解析。

代码如:

// GGA所有状态(GGA数据示例:$GPGGA,023543.00,2308.28715,N,11322.09875,E,1,06,1.49,41.6,M,-5.3,M,,*7D)
#define GGA_STATE_START     0  // $
#define GGA_STATE_HEAD1_G   1  // G
#define GGA_STATE_HEAD2_P   2  // P
#define GGA_STATE_HEAD3_G   3  // G
#define GGA_STATE_HEAD4_G   4  // G
#define GGA_STATE_HEAD5_A   5  // A
#define GGA_STATE_DATA      6  // ,023543.00,2308.28715,N,11322.09875,E,1,06,1.49,41.6,M,-5.3,M,,*
#define GGA_STATE_CHECK0    7  // 7
#define GGA_STATE_CHECK1    8  // D

static uint16_t gga_len = 0;
static uint8_t gga_state = GGA_STATE_START;
static void gps_gga_data_get(char in_data)
{
 switch (gga_state)
 {
  case GGA_STATE_START:
   if ('$' == in_data)
   {
    gga_len = 0;
    memset(rx_gps_gga_data, 0, GGA_DATA_MAX_LEN);
    rx_gps_gga_data[gga_len++] = in_data;
    gga_state = GGA_STATE_HEAD1_G;
   }
   else
   {
    gga_state = GGA_STATE_START;
   }
   break;
   
  case GGA_STATE_HEAD1_G:
   if ('G' == in_data)
   {
    rx_gps_gga_data[gga_len++] = in_data;
    gga_state = GGA_STATE_HEAD2_P;
   }
   else
   {
    gga_state = GGA_STATE_START;
   }
   break;
   
  case GGA_STATE_HEAD2_P:
   if ('P' == in_data)
   {
    rx_gps_gga_data[gga_len++] = in_data;
    gga_state = GGA_STATE_HEAD3_G;
   }
   else
   {
    gga_state = GGA_STATE_START;
   }
   break;
   
  case GGA_STATE_HEAD3_G:
   if ('G' == in_data)
   {
    rx_gps_gga_data[gga_len++] = in_data;
    gga_state = GGA_STATE_HEAD4_G;
   }
   else
   {
    gga_state = GGA_STATE_START;
   }
   break;
   
  case GGA_STATE_HEAD4_G:
   if ('G' == in_data)
   {
    rx_gps_gga_data[gga_len++] = in_data;
    gga_state = GGA_STATE_HEAD5_A;
   }
   else
   {
    gga_state = GGA_STATE_START;
   }
   break;
   
  case GGA_STATE_HEAD5_A:
   if ('A' == in_data)
   {
    rx_gps_gga_data[gga_len++] = in_data;
    gga_state = GGA_STATE_DATA;
   }
   else
   {
    gga_state = GGA_STATE_START;
   }
   break;
   
  case GGA_STATE_DATA:
   if ('*' == in_data)
   {
    rx_gps_gga_data[gga_len++] = in_data;
    gga_state = GGA_STATE_CHECK0;
   }
   else
   {
    rx_gps_gga_data[gga_len++] = in_data;
    if (gga_len > GGA_DATA_MAX_LEN)
    {
     gga_state = GGA_STATE_START;
    }
    else
    {
     gga_state = GGA_STATE_DATA;
    }
   }
   break;
   
  case GGA_STATE_CHECK0:
   rx_gps_gga_data[gga_len++] = in_data;
   gga_state = GGA_STATE_CHECK1;
   break;
   
  case GGA_STATE_CHECK1:
   rx_gps_gga_data[gga_len++] = in_data;
   printf("gga data : %s\n", rx_gps_gga_data);
   gga_state = GGA_STATE_START;
   break;
   
  default:
   break;
 }
}

这样就可以完整地接收到gga数据,每次走到GGA_STATE_CHECK1状态时的rx_gps_gga_data就是完整的gga数据,这时候就可以进行解析了,可以在这一步设置一个标志变量表明gga数据已经完全接收完毕,直到数据接收完毕了才做解析。

这种方法虽然可以比较好地接收数据,在单片机下很好用。但是在这里,相同的线程挂起时间情况下,每次uart_read只获取一个字节,这样会损耗一定的接收效率,有点拆东墙补西墙的感觉。

在我们这边的应用中,与算法所需的时序要求有冲突了,所以只能再想想其它方法。下面看看方法三。

方法三:时间戳法

这种方法需要明确每一帧数据包含有什么数据,以及数据输出的频率是多少。在相同的线程挂起时间情况下,先把用于uart_read接收数据的buffer设置得稍微大一点,看每一次最多能读取到多少个字节得数据以及读完一帧数据需要读几次串口数据。

然后我们可以通过时间来区分每一帧数据及每一包串口数据,该重新组包地就重新组包。

例如:每帧数据间隔200ms,线程挂起时间10ms,一帧数据有130字节,一帧数据由1包、2包串口数据组成。

可以通过时间戳来判断每一包之间是数据帧之间的间隔还是每一帧数据里的两个数据包之间地间隔,再做相应的逻辑处理即可很好地接收数据。

GPS数据解析

gps数据怎么解析呢?

方法可能很多,我们先看一下正点原子的解析方法:

大概分为两步,第一步先获取逗号的位置确定某个需要解析地字段,然后再将相应字段的字符串数据转换成数字。

这里分享一种简单实用的解析方法,思路与上面差不多,但是相对比较简单清晰些:

static bool gps_gga_data_parse(st_gps_gga_def *out_data, char *in_data)
{
 bool ret = FALSE;
 char *p_gga = in_data;
 
 if (NULL == p_gga)
 {
  return ret;
 }
 
 if (NULL != (p_gga = strstr(p_gga, "$GNGGA")))
 {
  printf("gga data : %s\n", p_gga);
  
  /* 数据校验 */
  if (TRUE == data_check(p_gga))
  {
   printf("gga data check success!\n");
   
   /* 解析出字符串 */
   printf("gga data parse: \n");
   for (int i = 0; i < GGA_STR_MAX; i++)
   {
    sscanf(p_gga, "%[^,]", gps_gga_str[i]);
    printf("%s\n", gps_gga_str[i]);
    p_gga = p_gga + (strlen(gps_gga_str[i]) + 1);
   }
   
   /* 字符串转数字 */
   out_data->latitude = atof(gps_gga_str[STR_LATITUDE]);
   out_data->longitude = atof(gps_gga_str[STR_LONGITUDE]);
   out_data->time = atof(gps_gga_str[STR_TIME]);
   out_data->quality = atof(gps_gga_str[STR_QUALITY]);
   ret = TRUE;
  }
  else
  {
   printf("gga data check error!\n");
  }
 }
    
    return ret;
}

这里使用sscanf+正则表达式来做解析。

sscanf(p_gga, "%[^,]", gps_gga_str[i]);

sscanf函数在做字符串相关解析时很好用,这里配合正则表达式来使用,上面这一句代码的意思就是从p_gga中取逗号前面的数据存放到gps_gga_str[i]中,因为gga数据都是用逗号隔开的,循环几次就可以把所有数据解析出来,很方便。

正则表达式学习资源如:

1、https://deerchao.cn/tutorials/regex/regex.htm
2、https://www.runoob.com/regexp/regexp-syntax.html

下面再看一下,sscanf+正则表达式的几种简单用法:

「1、取指定长度的字符串。」

如在下例中,取最大长度为4字节的字符串。

sscanf("123456 ""%4s", str);

「2、  取到指定字符为止的字符串。」

如在下例中,取遇到空格为止字符串。

sscanf("123456 abcdedf""%[^ ]", str);

「3、取仅包含指定字符集的字符串。」

如在下例中,取仅包含19和小写字母的字符串。

sscanf("123456abcdedfBCDEF""%[1-9a-z]", str);

「4、取到指定字符集为止的字符串。」

如在下例中,取遇到大写字母为止的字符串。

scanf("123456abcdedfBCDEF""%[^A-Z]", str);

sscanf+简单、易理解的正则表达式的方法有时候可以帮助我们很方便地进行字符串数据地解析。sscanf+复杂的正则表达式不太建议使用,因为代码可读性太差了。

另外,使用sscanf+正则表达式时有必要写点注释,有见过这种方式还好,有些后面看你代码的人可能没接触过正则表达式可能一时半会儿理解不了。

我之前大三出去实习的时候,在公司里就看到这样的代码,那时候知识储备还不够,第一次看到sscanf+正则表达式这种解析方法,但是搜索又搜索不到相关答案,很苦恼。所以,平时有必要写一些注释,利人利己。

参考:

1、正点原子《ATK-NEO-6M GPS模块》资料。

2、https://blog.csdn.net/absurd/article/details/1177092

猜你喜欢:

嵌入式大杂烩文章精选

MDK下一代开发工具——Keil Studio

为什么会有TCP/IP协议?

LeetCode刻意练习

在公众号聊天界面回复1024,可获取嵌入式资源;回复 ,可查看文章汇总。

文章都看完了不点个

嵌入式大杂烩 专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!
评论
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 167浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 87浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 113浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 40浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 103浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 66浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 80浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 141浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 125浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 63浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦