
简介基于AIS的船舶避碰数据采集系统设计PDF文档面向航海通信、船舶电子及嵌入式方向读者系统梳理了AIS基本组成、工作过程、通信协议和技术标准并完整呈现从数据采集、信号解码到显示交互的软硬件设计方案。内容覆盖静态信息船长船宽、船舶类型等、动态信息船位、对地航速、航向、转向率等、航行相关信息以及安全短消息的采集与分析重点解析DGPS、罗经、计程仪数据的滤波处理FSK鉴频解码电路、C5402 DSP芯片、RS-485接口与LCD显示电路设计同时给出软件流程与部分程序。资源为1个PDF文件压缩包大小1.96MB已有207人学习下载。借助该文档可快速梳理由传感器信号到船舶状态显示的完整链路适合作为本科毕业设计、课程项目或AIS避碰系统预研的参考资料能够为系统开发与论文撰写提供清晰、可落地的实现思路。1. 为什么AIS数据采集比避碰算法本身更早遇到瓶颈AIS 接收机输出的 NMEA 串口流看起来是一条条规整的 AIVDM 句子可真正送到避碰模块里经纬度会跳、航速会归零、目标会瞬移。很多团队把精力放在最后那段 DCPA/TCPA 计算上结果发现是数据采集链路先出了岔子。这篇博客按船舶避碰数据采集系统的常规做法拆开讲AIS 解码、采集架构、脏数据清洗、避碰参数计算以及狭水道阈值调节。适合正在做海事信息化、无人艇感知或船载辅助驾驶的工程师看完可以照着搭一套能接入真实 AIS 接收机的采集与预警链路。2. AIS报文解码把AIVDM句子变成可计算的避碰字段2.1 避碰需要哪几个AIS消息类型AIS 报文类型很多但避碰采集通常只关心两类动态报文和静态报文。动态报文决定目标此刻在哪里、朝哪个方向走静态报文决定目标有多大、要留多少余量。实际落地上消息 1/2/3 是 A 类船舶的位置报告更新间隔 2 到 10 秒消息 5 是静态与航次数据包括船长、船宽、吃水、目的地消息 18 是 B 类船舶动态快艇、渔船、内河船大多走它消息 24 是 B 类静态。只做避碰预警时消息 1/2/3/18 已经能算出相对运动但如果没有船长信息DCPA 的报警门限就很难按目标尺寸放宽或收紧。所以解析层的第一个设计决策是把消息 1/2/3/18 作为高频动态输入把消息 5/24 作为低频静态缓存。消息 5 通常 6 分钟才发一次但首次看见目标后的 6 分钟内必须用消息 1 的数据建立航迹消息 5 到了再补尺寸。见下面的消息类型对应表。消息类型名称典型更新间隔避碰用途1/2/3A类位置报告210 s动态MMSI、经纬度、SOG、COG、HDG5A类静态航次6 min 或变化时船长、船宽、吃水、目的地18B类位置报告330 s小型船舶动态24B类静态630 min小型船舶尺寸如果只做避碰消息 27远程长距离可以暂不接入它面向卫星 AIS位置精度和更新频率都差一到两个量级强行参与 DCPA 计算只会制造噪声。2.2 NMEA 句子结构与校验AIS 接收机输出的是标准 NMEA 语句常见格式像这样!AIVDM,1,1,,B,15M67FC000G?ufbEFepT3n00Sa,0*5C句子以!开头表示 AIVDM 协议后面用逗号分成 7 个字段总句数、当前句号、连续消息序号、频道、数据有效载荷、填充位、校验和。这里的有效载荷15M67FC000G?ufbEFepT3n00Sa 并不是直接可读的坐标而是一串 6 位 ASCII 编码的二进制串需要先转成原始 bit 流再按 AIS 协议的位置字段切分。校验和是用来丢弃坏帧的第一道关卡。它取!之后到*之前的字符 ASCII 码按位异或结果转成两位十六进制与*后的值比对。很多采集系统只做“字符串长度检查”就解码结果把错位 payload 解出来的错误坐标当作真实目标后续避碰计算全部白做。我一般会在解码线程里先做异或校验错误帧直接丢掉并计数这样后续清洗模块的负担会小很多。2.3 六位 ASCII 解码与位域提取AIS 的 6 位编码规则很简单每个可见字符 ASCII 码减 48如果结果大于 40 再减 8得到 063再填充成 6 位二进制。把整段 payload 的位串拼接起来后消息 1 的字段布局是前 6 位消息类型第 8 到 38 位为 MMSI第 50 到 60 位为 SOG第 61 到 89 位为经度第 89 到 116 位为纬度第 116 到 128 位为 COG第 128 到 137 位为船首向 HDG。写一个最小解析函数如下def unpack_ais_payload(payload: str) - str: 把 AIS payload 转成连续 bit 串 bits [] for ch in payload: v ord(ch) - 48 if v 40: v - 8 bits.append(f{v:06b}) return .join(bits) def decode_position_report(payload: str): 解析消息1/2/3返回避碰可用字段 b unpack_ais_payload(payload) if len(b) 143: return None msg_type int(b[0:6], 2) if msg_type not in (1, 2, 3): return None mmsi int(b[8:38], 2) sog_raw int(b[50:60], 2) lon_raw int(b[61:89], 2) lat_raw int(b[89:116], 2) cog_raw int(b[116:128], 2) hdg_raw int(b[128:137], 2) utc_sec int(b[137:143], 2) # 经纬度是二进制补码需要符号扩展 if lon_raw 2**27: lon_raw - 2**28 if lat_raw 2**26: lat_raw - 2**27 return { mmsi: mmsi, lon: lon_raw / 600000.0, lat: lat_raw / 600000.0, sog: sog_raw / 10.0, cog: cog_raw / 10.0, hdg: hdg_raw, utc_sec: utc_sec, }这个函数的核心是位串长度校验和补码转换。经纬度原始值单位是 1/10000 角分除以 600000 才得到十进制度如果是负数raw 的最高位会是 1直接int(bits,2)会得到一个大正数必须按位宽做符号扩展再除。SOG 字段单位是 0.1 节COG 字段单位是 0.1 度。如果将来要接 B 类消息 18位布局基本一致只需要把消息类型判断从(1,2,3)换成(18,)。2.4 无效值与保留值的处理AIS 协议里不少字段有特殊值SOG 的 1023 表示不可用1022 表示实际速度不低于 102.2 节COG 的 3600 表示没有 COGHDG 的 511 表示没有船首向MMSI 全 0 表示测试目标或岸基区域。避碰计算遇到这些值不能直接代入公式否则会出现 0 节速度的目标突然冲到你船艏的假警。需要在解码函数外统一做一层规整sog None if sog_raw in (1022, 1023) else sog_raw / 10.0 cog None if cog_raw 3600 else cog_raw / 10.0真实 AIS 数据里停泊船 SOG 为 0COG 可能是 0 也可能缺失。对停泊目标避碰系统仍要监视因为本船可能碰撞锚泊船但对 COG 缺失的目标无法推测其未来位置只能按静止目标处理。这个语义差别要在后续 DCPA 计算里单独分支不能机械地当成“目标速度 0”直接跳过。3. AIS采集侧架构串口/UDP接入、多接收机融合与时间对齐3.1 接收链路选型与接入方式AIS 接收机从 VHF 天线收到信号后通常会通过 RS232/RS422 串口输出 NMEA 句子有些带以太网口的设备也能直接输出 UDP/TCP。选型上船载采集主机与接收机距离近时优先走串口线缆确定、延迟低不依赖网络设备USB 转串口在工程样机上很方便但在机舱环境下容易受干扰掉线UDP 适合把 AIS 数据广播给多个终端代价是组播丢包和乱序需要接收端自己处理。接入方式典型接口适用场景主要风险串口 RS232/RS422DB9/端子接收机直连采集主机地线电位差需隔离USB 转串口USB-A/C原型验证、便携设备驱动稳定性、供电不足UDP 组播以太网多终端共享同一路 AIS丢包、乱序、五元组不对NTP/PPS 授时网口/串口多接收机融合时钟漂移、PPS脉冲极性对避碰系统而言数据到达顺序不能假定为时间顺序。通信链路缓存、操作系统调度、UDP 缓冲都会导致同一个目标的第 N 帧比第 N-1 帧后到或先到。所以架构里必须有带时间戳的队列而不是直接让解码线程把数据塞给避碰计算线程。3.2 多接收机融合的动机与频道分配常见船舶避碰误区是以为一台 AIS 接收机就能覆盖四周。实际上船体结构、集装箱堆垛、烟囱都会遮挡 VHF 信号尤其是船艏艉方向和大型船舶的盲区最明显。做数据采集系统时我一般建议至少部署两个接收机分别连接左舷和右舷天线或者前后甲板天线。两台接收机都在收 AIS 频道 A161.975 MHz和频道 B162.025 MHz但对于同一目标两台机器收到同一帧的时间差可能达到几十毫秒到几百毫秒。融合规则不要以报文内容里的 UTC 秒字段为准。消息 1 的 UTC 秒精度只有 1 秒无法区分两帧先后正确做法是采集主机对每个接收通道打本地时间戳时间是 Unix 时间戳精度至少到毫秒。如果两台接收机时钟不同步后到的帧反而会被当作先到导致航迹回退。常用方案是采集机启用 NTP并将接收机的 PPS 秒脉冲接进采集程序每秒钟校正一次时钟偏移。3.3 串口读帧线程与帧队列采集程序至少分成三层接收层、解码层、避碰层。接收层负责从串口或 UDP 读原始字节按回车换行切出完整 NMEA 句子后连同读取时间戳放入队列解码层从队列取句子校验、解码、规整再放进干净的航道队列避碰层只消费干净队列。这个分层能避免某个解码耗时不均匀的报文卡住后续计算。一个串口读取的最小实现如下import serial import queue import threading import time raw_queue queue.Queue(maxsize2048) def read_serial_loop(port: str, baudrate: int 38400): ser serial.Serial(port, baudratebaudrate, timeout0.1) buf bytearray() while True: chunk ser.read(256) if not chunk: continue recv_ts time.time() buf.extend(chunk) while b\r\n in buf: line, buf buf.split(b\r\n, 1) text line.decode(ascii, errorsignore) if text.startswith(!AIVDM) or text.startswith(!AIVDO): raw_queue.put((recv_ts, text))这里的recv_ts在读取到串口数据的时刻获取比解码后拿系统时间更接近真实接收时间。队列限制 2048 条是为了防止接收速度高于消费速度时内存上涨一旦队列满put会阻塞串口读线程自然放慢避免丢帧后追着系统跑。需要注意串口读线程里不要做校验和计算和 AIS 位域解析这些 CPU 操作会阻塞下一帧读取遇到连续大批量报文时反而丢失数据。3.4 多接收机去重与首帧选择两个接收机同时收到同一艘船同一时刻的报文时不能重复入轨。最简单做法是用“目标 MMSI 报文类型 UTC 秒字段 原始 payload 短哈希”做去重键两个接收机收到同一帧payload 完全一致UTC 秒也一样只保留先到达的一帧。如果两个接收机收到同一目标相邻两帧payload 可能不同就不去重交给后续航迹关联做时间排序。这里最容易踩坑的是把“同一秒内不同位置”误判为重复帧所以去重键里必须包含 payload 本身的哈希而不是只比较 MMSI 和时间戳。4. 数据清洗与航迹关联去掉岸台和漂移点才能谈避碰4.1 五类脏数据真实 AIS 链路里解码成功并不代表坐标可信。避碰数据采集系统必须在上层计算之前把无效、误导、重复的数据过滤掉。按我处理船载数据的经验最常见的五类脏数据按优先级排列如下表脏数据类型典型特征处理策略岸台/虚拟目标MMSI 以 00 开头位置固定直接丢弃或单独建层重复帧同一 MMSI、同一时戳、同一 payload按 payload 哈希去重位置漂移两帧间距离超过航速和间隔上限丢弃新帧保留旧轨迹无效字段SOG1023、COG3600、经纬度超界置空并标记不可算陈旧目标动态报文超过 3 分钟未更新从实时避碰列表移出其中最容易忽略的是岸台。AIS 基站 MMSI 通常形如00MIDxxxx它们也会周期性发送定位报告位置精度很高但它们不是船进入避碰计算后会产生大量虚假 DCPA。过滤器里应该维护一份“本船可见的 AIS 岸台 MMSI 前缀表”典型前缀就是00开头但不要只查前两位RN 航标的99前缀同样需要剥离。4.2 位置漂移过滤与速度合理性判断单条 GPS 故障、AIS 收发机老化的船会广播出跳跃几十海里的坐标。这类坏点的经纬度本身合法但和目标历史航迹完全对不上。常规做法是维护每个目标的上一可信位置用当前帧与前帧的时间差计算等效速度超过船型上限就丢弃。针对避碰场景上限不能直接写 60 节因为高速客船、水翼船确实能跑到 45 节以上留足余量后再判断import math from datetime import datetime def haversine_nm(lat1: float, lon1: float, lat2: float, lon2: float) - float: 返回海里距离 R 3440.07 dlat math.radians(lat2 - lat1) dlon math.radians(lon2 - lon1) a math.sin(dlat / 2) ** 2 math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2 return R * 2 * math.asin(math.sqrt(a)) last_good_pos {} MAX_SPEED_KNOTS 60.0 def is_position_plausible(mmsi: int, lat: float, lon: float, ts: float) - bool: if mmsi not in last_good_pos: last_good_pos[mmsi] (lat, lon, ts) return True prev_lat, prev_lon, prev_ts last_good_pos[mmsi] dt_hours (ts - prev_ts) / 3600.0 if dt_hours 0: return False dist haversine_nm(prev_lat, prev_lon, lat, lon) speed dist / dt_hours if speed MAX_SPEED_KNOTS: return False last_good_pos[mmsi] (lat, lon, ts) return True过滤逻辑的关键在于last_good_pos只更新通过校验的点坏点不会污染参考位置。如果连续多个坏点到来旧参考点的prev_ts会越来越大等效速度反而可能变小所以还要加一个时间上限两帧间隔超过 30 分钟就重新初始化参考点不做速度比较。这样既能过滤 GPS 跳变又不会把长时间失联后重新出现的正常目标误删。4.3 MMSI 不是航迹的唯一键避碰航迹关联常犯的错误是把 MMSI 当成唯一标识。实际上 AIS MMSI 可以被船主申请修改部分老旧设备还会因为配置错误广播其他船的 MMSI甚至同一艘船在两个消息类型中的 MMSI 相同但船长不一致。稳妥的关联键是“MMSI 船长分段 船型大类”从消息 5 或消息 24 缓存中获得船长后把船长四舍五入到 10 米等级再和 MMSI 组合成一个字符串作为航迹 ID。如果一艘船的静态信息从 100 米突然变成 300 米即使 MMSI 没变也应该开一条新航迹避免历史运动矢量被污染。4.4 时间序存储与查询数据采集系统不只是在线避碰还要留出事后分析能力。我建议把清洗后的 AIS 数据写入时序数据库如果是轻量部署直接用 PostgreSQL TimescaleDB 就行。建表时把时间戳和 MMSI 作为复合主键避免相同时间戳重复写入CREATE TABLE ais_reports ( ts TIMESTAMPTZ NOT NULL, mmsi INT NOT NULL, lat DOUBLE PRECISION NOT NULL, lon DOUBLE PRECISION NOT NULL, sog DOUBLE PRECISION, cog DOUBLE PRECISION, hdg DOUBLE PRECISION, nav_status SMALLINT, PRIMARY KEY (ts, mmsi) ); CREATE INDEX ON ais_reports (mmsi, ts DESC);ts用采集主机的统一时间而不是 AIS 报文字段里的 UTC 秒后者只有 1 秒精度且可能缺失。对避碰回放来说查询最近一段时间的航道数据时(mmsi, ts DESC)索引要比单独的(ts)索引好用得多。存储层不要直接存原始 NMEA 句子那是日志该做的事表结构应当偏平字段语义与解码后的避碰计算一一对应。5. 避碰风险判断DCPA/TCPA的工程实现与边界处理5.1 经纬度投影成平面坐标DCPA/TCPA 计算必须在平面直角坐标系里做直接拿经纬度做向量运算是错误的。本船与目标船的距离通常只有几海里使用一个简单的等距圆柱投影即可以本船当前位置为原点把目标位置投影到东向和北向的海里坐标。投影函数如下import math def local_enu_nm(lat0: float, lon0: float, lat: float, lon: float): 以 (lat0, lon0) 为原点把 (lat, lon) 转成东向、北向海里坐标 R 3440.07 dlat math.radians(lat - lat0) dlon math.radians(lon - lon0) mean_lat math.radians((lat0 lat) / 2) x_nm dlon * R * math.cos(mean_lat) y_nm dlat * R return x_nm, y_nm北向y_nm直接用纬度差乘地球半径东向x_nm要乘平均纬度的余弦。在 5 海里范围内这个近似的误差小于 1%超过 20 海里建议改用 UTM 投影避免东西向偏差变大。投影坐标系的量纲统一为海里速度的单位要对应使用节这样算出来的 TCPA 单位是小时后续换算分钟就很自然。5.2 相对速度与最近会遇点DCPA 是本船与目标船在相对运动方向上的最近距离TCPA 是到达这个最近距离还需要的时间。设目标船相对本船的平面位置矢量是(rx, ry)相对速度矢量是(vx_tgt - vx_own, vy_tgt - vy_own)最近会遇时间按下式计算tcpa -(rx * vrx ry * vry) / (vrx^2 vry^2)当tcpa为正值时目标船正在接近本船为负值时目标船正在远离此时避碰系统应当把风险置为安全。把tcpa代回相对位置表达式中就能得到最近会遇点与当前本船位置的距离就是 DCPA。完整的工程实现如下def compute_dcpa_tcpa( lat0: float, lon0: float, cog0: float, sog0: float, lat1: float, lon1: float, cog1: float, sog1: float ): 返回 (dcpa_nm, tcpa_min)sog 为 None 时代表不可用 if sog0 is None or sog1 is None or cog0 is None or cog1 is None: return None, None rx, ry local_enu_nm(lat0, lon0, lat1, lon1) vx0 sog0 * math.sin(math.radians(cog0)) vy0 sog0 * math.cos(math.radians(cog0)) vx1 sog1 * math.sin(math.radians(cog1)) vy1 sog1 * math.cos(math.radians(cog1)) vrx vx1 - vx0 vry vy1 - vy0 v2 vrx * vrx vry * vry if v2 1e-9: # 两船相对静止DCPA 就是当前距离TCPA 无意义 return math.hypot(rx, ry), None tcpa_hours -(rx * vrx ry * vry) / v2 if tcpa_hours 0: return math.hypot(rx, ry), 0.0 cpa_x rx tcpa_hours * vrx cpa_y ry tcpa_hours * vry dcpa_nm math.hypot(cpa_x, cpa_y) return dcpa_nm, tcpa_hours * 60.0注意sog为 0 时cog仍可能有效或缺失。锚泊船的 COG 往往是 0这时相对速度等于本船速度的反向计算没有问题但如果sog为 0 且cog缺失应按静止目标单独处理。v2 1e-9的情况出现在两船同向同速并行此时不存在相撞趋势DCPA 取当前距离TCPA 置空避免除以零。5.3 目标消失与陈旧数据AIS 动态报文的更新间隔由航速和航向变化决定船速低于 3 节时可能长达 3 分钟才发一帧。避碰计算不能要求每个目标每 2 秒来一次否则大量正常目标会被打上“丢失”标。我一般把目标分为三个状态活跃目标最近 30 秒内有报告、观望目标30 秒到 3 分钟、失效目标超过 3 分钟。只有活跃目标参与 DCPA/TCPA 实时计算观望目标只显示位置失效目标从避碰列表中移除。对观望目标如果直接拿上一次的位置做 DCPATCPA 会随着时间自然变大误报警反而减少但如果目标正在加速接近陈旧坐标会严重低估风险。因此在避碰系统里观望目标不参与预警门限判断只是保留轨迹用于事后重构。这个取舍比强行外推目标位置更安全因为外推误差在密集船区会制造大量假目标。5.4 预警分级门限DCPA 和 TCPA 必须同时满足才会报警。只看 DCPA 会造成远距离高速目标长期报警只看 TCPA 会造成近距离慢速目标被忽略。常用的分级门限如下但具体值要根据本船尺度和水域重新标定风险等级DCPATCPA人机交互安全 1.0 nm任意仅列表显示提示0.5 ~ 1.0 nm 20 min黄色高亮警告0.3 ~ 0.5 nm 12 min声音提醒严重 0.3 nm 6 min声光报警这个表里的 DCPA 用的是海里单位。近岸港口和水文复杂区域1 海里的安全门限太宽松报警会淹掉驾驶台但 0.5 海里门限在宽阔水域又可能偏晚。所以实际系统不能只设一组静态值需要把水域类型作为预警参数的一部分开阔海、近岸、狭水道分别挂不同的 DCPA/TCPA 配置。6. 狭水道动态阈值AIS目标前视扇区与DCPA档位狭水道和高密度锚地是 AIS 避碰误报的重灾区。本船两侧都是目标船任何一个从船艉或正横方向慢慢靠近的船都会触发 DCPA 小于 1 海里但真正需要提醒驾驶台的是前方航向上可能发生交会的目标。因此最后一层优化必须加“前视扇区”过滤只对位于本船船艏方向左右各 30 度以内的目标做预警级别计算。def in_forward_sector(target_bearing_deg: float, cog_own: float, sector_deg: float 60.0) - bool: target_bearing_deg 是目标相对本船的真方位cog_own 是本船对地航向 diff (target_bearing_deg - cog_own 180) % 360 - 180 return abs(diff) sector_deg / 2这里的真方位度数是 0 到 359本船 COG 也是绝对角度两个角度相减后映射到 [-180, 180)落在扇区内才参与报警。扇区大小建议与航速联动本船航速大于 20 节时收窄到 40 度低速进出港时放宽到 90 度因为转向灵活时正横方向的目标也可能进入运动轨迹。动态阈值方面不推荐只改 DCPA 一个数。更稳妥的是做成“水域 航速”两维索引的配置表开阔海域基础 DCPA 为 1.0 海里近岸为 0.6 海里狭水道为 0.3 海里同一个水域下本船速度每增加 10 节TCPA 门限增加 2 分钟。这样锚地里的静止船不会因为 TCPA 无穷大而反复报警高速航行时的接近趋势又能被提前捕捉。最后一层验证技巧是任何报警产生时打印目标 MMSI、相对方位、DCPA、TCPA 四个值并保留最近 30 秒的原始 AIS 帧。遇到误报先看相对方位是否落在前视扇区外再看 DCPA 是当前距离还是未来最近距离最后核对目标是否处于观望状态。大多数误报在三个字段里就能定位到原因不需要改计算公式。本文还有配套的精品资源点击获取