通信协议


本节旨在记录工作中遇到的一些通信协议。

CAN 总线原理与通信机制

CAN(Controller Area Network)总线是一种专为汽车电子系统设计的串行通信协议,主要用于不同 ECU(电子控制单元)之间的数据传输。其最大特点是具有高可靠性、实时性强、抗干扰能力强,现已广泛应用于汽车、工业自动化、医疗设备等领域。

物理结构

CAN 总线通常由两根双绞线构成,分别为:

  • CAN_H(高电平线)
  • CAN_L(低电平线)

它们以差分信号的形式工作,通过比较两者之间的电压差来进行数据传输,差分电压为0,称作隐性电平,表示逻辑1,差分电压为2V,称作显性电平,逻辑表达为0。

   状态             CAN_H      CAN_L     电压差    逻辑意义     电平类型
──────────────────────────────────────────────────────────────────────
显性电平(Dominant)   ≈3.5V     ≈1.5V      ≈2V       逻辑 0     主导电平
隐性电平(Recessive)  ≈2.5V     ≈2.5V      ≈0V       逻辑 1     被动电平

这种差分传输方式大大提高了抗干扰能力,即使在恶劣电磁环境中也能稳定工作。

节点结构

一个连接到 CAN 总线的设备都被视为一个节点,每个节点至少包含以下两部分:

  1. CAN 控制器:负责协议层的处理,如帧格式解析、仲裁、错误处理;
  2. CAN 收发器:负责物理层信号的收发与电平转换。

发送过程:

CPU → CAN控制器 → 逻辑电平信号 → CAN收发器 → 差分电平 → CAN总线

接收过程:

CAN总线差分电平 → CAN收发器 → 逻辑电平 → CAN控制器 → CPU

多节点通信与仲裁机制

CAN 总线支持多主机制,即所有节点都有发送权限。 那么当有多个节点时,如何确保数据不会混乱?

  1. 多主工作方式:最先向总线发送信息的节点获得总线的发送权。

  2. 非破坏性仲裁机制: 通过报文 ID 的优先级来决定哪个节点获得总线控制权。 具体流程:所有节点监听总线; 从SOF位开始,按位计算,显性电平会覆盖隐性点平,被覆盖的节点退出竞争,转为接收节点。

帧结构

CAN2.0B包括两种消息帧格式:标准帧和扩展帧。扩展帧格式如图。

根据用途可分为四种:

  • 数据帧:传输有效数据内容(最多 8 字节)。
  • 远程帧:请求远程节点发送特定 ID 的数据帧。
  • 错误帧:检测到错误时发出,用于错误恢复。
  • 过载帧:用于请求延迟下一个帧的发送(暂缓)。

J1939 协议

SAE J1939 是基于 CAN2.0B(扩展帧格式)的高层通信协议,专为商用车、工程机械、农用设备等车辆领域设计。它在 CAN 总线基础上,扩展了帧结构,并定义了一套固定的 PGN(Parameter Group Number,参数组编号)SPN(Suspect Parameter Number,疑似参数编号) 编码体系,用于组织和传输各类车辆参数与诊断信息。

消息帧格式

J1939 只适用于扩展帧格式,其标识符(Identifier)被严格划分为以下几部分:

  • 优先级 (Priority):3 位,数值越小,优先级越高(0~7)
  • 保留位 (Reserved) :1 位,固定为 0,保留。
  • 数据页 (Data Page):1 位,表示页码(扩展 PGN 用)
  • PDU 格式 (PDU Format, PF):8 位,确定是点对点还是广播。
  • PDU 特定 (PDU Specific, PS):8 位,若为点对点,表示目标地址;否则为 PGN 的一部分。
  • 源地址 (Source Address, SA):8 位,当前帧的发送节点地址(0~253)
  • 数据场 (Data Field):0~8 字节,实际传输的参数(如速度、电压等)

参数群编号 PGN 与 SPN

PGN 是 J1939 报文的核心,用于定义一类参数组(Parameter Group),描述“这是一类什么类型的报文”。PGN 是由标识符中的以下字段拼接而成的:

PGN = DP(1位)+ PF(8位)+ PS(8位,仅限广播帧)
  • 如果 PF ≥ 240(全局广播),PGN = DP + PF + PS(完整 18 位)。

  • 如果 PF < 240(点对点),PGN = DP + PF(PS 被视为目标地址,不算入 PGN)。

PGN 在表示时通常以十进制表示,如:

  • 61444 → 发动机状态(EEC1)
  • 65262 → 冷却液温度、油温等。

SPN 是 PGN 中的“子参数”,用于定义 PGN 报文中每个字段(字节或字节组合)的具体含义。例如:

PGN 报文名称 包含的 SPN(部分示例)
61444 EEC1(发动机控制器1) SPN 190: 发动机转速,SPN 512: 发动机-扭矩百分比
65262 ET1(发动机温度 1) SPN 110: 冷却液温度,SPN 174: 燃油温度

具体 PGN 与 SPN 参数含义可以参考:https://www.canfd.net/j1939pgn.html

应用示例

此示例主在通过设备连接车辆 CAN 总线,获取车辆参数信息。默认设备连接口为 can0。

首先测试连接、设置波特率。

candump can0 # 观察can0有没有数据传过来
ip -details link show can0	# 查看can0口波特率
ip link set can0 down	# 关闭can0
ip link set can0 up type can bitrate 250000		# 设置波特率为250kbps
ip link set can0 up		# 开启can0

CanRander.hpp 实现连接车辆CAN总线,解析发动机转速数据。

#pragma once
#include <linux/can.h>
#include <string>
#include "MessageQueue.hpp"
#include <atomic>

class CanReader {
public:
    CanReader(const std::string& iface, const std::string &device_id, MessageQueue& queue);
    ~CanReader();
    void run();

private:
    bool connectCanSocket();
    bool parseFrame(const struct can_frame& frame);
    void closeCANSocket();

private:
    std::string iface_;
    std::string device_id_;
    int can_socket_ = -1;
    MessageQueue& queue_;
    std::atomic<bool> running_{true};
};

CanRander.cpp 实现

#include "CanReader.hpp"

#include <sys/socket.h>
#include <sys/ioctl.h> 
#include <linux/can/raw.h>
#include <net/if.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include "utils.hpp"
#include "Logger.hpp"

CanReader::CanReader(const std::string& iface, const std::string &device_id, MessageQueue& queue)
    : iface_(iface), device_id_(device_id), queue_(queue){}

CanReader::~CanReader() {
    running_ = false;
    closeCANSocket();
}

bool CanReader::connectCanSocket() {
    if (can_socket_ >= 0) return true;  // 已连接

    // 初始化 SocketCAN
    struct ifreq ifr{};
    struct sockaddr_can addr{};
    can_socket_ = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (can_socket_ < 0) {
        LOG_ERR("[CAN] Socket creation failed.", can_socket_);
        return false;
    }

    strncpy(ifr.ifr_name, iface_.c_str(), IFNAMSIZ);
    if (ioctl(can_socket_, SIOCGIFINDEX, &ifr) < 0) {
        LOG_ERR("[CAN] ioctl failed.");
        return false;
    }
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    
    if (bind(can_socket_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        LOG_ERR("[CAN] Bind failed.");
        return false;
    }
    return true;
}

void CanReader::run() {
    struct can_frame frame{};
    while (running_) {
        if(!connectCanSocket()) {
            LOG_ERR("[CAN] read data error:", errno, ", message: ", strerror(errno));
            std::this_thread::sleep_for(std::chrono::milliseconds(2));
            continue;
        }
        int nbytes = read(can_socket_, &frame, sizeof(frame));
        if (nbytes < 0) {
            closeCANSocket();
            LOG_ERR("[CAN] read data error: ", nbytes, ", errno: ", errno, ", message: ", strerror(errno));
            continue;
        } 
        if (!parseFrame(frame)) {
        	LOG_INFO("[CAN] other data.");
        }
    }
}

bool CanReader::parseFrame(const struct can_frame& frame) {
    uint32_t pgn = (frame.can_id >> 8) & 0xFFFF;
    if (pgn == 0xF004) {
        float rpm = ((frame.data[4] << 8) | frame.data[3]) * 0.125f;
        LOG_INFO("[CAN] can rpm data: ", rpm);
        return true;
    }
    return false;
}

void CanReader::closeCANSocket() {
    if (can_socket_ >= 0) {
        close(can_socket_);
        can_socket_ = -1;
    }
}

Ntrip 协议

Ntrip(Networked Transport of RTCM via Internet Protocol)是基于互联网协议的 RTCM 网络传输协议,通过互联网传输和共享差分改正数据来实现精确定位和导航。

NTRIP 组成

Ntrip 有四个核心角色:

  • Ntrip Source:系统的数据源,提供差分数据和原始卫星观测数据。

  • Ntrip Server(Caster Source):从本地 GNSS 基站获取 RTCM 数据,上传给 Caster。

  • Ntrip Caster:类似于转发服务器,管理多个 Source 与 Client。

  • Ntrip Client(Rover端): 通过网络从 Caster 请求 RTCM 差分数据,并用于解算高精度位置。

Ntrip Server 将本地采集的差分数据(如RTCM)上传给 Ntrip Caster,是 Ntrip Client 接收到的差分数据的源头,每个 Ntrip Server 负责维护一个 Mountpoint,Server 不直接与 Client 通信,而是通过 Caster 中转。

Ntrip Caster 是整个系统的”中控”,就像一台差分数据 “交换机”,负责:管理多个差分源(Mountpoint)和多个客户端之间的连接与分发。

传统模式

对于传统固定基站,Ntrip Client 连接上 Ntrip Caster 后,无需发送任何位置信息,只需选择一个固定的差分基站(即指定 mountpoint)并接收其广播的 RTCM 差分数据,与自身接收到的 GNSS 原始观测数据(载波相位、伪距等)进行融合,得到厘米级高精度定位结果。

// Client 流程
1. 建立与 Ntrip Caster 的连接
2. 接收差分站下发的RTCM数据
3. 将RTCM数据与自身观测数据融合得到GPS。

优点:实现简单,无需额外逻辑;固定测站、工程测量场景适用。

缺点:离基站太远(>10~20km),RTK 精度下降,甚至无法解算;误差来源大(基线过长、对流层误差等)

VRS 模式

VRS(Virtual Reference Station)虚拟参考站,是一种基于用户当前位置动态生成差分数据的高精度定位服务方式。Client 在连接 Caster 后,主动上传自身的粗略位置(GGA语句),差分服务系统根据 Client 的位置动态生成一个虚拟基站,返回该虚拟基站点的定制化差分数据(RTCM 格式)。这使得移动站始终能够接收到来自“最近基站”的最优差分修正,提高解算精度和稳定性。

// Client 流程
1. 建立与 Ntrip Caster 的连接
2. 向 Caster 主动发送自身的 GGA 位置语句
3. 接收 Caster 下发的针对当前位置的 RTCM 差分数据流
4. 将 RTCM 数据与自身接收的 GNSS 原始观测数据融合,输出GPS。

相比于传统差分站,VRS 多了上传 GGA 数据的步骤。

优点:精度高,收敛快;适合流动站不断移动的场景(如无人车、测绘无人机);减少了对实际物理基站的建设和维护压力。

缺点:必须周期性注入 GGA;部分 VRS 服务为商业服务,需要账户认证、订阅许可等。

RTCM3.2

RTCM(Radio Technical Commission for Maritime Services) 是由美国海事无线电技术委员会发布的标准,广泛应用于高精度 GNSS(如 GPS/北斗/GLONASS/Galileo)定位中,用于传输差分修正数据

数据结构

┌──────────┬──────────┬─────────────────┬──────────────┬────────┐
│ Preamble │ Reserved │  Message Length │   Payload    │   CRC  │
│  (8bit)  │  (6bit)  │     (10bit)     │   (N bits)   │  24bit │
   同步码		保留位			信息长度		 数据信息	 	
└──────────┴──────────┴─────────────────┴──────────────┴────────┘

相关资料:

NMEA

NMEA(National Marine Electronics Association)定义了 GNSS 模块标准输出格式,广泛用于串口输出位置信息,纯文本格式,易于解析。

特点:

  • ASCII 文本格式,逗号分隔;
  • 标准语句以 $ 开头,以 \r\n 结束;
  • 用途:用于串口读取定位结果;
  • 每秒多条消息输出(典型为1Hz或10Hz)。

数据格式

$<语句类型>,<字段1>,<字段2>,...,<字段N>*<校验和><CR><LF>

示例:
$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.95,545.4,M,46.9,M,,*47
$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
$GPGSA,A,3,04,05,09,12,24,25,29,31,,,,1.8,1.0,1.5*33
$GPGSV,2,1,08,01,40,083,41,02,17,045,42,03,14,273,43,04,09,137,38*76
$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48
$GPGLL,4916.45,N,12311.12,W,225444,A*1D

核心语句

  • GPGGA
$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>,<13>,<14>*<15>
<1> UTC时间,格式为hhmmss.sss。  
<2> 纬度,格式为ddmm.mmmm(前导位数不足则补0)。   
<3> 纬度半球,N或S(北纬或南纬)。   
<4> 经度,格式为dddmm.mmmm(前导位数不足则补0)。 
<5> 经度半球,E或W(东经或西经)。   
<6> 定位质量指示,0=定位无效,1=定位有效。   
<7> 使用卫星数量,从00到12(前导位数不足则补0)。  
<8> 水平精确度,0.5到99.9。   
<9> 天线离海平面的高度,-9999.9到9999.9米   
<10> 高度单位,M表示单位米。   
<11> 大地椭球面相对海平面的高度(-999.9到999.9)。   
<12> 高度单位,M表示单位米。   
<13> 差分GPS数据期限(RTCM SC-104),最后设立RTCM传送的秒数量。   
<14> 差分参考基站标号,从0000到1023(前导位数不足则补0)。   
<15> 校验和。
  • GPRMC
$GPRMC,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>*<13>
<1> UTC(Coordinated Universal Time)时间,hhmmss(时分秒)格式   
<2> 定位状态,A=有效定位,V=无效定位   
<3> Latitude,纬度ddmm.mmmm(度分)格式(前导位数不足则补0)   
<4> 纬度半球N(北半球)或S(南半球)   
<5> Longitude,经度dddmm.mmmm(度分)格式(前导位数不足则补0)   
<6> 经度半球E(东经)或W(西经)   
<7> 地面速率(000.0~999.9节,Knot,前导位数不足则补0)   
<8> 地面航向(000.0~359.9度,以真北为参考基准,前导位数不足则补0)   
<9> UTC日期,ddmmyy(日月年)格式   
<10> Magnetic Variation,磁偏角(000.0~180.0度,前导位数不足则补0)   
<11> Declination,磁偏角方向,E(东)或W(西)   
<12> Mode Indicator,模式指示(仅NMEA0183 3.00版本输出,A=自主定位,D=差分,E=估算,N=数据无效)   
<13> 校验和。
  • GPGSA
$GPGSA,<1>,<2>,<3>,<3>,<3>,<3>,<3>,<4>,<5>,<6>,<7>
<1>模式 :M = 手动, A = 自动。 
<2>定位型式 1 = 未定位, 2 = 二维定位, 3 = 三维定位。 
<3>PRN 数字:01 至 32 表天空使用中的卫星编号,最多可接收12颗卫星信息。   
<4> PDOP位置精度因子(0.5~99.9)   
<5> HDOP水平精度因子(0.5~99.9)   
<6> VDOP垂直精度因子(0.5~99.9)   
<7> Checksum(检查位)
  • GPGSV
$GPGSV, <1>,<2>,<3>,<4>,<5>,<6>,<7>,…,<4>,<5>,<6>,<7>*<8>
<1> 总的GSV语句电文数。   
<2> 当前GSV语句号。   
<3> 可视卫星总数,00至12。   
<4> 卫星编号,01至32。   
<5> 卫星仰角,00至90度。   
<6> 卫星方位角,000至359度。实际值。   
<7> 信噪比(C/No),00至99dB;无表未接收到讯号。   
<8> 校验和。
  • GPVTG
$GPVTG,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>∗<10>
<1> 真北航向,000 - 359,(前导位数不足则补0) 
<2> T=真北参照系 
<3> 磁北航向,000 - 359,(前导位数不足则补0) 
<4> M=磁北参照系 
<5> 水平运动速度(0.00)(前导位数不足则补0) 
<6> N=节,Knots 
<7> 水平运动速度(0.00)(前导位数不足则补0) 
<8> K=公里/时,km/h 
<9> 模式指示(仅 NMEA 0183 3.0 版本输出,A=自主定位,D=差分,E=估算,N=数据无效)
<10> 校验值.
  • GPGLL
$GPGLL,<1>,<2>,<3>,<4>,<5>,<6>,<7>*<8>  
<1> 纬度ddmm.mmmm,度分格式(前导位数不足则补0) 
<2> 纬度N(北纬)或S(南纬) 
<3> 经度dddmm.mmmm,度分格式(前导位数不足则补0) 
<4> 经度E(东经)或W(西经) 
<5> UTC时间,hhmmss.sss格式 
<6> 定位状态,A=定位,V=未定位 
<7> 模式指示(仅 NMEA 0183 3.0 版本输出,A=自主定位,D=差分,E=估算,N=数据无效)
<8> 校验值

文章作者: LSJune
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LSJune !
评论
  目录