简介:一份基于AIS的船舶避碰数据采集系统设计PDF,面向航海电子、船舶通信、嵌入式系统方向的师生与工程技术人员,可服务于课程设计、毕业设计、船舶设备开发及通航安全研究等场景,帮助读者建立从船载传感信息接入到VHF无线收发、再到避碰预警的完整设计思路。内容依据实际系统方案展开,详细介绍了AIS的组成与工作过程、ITU-R M.1371等通信协议,以及DGPS差分定位数据、罗经航向数据、计程仪速度数据的滤波处理;硬件部分覆盖FSK鉴频与解码电路、C5402 DSP主控模块、RS-485接口和LCD显示电路,软件部分则说明了数据处理、错误检测和主要程序流程。压缩包内共1个PDF文件,大小1.96MB,结构规整,含摘要、目录和章节,适合直接阅读打印;读者可按章节逐步理解天线信号接收、信号解码、信息显示与安全短消息收发的要点。该资源已有207人学习下载,对于希望快速掌握船舶避碰数据采集系统整体方案和关键模块设计的读者,具备较好的参考与借鉴价值。
1. 基于AIS的船舶避碰数据采集系统要解决的核心问题
船与船之间判断会不会撞,不能光靠肉眼,也不能光靠雷达。雷达给的是回波轮廓,分辨不出对方船名、航速和转向意图;AIS(船舶自动识别系统)把这些信息以数字化明文广播出来,行业里做避碰辅助决策,首先拿到的数据源就是AIS。这套采集系统要解决的问题很直接:把往返播发的AIS报文完整收下来,解析成可计算的动态与静态要素,再统一时间基准和坐标框架,为后续CPA/TCPA危险判断提供可靠输入。
在VTS和船载避碰告警的实际工程里,AIS数据采集通常不是买一台接收机接上串口就能投产的。VHF信道的时隙冲突、报文丢失、波特率不匹配、NMEA语句校验和错误、双信道重复消息,这些都会直接影响避碰判断。这篇文章面向要实现或维护这类系统的船舶电子、导航信息化从业者,从协议解析、接收链路、存储设计一路讲到避碰参数计算与验证方法,给出可直接落地的技术路径。
2. AIS报文协议与NMEA 0183解析:避碰要读哪些字段
2.1 与避碰相关的AIS消息类型和关键字段
AIS工作在VHF频段的87B(161.975 MHz)与88B(162.025 MHz)两个信道上,采用GMSK调制和9600 bps的HDLC帧结构。Class A船舶使用SOTDMA自组织时分多址,Class B船舶使用CSTDMA载波侦听时分多址,两者都在由2250个时隙组成的一分钟周期内抢占发送机会。做数据采集的人不需要把整个SOTDMA状态机实现一遍,但一定要知道哪几条消息类型直接参与避碰计算。
- 类型1、2、3:Class A船位报告,包含本船经纬度、对地航速SOG、对地航向COG、真航向HDG、转向率ROT和导航状态。船舶在航行状态下大约每2到10秒发送一次,锚泊时最长间隔3分钟。
- 类型5:Class A静态与航次消息,包含船名、呼号、IMO编号、船长船宽、船舶类型、吃水、目的港和ETA,斜行每6分钟发送一次。
- 类型18:Class B船位报告,字段比Class A少,没有ROT和导航状态等,发送间隔通常为30秒。
- 类型24:Class B静态消息,分为A、B两部分,A部分是船名,B部分是船型、尺寸和天线位置,用于替代类型5。
避碰计算的实际输入是类型1、2、3和18中的动态度字段,船型尺寸则来自类型5和24。如果只统计轨迹,忽略静态消息,会追不上目标船的实际轮廓,后续计算安全会遇距离时也没有依据。
2.2 AIVDM报文位结构与6-bit ASCII解码
AIS链路层数据在网络层以NMEA 0183句子传输,最常见前缀是!AIVDM。一条完整的句子如下:
!AIVDM,1,1,,A,15M67FC000G?ufbE`FepT@3n00Sa,0*5C字段之间用逗号分隔:第一个1是消息片段总数,第二个1是当前片段序号,紧接着的是连续消息ID和无线信道代码,逗号后面那串15M67...才是真正的payload,末尾的0是填充位数量,5C是异或校验和。AIS payload使用6-bit ASCII编码,每个字符表示6个比特,因此一条168 bit的位置报告刚好由28个可见字符组成。
6-bit ASCII的解码规则并不复杂:把字符的ASCII码减48,如果结果大于40再减8,就得到原始6位值。下面给出可以直接使用的解码函数。
def bits_from_payload(payload: str) -> str: """将AIVDM payload还原为连续的位串""" bits = "" for ch in payload: v = ord(ch) - 48 if v > 40: # ASCII 0x60-0x77 的字符 v -= 8 bits += format(v, "06b") return bits def int_from_bits(bits: str, start: int, length: int) -> int: """从位串中取无符号整数""" return int(bits[start:start + length], 2) def signed_from_bits(bits: str, start: int, length: int) -> int: """从位串中取有符号整数,按二进制补码解释""" v = int(bits[start:start + length], 2) if v & (1 << (length - 1)): v -= 1 << length return v这里payload是NMEA句子中!AIVDM,1,1,,A,后面的那一段。6-bit ASCII编码表有40个常规字符和40个偏移字符,v > 40的判断专门处理0x60及以上的字符,比如反引号`和小写字母。补码解释用于处理经度、纬度、ROT这些有符号字段,避免把负值当成巨大的正整数。
消息类型1、2、3的重要位段定义如下,起始位从0开始计数:
| 字段 | 起始位 | 长度 | 计算说明 |
|---|---|---|---|
| 消息类型 | 0 | 6 | 1、2、3表示Class A船位 |
| 重复指示 | 6 | 2 | 0为原始发射 |
| MMSI | 8 | 30 | 9位海上移动业务识别码 |
| 导航状态 | 38 | 4 | 0在航、1锚泊、2失控等 |
| 转向率ROT | 42 | 8 | 补码,-128为不可用 |
| 对地航速SOG | 50 | 10 | 除以10得到节 |
| 位置精度 | 60 | 1 | 0低精度,1高精度 |
| 经度 | 61 | 28 | 除以600000得到度,东经正 |
| 纬度 | 89 | 27 | 除以600000得到度,北纬正 |
| 航迹向COG | 116 | 12 | 除以10得到度 |
| 真航向HDG | 128 | 9 | 整数度,511为不可用 |
| UTC秒 | 137 | 6 | 60到63为无效 |
解析函数只需要把位段按表格切出来。注意经纬度的单位是1/10000角分,即每一度对应600000个单位,而不是常见的1e7。这个单位换算容易写错,建议把600000定义为模块级常量。
def parse_position_report(payload: str) -> dict: """解析Class A位置报告,只保留避碰计算需要的字段""" b = bits_from_payload(payload) msg_type = int_from_bits(b, 0, 6) if msg_type not in (1, 2, 3): raise ValueError(f"不是Class A位置报告: type={msg_type}") rot_signed = signed_from_bits(b, 42, 8) sog_raw = int_from_bits(b, 50, 10) lon_raw = signed_from_bits(b, 61, 28) lat_raw = signed_from_bits(b, 89, 27) return { "mmsi": int_from_bits(b, 8, 30), "nav_status": int_from_bits(b, 38, 4), "rot_deg_min": None if rot_signed == -128 else rot_signed / 4.733, "sog_knots": None if sog_raw >= 1023 else sog_raw / 10.0, "lon": lon_raw / 600000.0, "lat": lat_raw / 600000.0, "cog_deg": int_from_bits(b, 116, 128) / 10.0, "hdg_deg": int_from_bits(b, 128, 137), "utc_sec": int_from_bits(b, 137, 143), }参数说明:sog_raw >= 1023视为无效,因为SOG的10位全为1表示无可用数据,1022表示“102.2节或更高”,实际中不会出现在船舶上;ROT的-128表示设备没有给出转向信息,其余值除以4.733得到度/分钟;utc_sec取自消息内的时间戳,但它描述的是消息生成时刻的秒字段,不是接收时刻,采集端必须额外记录自己的接收时间,避碰计算统一以接收端时间为准。
2.3 速度、转向率与位置精度对避碰的影响
SOG和COG直接决定对方船的相对运动矢量,ROT则用来预测目标船未来几秒的航向变化趋势。在追越和交叉相遇场景中,ROT比瞬时航向更能说明对方是否在转向让船。ROT的量化间隔是4.733度/分钟,低速转向时这个分辨率偏低,可能超过半分钟才能体现出一个航向变化;在计算TCPA时不直接把ROT并入线性外推,而是把它当作短时预警的辅助判据。
位置精度字段需要单独处理。AIS的定位源多数是GPS,部分船舶接入了差分信号。位置精度为1表示高精度差分定位,定位误差在10米以内;为0表示普通定位,误差可能达到几十米。两条船在小倾角对遇时,DCPA本身就只有几百米,10米和50米的定位误差足以改变危险等级。建议在数据入库时保留这个字段,并在CPA计算中给不同精度来源的目标船设置额外安全裕度。
3. 采集系统整体链路:天线、接收机、网关与数据落库
3.1 接收链路:从VHF天线到串口服务器
一个标准的岸基AIS接收站包括VHF天线、低噪声放大模块(通常集成在接收机内部)、AIS接收机、串口服务器和采集服务器。AIS接收机解调两个VHF信道后输出NMEA 0183句子,标准波特率是38400 bps,数据格式为8位无校验、1位停止位。常见的工业串口服务器把RS-422或RS-232转为TCP,采集端通过Socket连接,避免长距离串口线带来的电平损耗和干扰。
天线安装高度和位置直接制约接收距离。AIS信号在海上的传播接近视距,天线高度每提高10米,理论视距增加约12.8%。接收系统的工程重点不是追求最远距离,而是保证覆盖区内的时隙冲突可控。在船舶密度大的港口,两个相邻时隙的报文会互相叠加,硬件接收机无法解码,表现为接收端消息速率周期性下降。可以架设两套天线分别接两台接收机,一台偏向航道入口,一台覆盖锚地,后端按MMSI和时间戳做去重。
高通公司的AIS接收器件方案在行业OEM设备中确实有出现,但那是芯片级选型,并不影响上层数据格式。做采集系统的人只需要按NMEA标准解析上层句子,不必关心接收机内部的射频前端处理流程。
3.2 数据接入层的并发处理与乱序
串口服务器输出的是字节流,应用层要自行完成NMEA分帧。AIVDM句子以!开头,以\r\n结尾,句子最大长度为82字符,多片段消息还需要按!AIVDM,2,2等形式拼接。下面的代码给出一个最小可用的串口读取循环。
import serial def read_nmea_stream(port: str, baud: int = 38400): """读取串口并逐条产出NMEA句子""" with serial.Serial(port, baud, timeout=0.2) as ser: buffer = b"" while True: buffer += ser.read(4096) while b"\r\n" in buffer: line, buffer = buffer.split(b"\r\n", 1) text = line.decode("ascii", errors="ignore") if text.startswith("!AIVDM") or text.startswith("!AIVDO"): yield text这里用split(b"\r\n", 1)按行切分,每次只处理一个完整句子,避免了字节碎片的拼接问题。timeout=0.2保证串口在空闲时不会阻塞主循环,允许上层ready做心跳监控。如果使用TCP方式接入串口服务器,把serial.Serial换成socket.makefile即可,解析逻辑完全相同。
多接收机的情况下会出现两条问题:同一艘船的两个信道消息几乎同时到达,或者两台接收机对同一条消息重复上报。去重逻辑不能只按MMSI去重,因为同一MMSI的船位是周期性变化的。常见的做法是用(mmsi, utc_sec, lon, lat)作为去重键,或者比较相邻两条同MMSI消息的接收时间差,小于500毫秒的视为重复。消息时间戳的乱序也要处理,接收端收到消息的本地时间可能早于前一条,写入数据库前需要按MMSI缓存最近一条的位置和时间,避免把历史消息当成最新船位。
3.3 存储选型:时序库与关系表的取舍
AIS的数据量和更新特征很明确:繁忙港口一天可能收到数千万条位置报告,每条解析后包含MMSI、时间、经纬度、航速、航向等十来个字段。避碰系统多数按最近10到30分钟的窗口做实时计算,同时需要保留几个月到一年的轨迹数据用于事后复盘。这种情况下,单一数据库很难同时满足高写入和时空查询需求。
下面给出三种常见方案的对比,按项目的技术栈取舍即可:
| 存储方案 | 实时写入能力 | 轨迹回放查询 | 运维成本 |
|---|---|---|---|
| PostgreSQL + TimescaleDB | 每秒数千条 | 超表按时间分区,查询方便 | 需要维护扩展和分区策略 |
| InfluxDB | 每秒上万点 | 时序查询方便,但轨迹回放不够直观 | 老版本集群复杂 |
| MySQL 普通表按日分区 | 每秒几百条 | 索引合适时可接受 | 结构简单,适合小站 |
小规模岸基站点直接用MySQL按日分区就够用,写入和查询都简单;需要做长时间范围历史分析和实时流计算的项目,直接上TimescaleDB。无论选哪种,时间字段必须用UTC,不能用当地时间。AIS船位是在全球范围内交换的,一旦混入时区偏移,CPA计算会差出一整个避碰决策周期。
4. 避碰数据融合计算:CPA/TCPA与危险船判定
4.1 把经纬度轨迹转为相对运动参数
AIS给出的经纬度是球面坐标,而避碰判断关心的是平面上两艘船的相对距离和接近速率。在开阔水域的几海里范围内,直接用等距圆柱近似即可满足工程精度:把本船位置作为原点,目标船投影到以米为单位的ENU平面坐标上。每度纬度对应的地面距离约为111319.9米,每度经度对应的距离需要乘以本船所在纬度的余弦值。
这种投影在50海里范围内误差可以忽略,超过100海里后误差明显增大,但AIS避碰通常不关心那么远的距离。远距离目标船进入警戒圈之前,先按经纬度粗筛,距离小于20海里再用投影坐标做精确计算。
4.2 CPA/TCPA的完整计算代码
最近会遇距离DCPA和最近会遇时间TCPA是避碰判断的核心输出。计算思路是:目标船相对本船的位置矢量减去本船速度矢量,得到相对速度;位置矢量在相对速度方向上的投影除以相对速度大小,取负号就得到最近会遇时间;再把该时间代回相对位置公式,得到最近点距离。
import math from dataclasses import dataclass NM_TO_M = 1852.0 @dataclass class VesselState: lat: float lon: float course: float # 对地航向,度 speed: float # 对地航速,节 def project_to_enu(ref_lat: float, ref_lon: float, lat: float, lon: float): """以参考点为原点的等距圆柱投影,返回x(东)、y(北)米""" lat0_r = math.radians(ref_lat) x = (lon - ref_lon) * 111319.9 * math.cos(lat0_r) y = (lat - ref_lat) * 111319.9 return x, y def course_speed_to_velocity(course_deg: float, speed_kn: float): """航向与航速转ENU速度分量,单位米/秒""" r = math.radians(course_deg) v = speed_kn * 0.514444 return v * math.sin(r), v * math.cos(r) def calc_cpa_tcpa(own: VesselState, target: VesselState): """返回(DCPA米, TCPA秒)""" tx, ty = project_to_enu(own.lat, own.lon, target.lat, target.lon) dx0, dy0 = tx, ty ovx, ovy = course_speed_to_velocity(own.course, own.speed) tvx, tvy = course_speed_to_velocity(target.course, target.speed) vrx, vry = tvx - ovx, tvy - ovy v2 = vrx * vrx + vry * vry if v2 < 1e-9: return math.hypot(dx0, dy0), 0.0 tcpa = -(dx0 * vrx + dy0 * vry) / v2 if tcpa < 0: tcpa = 0.0 dcpa = math.hypot(dx0 + vrx * tcpa, dy0 + vry * tcpa) return dcpa, tcpa参数说明:own和target的course统一用对地航向COG,speed用对地航速SOG,单位分别是度和节。project_to_enu把目标船投影到以本船为原点的平面坐标,本船自身坐标始终是(0,0),所以原始相对位置直接取目标船投影值。tcpa如果是负值,说明目标船正在远离,此时把tcpa截断为0,DCPA退化为当前距离。v2接近0表示两船同速同向,此时相对位置不再变化,直接返回当前距离。
阈值的设定没有统一标准,下面是业内常见的经验值:
| 航行场景 | DCPA阈值(海里) | TCPA阈值(分钟) |
|---|---|---|
| 开阔洋面 | 1.0 | 30 |
| 近岸航路 / 分道通航 | 0.5 | 15 |
| 港区 / 锚地附近 | 0.3 | 10 |
| 狭窄水道 / 渡轮区 | 0.2 | 5 |
阈值需要按船舶类型做分级。大型油轮和集装箱船的操纵性差,同样的DCPA和TCPA下应该更早告警;高速客船虽然反应快,但接近速度快,TCPA阈值要适当放大。实际部署时把阈值表做成可配置项,由航行管理人员按港口交通流调整。
4.3 危险船排序与位置修正
计算得到全部目标船的DCPA和TCPA之后,不能只按DCPA从小到大排序。一艘DCPA很小但TCPA为负的船正在远离,不构成当前威胁;一艘DCPA为0.8海里但TCPA只有6分钟的船,反而需要立即关注。合理的排序方式是综合两个维度:先按TCPA筛选掉远离目标,再按DCPA/TCPA组合值排序,或者直接用带权评分函数。
AIS位置还有两个容易忽略的偏差。第一,船位报告的是GNSS天线位置,不是船舶几何中心。一艘180米长的集装箱船,天线可能装在驾驶台顶部,距离船首七八十米。计算DCPA时如果不加修正,两船实际上已经擦碰,计算结果却显示有0.1海里的富余。修正方法是从类型5或24中读取船舶尺寸和天线位置偏移,把目标船位置沿其航向方向平移到船首或船体边缘。第二,AIS更新间隔在低速或锚泊状态下会拉长到3分钟,在这段空窗期内,船位和航向都是过时的。对TCPA小于更新周期的目标船,需要用当前航速航向做外推,同时把置信度标记降低,避免告警界面出现“幻影碰撞”。
5. 采集系统的自测与工程落地技巧
5.1 用信号发生器与回放做端到端验证
没有条件上实船的团队,可以用带AIS回放功能的北斗/GNSS模拟器或开源AIS消息生成器,把标准NMEA句子注入接收机的串口或直接注入数据接入层。验证时先确认解析结果和报文字段一一对应,再人为加入乱序、重复和缺失字段。工作的顺序是:先验证单条消息的解析正确性,再验证两条消息之间的时间推进,最后压测数据接入层在每秒几百条消息下的处理延迟。
回放数据建议直接用VTS系统导出的历史日志。把日志文件按时间戳重放,数据接入层不做任何特殊处理,解析后与日志里的GPS基准轨迹对比,统计位置误差和漏报率。这里有一个实用校验技巧:选择一艘位置连续的目标船,计算相邻两帧之间的航速反推值,与报文里的SOG比较,误差超过0.5节就说明时间戳或解析位段有问题。
5.2 数据质量监控的实用指标
采集系统上线后需要持续监控下面几项指标:每秒消息数、每分钟活跃MMSI数、NMEA校验和错误率、单船更新间隔超过阈值的比例、连续位置跳跃的数量。校验和错误率高说明射频链路有干扰或串口连接接触不良;单船更新间隔异常说明接收机丢失了该船的大量时隙,表面上是数据采集问题,实际上可能已经影响避碰决策。质量监控代码可以从下面这个简化的位置质量检查入手。
def check_position_quality(row, last_row, max_speed_kn=60): """检查单条船位数据的合理性,返回告警标记""" if abs(row["lat"]) > 90 or abs(row["lon"]) > 180: return "invalid_coord" if last_row is None: return "ok" dist_m = haversine_meters(row, last_row) dt_s = row["rcv_ts"] - last_row["rcv_ts"] if dt_s > 0 and dist_m / dt_s > max_speed_kn * 0.514444: return "jump" return "ok"max_speed_kn按船型设置,普通商船取60节足够覆盖高速客船,超过这个速度的位置跳跃大概率是解析错误或消息串台。位置跳跃发现后不要直接丢弃,还要检查该MMSI下一条消息是否恢复正常,连续多次跳跃才需要进入黑名单。这样既避免单条坏数据触发误告警,又能及时发现接收信道持续冲突的问题。
5.3 高频数据下的去重与缓存策略
实时避碰计算窗口通常只有10到30分钟,内存缓存可以显著降低数据库压力。用Redis或进程内哈希表维护每个MMSI的最新船位,只有当位置变化超过设定阈值时才写库,静止船舶每3分钟写一条即可。同一MMSI在双信道同时出现在缓存中时,以接收到时间较晚的一条为准,不保留两条记录。
数据接入层处理多片段AIS消息时,要设置一个拼接超时,比如3秒内未收齐所有片段就丢弃。AIS船位报告都是单片段消息,多片段主要用于类型5和24静态消息,这类消息晚几秒不影响避碰判断。给每种消息类型设置独立的处理优先级:位置报告最高,静态消息次之,基站和航标消息直接透传进原始日志即可,避免接收机缓存堆满造成新数据丢包。
本文还有配套的精品资源,点击获取