尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AIS数据链路全解析:从串口驱动到AIVDM解码与Python解析

AIS数据链路全解析:从串口驱动到AIVDM解码与Python解析 简介这份资源围绕AIS船舶自动识别系统展开聚焦驱动开发、信号解码与数据解析三大环节适合从事海洋信息化、水上通信或嵌入式软件开发的工程师及学习者。包体为gz压缩格式共3个文件其中2个txt文档用于说明协议原理与解析流程1个cpp文件提供解码实现示例整体仅5KB小巧精炼。已有1033人学习下载。内容涵盖AIS的VHF信道特性、ITU-R M.1371报文格式、射频信号解调与二进制解析、坐标转换、设备驱动与上层应用接口等关键知识点并解释了静态信息MMSI、船名、呼号与动态信息位置、速度、航向的提取方式以及无线传输中的错误检测与纠正机制。通过阅读可快速建立AIS数据链路整体认知并参考cpp代码理解解码函数与报文结构处理方式为后续开发船舶监控或防碰撞预警系统打下基础。1. AIS 数据链路到底难在哪串口乱码只是第一道坎凌晨的河道监控值班室刚装好的 AIS 接收机指示灯常亮串口助手却吐出一屏乱码好不容易看到!AIVDM开头了下一句又断成半截把语句解析出来船位却落在非洲大陆上。这套“驱动 → 解码 → 解析”的链路AIS 教程里很少一次讲全但实际做船舶监控、通航流量统计、海事数据服务的人几乎每天都要跟它打交道。驱动解决的是“电脑怎么认出接收机”解码解决的是“AIVDM 字符串里到底塞了什么位”解析解决的是“怎么变成能落库、能画轨迹的业务数据”。这篇文章按这条链路往下走每一步给你能直接抄的命令和 Python 代码顺带把波特率、位偏移、南北纬换算这些容易翻车的细节交代清楚。2. 把 AIS 接收机接进电脑从串口驱动到 NMEA 数据流2.1 先认清你的接收机USB 转串口芯片与驱动选型拿到一台 AIS 接收机第一件事不是插线而是确认它里面那颗 USB 转串口芯片是什么型号。市面上一两百块的国产接收机绝大多数用 CH340中端设备常见 CP2102 或 CP2105老牌海事设备则偏爱 FT232。三者驱动完全不同装错了就出现“设备管理器里黄叹号”的经典现象系统识别到了设备但不知道如何跟它对话。在 Linux 下先插上设备用lsusb看芯片的厂商 ID 和产品 ID。CH340 固定是1a86:7523CP210x 是10c4:ea60FT232 是0403:6001。这个信息能直接告诉你该去哪里找驱动也能在买二手板子时防止卖家拿 CH340 冒充 CP2102。lsusb # Bus 001 Device 004: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter dmesg | grep -i tty # usb 1-1: ch341-uart converter now attached to ttyUSB0lsusb的输出里如果看到1a86:7523说明是 CH340Linux 内核自带ch341驱动插上即用。dmesg会给出设备节点名一般是/dev/ttyUSB0。Windows 下则要去芯片厂商官网下对应驱动CH340 装 WCH 官方版CP210x 装 Silicon Labs 的 CP210x VCP 驱动FT232 装 FTDI VCP 驱动。这里有个容易踩的坑Windows 更新有时会自动给 CH340 装一个旧版驱动能识别但一读写就卡死遇到这种情况最好到设备管理器里手动卸载再装厂商版。跟显卡驱动那种“工具箱、超频面板”完全是两码事串口驱动就三个要求识别正确、波特率能切、缓冲区不丢数。2.2 串口参数与最小读取命令9600 8N1 背后的协议约定AIS 接收机输出的 NMEA 0183 语句标准波特率是 38400但很多国产设备出厂默认 9600两条路都走8N18 个数据位、无校验、1 个停止位。这个参数是 NMEA 0183 的物理层约定不能乱改。如果接收机同时输出 GPS 语句和 AIS 语句有的设备还允许把 GNSS 数据关掉只留!AIVDM能省不少带宽。Linux 下先用stty配置串口再用cat裸读数据这是排除驱动问题最快的办法stty -F /dev/ttyUSB0 9600 raw -echo timeout 5 cat /dev/ttyUSB0raw模式禁止了终端对特殊字符的处理保证读到的是原始字节-echo防止输入回显干扰数据。如果 5 秒内没有任何输出先别怀疑设备坏了——大概率是波特率不对换成 38400 再试一次。再不行就用示波器或逻辑分析仪抓 RX 引脚波形直接量出实际波特率这是排查“设备明明在发电脑却收不到”的终极手段。2.3 确认数据在流用 Python 写第一行串口读取cat能出数据后就该换成 Python 脚本为后面的解码解析打底。Python 的pyserial库在这里是标配readline()默认以\n作为结束符正好对上 NMEA 语句的\r\n行尾。import serial ser serial.Serial( port/dev/ttyUSB0, baudrate9600, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout1 ) while True: line ser.readline() if line.startswith(b!): print(line.decode(ascii, errorsreplace), end)这里做了两层过滤startswith(b!)只保留以感叹号开头的语句避开串口刚上电时的乱码残留decode时报错替换而不是抛异常避免单个坏字节让整个程序退出。要注意的是timeout1很重要没有这个参数readline()会一直阻塞一旦上游数据中断程序就死等在那里后面做长时间值守时这是头号隐患。先跑通这一步看到!AIVDM开头的语句稳定输出才算真正拿到了 AIS 原始数据接下来才进入解码环节。3. AIS 报文解码把 AIVDM 字符串拆回位字段3.1 AIVDM 语句结构拆解前缀、填充位与 6-bit ASCII 编码一条典型的 AIS 语句长这样!AIVDM,1,1,,B,15M67FC000G?ufbE,0*4D按逗号拆开AIVDM是语句类型1是当前帧序号1是总帧数空字段是连续消息的序号B表示接收信道15M67FC000G?ufbE是消息的 payload0是填充位数*4D是校验和。这条是单帧消息所以“当前帧/总帧”都是 1多帧消息会在后面看到2,1、2,2这样的组合需要先拼接再解码。Payload 里每个字符不是直接的 ASCII 码而是一种 6-bit 编码规则是把字符的 ASCII 码减 48如果得到的值大于等于 40再减 8。这个规则跟 Base64 表面上像但映射表完全不同——Base64 按A-Za-z0-9/的顺序映射而 AIS 直接基于 ASCII 数值偏移所以“数字 0-9 对应 0-9大写 A 对应 17”这种差异初学的人最容易在这里翻车。填充位数表示 payload 末尾补了几个无效 0 位解码时要去掉否则位偏移整体错位。3.2 用 Python 动手解码位运算把 6-bit 值还原成字段解码第一步实现字符到 6-bit 二进制的转换注意高位在前def payload_to_bits(payload: str) - str: bits for ch in payload: v ord(ch) - 48 if v 40: v - 8 bits f{v:06b} return bitsf{v:06b}把每个 6-bit 值补零成 6 位二进制字符串拼起来就是完整的位流。由于 AIS 的字段是定长的接下来只要按位偏移切取就可以。消息类型 1Class A 位置报告的核心字段布局如下起点从位 0 开始字段起始位长度取值与比例消息类型06固定为 1 / 2 / 3重复指示620-3MMSI830船舶识别码航行状态3840在航5锚泊转向率428有符号数对地航速5010除以 10单位节经度6128除以 600000纬度8927除以 600000注意南北纬换算对地航向11612除以 10单位度真实航向1289单位度对应的解码函数def bits_to_int(bits: str, start: int, length: int) - int: return int(bits[start:start length], 2) def decode_type1(payload: str) - dict: bits payload_to_bits(payload) lon_raw bits_to_int(bits, 61, 28) lat_raw bits_to_int(bits, 89, 27) lon lon_raw / 600000.0 lat lat_raw / 600000.0 if lon 180: lon - 360 if lat 90: lat -(lat - 90) # 南纬取负 return { type: bits_to_int(bits, 0, 6), mmsi: bits_to_int(bits, 8, 30), sog: bits_to_int(bits, 50, 10) / 10.0, lon: lon, lat: lat, cog: bits_to_int(bits, 116, 12) / 10.0, heading: bits_to_int(bits, 128, 9), }这里面有两个参数值得多说。对地航速 102.3 是“不可用”的标记值拿到这个值应该置为None而不是当成真实航速经度大于 180 时减 360是因为 AIS 用 0-360 编码181 到 360 表示西经。纬度同理0-90 是北纬91-180 是南纬但不同解码库对南纬的处理习惯不完全一样有的取lat - 90再标负号有的直接返回180 - lat_raw建议拿一台真实接收机的数据验证自己的换算逻辑别直接抄网上代码。3.3 校验和验证每条语句都值得做一次异或NMEA 语句的*后面跟两位十六进制是从!之后到*之前所有字节的异或结果。解析器如果不做校验和验证遇到被噪声污染的语句会把错误船位当成真实数据送进数据库这是很多监控平台数据混乱的根源之一。def check_nmea(sentence: str) - bool: body, _, checksum sentence.lstrip(!$).partition(*) calc 0 for ch in body: calc ^ ord(ch) return int(checksum, 16) calcpartition(*)把语句切成主体和校验两部分再逐字节异或。这个函数放在解析流程最前面每条语句先验校验和失败了直接丢弃不进入解码逻辑。实际运行中串口噪声导致的坏语句比例通常在 0.5% 以下但在电磁环境复杂的船厂或港口这个比例会明显上升不做校验的后果就是地图上莫名出现一堆跨越半个地球的轨迹点。4. 从解码到业务数据船舶动态信息的结构化落地4.1 设计解析层逐句解析与多句重组的两种组织方式解码函数拿到了单条消息的字段但真实串口数据流里还有两个问题一是 GNSS 语句$GPGGA和 AIS 语句混在一起二是多帧消息需要先拼接。设计解析层时我习惯先用一个分发函数把!开头的语句按类型分流def dispatch(line: str, checksum_ok: bool) - dict | None: if not checksum_ok: return None parts line.split(,) if parts[0] !AIVDM or parts[0] !AIVDO: return process_vdm(parts) return Noneprocess_vdm里再判断帧序号。单帧消息直接调decode_type1多帧消息则先缓存在字典里等所有帧到齐再按顺序拼接 payloadpending {} def process_vdm(parts: list) - dict | None: total int(parts[2]) index int(parts[1]) seq parts[3] payload parts[5] fill_bits int(parts[6]) if total 1: return decode_payload(payload, fill_bits) key (seq, parts[4]) if key not in pending: pending[key] {} pending[key][index] payload if len(pending[key]) total: joined .join(pending[key][i] for i in range(1, total 1)) del pending[key] return decode_payload(joined, fill_bits) return None多帧消息缓存时一定要带超时清理机制。实际使用中经常遇到第一帧到了、第二帧被噪声吃掉的情况如果不清理这个pending字典会越来越大最后变成内存泄漏。我一般给每个 key 记一个时间戳超过 30 秒没有收满就直接丢弃。4.2 输出 JSON 的最小解析脚本把串口数据变成业务数据把上面的模块串起来就是一个能直接跑的最小完整解析器。输入是串口原始行输出是 JSON 字典可以直接交给数据层落库import json def parse_aismessage(line: str) - dict | None: line line.strip() if not line.startswith(!) or * not in line: return None if not check_nmea(line): return None parts line.split(,) if parts[0] not in (!AIVDM, !AIVDO): return None if parts[1] 1 and parts[2] 1: msg_type bits_to_int(payload_to_bits(parts[5]), 0, 6) if msg_type 1: return decode_type1(parts[5]) return None while True: raw ser.readline().decode(ascii, errorsreplace) parsed parse_aismessage(raw) if parsed: print(json.dumps(parsed, ensure_asciiFalse))parts[0]直接用!AIVDM比较是因为语句前缀不会变用in判断可以兼容某些老式设备输出的!BSVDM基站转发语句。业务侧如果只需要位置数据对parsed里的type做一次白名单过滤就行类型 5静态数据和类型 24B 类静态数据应该走另一套解析逻辑不要混在位置消息里。4.3 B 类与 A 类船台的差异为什么省掉的字段不能乱补类型 1 是 A 类船台的位置报告字段详尽包含航行状态、转向率这些。类型 18 是 B 类船台的位置报告只保留核心字段MMSI、经纬度、航速、航向没有航行状态和转向率。类型 24 是 B 类静态数据分 A/B 两半A 半段有船名B 半段有船型、呼号、船长船宽。做数据服务的人常犯的一个错是把类型 18 当类型 1 解码结果 MMSI 对不上经纬度全是错的。B 类设备本身就比 A 类便宜数据字段省是省在协议层不是设备故障。解析层应该把类型 18 单独写一个decode_type18字段偏移跟类型 1 在 MMSI 之前完全一致但后面没有航行状态和转向率经纬度起始位还是 61只是中间的字段变短了。抄代码时务必对照位偏移表逐个确认这跟 CAN 协议报文解析是同一个思路先定基线再按位切任何一位偏移错了整段数据都是废的。5. AIS 实战排障串口乱码、半包数据与坐标跳变的 5 个常见坑5.1 现象 1串口能打开但全是乱码驱动正常、设备节点存在、cat也能读到字节但内容全是~^~^之类的乱码。先量波特率AIS 接收机常见的输出波特率是 38400但不少国产设备出厂烧录的是 9600还有一部分海事基站转发会用 4800。解决用逻辑分析仪抓 RX 引脚的 UART 波形直接读帧宽度。再有一个隐蔽原因USB 转串口模块的 TX 接了接收机的 TX两个输出脚对接电平被拉死。解决交叉接线模块 TX 接设备 RX设备 TX 接模块 RXGND 必须共地。这属于硬件接线玄学但排查顺序比换芯片快得多。5.2 现象 2语句经常断成半截JSON 解析器报错典型表现是!AIVDM,1,1,,B,15M67FC000G?ufbE,0*4D读到一半后面没了或者一行里混进两句话的尾巴。常见原因是 USB 转串口芯片的接收缓冲区溢出尤其在串口助手里开启自动滚动时上位机处理不过来。解决在readline()之外再加一层按!和*切分的数据提取避免依赖行尾的换行符把timeout调短到 0.1 秒积攒到!开头的数据再进入解析。还有一类是 Windows 下串口号被休眠的 USB 控制器回收解决在设备管理器里关掉 USB 设备的“允许计算机关闭此设备以节约电源”。5.3 现象 3经纬度偶尔跳变到非洲解析结果里 99% 的船位正确偶尔一艘船瞬间出现在几千公里外。先查是不是把类型 5 的静态数据当位置消息解析了类型 5 里没有经纬度按类型 1 的偏移去切截到的必然是垃圾值。再查校验和有没有做不做校验的解析器会把被噪声污染的半句当完整语句处理。还有一个隐蔽点在pending缓存多帧消息拼接顺序错乱会让整个位流错位。解决在这个跳变位置打印原始语句和解码后的 bits逐位对照字段表基本一眼就能看出是哪个环节出的问题。5.4 现象 4同一艘船出现多个 MMSI一个 MMSI 对应一艘船这是原则但实际数据里经常看到同一船名挂两三个 MMSI。常见原因有三类A 类和 B 类设备确实是两个独立 MMSI船上装了两台解析器把!AIVDO本船和!AIVDM他船混在一起本船的 MMSI 被当成目标船还有一种是 6-bit 编码的字母大小写换算错误导致 MMSI 的值偏了一位。解决在解析层按住 MMSI 时间窗做聚类如果两个 MMSI 轨迹高度重叠且持续超过 10 分钟大概率是同一艘船建议业务侧维护一个 MMSI 别名表。5.5 现象 5长时间运行后程序卡死或内存上涨连续跑三四天进程还在但不再输出数据或者内存从 50MB 涨到 2GB。前者是读线程阻塞readline()不设超时串口异常断了就一直等后者是pending字典和异常对象没有被清理。解决给serial.Serial加timeout1外层再用一个计数变量做 watchdog连续 30 秒没有任何语句就自动重开串口。pending清理我用最简单的时间戳方案每次访问时检查time.time() - ts 30就删除。这种长期值守的 bug 很难在短时间测试里暴露但监控系统跑起来就是几个月必须在一开始就防住。6. 进阶把 AIS 数据落库并用时间窗做多船轨迹拼接解析层输出 JSON 后下一步自然是落库和轨迹分析。这里给一个能直接用的 SQLite 表结构配合一个简单的时间窗拼接方法。CREATE TABLE IF NOT EXISTS ais_positions ( id INTEGER PRIMARY KEY AUTOINCREMENT, mmsi TEXT NOT NULL, ts INTEGER NOT NULL, lon REAL NOT NULL, lat REAL NOT NULL, sog REAL, cog REAL, heading INTEGER ); CREATE INDEX idx_mmsi_ts ON ais_positions(mmsi, ts);写库的批量插入比单条插入快一个数量级攒够 100 条再executemany一次。分析轨迹时可以用 SQL 的LAG函数做相邻点校验SELECT mmsi, ts, lon, lat, LAG(lat) OVER (PARTITION BY mmsi ORDER BY ts) AS prev_lat, LAG(ts) OVER (PARTITION BY mmsi ORDER BY ts) AS prev_ts FROM ais_positions WHERE mmsi 413123456 ORDER BY ts DESC LIMIT 100;拿到前后两个点后算一下距离除以时间差如果超过 60 节且持续多个周期就是定位跳变需要在应用层过滤掉。设备 30 秒不发位置是正常的超过 5 分钟没更新才需要标记离线。我的习惯是原始 NMEA 语句必须先存档再进解析器。按天存成文件或者直接存一份原样数据表。解码器写错了可以拿历史数据重放解析器写错了也能随时回炉没有原始数据的调优就是在黑匣子里猜。这套链路踩过一遍之后你会发现 AIS 最难的并不是算法而是每个环节都在细节里藏坑。希望这篇教程能帮你把串口后的每一步都走稳。本文还有配套的精品资源点击获取
返回列表