资讯详情

AIS解码实战:从NMEA语句到船舶坐标的完整链路解析

📅 2026/10/18 15:33:26 | 华诺云谱 👁 阅读
AIS解码实战:从NMEA语句到船舶坐标的完整链路解析
简介面向船舶自动识别系统AIS技术入门与开发者的精简资源包适用于海事监控、船舶导航等应用场景聚焦AIS协议理解、信号解码、驱动控制与数据解析等关键环节帮助读者从射频信号到应用层展示建立完整认知。包内共3个文件压缩后仅5KB包含两个txt说明文档与一个cpp示例程序txt可用于梳理ITU-R M.1371报文格式、VHF双频道161.975MHz/162.025MHz通信机制以及驱动设计中的中断处理和DMA接口cpp则展示了二进制报文解析、坐标转换等核心解码逻辑并给出静态信息、动态信息与安全信息的提取思路便于对照学习。目前已有1036人学习下载。该资源虽体量小巧但覆盖从底层解调、驱动调用到上层解析的完整链路以源码加说明的形式组织帮助通信、航海信息化初学者快速掌握动手解析的基本路径也可作为进一步开发船舶监控或防碰撞预警功能的起点。1. AIS解码为什么NMEA日志里的一串乱码能还原出整片海域任何一个第一次接触AIS数据的开发者都会撞上同一个反直觉现象抓包工具里看到的是一条看似正常的NMEA文本以!AIVDM开头后面跟着逗号分隔的字段。可当你把最后一个字段那串由数字和大写字母组成的载荷直接按ASCII打印出来时得到的是彻底无法理解的字符——不是中文、不是英文甚至不是Base64。而正是这串乱码经过驱动采集、解码、解析三个环节之后能还原出船舶的经纬度、航速、航向、船名、目的地、IMO编号甚至能推算出它下一次转向的旋回速率。AIS解码的真正难点不在串口读取也不在JSON输出而在于那一层被绝大多数教程一笔带过的伪文本机制载荷不是文本而是用六位比特打包的二进制报文。如果你不理解这个打包规则即使拿到了完整语句也只会对着解码结果怀疑人生——为什么船名是对的、经纬度却跑到非洲内陆为什么同一个报文解两次居然解出两种结果这篇拆解按容器层 → 比特层 → 语义层的顺序把AIS从天线到坐标的完整链路讲透。搞懂它你手里的接收机才不是一个黑匣子。2. NMEA 0183与AIVDM语句驱动层的数据容器设计2.1 为什么AIS报文被塞进NMEA这个老古董协议里NMEA 0183诞生于上世纪80年代最初是给航海电子设备之间传数据的串行协议波特率只有4800。它的报文结构极其简单一个$或!开头后面跟设备标识和语句类型逗号分隔字段*后跟两位十六进制校验码以回车换行结束。AIS选择挂在NMEA 0183下是典型的工程妥协船上的GPS、测深仪、雷达全都用这个协议AIS接收机只要把自己的输出格式对齐NMEA就能直接共享现有的串口分发基础设施。但AIS报文本身的长度远超普通NMEA语句。一条完整的AIS位置报告在压缩前超过100字节4800波特率下传输需要约200毫秒而AIS的TDMA时隙结构要求报文必须在26毫秒内完成发送否则会踩到下一个时隙。所以AIS没有直接用NMEA的原始字符传输而是先把AIS二进制载荷压缩成六位ASCII——每个字符表示6个比特然后用NMEA的VDM本船接收的来自他船的AIS数据或VDO本船发送的AIS数据语句类型封装。这个套娃结构就是驱动层最难理解也最关键的地方。2.2 AIVDM语句的字段拆解每一段都是有意义的一条典型的语句长这样!AIVDM,1,1,,B,177KQJ5000G?tOKRA1wUbN0TKH,0*5C按逗号拆分字段含义如下!AIVDM语句标识符!表示AIS专用语句AIVDM表示他船数据AIVDO表示本船数据。1本条消息被拆分成的总帧数。AIS长报文如静态与航次数据会拆成多条语句传输。1当前帧序号。空连续帧的序列ID用于把多帧拼接回同一条消息单帧消息此处为空。B信道代码A表示AIS 1信道161.975MHzB表示AIS 2信道162.025MHz。两个信道交替使用所以驱动层会同时收到A和B两种前缀的语句。177KQJ5000G?tOKRA1wUbN0TKH载荷六位ASCII编码的二进制数据。0填充位数。当实际比特数不是6的倍数时解码层需要右移补齐。这个值在99%的情况下是0但一旦非0忽略它就会导致最后几个字节的解析错位。5CCRC校验值对整个语句除!AIVDM,1,1,,B,之外的字符串即载荷加填充位加星号前的内容做异或校验。2.3 多帧消息与重组时序消息5为什么吃掉两条NMEA语句AIS有一类报文承载船舶静态信息船名、呼号、IMO编号、船舶类型、艏吃水等总长度超过420比特。六位ASCII编码后仍然超过NMEA单条语句的载荷上限于是发射机把它切成两段用两条连续的VDM语句发出。重组规则容易被忽略两条语句的消息数量字段都是2当前帧序号分别是1和2且序列ID字段必须相同。接收机必须按帧序号拼接载荷再把拼接后的比特流一次解码。我见过不少简单实现直接丢弃帧序号大于1的语句导致船名和呼号永远解不出来——因为船名数据恰好落在第一帧里但第二帧里的航次信息才是解码验证的关键。驱动层处理AIS数据时还有一个隐蔽的坑串口的一行读取并不保证恰好对应一条完整的NMEA语句。数据到达时可能半行、一行半、甚至一条语句被拆在两次读操作之间。合格的驱动代码必须先做字节缓冲按行尾符 切割再做语句合法性检查最后才进入解码流程。3. 六位ASCII解码与比特重排整个AIS机制的心脏3.1 查表法字符到六位比特的映射规则AIS载荷不是Base64虽然两者都用了六位编码思想但映射表完全不同。AIS使用的是AIS ASCII表也常被称为六位ASCII表映射规则如下字符0到WASCII码48到87映射为数值0到39对应二进制000000到100111。字符ASCII码96到wASCII码119映射为数值40到63对应101000到111111。中间跳过了一段ASCII码88到95对应X、Y、Z、[、\、]、^、_这些字符在合法AIS报文中永远不会出现。这个查表逻辑用任何语言实现都很简单但新手最容易犯的错是把它当Base64解码——两者的数值映射完全不同用错表解出来的比特流从一开始就是错的。正确的做法是逐个字符查表把每个字符的六位数值按顺序写入一个比特数组或累加成一个长整数流。3.2 解出比特流之后位填充Bit Stuffing机制AIS报文沿用了HDLC风格的位填充发送端在原始比特流中每遇到连续的五个1就在其后插入一个0。接收端必须反向操作——扫描比特流每遇到连续的五个1就把后面的那个0删除。为什么要做位填充因为AIS的TDMA时隙同步依赖特定的标志序列0x7E二进制01111110。如果没有位填充机制原始数据里一旦出现连续的111111接收机就会误判为帧结束标志导致整个报文被截断。位填充保证原始数据里不会出现超过五个连续的1从而让0x7E标志保持唯一性。解码时如果不做去填充报文的比特位置会全部错位——因为你多算了那些被插入的0。这个错误的典型症状是报文头部的前几个字段消息类型、重复指示、MMSI看起来正常但从某个字段开始全部错乱且错乱位置恰好出现在数据中出现11111的地方。而且注意报文中的消息类型字段本身就可能包含连续四个1如果它后面正好跟一个1就会触发填充所以跳过填充处理的错误不是概率事件而是必然事件。3.3 一个最小可运行的解码器骨架下面给出解码器最核心的比特提取逻辑不涉及具体消息类型解析def decode_payload(payload: str, fill_bits: int) - tuple: 将AIS六位ASCII载荷转换为原始比特数组再去除填充位。 返回 (bit_array, bit_count) # 第一步六位ASCII查表 table {} for i, ch in enumerate(0123456789:;?ABCDEFGHIJKLMNOPQRSTUVWXabcdefghijklmnopqrstuvw): table[ch] i bits [] for ch in payload: if ch not in table: raise ValueError(f非法字符 {ch} 出现在载荷中) value table[ch] # 高位在前逐个写入比特 for shift in range(5, -1, -1): bits.append((value shift) 1) # 第二步去除填充位 cleaned [] one_count 0 for bit in bits: if one_count 5: # 这个0是发送端强制插入的填充位跳过 one_count 0 continue if bit 1: one_count 1 else: one_count 0 cleaned.append(bit) # 第三步减去尾部填充位数payload长度*6 - fill_bits才是真实数据长度 real_bits len(cleaned) - fill_bits return cleaned[:real_bits], real_bits参数说明fill_bits来自NMEA语句的倒数第二个字段。这里的关键逻辑是——发送端的填充插入发生在前面的连续五个1之后而fill_bits是整条载荷末尾为了凑齐6比特倍数而补的0。两种填充含义完全不同去填充时要去掉的是前一种而不是末尾那几个。如果混淆了这两个概念你会在某些报文上解出奇怪的结果前几个字段正常越往后偏差越大。正确做法是先去除HDLC风格的填充位再按fill_bits截断真实数据长度。上面的代码为了清晰先做了去填充再做截断实际性能敏感的实现可以把两步合并为一次遍历。3.4 校验码解码前的最后一道关卡NMEA语句末尾的*5C是对!AIVDM之后、*之前的全部字符从1,1,,B,...开始做逐字节异或。实现时注意异或的输入是ASCII码本身不是解码后的比特流。校验通过意味着传输过程没有丢字符但不保证AIS报文本身在无线链路上没有比特错误——AIS有自己的CRC但那是在载荷内部的检错字段解码后还需要单独验证。一个健壮的驱动应当在解码前先做NMEA校验校验失败直接丢弃整条语句不进入解码流程。这能拦掉大量因为串口噪声、线路干扰产生的畸形数据。很多解码结果偶尔跳变的玄学问题根因就是跳过了这一步把被干扰的坏报文当作有效数据送进了解析器。4. 消息类型与字段布局把比特流翻译成船舶动态和静态信息4.1 消息1/2/3位置报告类的位图布局拿到干净的比特流后解码进入语义层。AIS消息1/2/3船位报告是最常见的数据总长度为168比特其字段布局有固定规则比特偏移字段名位数缩放/单位取值说明0-5消息类型6整数该值为1、2或36-7重复指示2整数基站中继计数0为默认8-37MMSI30整数船舶唯一标识需转十进制38-41航行状态4枚举0主机使用中1锚泊5靠泊15未定义42-49转向速率8特殊有符号数-128表示不可用50-87对地航速100.1节1023表示不可用1022表示≥102.2节88-142经度281/600000分有符号数181×600000表示不可用143-142纬度271/600000分有符号数91×600000表示不可用160-167对地航向120.1度3600表示不可用168时戳6秒0-5960表示不可用4.2 经纬度的整数编码为什么用1/600000分而不是直接存浮点数AIS报文里的经纬度字段故意不用浮点数而是把角度乘以600000后取整。以经度为例120.5度会被编码为120.5 × 600000 72300000。这个设计的核心动机是浮点数的二进制表示不规范不同实现可能产生尾数误差而整数编码在16位和32位处理器上都能以固定语义解析且在VHF无线链路上按位传输时整数的位序不受IEEE 754标准版本影响。从字段布局看28比特有符号整数能表示的范围是-134,217,728到134,217,727换算成度大约是±223度覆盖全球经度范围绰绰有余。解码时只需做一次value / 600000.0就能得到标准十进制度坐标。一个常见的实现错误是把编码值直接当作浮点数除以600000这没问题但反过来——把收到的经纬度当作以度为单位的整数去乘600000在枚举某些消息类型时就会差两个数量级。4.3 消息5船舶静态数据两次解帧与位图偏移的计算消息5是最考验解析器完整性的报文明其总长度是424比特被拆成两条NMEA语句传输。它的字段布局中船名第143-201比特和目的地第303-421比特等字段是可变长度的文本域用六位ASCII表编码左对齐右侧填充字符数值31。解码时遇到需要去除——注意在AIS六位ASCII表中映射为数值31它是合法的填充字符不是错误。更麻烦的是消息5的位填充跨度大、字段多一旦位偏移计算错一位船名就会变成一段不可读的乱码。我的经验是解析消息5时不要先解出全部字段再去凑数据而是先按位偏移分段取出每个字段的比特再逐字段解码验证。以船名为例先取第143到201比特共59位转成六位ASCII字符序列再删除尾部所有剩下的就是船名。如果先拼接比特流再一次性转文本某个字段的多一比特会殃及所有后续字段。4.4 消息4基站报告与消息18/24B类船位报告的差异点消息4是基站发送的自身位置报告结构和消息1/2/3大部分相同但少了转向速率字段多了定位精度和RAIM接收机自主完整性监测标志。消息18和24是B类AIS设备无需强制安装AIS的小型船舶使用的精简版位置报告其经纬度编码方式与A类一致但没有航行状态和转向速率字段。不同消息类型的解码逻辑不能共用一个位偏移模板。很多第三方解析库把消息1、2、3、18捆在一起解析虽然大部分字段对齐但在消息18上解析转向速率就会读出无意义的数据。正确的架构是先用消息类型字段做分发每种类型各自定义位偏移模板模板之间不共享。5. AIS解码避坑指南五个最容易翻车的实测问题5.1 跳过NMEA校验坏报文混入导致坐标跳变现象解码出的船舶位置偶尔会瞬间跳到几千公里外的陆地上持续一两秒后又恢复正常。日志里这条船的MMSI和船名始终不变只有经纬度异常。原因串口线路上受到电磁干扰时NMEA语句的某个字符可能被改写。如果驱动层不校验语句尾部的CRC这段被污染的载荷照样进入解码流程解出来的坐标自然错得离谱。解决在驱动层强制对每条NMEA语句做逐字节异或校验校验失败立即丢弃。这个操作在解码之前、缓冲区切割之后执行。坚持这个习惯后坐标跳变问题基本绝迹。5.2 把位填充和填充位数混淆消息5船名变成乱码现象类型5消息解析时前面几个字段正常船名偶尔出现一两个错乱字符且错误是间歇性的换一处接收环境后错误消失。原因发送端插入的填充位连续五个1后的0被当成了真实数据比特。船名这种长文本字段跨越多个比特区间错误的插入位让后续所有字段整体偏移了若干比特。解决在比特流进入字段解析前先执行去填充操作见第3.3节代码再根据fill_bits截断。两者顺序不能颠倒也不可省略其一。5.3 字节序/位序混淆MMSI解出来少一位现象MMSI字段解出来就是错的而且错得很规律——原本应该是九位数字解出来只有八位或者最高位被截断了。原因AIS的比特流是高位在前MSB first。有些解析器直接用字节流读取再调用toInt()这在低位在前的小端平台上就会翻车。还有人对准了字节却没有对准比特——MMSI字段从第8比特开始而不是从第0字节开始。解决不要在字节层面操作使用按位偏移取字段的抽象函数内部的比特序始终固定为高位在前。测试每个字段的极值如MMSI为全1时解出的值必须等于2的30次方减1。5.4 多帧消息只处理了第一帧静态信息永远缺失现象海量的消息1位置报告解析正常消息5船舶静态数据却一条都解不出来。日志中能看到!AIVDM,2,1,...开头的语句但它们全部被忽略了。原因解析器没有实现帧缓存和重组。消息5被拆成两条NMEA语句发送驱动层必须等两条都到达后把两段载荷拼接成完整的比特流再解码。忽略第二帧意味着载荷长度不足420比特解码器只解出了前半段的几个字段。解决用序列ID做键值缓存所有总帧数1的语句。收到后续帧后按帧序号拼接拼满后释放缓存、执行解码。同时设置超时机制通常5秒内未收齐则丢弃防止缓存无限增长。5.5 双信道数据混用同一船舶出现两条航迹现象渲染船舶轨迹时发现同一艘船同时出现在两个相距几海里的位置上且两个位置交替出现。查看原始报文时发现同一MMSI同时存在于A信道和B信道的语句中。原因AIS设备在两个TDMA信道上交替发射接收机也会在两个信道上交替收。如果解析时把A、B信道数据混在一个船舶状态实体里而两个信道的数据到达时间不同就会导致状态被来回覆盖。这不是解码错误而是驱动层缺失了信道标识的隔离机制。解决在解析后的输出数据中保留信道字段NMEA语句中的第5个字段并且按 (MMSI, 信道) 组合维护船舶状态。需要合并两信道的数据时采用较高时间戳优先的策略而不是简单的覆盖写。6. AIS选择文本化比特流的设计取舍从自组织协议到解析器的验证之道这里值得停下来想一想AIS明明有独立的二进制数据链路层为什么在NMEA封装这一层偏偏要选择六位ASCII这种伪文本格式而不是直接传原始比特直接传二进制的VHF调制本来如此但输出的串行协议上NMEA 0183的发明时间远远早于AIS沿用!AIVDM文本语句的最大收益是兼容性——船舶已有的NMEA显示设备、记录仪、数据分发软件只需要加一行解析代码就能处理AIS数据。而且文本格式让调试变得异常方便你可以用任何串口工具直接查看AIS流量而不用先解开二进制帧。代价自然就是载荷膨胀和额外的编解码开销——但NMEA 0183的带宽瓶颈放在1980年代是硬约束在今天的串口和以太网链路上已经无所谓。另一个值得看到的设计取舍是位填充它增加了解码复杂度但换来了无线帧同步的可靠性。这个取舍在AIS的TDMA时隙体系内是强制性的——没有位填充任何一段偶然出现的111111模式都会让接收机误判帧边界导致整个时隙内的数据全部报废。理解了这一点你就能理解为什么解码器的第一步必须是去掉填充位而不是直接查字段表。这里还要强调一个解码器的验证方法它比任何单元测试都实在用自己的编码器把解出来的字段重新编码成AIS报文再走一遍自己写的解码器看能否得到完全相同的字段值。这个过程我称之为回环测试它能同时暴露位填充错误、位偏移错误和缩放因子错误——这三类错误在单向解码时往往隐藏得很深回环时任何一个不一致都会对不上。当年我写某个跨平台AIS解析模块时就是用回环测试抓出了一个只在特定MMSI值下出现的位序问题那个问题用真实报文检测了三天都没暴露。从那以后我每次改完解析器都强制走一遍回环测试用几百条真实录制的报文做语料先解再编再解逐字段比对。这比对着文档反复查位偏移有效率得多也让我在拿到新消息类型时不再心虚。希望帮到你——毕竟AIS解码的坑从来不在能不能拿到数据而在拿到的数据到底能不能信。理解六位ASCII与位填充这两层机制再配上回环验证的习惯你的AIS解析就真的从黑匣子变成了透明管道。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑