AIS解码全解析:从NMEA句子到船舶动态的bit级还原

发布时间:2026/9/10 11:36:17
AIS解码全解析:从NMEA句子到船舶动态的bit级还原 简介随着海上交通智能化发展AIS报文解析已成为船舶监控和导航系统中不可或缺的一环这一源码资源包正面向AIS数据解析需求适合嵌入式开发、航海通信技术学习及船舶监控软件二次调试等场景。资源仅含一个C源码文件和一个TXT说明文档压缩后仅1KB结构十分紧凑。C源码演示了从原始二进制信号到美国信息交换标准码ASCII的转换、循环冗余校验以及船舶位置、速度、航向等关键信息提取的完整流程配套的TXT文档进一步补充了解码原理、字段含义与使用注意事项便于对照源码逐行理解。借助这份极简示例读者可快速掌握AIS报文从接收到校验再到结构化输出的核心链路并能在现有代码基础上扩展或移植到实际项目中。目前已有587人学习下载能帮助希望以最短路径理解AIS解码机制并自行开发相关工具的开发者和学习者节省摸索时间。1. AIS_decode.c 到底在解什么从串口 NMEA 到船舶动态把 AIS.rar 解压之后里面真正值得花时间读的就是 AIS_decode.c 和 pudn.txt。AIS 接收机从 VHF 频段收到的是 GMSK 调制后的 HDLC 帧经过解调后输出成!AIVDM开头的 NMEA 0183 句子。AIS_decode.c 不碰射频解调它的工作从 NMEA payload 开始先把 6bit ASCII 还原成原始 bit 流再做 CRC-16-CCITT 校验最后按消息类型抽取出 MMSI、经纬度、对地速度、航向这些结构化字段。这个过程看起来简单但 6bit 字符表、填充位、位序这三样东西只要错一个解析出来的坐标就完全不对。本文会把这条链路拆开讲最后给出编译和调试命令适合正在做船舶监控、AIS 数据采集或想移植参考解码器的工程师。2. AIS 帧结构与 NMEA 封装先把 6bit ASCII 还原成二进制位流2.1 空中接口是 bit 流串口上是 ASCII中间隔着一层映射AIS 的每条消息在空中是连续的 bit 流长度按消息类型不同常见的有 168bit、424bit最短的二进制广播消息只有 72bit。发送端把 bit 流按 6bit 一组切分每组取值 0~63再加 48 变成 ASCII 可打印字符0~W。这就是为什么你在串口助手或者日志文件里看到的 AIS payload 是一串可读字母数字而不是乱码。反过来解码的第一步必须是payload[i] - 48把每个字符还原成 6bit 值再拆成二进制位。这里最容易踩的坑是把 AIS 的 6bit 编码和 Base64 混在一起。Base64 虽然也是 6bit 到可见字符的映射但字符表和映射规则完全不同拿 Base64 解码器去解 AIS payload出来的字节序全乱。判断方法很直接解完一组已知消息后看 CRC 是否一致不一致就基本可以确定字符表用错。2.2 AIS_decode.c 里拆分 AIVDM 字段的常用写法NMEA 0183 的 AIVDM 句子用逗号分隔字段payload 固定在第 6 个字段填充位在第 7 个字段。AIS_decode.c 里常见入口如下#include stdio.h #include string.h #include stdlib.h int ais_extract_payload(const char *nmea, char *payload_out, int *fill_bits) { char line[256]; int field 0; char *token; if (nmea NULL || payload_out NULL || fill_bits NULL) return -1; if (strlen(nmea) sizeof(line)) return -1; strncpy(line, nmea, sizeof(line) - 1); line[sizeof(line) - 1] \0; for (token strtok(line, ,); token ! NULL; token strtok(NULL, ,)) { field; if (field 6 token[0] ! \0) { strncpy(payload_out, token, 128); } else if (field 7) { *fill_bits atoi(token); } } return (field 7) ? 0 : -1; }这段代码有两点值得注意。第一strtok会修改原字符串所以必须先strncpy一份否则调用方保存的原文会被破坏。第二多句消息里第一句的 payload 字段可能是空的token[0] ! \0就是为了跳过这种空字段同时避免下一步strlen(payload)得到 0 后在位转换里死循环。fill_bits的范围需要在调用侧再校验一次不在 0~5 内的行直接丢弃因为后续解码会把填充位当成真实 bit导致 CRC 和坐标一起错。2.3 一条完整 AIVDM 句子的字段结构以样例!AIVDM,1,1,,A,15NPOOPP00o?bbE,0*5C说明字段序号示例值含义与处理方式1!AIVDM句子标识VDM表示收到他船数据VDO表示本船数据21总句数长消息拆成多条时大于 131当前句序号拼接 payload 时必须按此字段排序4空多句消息的组标识单句时为空拼接时用于区分不同消息5A接收频道A 为 AIS 1B 为 AIS 2615NPOOPP00o?bbE6bit ASCII payloadAIS_decode.c 的主输入70填充 bit 数取值范围 0~585CNMEA 行 XOR 校验与消息内 CRC 无关这里第 8 个字段的*5C是整行的异或校验生成范围是!之后到*之前的字符。AIS_decode.c 里如果只做 payload 解码通常忽略这个 XOR直接跳到第 6 字段。但如果你要对多句话组合行做完整性过滤先校 XOR 能省掉不少无效行长。2.4 payload 字符还原为 bit 数组strlen(payload) * 6就是 payload 展开后的 bit 数。实际操作不能直接把这个值当有效 bit 数因为消息末尾可能还有填充的 0要减去第 7 字段的 fill bits。下面这个函数把字符数组展开成 bit 数组int ais_payload_to_bits(const char *payload, uint8_t *bits, int max_bits) { int len strlen(payload); int bit_index 0; for (int i 0; i len; i) { int value payload[i] - 48; if (value 0 || value 63) return -1; for (int j 5; j 0; j--) { if (bit_index max_bits) { bits[bit_index] (value j) 1; } } } return bit_index; }这里payload[i] - 48对0得到 0对W得到 55覆盖 0~63 的范围。内层循环从j5到j0也就是把 6bit 值从高位到低位依次写入bits保证后续字段抽取时 bit0 永远是消息的起始位。如果你把循环方向倒过来解析出来的 MMSI 会被镜像成完全不同的数而且 CRC 也不会通过。调用后拿返回值减去fill_bits才是真正参与消息解析和 CRC 计算的 bit 长度。3. CRC-16-CCITT 校验与填充位AIS 解码里最常见的两个错位点3.1 AIS 采用的是 LSB-first 的反向多项式实现AIS 消息在 HDLC 帧内自带 16bit FCS使用 CCITT 多项式x^16 x^12 x^5 1。协议要求在计算时按 LSB-first 的顺序处理每个字节初始值 0xFFFF。这个参数组合与常见的 CRC-16/CCITT-FALSE多项式 0x1021MSB-first不同也和 MODBUS 的 CRC-16 不同。AIS 的解码器里实现的是反向查表0x8408查表时可以写成crc (crc 1) ^ 0x8408的循环体。这里最大的坑是多数通用 CRC 计算工具默认用0x1021正向算法你拿去算 AIS 数据得到的结果与报文里的 FCS 永远差一截。下面是 AIS_decode.c 里常见的按位计算实现uint16_t ais_crc16(const uint8_t *data, size_t len) { uint16_t crc 0xFFFF; for (size_t i 0; i len; i) { crc ^ data[i]; for (int bit 0; bit 8; bit) { if (crc 1) crc (crc 1) ^ 0x8408; else crc 1; } } return crc; }这段代码不做输入反转也不做输出反转。0x8408是对多项式0x1021做 bit-reversal 之后的值配合 LSB-first 逐位处理等效于硬件上从最低位开始移位异或。如果返回的 CRC 与消息末尾两字节比较时反了请试试先比较低字节再比较高字节。更高阶的做法是构造完整帧也就是消息字节加 FCS 一起丢进ais_crc16标准残值为0xF0B8可以用它验证你的字节序是否与协议一致。3.2 CRC 参数表速查CRC-16-CCITT 参数AIS 采用的值生成多项式x^16 x^12 x^5 1正向多项式常数0x1021反向多项式常数0x8408初始值0xFFFF输入数据位处理LSB-first输出异或无消息内 FCS 位置payload 的最后 16bit完整帧校验残值0xF0B8实际排错时先用 CRC 做一个过滤层ais_crc16(bits, bit_len / 8)不等于消息末尾的 16bit 时不需要再解析经纬度。因为位流已经错了后面解析出来的船舶位置要么是乱跳的点要么是明显的非法值。3.3 填充位参与 CRC 导致的错误一个非常隐蔽的错误是ais_payload_to_bits返回的bit_len包含尾部填充如果不减fill_bitsCRC 会把 AIS 消息本身的填充 0 当成数据段末尾同时正确的 16bit FCS 被挤到后面当作普通数据参与计算结果自然永远不匹配。解决方法是在调用 CRC 前先做一次长度修正int actual_bit_len bit_len - fill_bits; uint16_t crc ais_crc16(bits, actual_bit_len / 8);因为actual_bit_len通常是 8bit 的整倍数少数不是时需要按 AIS 协议在最后一个不完整字节的高位补 0使 CRC 输入按字节对齐。这一点在嵌入式代码里经常被忽略尤其是在消息类型 8 的 168bit 类消息中。补 0 后再算 CRC就会看到真正合理的校验值。如果仍不匹配再去检查 2.4 中 bit 数组的生成方向以及fill_bits的取值范围是否被atoi解析成了负数。4. 消息类型 1/2/3 位字段抽取把 168bit 还原成位置航线4.1 位字段布局与偏移速查AIS 消息类型 1/2/3 都是船舶动态位置报告格式完全一样区别只在消息类型字段的值。整条消息固定 168bit解析时不需要像 JSON 那样按字节对齐只需要一个能读取任意起始位置和长度的位段函数。下面给出常见的解码字段表里面的偏移是从 bit0 开始记的开始 bit位宽字段解析说明06消息类型1/2/3 代表位置报告62重复次数转发次数0 表示原始消息830MMSI无符号整数384导航状态0在航1锚泊5系泊8靠泊15无定义428转向率 ROT带符号数-128不可用5010对地速度 SOG0~1022 代表 0~102.2 节1023不可用601位置精度0低精度1高精度6128经度补码单位 1/10000 分1 度6000008927纬度补码单位 1/10000 分91 度以上不可用11612对地航向 COG0~3599单位 0.1 度3600不可用1289真航向0~359 度511不可用1376UTC 秒0~5960不可用61定位系统手动输入从这张表可以看出来AIS 并不会为每个字段单独对齐到字节边界MMSI 从 bit8 开始SOG 从 bit50 开始中间穿插的导航状态和 ROT 字段都不是字节整数倍。所以解码器的核心能力不是读结构体而是能精确截取任意连续的 bit 段。4.2 通用 bit 位段读取函数static unsigned int get_bits(const uint8_t *bits, int start, int len) { unsigned int result 0; for (int i 0; i len; i) { int pos start i; int byte_index pos 3; int bit_in_byte 7 - (pos 7); result (result 1) | ((bits[byte_index] bit_in_byte) 1); } return result; }pos 3得到字节索引pos 7得到该 bit 在字节内的位置并用7 - ...算出它在字节里的位移。这样保证 bit0 在第一个字节的 MSBbit7 在第一个字节的 LSB与 AIS 空中接口的 bit 顺序保持一致。使用这个函数时要注意len不能超过 32否则unsigned int溢出。AIS 里最长字段是 MMSI 的 30bit所以 32bit 足够覆盖。在嵌入式环境里如果unsigned int是 16bit必须改用uint32_t并显式做类型转换。4.3 位置报告解码与符号扩展下面的函数把类型 1/2/3 的 168bit 转成经纬度和航速typedef struct { uint32_t mmsi; double longitude; double latitude; double sog_knots; double cog_deg; int heading; } ais_position_report; int ais_decode_pos_report(const uint8_t *bits, int bit_len, ais_position_report *out) { int type; if (bit_len 168) return -1; type get_bits(bits, 0, 6); if (type 1 || type 3) return -2; out-mmsi get_bits(bits, 8, 30); out-sog_knots get_bits(bits, 50, 10) / 10.0; uint32_t raw_lon get_bits(bits, 61, 28); int32_t lon (int32_t)(raw_lon 4) 4; out-longitude lon / 600000.0; uint32_t raw_lat get_bits(bits, 89, 27); int32_t lat (int32_t)(raw_lat 5) 5; out-latitude lat / 600000.0; out-cog_deg get_bits(bits, 116, 12) / 10.0; out-heading get_bits(bits, 128, 9); if (out-heading 511) out-heading -1; return 0; }这里最值得说的是两步位移的符号扩展。raw_lon 4把 28bit 的补码值左移到 int32 的最高位然后算术右移 4 位编译器会保留符号位最终得到正确的带符号整数。直接对uint32_t做强制转换不够因为无符号类型右移是逻辑右移负数会变成一个很大的正数。经纬度的除法用 600000 是因为协议单位是 1/10000 角分1 度等于 60 角分所以 600000。SOG 和 COG 分别是 0.1 节和 0.1 度直接除以 10。出现1023、3600、511这些特殊值时调用方应当标记数据无效而不是把它画到地图上。AIS_decode.c 如果直接把cog_deg当普通 double 输出很容易在显示层出现一条 360 度的假航向。4.4 消息类型分发与多句报文类型 4 的基站报告同样 168bit但字段布局与类型 1/2/3 不同类型 5 的静态数据是 424bit中间还夹着 6bit 字符型字段。实际工程里比较稳的写法是先用get_bits(bits, 0, 6)取消息类型再按类型分发到不同解析函数。AIS_decode.c 如果要把类型 4 或 5 也解析出来不能复用位置报告的偏移否则 MMSI 之后读出来的就是另一组完全没意义的数字。长消息被拆成 2~3 句时需要按句子序号把多段 payload 拼接后再调解析函数拼接时用strcat就行但组序号必须一致否则会把两条不同消息拼在一起。5. 把 AIS_decode.c 跑起来编译命令、测试向量和三个排错点5.1 最小测试主函数与编译为了验证上面四个函数是否配合正常可以写一个十几行的测试主函数把 argv[1] 当作完整 AIVDM 句子传入int main(int argc, char *argv[]) { char payload[128] {0}; int fill_bits 0; uint8_t bits[256] {0}; ais_position_report pos; int bit_len; if (argc 2) return 1; if (ais_extract_payload(argv[1], payload, fill_bits) 0) return 2; bit_len ais_payload_to_bits(payload, bits, sizeof(bits)); if (bit_len 0) return 3; bit_len - fill_bits; if (ais_decode_pos_report(bits, bit_len, pos) 0) return 4; printf(mmsi:%u\nlon:%.5f\nlat:%.5f\nsog:%.1f\ncog:%.1f\nheading:%d\n, pos.mmsi, pos.longitude, pos.latitude, pos.sog_knots, pos.cog_deg, pos.heading); return 0; }编译命令gcc -O2 -Wall -o ais_decode ais_decode_test.c ./ais_decode !AIVDM,1,1,,A,15NPOOPP00o?bbE,0*5C这段代码先把句子解析到 payload再按 6bit 展开减去fill_bits之后位置解析。输出大致是mmsi:367430530、sog:0.0以及一个位于西雅图附近的经纬度。使用-Wall能看到get_bits里uint8_t被先移位再赋值的告警这个是正常的只要结果没有溢出即可。5.2 三个排错点与优先级提示任何一位解析结果异常时先检查 CRC再检查 fill_bits最后才查字段偏移。第一个排错点是 CRC 永远不对。把收到的最后两个字节和ais_crc16计算结果逐字节打印出来确认高低字节是否互换。第二个是经纬度正好翻倍或减半这通常是 600000 的进制搞错或者是 bit 数组生成时循环方向反了导致 28bit 经度被镜像。第三个是消息 1 能解消息 4 或 5 全乱这种情况多半是解析函数直接复用了位置报告布局没有按消息类型分发。5.3 让调试更快的两个辅助命令在 Linux 下可以用printf加xxd快速看 payload 的十六进制echo !AIVDM,1,1,,A,15NPOOPP00o?bbE,0*5C | \ awk -F, {print $6} | tr -d \n | xxd -pawk提取第 6 个字段xxd -p输出 hex把 hex 与ais_payload_to_bits生成的 bit 数组逐字节比对能快速定位是字符串解码的问题还是位序的问题。第二个命令是用crc16脚本对同一句话做一次独立校验确保 C 代码里的0x8408实现没被编译器优化掉。这里的思路是凡是在通信协议里出现“某个字节差一点”的 bug优先怀疑字节序其次是 bit 顺序而不是去改解码公式。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询