资讯动态

AIS报文解码与AIVDM解析:从NMEA串口数据到船舶MMSI与经纬度

发布时间:2026/10/18 3:42:26 来源:尧图企业网站定制
简介面向AIS船舶自动识别系统开发者与航海信息化初学者的入门资料包围绕从射频信号到业务数据的完整链路展开讲解。内容包括ITU-R M.1371 VHF数据传输协议、二进制报文解调与解码算法、接收设备驱动接口以及经纬度坐标转换与船舶信息解析可用于理解海上交通监控、港口管理和防碰撞预警等场景下的典型工程实现。压缩包仅5KB共3个文件两个TXT文档分别整理协议要点与报文解析步骤一个C源文件提供核心解码逻辑便于对照研究二进制报文解析、错误检测与坐标换算。目前已有1036人学习下载。依托这份精简内容读者能快速建立AIS数据从解调、驱动采集到业务展示的整体认知也可为后续驱动调试或解析软件开发提供可运行的入门参考适合希望快速上手海洋信息化开发的学习者。1. 先立框架AIS 从射频信号到可用数据的完整链路不少人下载 AIS 教程和驱动包上手第一步就卡在「收到一串 $AIVDM 开头的字符串看着像乱码不知道下一步该干嘛」。这套资源的价值在于它把驱动、解码、解析三条线一次性串起来了——从接收机串口读到原始 6-bit 编码数据到切分成字段、还原成船舶的完整 AIS 解码报文再到输出 MMSI、船位、航速、船名这些结构化信息。换句话说它解决的是「收了一堆报文却用不起来」的断层问题。适合正在做船舶监控、水域态势分析或者想自建 AIS 接收节点但没有现成解析代码的开发者。整套链路里解码和解析是核心驱动和配置是前置下面的内容完全按照“收到原始字符串之后”这个真实场景来展开。2. 解码前的必修课AIS 报文协议结构与六位二进制编码2.1 AIS 报文的物理层与数据链路层基础AIS 工作在 VHF 频段分 AIS 1161.975 MHz和 AIS 2162.025 MHz两个频道采用自组织时分多址SOTDMA机制。每艘船在固定的时隙内发送自己的状态信息接收机只需要在这两个频点上轮流监听并解调出二进制数据流。这套教程里附带驱动如果正常工作你会在串口或者网络端口上看到连续的 NMEA 0183 语句关键字是!AIVDM或者!AIVDO。前者是收到的他船信息后者是本机发送的信息绝大部分对接场景只需要关心前者。在进入解码之前必须理解 AIS 报文的承载格式。一个标准的 AIS NMEA 语句长这样!AIVDM,1,1,,B,15M67FC000G?ufbEFepTTwN00N,0*51这里每个字段都用逗号分隔含义是语句标识!AIVDM、消息总数、当前消息编号、消息顺序 ID、接收频道、数据内容payload、填充位、校验和前的内容。真正需要解码的是第 6 个字段15M67FC000G?ufbEFepTTwN00N这就是 AIS 原始二进制数据的 6-bit 字符表示。后续所有解析都要从这串字符入手。2.2 6-bit ASCII 编码规则与字段位宽表AIS 的 payload 不是明文字符串而是用 6-bit ASCII 表示的二进制数据。每个字符的取值范围是 ASCII 码 48 到 119对应二进制值 0x00 到 0x3F。解码的时候要先把每个字符转成对应的 6-bit 数值再按位拼接成完整的二进制串然后按照消息类型对应的字段位宽去切分。字符到 6-bit 数值的换算规则很简单把字符的 ASCII 码值减去 48如果结果大于 40 则再减去 8。比如字符H的 ASCII 码是 7272 - 48 2424 40所以H对应二进制值 24即 011000。需要把这个规则写成一个最基础的函数后续所有解码都依赖它。教程里给的驱动和解析工具包一般会封装好这个函数但是自己实现一遍有助于排查乱码问题。常见的 AIS 消息类型中类型 1、2、3 是位置报告类型 5 是静态与航次信息类型 18 是 B 类设备位置报告。它们共用一个头部结构前 6 位是消息类型MMSI 类型接着的 30 位是用户 IDMMSI。从这里开始不同消息类型的字段位宽就不一样了。下表列了位置报告类消息1/2/3的核心字段字段起始位位宽含义比例/单位消息类型061/2/3无MMSI630船台识别码无导航状态3840-15 对应不同状态无经度61281/600000 分东经为正纬度89271/600000 分北纬为正对地航速50100.1 节无对地航向116120.1 度无经纬度在 AIS 协议里是不带符号的整数用 1/600000 分作为单位。解码时要根据最高位判断正负把超出范围的数值判为无效数据。某厂商的接收机偶尔会发送一条经度等于 181 度的错误报文这就是没做有效性判断导致的。2.3 校验和计算与语句合并逻辑NMEA 语句里的校验和是*后面两位十六进制数计算方式是从!之后到*之前所有字符的异或结果。写解析器的时候必须先做校验和验证再进解码流程。如果校验失败说明数据在传输中被破坏常见原因是串口波特率不匹配导致字节错位或者是 USB 转串口的驱动丢字节。消息条数第一个数字和消息编号第二个数字决定了一条拆成多帧的 AIS 消息怎么合并。类型 5 的消息因为 payload 超过 82 个字符往往被拆成两条 NMEA 语句此时需要把两帧的 payload 字符串按顺序拼接再去掉填充位最后一个字段的值再做 6-bit 转换。教程里通常会给一个合并函数接收一个语句列表返回拼接后的 payload 字符串。这个逻辑在批量处理历史日志时特别容易出问题后面避坑章节会细说。3. 驱动与数据采集从串口和网络端口读取 AIS 原始数据3.1 串口接收驱动的通用配置参数AIS 接收机通过 USB 转串口模块连到电脑后在 Linux 上会生成/dev/ttyUSB0或/dev/ttyACM0设备节点在 Windows 上是 COM 口。AIS 接收机最常见的串口参数是 38400 波特率、8 位数据位、1 位停止位、无校验。不过频率很高的问题是接收机被配置成了 9600 波特率或者用户拿之前用过的 GPS 驱动配置4800 波特率去连 AIS结果什么都读不到或者全是乱码。拿到设备先看说明书或者驱动文档里默认的波特率我就是先在串口助手里换四个常用波特率逐一测试而不是直接改代码。Linux 下用 Python 的 pyserial 库读取串口数据是一个常见做法核心配置如下import serial # 打开串口超时设短一点便于按行读取 ser serial.Serial( port/dev/ttyUSB0, baudrate38400, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout0.5 ) with ser: while True: line ser.readline() if not line: continue # strip 去掉换行符保留完整的NMEA语句 nmea_str line.decode(ascii, errorsignore).strip() if nmea_str.startswith(!AIVDM): print(nmea_str)这段代码最关键的是timeout0.5这个参数。AIS 接收机的报文不是匀速到达的船舶密集的水域一秒可能来几十条空旷水域几秒才一条。如果 timeout 设为 0readline 会一直阻塞等待直到读到换行符这会占用循环导致后续处理卡顿如果设得太大实时性又受影响。0.5 秒是一个在吞吐量和响应速度之间比较平衡的取值。第二个要点是errorsignoreAIS 报文在某些驱动下会在行首出现零字节直接 decode 会抛异常忽略掉不可解码字符能保证程序不中断。3.2 网络 UDP 与 TCP 接收方式的差异如果用的是带网络接口的 AIS 接收机或者接了信号转发器比如通过局域网共享 AIS 数据接收方式就变成 UDP 监听或 TCP 连接。这两种方式在稳定性上差别挺大。UDP 适合不要求可靠性的场景——AIS 数据本身是周期广播的丢一条位置报告下一分钟还会再发所以很多雷达和监控系统用 UDP 就够了。TCP 则保证字节顺序和完整性适合做数据落地存储或交给下游解析模块的场景。UDP 接收的代码比串口更短因为不需要管串口参数只需绑定 IP 和端口import socket # 监听本机 10110 端口这个端口常用于 AIS 数据流 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((0.0.0.0, 10110)) sock.settimeout(1.0) while True: try: data, addr sock.recvfrom(4096) text data.decode(ascii, errorsignore) # 网络流可能一条 datagram 里有多条 NMEA按换行拆分 for nmea in text.splitlines(): if nmea.startswith(!AIVDM): print(nmea) except socket.timeout: continue这段代码相比串口版有一个特别容易踩坑的地方网络数据可能一次收一个包里包含很多条语句也可能一条语句被拆成两个 TCP 段。所以我先用splitlines()拆行保证每条 NMEA 语句是完整的。如果直接用recvfrom拿到的整块数据去做校验和和解析大概率在校验和这一步就开始报错。3.3 驱动异常的最快定位手段如果程序跑起来什么都没有首先不要怀疑解码逻辑先把数据流抓出来看。最直接的判断方式是看原始输出里有没有!AIVDM前缀的语句。没有前面的链路就有问题设备没连上、驱动没装好、串口被占用、或者接收机天线没接。有语句但是解码结果不对才是解析逻辑的问题。用dmesg | grep tty在 Linux 上可以确认 USB 设备是否被识别为串口。如果在 Windows 上设备管理器里看 COM 口旁边有没有黄色感叹号有感叹号就是驱动不对先换驱动而不是调程序。4. 解码与解析把 AIVDM 变成船舶结构化信息4.1 核心解码器构建从字符串到二进制流完成字符串到二进制的转换是整个解码器最基础的一层。把每个 6-bit 字符还原成对应数值之后还要把它们拼成一个连续的二进制字符串再按字段位宽去切片。这个过程如果全部用 Python 的字符串操作来做虽然直观但性能差一些用位运算来做则要处理跨字节边界代码复杂度高一些。常规做法是先把所有 6-bit 值累积成一个整数列表然后用循环切片。下面这段实现了核心转换和类型 1 消息的位置解析def ascii_6bit_to_int(ch): 将AIS 6-bit字符转为数值 规则ASCII码减48超过40则再减8 v ord(ch) - 48 if v 40: v - 8 return v def payload_to_bits(payload): 将payload字符串拼接为连续二进制字符串 bits for ch in payload: v ascii_6bit_to_int(ch) bits format(v, 06b) return bits def parse_type1(bits): msg_type int(bits[0:6], 2) mmsi int(bits[6:36], 2) # 经纬度字段带符号直接转int后判断并转换单位 lon_raw int(bits[61:89], 2) lat_raw int(bits[89:116], 2) if lon_raw 0x4000000: # 检查符号位 lon_raw - 0x8000000 if lat_raw 0x2000000: lat_raw - 0x4000000 return { type: msg_type, mmsi: mmsi, lon: lon_raw / 600000.0, lat: lat_raw / 600000.0, sog: int(bits[50:60], 2) / 10.0, cog: int(bits[116:128], 2) / 10.0, }这段代码里有几个值得注意的边界处理。首先是经纬度符号判定AIS 协议用二进制补码表示负值经度 28 位中最高位是符号位纬度 27 位中符号位在 27 位这一组里的最高位。所以我用半范围的数值来判断正负然后减去完整范围的补码偏移量。如果直接按 int 解析负的经度会被解成一个 1.8 亿左右的大正数再除以 600000 就变成 300 多度明显错得离谱。另一个是航速字段的处理SOG实际值是 0 到 1023其中 1023 表示不可用所以要加个判断如果等于 1023 则输出 None 而不是 102.3 节。4.2 位置报告与静态信息消息的差异化解析类型的区分是解析器的分水岭。前面 parse_type1 其实对类型 2、3 也适用因为它们的字段布局完全相同。而类型 5静态与航次信息的字段布局完全不同它的 payload 更长且拆分方式不同。类型 5 消息的核心字段从第 38 位开始是 IMO 编号接着 42 位是船名6-bit 转字符6 位一组再往后是船型、船舶尺寸、吃水、目的地等。船名需要把 6-bit 值再映射回 ASCII 字符规则是值在 1 到 26 之间对应 A 到 Z值 32 对应空格值 32 以上的按 ASCII 表映射。实际写解析器时通常会先读消息类型然后把每条消息路由到对应的解析函数。下面是一个简单的 dispatch 逻辑def decode_aivdm(payload, fill_bits): bits payload_to_bits(payload) # 去掉填充位避免填充0干扰字段解析 bits bits[:-fill_bits] if fill_bits else bits msg_type int(bits[0:6], 2) if msg_type in (1, 2, 3): return parse_type1(bits) elif msg_type 4: return parse_type4(bits) elif msg_type 5: return parse_type5(bits) elif msg_type 18: return parse_type18(bits) else: # 保留消息类型编号方便排查不中断循环 return {type: msg_type, mmsi: int(bits[6:36], 2)}删掉填充位的操作容易漏掉。AIS 消息总长度不一定恰好是 6 的整数倍当最后一帧的比特数不够时会用 0 补齐同时 NMEA 语句最后一个字段给出填充位的数量。如果不去掉这些占位 0整数解析时会把填充位当成有效位导致最后一个字段解析多出几个比特数值会偏大。我见过的最典型翻车案例是航向字段没去填充位所有船的航向都是 409.5 度其实是 10 位字段里混入了两个多余的 0 位。4.3 数据类型转换与输出格式标准化解码后的数据要落地就涉及数据类型转换。AIS 原始字段大多是整数或低精度定点数但输出对接数据库或者前端展示时要统一成标准的 JSON 或 CSV。经纬度统一转成十进制度并保留 6 位小数MMSI 用字符串存储避免前后端数值精度问题。下面是输出格式化的示例def to_json(parsed): return { mmsi: str(parsed[mmsi]), lat: round(parsed[lat], 6), lon: round(parsed[lon], 6), sog: round(parsed[sog], 1) if parsed[sog] is not None else None, cog: round(parsed[cog], 1) if parsed[cog] is not None else None, }这里把 MMSI 转换为字符串是有实际原因的。MMSI 是一个 9 位数字在 JavaScript 或某些前端框架里如果当数字处理超过 2^53 会有精度风险虽然 MMSI 只有 9 位暂时还好但字符串是最安全的方式。SOG 和 COG 的空值则保持为 None不要用 0 或 1023 这类魔法值去替代否则后续做航速统计时会出现一堆速度为 0 的无效点。5. 常见问题与排查驱动、乱码、丢包的四个坑5.1 读串口没有任何输出现象程序跑了串口打开但控制台没有任何 AIS 语句打印。用串口助手也收不到数据。原因最常见的是波特率配置错误。AIS 接收机的默认波特率不统一不同厂商差异很大部分老设备只有 4800多数现代模块是 38400少数支持 57600。其次是 USB 转串口的芯片驱动没装上在 Linux 下表现为设备节点没创建在 Windows 下表现为设备管理器里 COM 口是灰色的。解决先用dmesg | grep tty确认设备节点再依次尝试全部常用波特率。我在做一个多船监控模拟项目时某款接收机就是 4800 波特率而教程默认 38400换了之后数据马上出来了。如果设备有指示灯收数据时灯会闪也可以用来判断前端是否在正常工作。5.2 能收到 AIVDM 但解析出的坐标在赤道附近跳变现象解码后经纬度全部落在 0 度附近或者坐标点一会儿在赤道一会儿在非洲大陆。原因经纬度符号位处理错误。这是 AIS 解码特有的一个典型问题——经度 28 位、纬度 27 位都用二进制补码表示但很多人会直接把它当无符号整数解析导致所有负坐标都变成超级大的正坐标最终除以 600000 后落在异常区域。解决回到 4.1 中的 parse_type1对经度字段先判断 raw 值是否大于0x4000000即符号位为 1是则减去0x8000000纬度同理用0x2000000和0x4000000做判断。我写了一个通用的补码转有符号整数函数把字段切片和补码转换封装在一起一次改好到处复用不再每次手写判断。5.3 类型 5 消息缺失或者船名解析出来是乱码现象能收到类型 1/2/3 的位置信息但类型 5 的船名、目的地字段全是或乱码部分语句直接报 CRC 校验错误。原因类型 5 消息被拆成两条 NMEA 语句而解析逻辑没有做拼接而是直接对第一帧的 payload 做了解码。多余的帧被当成另一条独立消息传给解码器后半段字段自然对不上。另外船名长度不足 20 个字符时 AIS 协议会用填充如果不对做截断处理数据库里的船名字段就会带着尾巴。解决写一个按消息 ID 缓存帧的拼包逻辑仅当接收到的帧数等于消息总帧数时将全部 payload 拼接后交给解码器。对船名字段解析输出时做清洗遇到直接截断。代码实现很简单但要注意用 dict 管理帧缓冲时长时间收不到最后一帧的缓存要设定超时清理防止内存增长。5.4 校验和一致失败但语句格式看起来沒问题现象所有 NMEA 语句都能显示但*后面的校验和跟本地计算的结果永远对不上而且检查字符和算式也没问题。原因串口数据中混入了\r\n或者 USB 驱动把两个 AIS 频道的原始数据在帧间插入了不可见字符。更有一种情形是接收机输出的不是 NMEA 封装而是原始 6-bit 数据流此时每一行本身就不是完整语句自然没有合法的校验和。解决在读取串口后先做一次数据清洗把\r去掉只保留\n作为行分隔符。如果发现多行数据挤在一个 buffer 里用splitlines()逐行分离再逐条校验。要是数据来源是原始 AIS 帧而非 NMEA 封装就需要先做位同步和去填充再按 6-bit 组装 payload。这种情况通常在低成本的 DIY 接收模块上遇到换用官方驱动可解决。6. 最后的进阶离线重放与解析验证拿到教程和工具包之后最值得先做的一件事不是直接对接真实接收机而是用一段历史 NMEA 日志做离线重放验证。我一般会在/tmp放一个ais_sample.log里面存几十条真实场景的 AIVDM 语句然后用脚本逐行回放给解析器。这样做的好处是不依赖外设可以随时回归测试对比已知的正确输出能快速定位是驱动问题还是解析逻辑问题。重放脚本的核心是构造一个假的串口或网络源把文件里的每一行当作实时到达的数据喂给解析模块。这样你可以在没有硬件的情况下把整个数据链路跑通也能在改了解码逻辑后快速验证有没有引入回归。常见做法是用 Python 的队列加定时器模拟真实接收间隔import queue import time import threading replay_queue queue.Queue() def load_log(path): with open(path, r, encodingutf-8) as f: lines [line.strip() for line in f if line.startswith(!AIVDM)] for line in lines: replay_queue.put(line) time.sleep(0.05) def worker(): while True: line replay_queue.get() if line is None: break decoded decode_aivdm(line) if decoded and decoded.get(lat) and decoded.get(lon): print(decoded)这里load_log用了time.sleep(0.05)也就是模拟 20 条每秒的接收频率非常接近真实航道的中等密度场景。如果要模拟繁忙水域可以去掉 sleep 或者降到 0.01 秒。另外一个我强烈推荐的做法是抓取同一艘船的类型 1 和类型 5 消息做交叉校验。位置消息里只有 MMSI 没有船名静态消息里有 MMSI 和船名通过 MMSI 把两边的记录关联起来就可以验证类型 5 的拼接逻辑是否正确。如果在重放日志里发现某条船的类型 5 消息总是只解析出半截船名那基本可以确定拼包逻辑漏了帧。离开这个项目后我养成的习惯是每次拿到新的 AIS 数据集或驱动包第一件事不是接硬件而是先重放一遍已知输出。只要日志和解析器都在五分钟就能判断这套资源是否值得深入投入。希望你也能在 AIS 解码这条链路上少走几个弯希望这些经验对你有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑