
简介这份资源面向希望系统掌握AIS船舶自动识别系统的开发者与学习者围绕驱动、解码、解析三个环节展开帮助读者理解从射频信号接收、数字信号处理到报文信息提取与应用的完整链路。压缩包共3个文件以txt文本与cpp源码为主前者可用于存放原始数据与解码输出样本后者承载解码与解析的核心实现整体约5KB体量轻便便于快速阅读与二次开发。目前已有1037人学习下载具备一定的参考基础。内容涉及ITU-R M.1371报文格式、二进制数据解析、坐标转换、错误检测与纠正以及驱动与硬件交互、解析软件的数据过滤与报警思路适合作为AIS入门与工程实践的对照材料帮助读者建立从信号到可用船舶信息的整体认知。1. AIS 解码为什么一条 256 位的报文能讲清一艘船的全部行踪海上交通里有一类报文长度固定、字段紧凑、没有分隔符接收端拿到的往往只是一串十六进制字符。很多人第一次接触 AIS 解析会以为难点在“把字段切开”真正上手才发现切字段只是表层底层是一套围绕比特位定义的自描述编码体系同样的 168 位或 256 位载荷前 6 位决定报文类型后面每一段的偏移和长度都随类型变化。搞懂这套机制你才能明白为什么解码器必须先判类型再取字段、为什么驱动层要做位对齐、为什么一条报文解析错一位后面全乱。AIS 解码教程、驱动、解析这三个词本质上对应接收链路的三层物理层收比特、链路层组帧、应用层还原语义。把这三层拆开看一条报文如何变成船位、航向、船名就不再是黑匣子。2. 报文结构6 位类型码如何决定后面所有字段的偏移AIS 报文不是定长结构体而是一棵以报文类型为根的分发树。接收端先读前 6 位得到 0 到 63 之间的类型号再查表决定后续字段的布局。这个设计的好处是扩展性强新增报文类型不影响旧类型解析代价是解码器必须维护一张类型到字段表的映射不能用一个通用结构体硬套。2.1 六位字符集与比特打包为什么不能按字节切AIS 载荷里的文本字段用的是 6 位 ASCII 子集不是标准 8 位字符。一个字符占 6 位意味着它不落在字节边界上。常见的实现思路是先把整段载荷展开成比特流再按偏移取位而不是直接对字节数组做切片。下面这段 Python 展示了最基础的取位逻辑def get_bits(payload_bits, start, length): # payload_bits 是 0/1 组成的字符串start 为起始位length 为位长 chunk payload_bits[start:start length] if len(chunk) length: raise ValueError(payload too short) return int(chunk, 2) def get_sixbit_text(payload_bits, start, char_count): # 6 位字符集值 0-63 映射到可打印字符这里只做示意映射 table ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\#$%()*,-./0123456789:;? out [] for i in range(char_count): v get_bits(payload_bits, start i * 6, 6) out.append(table[v]) return .join(out).strip( )get_bits的start和length都以位为单位不是字节。get_sixbit_text里每个字符固定跨 6 位所以第 i 个字符的起始位是start i * 6。这段代码的关键不在映射表而在“先转比特串再取位”这个顺序。如果先按字节切再拼跨字节的 6 位字符会被切断解析出来的船名会出现乱码这是血泪经验里最常见的一类翻车。2.2 类型 1/2/3 位置报文的字段布局位置报文是最常被解析的类型载荷 168 位。前 6 位是类型号紧接着 2 位是重复指示然后 30 位 MMSI4 位航行状态8 位转向率10 位对地航速1 位位置精度28 位经度27 位纬度12 位对地航向9 位真航向6 位时间戳。这些字段的偏移是固定的但每个字段的物理量纲不同解码时要做缩放。字段起始位位长缩放说明类型号06无1/2/3 为位置报文MMSI830无船舶唯一标识对地航速5010除以 10单位节1023 表示不可用经度6128除以 600000单位度181 度表示不可用纬度8927除以 600000单位度91 度表示不可用对地航向11612除以 10单位度3600 表示不可用这张表说明一个设计取舍AIS 用定点整数传输物理量接收端负责缩放。经度用 28 位、除以 600000精度约 1.8 米足够海上避碰使用又比浮点传输省位。解码器如果忘了缩放得到的经度会是几十万量级看起来像坐标但完全不能用。2.3 多分片报文类型 5 和类型 24 为什么要拼船名、呼号、目的地这类文本字段较长单条报文放不下于是拆成多分片。类型 5 静态与航程报文占 424 位分 2 片类型 24 静态数据报分 A、B 两部分。分片报文带分片计数和分片序号接收端要按 MMSI 缓存等所有分片到齐再拼。常见的实现思路是用一个字典做重组缓冲键是 MMSI 加分片类型值是已收到的分片位串。拼的时候按分片序号排序去掉每片的头部再连接。这里有个容易忽略的点分片可能乱序到达也可能丢片。解码器要设超时清理否则缓冲会一直涨。拼装逻辑本身不复杂复杂的是边界处理比如同一艘船短时间内发了两组分片键冲突会导致新旧数据混拼。3. 驱动层从射频比特到完整报文的组帧过程驱动层做的事是把接收机吐出的原始比特流变成一条条边界清晰的报文。这一层不关心字段含义只关心帧头、帧尾、校验和转义。很多人解析失败问题不在应用层字段表而在驱动层组帧没做对。3.1 HDLC 风格组帧与比特填充AIS 链路层借鉴了 HDLC 的帧结构用 01111110 作为帧标志。载荷里如果出现连续 5 个 1发送端会插入一个 0接收端要删掉这叫比特填充。驱动层如果只按帧标志切分而不做去填充载荷的位偏移会整体错位后面所有字段全错。def destuff(bits): # 去掉发送端插入的 0连续 5 个 1 后的 0 是填充位 out [] ones 0 for b in bits: if b 1: ones 1 out.append(b) else: if ones 5: ones 0 # 这个 0 是填充位丢弃 continue ones 0 out.append(b) return .join(out)destuff遍历比特串维护连续 1 的计数。当遇到 0 且前面正好 5 个 1 时这个 0 被丢弃计数归零。注意丢弃后不把 0 写进输出这是去填充和普通遍历的区别。如果驱动层漏了这一步载荷会多出若干 0类型号可能从 1 变成 2 或别的值表现为“报文类型莫名其妙不对”。3.2 CRC 校验为什么错一位就整帧丢弃组帧完成后要校验。AIS 用 16 位 CRC覆盖除帧标志外的载荷。校验不通过时正确做法是整帧丢弃而不是尝试纠错。原因是 AIS 报文没有重传机制错帧的字段偏移已经不可信强行解析只会产出错误船位。驱动层通常把 CRC 计算和比对放在组帧之后、上交之前。实现上可以用查表法加速也可以逐位算。关键参数是初始值和多项式不同实现如果初始值不一致会出现“同一帧一边能过一边过不了”的玄学现象。排查这类问题时先拿一帧已知正确的报文做基准对比 CRC 中间值比盲改代码有效。3.3 通道与时间戳驱动层为什么要打标一条报文从射频到应用层中间有缓存和排队。驱动层在收帧时打上通道号和时间戳应用层才能判断这条位置报文的时效。时间戳如果等到应用层再打排队延迟会让“当前船位”变成“历史船位”。这个设计动机很实际海上避碰对时效敏感几百毫秒的偏差可能影响判断。常见的实现是在中断或收包回调里取单调时钟和报文一起入队。应用层解析时用时间戳做过期判断超过一定窗口的位置报文不再参与显示。这一步不做界面上会出现船位跳变看起来像解析错误实际是时序问题。4. 应用层解析字段语义还原与状态机应用层拿到的是已经校验通过的比特串要做的是按类型分发、按字段表取值、做量纲转换、维护船舶状态。这一层最容易出的问题不是取位而是状态管理。4.1 类型分发与字段表驱动解析器的骨架通常是一张类型表每个类型对应一个字段描述列表每个描述包含名称、起始位、位长、类型、缩放因子。解析时遍历描述列表统一取值。这种表驱动写法比手写 if-else 好维护新增类型只加表项。POSITION_FIELDS [ (msg_type, 0, 6, uint, 1), (mmsi, 8, 30, uint, 1), (sog, 50, 10, uint, 0.1), (lon, 61, 28, int, 1 / 600000), (lat, 89, 27, int, 1 / 600000), (cog, 116, 12, uint, 0.1), ] def parse_position(bits): result {} for name, start, length, kind, scale in POSITION_FIELDS: raw get_bits(bits, start, length) if kind int and raw (1 (length - 1)): raw - (1 length) # 有符号字段补码还原 result[name] raw * scale return resultkind为int时要处理补码经度和纬度是有符号的直接按无符号取会得到巨大正值。scale在表里直接写成缩放因子避免解析后再散落各处做除法。这段代码的取舍是把“取位”和“语义”分开get_bits只管位parse_position只管语义两层职责清晰排查时容易定位是位偏移错还是缩放错。4.2 不可用值与边界判断AIS 协议为很多字段定义了“不可用”编码比如航速 1023、经度 181 度、纬度 91 度、航向 3600。解析器如果直接把不可用值当有效值缩放会得到离谱的坐标或速度。正确做法是在字段表里标记不可用阈值解析后置空或标记无效。这个细节在教程里常被一笔带过但实际影响很大。一条船刚开机、位置未定位时发的就是不可用值。界面如果显示成 181 度经度用户会以为解析器坏了。排查时先看原始值是否等于不可用编码再看缩放顺序反了会白费很多时间。4.3 船舶状态缓存与更新策略解析出的单条报文只是快照实际应用要维护每艘船的最新状态。常见做法是以 MMSI 为键建状态表位置报文更新动态字段静态报文更新船名呼号分片报文拼完再更新。更新时要注意字段来源优先级比如类型 5 和类型 24 都带船名后到的覆盖先到的但要防止旧分片覆盖新数据。状态表还要处理过期。船驶出接收范围后不再有报文状态表如果不清界面会一直显示旧船位。常见的实现是给每条船记最后更新时间定时扫描清理超过窗口的条目。这个策略没有统一标准取决于接收范围和业务需求但必须有否则内存和显示都会出问题。5. 避坑与排查解码链路上最容易翻车的几个点5.1 现象船名解析出乱码位置却正常原因通常是 6 位字符集映射表写错或者文本字段的起始位算错。位置字段是数值缩放后看起来合理掩盖了位偏移问题。解决方法是单独拿一条类型 5 报文把载荷展开成比特串手工数前 6 位类型、后面各字段偏移和代码里的表逐项对齐。字符集映射表要确认 0 到 63 的完整顺序不能只抄一部分。5.2 现象同一帧报文有时能解析有时不能先查 CRC 初始值和多项式是否和发送端一致再查比特填充是否在 CRC 计算之前去掉。顺序错了会导致 CRC 覆盖的位串和发送端不一致。另一个常见原因是驱动层把帧标志也纳入了载荷多算了 8 位。排查时打印 CRC 计算前的比特串长度和协议规定的载荷长度对比长度不对就是组帧边界错了。5.3 现象分片报文拼出来的船名缺字或多字分片重组时每片的头部长度没去掉或者分片序号排序错。类型 5 的每个分片都带自己的头部拼的时候要去掉每片的头部再连接载荷部分。如果直接把整片位串首尾相接中间会多出头部位表现为船名中间插入乱字符。解决方法是重组前先按分片序号排序逐片去掉头部再连接最后按总长度截断。5.4 现象位置显示在陆地上或漂移先确认经纬度缩放因子是不是 600000再确认有符号处理是否正确。经度 28 位、纬度 27 位纬度位长少一位补码判断的阈值不同用同一个阈值会出错。另一个原因是把不可用值当有效值181 度和 91 度要单独判断。排查时打印原始整数和缩放后的值对照已知船舶的真实位置偏差量级能直接指向缩放还是符号问题。5.5 现象状态表里船越来越多内存持续上涨没有过期清理或者清理窗口设得太大。接收范围外的船不再发报文但状态表条目还在。解决方法是给每条船记最后更新时间定时扫描删除超过窗口的条目。窗口大小要结合接收半径和报文频率定太小会导致船位闪烁太大则内存涨。这个参数没有标准答案需要按实际场景调。6. 设计取舍为什么 AIS 解析器要分层而不是一把梭把整条链路摊开看AIS 解析的每个设计选择背后都有明确的约束。6 位字符集和定点整数是为了在窄带信道上省位分片机制是为了让长文本能传输CRC 整帧丢弃是因为没有重传通道驱动层打时间戳是因为海上避碰对时效敏感。这些约束决定了实现上必须分层驱动层管比特和帧边界应用层管语义和状态中间用校验和类型分发隔开。分层带来的直接好处是可排查。解析出错时先看驱动层组帧后的比特串长度和 CRC再看应用层字段偏移和缩放两层各自有明确的输入输出不用在一大坨代码里猜。代价是层间接口要设计好比如比特串的表示方式、时间戳的传递、分片缓冲的归属这些接口定不好分层反而增加复杂度。我一般会这么理解AIS 解析器不是一个“把十六进制转成 JSON”的函数而是一条有状态的处理流水线。流水线上每一站只做一件事每件事都有可验证的中间结果。验证方法也很直接拿已知正确的报文做基准逐层对比中间输出哪一层对不上就停在哪一层查。从那以后我每次接新的 AIS 数据源都强制先跑一遍基准报文确认驱动层组帧和 CRC 没问题再往上查字段这个习惯省掉了大量来回猜的时间。希望帮到你。本文还有配套的精品资源点击获取