J1939 文档

概述 / 什么是 J1939

SAE J1939 在 CAN 上定义了一个更高的层协议。它实现了一个更复杂的寻址方案,并将最大数据包大小扩展到 8 字节以上。存在一些派生的规范,它们在应用层面上与原始 J1939 不同,例如 MilCAN A、NMEA2000,特别是 ISO-11783 (ISOBUS)。最后一个规范指定了所谓的 ETP(扩展传输协议),该协议已包含在此实现中。这导致最大数据包大小为 ((2 ^ 24) - 1) * 7 字节 == 111 MiB。

使用的规范

  • SAE J1939-21 : 数据链路层

  • SAE J1939-81 : 网络管理

  • ISO 11783-6 : 虚拟终端(扩展传输协议)

动机

鉴于存在类似于 BSD 套接字的 API 的 SocketCAN,我们发现了一些理由来证明 J1939 使用的寻址和传输方法的内核实现是合理的。

  • 寻址:当 ECU 上的进程通过 J1939 进行通信时,它不一定需要知道其源地址。虽然每个 ECU 至少有一个进程应该知道源地址。其他进程应该能够重用该地址。这样,为同一个 ECU 协作的不同进程的地址参数就不会重复。这种工作方式与 UNIX 概念密切相关,即程序只做一件事,并且做得很好。

  • 动态寻址: J1939 中的地址声明对时间要求很高。此外,在地址协商期间,应正确处理数据传输。将此功能放入内核消除了 _每个_ 通过 J1939 通信的用户空间进程的要求。这产生了一个具有适当寻址的一致的 J1939 总线。

  • 传输: TP 和 ETP 都重用一些 PGN 来通过它们中继大数据包。因此,不同的进程可能会使用相同的 TP 和 ETP PGN,而实际上并不知道。不同的进程之间的各个 TP 和 ETP 会话 _必须_ 被序列化(同步)。内核正确地解决了这个问题,并消除了 _每个_ 通过 J1939 通信的用户空间进程对序列化(同步)的要求。

J1939 定义了一些其他功能(中继、网关、快速数据包传输等)。这些功能的内核内代码不会有助于协议的稳定性。因此,这些部分留给用户空间处理。

J1939 套接字在 CAN 网络设备上运行(参见 SocketCAN)。任何在 CAN 原始套接字上运行的 J1939 用户空间库仍然可以正常运行。由于此类库不与内核内实现通信,因此必须注意这两个库不要相互干扰。实际上,这意味着它们不能共享 ECU 地址。库或内核内系统专门使用单个 ECU(或虚拟 ECU)地址。

J1939 概念

发送到 J1939 堆栈的数据

从用户空间发送到 J1939 堆栈的数据缓冲区本身不是 CAN 帧。相反,它们是有效负载,J1939 堆栈根据缓冲区的大小和传输类型将其转换为适当的 CAN 帧。缓冲区的大小会影响堆栈处理数据的方式,并确定用于传输的内部代码路径。

不同缓冲区大小的处理

  • 大小为 8 字节或更小的缓冲区

    • 这些在内部由堆栈作为简单会话处理。

    • 堆栈直接将缓冲区转换为单个 CAN 帧,而无需分片。

    • 这种类型的传输不需要接收端的实际客户端(接收器)。

  • 最大 1785 字节的缓冲区

    • 这些会自动作为 J1939 传输协议 (TP) 传输处理。

    • 在内部,堆栈将缓冲区拆分为多个 8 字节的 CAN 帧。

    • TP 传输可以是单播或广播。

    • 广播 TP:不需要另一端的接收器,并且可以用于广播场景。

    • 单播 TP:需要在另一端有一个活动的接收器(客户端)来确认传输。

  • 从 1786 字节到 111 MiB 的缓冲区

    • 这些作为 ISO 11783 扩展传输协议 (ETP) 传输处理。

    • ETP 传输用于更大的有效负载,并在内部拆分为多个 CAN 帧。

    • ETP 传输(单播):需要在另一端有一个接收器来处理传入数据并确认传输的每个步骤。

    • ETP 传输不能像 TP 传输一样广播,并且始终需要接收器才能运行。

使用 `MSG_DONTWAIT` 的非阻塞操作

当与 MSG_DONTWAIT 标志结合使用时,J1939 堆栈支持非阻塞操作。在此模式下,堆栈会尝试获取尽可能多的数据,以socket的可用内存允许的范围内。它返回成功获取的数据量,用户空间负责监视此值并处理部分传输。

  • 如果堆栈无法获取整个缓冲区,它会返回成功获取的字节数,用户空间应处理剩余部分。

  • 错误处理:使用 MSG_DONTWAIT 时,用户必须依靠错误队列来检测传输错误。有关如何订阅错误通知的详细信息,请参阅 SO_J1939_ERRQUEUE 部分。如果没有错误队列,用户空间就没有其他方式来通知非阻塞操作期间的传输错误。

行为和要求

  • 简单传输(<= 8 字节):不需要另一端的接收器,因此可以轻松发送,而无需地址声明或与目标协调。

  • 单播 TP/ETP:需要在另一端有一个接收器才能完成传输。接收器必须确认传输才能使会话成功进行。

  • 广播 TP:允许在没有接收器的情况下发送数据,但仅适用于 TP 传输。ETP 不能广播,并且始终需要接收客户端。

这些不同的行为在很大程度上取决于提供给堆栈的缓冲区的大小,并且会根据有效负载大小选择适当的传输机制(TP 或 ETP)。堆栈会自动管理大型有效负载的分片和重组,并确保为每个会话生成和传输正确的 CAN 帧。

PGN

J1939 协议使用具有以下结构的 29 位 CAN 标识符

29 位 CAN-ID

CAN-ID 中的位位置

28 ... 26

25 ... 8

7 ... 0

优先级

PGN

SA(源地址)

PGN(参数组号)是一个用于标识数据包的数字。PGN 的组成如下

PGN

CAN-ID 中的位位置

25

24

23 ... 16

15 ... 8

R(保留)

DP(数据页)

PF(PDU 格式)

PS(PDU 特定)

在 J1939-21 中,PDU1 格式(其中 PF < 240)和 PDU2 格式(其中 PF >= 240)之间存在区别。此外,当使用 PDU2 格式时,PS 字段包含所谓的组扩展,它是 PGN 的一部分。当使用 PDU2 格式时,组扩展在 PS 字段中设置。

PDU1 格式(特定)(点对点)

CAN-ID 中的位位置

23 ... 16

15 ... 8

00h ... EFh

DA(目标地址)

PDU2 格式(全局)(广播)

CAN-ID 中的位位置

23 ... 16

15 ... 8

F0h ... FFh

GE(组扩展)

另一方面,当使用 PDU1 格式时,PS 字段包含所谓的目的地地址,该地址 _不是_ PGN 的一部分。当从用户空间到内核(或反之亦然)通信 PGN 并且使用 PDU1 格式时,PGN 的 PS 字段应设置为零。目的地地址应在其他地方设置。

关于 PGN 到 29 位 CAN 标识符的映射,目的地地址应由内核从标识符的适当位获取/设置到标识符的适当位。

寻址

可以使用静态和动态寻址方法。

对于静态地址,内核不进行额外的检查,并且提供的地址被认为是正确的。此责任由 OEM 或系统集成商承担。

对于动态寻址,即所谓的地址声明,内核中预见了额外的支持。在 J1939 中,任何 ECU 都以其 64 位 NAME 标识。在成功声明地址时,内核会跟踪正在声明的 NAME 和源地址。这用作筛选方案的基础。默认情况下,将拒绝目的地不是本地的数据包。

允许混合模式数据包(从静态地址到动态地址或反之亦然)。BSD 套接字为获取/设置本地和远程地址定义了单独的 API 调用,这些调用适用于 J1939 套接字。

筛选

J1939 为每个套接字定义了白名单筛选器,用户可以设置这些筛选器以便接收 J1939 流量的子集。筛选可以基于

  • SA

  • SOURCE_NAME

  • PGN

当单个套接字有多个筛选器,并且传入的数据包与其中几个筛选器匹配时,该套接字仅接收一次该数据包。

如何使用 J1939

API 调用

在 CAN 上,您首先需要打开一个套接字才能通过 CAN 网络进行通信。要使用 J1939,#include <linux/can/j1939.h>。从那里,也将包含 <linux/can.h>。要打开套接字,请使用

s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);

J1939 使用 SOCK_DGRAM 套接字。在 J1939 规范中,连接是在传输协议会话的上下文中提及的。这些会话仍然将数据包传递到另一端(使用多个 CAN 数据包)。不支持 SOCK_STREAM

成功创建套接字后,通常会使用 bind(2) 和/或 connect(2) 系统调用将套接字绑定到 CAN 接口。绑定和/或连接套接字后,您可以 read(2)write(2) 从/到套接字,或者像往常一样在套接字上使用 send(2)sendto(2)sendmsg(2)recv*() 对应操作。下面还介绍了 J1939 特定的套接字选项。

为了发送数据,必须成功执行 bind(2)bind(2) 将本地地址分配给套接字。

与 CAN 不同的是,有效负载数据只是发送的数据,没有其头信息。头信息源自提供给 bind(2)connect(2)sendto(2)recvfrom(2) 的 sockaddr。大小为 4 的 write(2) 将产生一个包含 4 个字节的数据包。

sockaddr 结构具有扩展,可用于 J1939,如下所示

struct sockaddr_can {
   sa_family_t can_family;
   int         can_ifindex;
   union {
      struct {
         __u64 name;
                  /* pgn:
                   * 8 bit: PS in PDU2 case, else 0
                   * 8 bit: PF
                   * 1 bit: DP
                   * 1 bit: reserved
                   */
         __u32 pgn;
         __u8  addr;
      } j1939;
   } can_addr;
}

can_familycan_ifindex 的作用与其他 SocketCAN 套接字相同。

can_addr.j1939.pgn 指定 PGN(最大 0x3ffff)。各个位的指定如上所述。

can_addr.j1939.name 包含 64 位 J1939 NAME。

can_addr.j1939.addr 包含地址。

bind(2) 系统调用分配本地地址,即发送数据包时的源地址。如果在 bind(2) 期间设置了 PGN,则它将用作 RX 筛选器。即,仅接收具有匹配 PGN 的数据包。如果设置了 ADDR 或 NAME,它也将用作接收筛选器。它将匹配传入数据包的目标 NAME 或 ADDR。只有在 CAN 总线上为此名称完成了适当的地址声明并由内核注册/缓存后,NAME 筛选器才能工作。

另一方面,connect(2) 分配远程地址,即目标地址。来自 connect(2) 的 PGN 用作发送数据包时的默认 PGN。如果设置了 ADDR 或 NAME,它将用作默认的目标 ADDR 或 NAME。此外,在 connect(2) 期间设置的 ADDR 或 NAME 用作接收筛选器。它将匹配传入数据包的源 NAME 或 ADDR。

write(2)send(2) 都将发送一个数据包,其中包含来自 bind(2) 的本地地址和来自 connect(2) 的远程地址。使用 sendto(2) 覆盖目标地址。

如果设置了 can_addr.j1939.name(!= 0),则内核会查找 NAME 并使用相应的 ADDR。如果未设置 can_addr.j1939.name(== 0),则使用 can_addr.j1939.addr

创建套接字时,会设置合理的默认值。可以使用 setsockopt(2)getsockopt(2) 修改某些选项。

与 RX 路径相关的选项

  • SO_J1939_FILTER - 配置筛选器数组

  • SO_J1939_PROMISC - 禁用由 bind(2)connect(2) 设置的筛选器

默认情况下,无法发送或接收广播数据包。要启用发送或接收广播数据包,请使用套接字选项 SO_BROADCAST

int value = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &value, sizeof(value));

下图说明了 RX 路径

               +--------------------+
               |  incoming packet   |
               +--------------------+
                         |
                         V
               +--------------------+
               | SO_J1939_PROMISC?  |
               +--------------------+
                        |  |
                    no  |  | yes
                        |  |
              .---------'  `---------.
              |                      |
+---------------------------+        |
| bind() + connect() +      |        |
| SOCK_BROADCAST filter     |        |
+---------------------------+        |
              |                      |
              |<---------------------'
              V
+---------------------------+
|      SO_J1939_FILTER      |
+---------------------------+
              |
              V
+---------------------------+
|        socket recv()      |
+---------------------------+

与 TX 路径相关的选项:SO_J1939_SEND_PRIO - 更改套接字的默认发送优先级

recvmsg(2)

在大多数情况下,如果您想提取比 recvfrom(2) 可以提供的更多信息,则需要 recvmsg(2)。例如,数据包优先级和时间戳。目标地址、名称和数据包优先级(如果适用)附加到 recvmsg(2) 调用中的 msghdr 中。可以使用 cmsg(3) 宏提取它们,其中 cmsg_level == SOL_J1939 && cmsg_type == SCM_J1939_DEST_ADDRSCM_J1939_DEST_NAMESCM_J1939_PRIO。返回的数据是 prioritydst_addruint8_t,以及 dst_nameuint64_t

uint8_t priority, dst_addr;
uint64_t dst_name;

for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
        switch (cmsg->cmsg_level) {
        case SOL_CAN_J1939:
                if (cmsg->cmsg_type == SCM_J1939_DEST_ADDR)
                        dst_addr = *CMSG_DATA(cmsg);
                else if (cmsg->cmsg_type == SCM_J1939_DEST_NAME)
                        memcpy(&dst_name, CMSG_DATA(cmsg), cmsg->cmsg_len - CMSG_LEN(0));
                else if (cmsg->cmsg_type == SCM_J1939_PRIO)
                        priority = *CMSG_DATA(cmsg);
                break;
        }
}

setsockopt(2)

setsockopt(2) 函数用于配置 J1939 通信的各种套接字级别选项。支持以下选项

SO_J1939_FILTER

bind(2)connect(2) 的默认行为对于特定用例不足时,SO_J1939_FILTER 选项至关重要。默认情况下,bind(2)connect(2) 允许将套接字与单个单播或广播地址关联。但是,在某些情况下,需要对传入消息进行更精细的控制,例如按参数组号 (PGN) 而不是按地址进行筛选。

例如,在一个传输多种类型的 J1939 消息的系统中,一个进程可能只对这些消息的子集感兴趣,例如特定的 PGN,而不希望接收所有发送到其地址或广播到总线的消息。

通过应用 SO_J1939_FILTER 选项,您可以基于以下内容筛选消息

  • 源地址 (SA):筛选来自特定源地址的消息。

  • 源名称:筛选来自具有特定 NAME 标识符的 ECU 的消息。

  • 参数组号 (PGN):专注于接收具有特定 PGN 的消息,筛选掉不相关的消息。

当出现以下情况时,此筛选机制特别有用

  • 您想根据消息的 PGN 接收消息的子集,即使地址相同。

  • 您需要处理广播和单播消息,但只关心某些消息类型或参数。

  • bind(2)connect(2) 函数只允许绑定到单个地址,如果进程需要处理多个 PGN 但不想打开多个套接字,则这可能不够用。

要删除现有的筛选器,您可以将 optval == NULLoptlen == 0 传递给 setsockopt(2)。这将清除所有当前设置的筛选器。如果要 更新 筛选器集,则必须将更新后的筛选器集传递给 setsockopt(2),因为新的筛选器集将完全 替换 旧的筛选器集。此行为可确保放弃任何先前的筛选器配置,并且仅应用新的筛选器集。

删除所有筛选器的示例

setsockopt(sock, SOL_CAN_J1939, SO_J1939_FILTER, NULL, 0);

最大筛选器数量: 可以使用 SO_J1939_FILTER 应用的最大筛选器数量由 J1939_FILTER_MAX 定义,该值设置为 512。这意味着您可以配置最多 512 个单独的筛选器以满足您的特定筛选需求。

实际用例:监视地址声明

一个实际的用例是通过筛选与地址声明相关的特定 PGN 来监视 J1939 地址声明过程。这允许进程监视和处理地址声明,而无需处理不相关的消息。

示例

struct j1939_filter filt[] = {
    {
        .pgn = J1939_PGN_ADDRESS_CLAIMED,
        .pgn_mask = J1939_PGN_PDU1_MAX,
    }, {
        .pgn = J1939_PGN_REQUEST,
        .pgn_mask = J1939_PGN_PDU1_MAX,
    }, {
        .pgn = J1939_PGN_ADDRESS_COMMANDED,
        .pgn_mask = J1939_PGN_MAX,
    },
};
setsockopt(sock, SOL_CAN_J1939, SO_J1939_FILTER, &filt, sizeof(filt));

在此示例中,套接字将仅接收具有与地址声明相关的 PGN 的消息:J1939_PGN_ADDRESS_CLAIMEDJ1939_PGN_REQUESTJ1939_PGN_ADDRESS_COMMANDED。这在您希望监视和处理地址声明而不被 J1939 网络上的其他流量淹没的情况下特别有用。

SO_J1939_PROMISC

SO_J1939_PROMISC 选项启用套接字级别的混杂模式。启用此选项后,套接字将接收所有 J1939 流量,而不管 bind()connect() 设置的任何筛选器。这类似于为以太网接口启用混杂模式,其中捕获网络段上的所有流量。

但是,与 SO_J1939_PROMISC 相比,`SO_J1939_FILTER` 具有更高的优先级。这意味着即使在混杂模式下,您也可以通过使用 SO_J1939_FILTER 应用特定的筛选器来减少接收到的数据包数量。筛选器将限制传递到套接字的数据包,从而允许在混杂模式处于活动状态时进行更精细的流量选择。

此选项的可接受值大小为 sizeof(int),并且该值仅在 0 和非零之间区分。值为 0 将禁用混杂模式,而任何非零值将启用它。

这种组合对于调试或监视特定类型的流量,同时仍然捕获广泛的消息集很有用。

示例

int value = 1;
setsockopt(sock, SOL_CAN_J1939, SO_J1939_PROMISC, &value, sizeof(value));

在此示例中,将 value 设置为任何非零值(例如,1)将启用混杂模式,从而允许套接字接收网络上的所有 J1939 流量。

SO_BROADCAST

SO_BROADCAST 选项启用广播消息的发送和接收。默认情况下,J1939 套接字禁用了广播消息。启用此选项后,将允许套接字在 J1939 网络上发送和接收广播数据包。

由于 CAN 总线是一种共享介质,因此总线上发送的所有消息对所有参与者都是可见的。在 J1939 的上下文中,广播指的是使用特定的目标地址字段,其中目标地址被设置为一个表示消息是发送给所有参与者的值(通常是全局地址,例如 0xFF)。启用广播选项允许套接字发送和接收此类广播消息。

此选项可接受的值大小为 sizeof(int),并且该值仅区分 0 和非零值。值为 0 时禁用发送和接收广播消息的能力,而任何非零值都将启用它。

示例

int value = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &value, sizeof(value));

在此示例中,将 value 设置为任何非零值(例如,1)都将启用套接字以发送和接收广播消息。

SO_J1939_SEND_PRIO

SO_J1939_SEND_PRIO 选项设置套接字发出的 J1939 消息的优先级。在 J1939 中,消息可以具有不同的优先级,并且数值越小表示优先级越高。此选项允许用户通过调整 CAN 标识符中的优先级位来控制从套接字发送的消息的优先级。

此选项可接受的 大小sizeof(int),并且该值应在 0 到 7 的范围内,其中 0 是最高优先级,7 是最低优先级。默认情况下,如果未显式配置此选项,则优先级设置为 6

请注意,只有当进程具有 CAP_NET_ADMIN 权限时,才能设置优先级值 01。这些值保留用于高优先级流量,并且需要管理权限。

示例

int prio = 3;  // Priority value between 0 (highest) and 7 (lowest)
setsockopt(sock, SOL_CAN_J1939, SO_J1939_SEND_PRIO, &prio, sizeof(prio));

在此示例中,优先级设置为 3,这意味着发出的消息将以中等优先级级别发送。

SO_J1939_ERRQUEUE

SO_J1939_ERRQUEUE 选项使套接字能够从错误队列接收错误消息,从而提供有关 J1939 通信期间发生的传输失败、协议违规或其他问题的诊断信息。设置此选项后,用户空间需要处理 MSG_ERRQUEUE 消息。

SO_J1939_ERRQUEUE 设置为 0 将清除错误队列中当前存在的任何错误消息。启用后,可以使用 recvmsg(2) 系统调用检索错误消息。

订阅错误队列时,可以访问以下错误事件

  • ``J1939_EE_INFO_TX_ABORT``:传输中止错误。

  • ``J1939_EE_INFO_RX_RTS``:接收 RTS(发送请求)控制帧。

  • ``J1939_EE_INFO_RX_DPO``:接收带有数据页偏移量 (DPO) 的数据包。

  • ``J1939_EE_INFO_RX_ABORT``:接收中止错误。

错误队列可用于使用会话 ID (tskey) 将错误与特定的消息传输会话相关联。会话 ID 通过 SOF_TIMESTAMPING_OPT_ID 标志分配,该标志通过启用 SO_TIMESTAMPING 选项来设置。

如果激活了 SO_J1939_ERRQUEUE,则用户需要从错误队列中拉取消息,这意味着仅使用 recv(2) 已不再足够。用户必须使用带有适当标志的 recvmsg(2) 来处理错误消息。否则可能会导致套接字因队列中未处理的错误消息而被阻塞。

强烈建议在大多数情况下将 SO_J1939_ERRQUEUESO_TIMESTAMPING 结合使用。这样可以实现适当的错误处理以及会话跟踪和时间戳记录,从而提供对消息传输和错误的更详细分析。

此选项可接受的 大小sizeof(int),并且该值仅区分 0 和非零值。值为 0 时禁用错误队列接收并清除任何现有错误消息,而任何非零值都将启用它。

示例

int enable = 1;  // Enable error queue reception
setsockopt(sock, SOL_CAN_J1939, SO_J1939_ERRQUEUE, &enable, sizeof(enable));

// Enable timestamping with session tracking via tskey
int timestamping = SOF_TIMESTAMPING_OPT_ID | SOF_TIMESTAMPING_TX_ACK |
                   SOF_TIMESTAMPING_TX_SCHED |
                   SOF_TIMESTAMPING_RX_SOFTWARE | SOF_TIMESTAMPING_OPT_CMSG;
setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING, &timestamping,
           sizeof(timestamping));

启用后,可以使用 recvmsg(2) 检索错误消息。通过将 SO_J1939_ERRQUEUESO_TIMESTAMPING(启用 SOF_TIMESTAMPING_OPT_IDSOF_TIMESTAMPING_OPT_CMSG)相结合,用户可以跟踪消息传输,检索精确的时间戳,并将错误与特定会话相关联。

有关启用时间戳和会话跟踪的更多信息,请参阅 SO_TIMESTAMPING 部分。

SO_TIMESTAMPING

SO_TIMESTAMPING 选项允许套接字接收与 J1939 中的消息传输和接收相关的各种事件的时间戳。此选项通常与 SO_J1939_ERRQUEUE 结合使用,以提供详细的诊断信息、会话跟踪和消息传输的精确计时数据。

在 J1939 中,用户空间提供的所有有效负载,无论大小,都由内核作为会话处理。这包括单帧消息(最多 8 个字节)和多帧协议,例如传输协议 (TP) 和扩展传输协议 (ETP)。即使对于小的单帧消息,内核也会创建一个会话来管理传输和接收。会话的概念允许内核管理协议的各个方面,例如重新组装多帧消息和跟踪传输的状态。

从错误队列接收扩展错误消息时,错误信息通过 struct sock_extended_err 传递,该结构可以通过使用 recvmsg(2) 系统调用检索的控制消息 (cmsg) 访问。

J1939 中扩展错误消息通常有两个来源

  1. serr->ee_origin == SO_EE_ORIGIN_TIMESTAMPING:

    在这种情况下,serr->ee_info 字段将包含以下时间戳类型之一

    • SCM_TSTAMP_SCHED:此时间戳对于扩展传输协议 (ETP) 传输和简单传输(8 字节或更少)有效。它指示消息或一组帧何时被安排用于传输。

      • 对于简单传输(8 字节或更少),它标记消息排队并准备好发送到 CAN 总线的时间点。

      • 对于 ETP 传输,它在发送方收到 CTS(允许发送)帧后发送,指示已安排一组新帧用于传输。

      • 传输协议 (TP) 情况目前未为此时间戳实现。

      • 在接收方,ETP 的此事件对应项由 J1939_EE_INFO_RX_DPO 消息表示,该消息指示接收到数据页偏移量 (DPO) 控制帧。

    • SCM_TSTAMP_ACK:此时间戳指示消息或会话的确认。

      • 对于简单传输(8 字节或更少),它标记消息已发送并且已收到来自 CAN 控制器的回声确认,指示该帧已传输到总线上。

      • 对于多帧传输(TP 或 ETP),它表示整个会话已被确认,通常在接收到消息结束确认 (EOMA) 数据包之后。

  2. serr->ee_origin == SO_EE_ORIGIN_LOCAL:

    在这种情况下,serr->ee_info 字段将包含以下 J1939 堆栈特定的消息类型之一

    • J1939_EE_INFO_TX_ABORT:此消息指示消息或会话的传输已中止。中止的原因可能来自各种来源

      • CAN 堆栈故障:J1939 堆栈无法将帧传递到 CAN 框架以进行传输。

      • 回声失败:J1939 堆栈未收到来自 CAN 控制器的回声确认,这意味着该帧可能未成功传输到 CAN 总线。

      • 协议级别问题:对于多帧传输 (TP/ETP),这可能包括与协议相关的错误,例如接收方发出的中止信号或协议级别的超时,这会导致会话过早终止。

      • 相应的错误代码存储在 serr->ee_data(内核端的 session->err)中,提供了有关中止的具体原因的更多详细信息。

    • J1939_EE_INFO_RX_RTS:此消息指示 J1939 堆栈已收到发送请求 (RTS) 控制帧,指示使用传输协议 (TP) 或扩展传输协议 (ETP) 开始多帧传输。

      • 它通知接收方发送方已准备好传输多帧消息,并包括有关总消息大小和要发送的帧数的详细信息。

      • 诸如 J1939_NLA_TOTAL_SIZEJ1939_NLA_PGNJ1939_NLA_SRC_NAMEJ1939_NLA_DEST_NAME 之类的统计信息与 J1939_EE_INFO_RX_RTS 消息一起提供,从而提供有关传入传输的详细信息。

    • J1939_EE_INFO_RX_DPO:此消息指示 J1939 堆栈已收到数据页偏移量 (DPO) 控制帧,该控制帧是扩展传输协议 (ETP) 的一部分。

      • DPO 帧通过指示传输的数据中的偏移位置来表示 ETP 多帧消息的继续。它通过识别正在接收的消息的哪个部分来帮助接收方管理大型数据集。

      • 它通常与发送方的对应 SCM_TSTAMP_SCHED 事件配对,该事件指示何时安排下一组帧进行传输。

      • 此事件包括诸如 J1939_NLA_BYTES_ACKED 之类的统计信息,该统计信息跟踪在该会话中直到该点为止已确认的字节数。

    • J1939_EE_INFO_RX_ABORT:此消息指示多帧消息(传输协议或扩展传输协议)的接收已中止。

      • 中止可能由协议级别错误(例如超时、意外帧或来自发送方的特定中止请求)触发。

      • 此消息表示接收方无法继续处理传输,并且会话已终止。

      • 相应的错误代码存储在 serr->ee_data(内核端的 session->err)中,从而提供有关中止原因的更多详细信息,例如协议违规或超时。

      • 收到此消息后,接收方会丢弃部分接收的帧,并且多帧会话被认为是不完整的。

在这两种情况下,如果启用了 SOF_TIMESTAMPING_OPT_ID,则 serr->ee_data 将设置为会话的唯一标识符 (session->tskey)。这允许用户空间通过其会话标识符跨多个帧或阶段跟踪消息传输。

在所有其他情况下,serr->ee_errno 将设置为 ENOMSG,但 J1939_EE_INFO_TX_ABORTJ1939_EE_INFO_RX_ABORT 情况除外,在这些情况下,内核会将 serr->ee_data 设置为存储在 session->err 中的错误。所有特定于协议的错误都将转换为标准内核错误值并存储在 session->err 中。这些错误值在系统调用和 serr->ee_errno 中统一。一些已知的错误值在 J1939 堆栈中的错误代码 部分中进行了描述。

当提供 J1939_EE_INFO_RX_RTS 消息时,它将包括以下多帧消息(TP 和 ETP)的统计信息

  • J1939_NLA_TOTAL_SIZE:会话中消息的总大小。

  • J1939_NLA_PGN:参数组号 (PGN),用于标识消息类型。

  • J1939_NLA_SRC_NAME:源 ECU 的 64 位名称。

  • J1939_NLA_DEST_NAME:目标 ECU 的 64 位名称。

  • J1939_NLA_SRC_ADDR:发送 ECU 的 8 位源地址。

  • J1939_NLA_DEST_ADDR:接收 ECU 的 8 位目标地址。

  • 对于其他消息(包括单帧消息),仅包括以下统计信息

    • J1939_NLA_BYTES_ACKED:会话中成功确认的字节数。

SO_TIMESTAMPING 的主要标志包括

  • SOF_TIMESTAMPING_OPT_ID:启用为每个传输使用唯一会话标识符 (tskey)。此标识符有助于跟踪消息传输和错误,使其成为用户空间中的不同会话。启用此选项后,serr->ee_data 将设置为 session->tskey

  • SOF_TIMESTAMPING_OPT_CMSG:通过控制消息 (struct scm_timestamping) 发送时间戳信息,允许应用程序检索与数据一起的时间戳。

  • SOF_TIMESTAMPING_TX_SCHED:提供消息安排用于传输时的时间戳 (SCM_TSTAMP_SCHED)。

  • SOF_TIMESTAMPING_TX_ACK:提供消息传输已完全确认时的时间戳 (SCM_TSTAMP_ACK)。

  • SOF_TIMESTAMPING_RX_SOFTWARE:提供与接收相关的事件的时间戳(例如,J1939_EE_INFO_RX_RTSJ1939_EE_INFO_RX_DPOJ1939_EE_INFO_RX_ABORT)。

这些标志支持对消息生命周期的详细监视,包括传输调度、确认、接收时间戳以及收集有关通信会话的详细统计信息,特别是对于像 TP 和 ETP 这样的多帧有效负载。

示例

// Enable timestamping with various options, including session tracking and
// statistics
int sock_opt = SOF_TIMESTAMPING_OPT_CMSG |
               SOF_TIMESTAMPING_TX_ACK |
               SOF_TIMESTAMPING_TX_SCHED |
               SOF_TIMESTAMPING_OPT_ID |
               SOF_TIMESTAMPING_RX_SOFTWARE;

setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING, &sock_opt, sizeof(sock_opt));

动态寻址

必须区分使用已声明的地址和进行地址声明。要使用已声明的地址,必须填写 j1939.name 成员并将其提供给 bind(2)。如果该名称先前已声明地址,则所有进一步发送的消息都将使用该地址。并且将忽略 j1939.addr 成员。

此例外的 PGN 为 0x0ee00。这是“地址声明/无法声明地址”消息,如果需要,内核将使用该 PGN 的 j1939.addr 成员。

要声明地址,可以使用以下代码示例

struct sockaddr_can baddr = {
        .can_family = AF_CAN,
        .can_addr.j1939 = {
                .name = name,
                .addr = J1939_IDLE_ADDR,
                .pgn = J1939_NO_PGN,    /* to disable bind() rx filter for PGN */
        },
        .can_ifindex = if_nametoindex("can0"),
};

bind(sock, (struct sockaddr *)&baddr, sizeof(baddr));

/* for Address Claiming broadcast must be allowed */
int value = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &value, sizeof(value));

/* configured advanced RX filter with PGN needed for Address Claiming */
const struct j1939_filter filt[] = {
        {
                .pgn = J1939_PGN_ADDRESS_CLAIMED,
                .pgn_mask = J1939_PGN_PDU1_MAX,
        }, {
                .pgn = J1939_PGN_REQUEST,
                .pgn_mask = J1939_PGN_PDU1_MAX,
        }, {
                .pgn = J1939_PGN_ADDRESS_COMMANDED,
                .pgn_mask = J1939_PGN_MAX,
        },
};

setsockopt(sock, SOL_CAN_J1939, SO_J1939_FILTER, &filt, sizeof(filt));

uint64_t dat = htole64(name);
const struct sockaddr_can saddr = {
        .can_family = AF_CAN,
        .can_addr.j1939 = {
                .pgn = J1939_PGN_ADDRESS_CLAIMED,
                .addr = J1939_NO_ADDR,
        },
};

/* Afterwards do a sendto(2) with data set to the NAME (Little Endian). If the
 * NAME provided, does not match the j1939.name provided to bind(2), EPROTO
 * will be returned.
 */
sendto(sock, dat, sizeof(dat), 0, (const struct sockaddr *)&saddr, sizeof(saddr));

如果在传输后 250 毫秒内没有人争夺该地址声明,则内核会将 NAME-SA 分配标记为有效。有效的分配将与其他有效的 NAME-SA 分配一起保留。从那时起,任何绑定到 NAME 的套接字都可以发送数据包。

如果另一个 ECU 声明该地址,则内核将标记 NAME-SA 已过期。绑定到 NAME 的任何套接字都无法发送数据包(地址声明除外)。要声明另一个地址,绑定到 NAME 的某些套接字必须再次 bind(2),但仅将 j1939.addr 更改为新的 SA,然后必须发送有效的地址声明数据包。这将为此 NAME 重新启动内核(以及总线上任何其他参与者)中的状态机。

can-utils 还包括 j1939acd 工具,因此它可以用作代码示例或默认的地址声明守护程序。

发送示例

静态寻址

此示例将从 SA 0x20 向 DA 0x30 发送 PGN (0x12300)。

绑定

struct sockaddr_can baddr = {
        .can_family = AF_CAN,
        .can_addr.j1939 = {
                .name = J1939_NO_NAME,
                .addr = 0x20,
                .pgn = J1939_NO_PGN,
        },
        .can_ifindex = if_nametoindex("can0"),
};

bind(sock, (struct sockaddr *)&baddr, sizeof(baddr));

现在,套接字“sock”已绑定到 SA 0x20。由于没有调用 connect(2),因此此时我们只能使用 sendto(2)sendmsg(2)

发送

const struct sockaddr_can saddr = {
        .can_family = AF_CAN,
        .can_addr.j1939 = {
                .name = J1939_NO_NAME;
                .addr = 0x30,
                .pgn = 0x12300,
        },
};

sendto(sock, dat, sizeof(dat), 0, (const struct sockaddr *)&saddr, sizeof(saddr));

J1939 堆栈中的错误代码

本节列出了与 J1939 堆栈交互时可能暴露给用户空间的所有潜在内核错误代码。它包括标准错误代码和源自特定于协议的中止代码的错误代码。

  • EAGAIN:操作将阻塞;重试可能会成功。一个常见原因是存在活动的 TP 或 ETP 会话,并且尝试在同一对等方之间启动新的重叠 TP 或 ETP 会话。

  • ENETDOWN:网络已关闭。当 CAN 接口切换到“down”状态时,会发生这种情况。

  • ENOBUFS:没有可用的缓冲区空间。当 CAN 接口的传输 (TX) 队列已满并且无法再将任何消息排队时,会发生此错误。

  • EOVERFLOW:值对于定义的数据类型来说太大。在 J1939 中,如果请求的数据位于排队的缓冲区之外,则可能会发生这种情况。例如,如果 CTS(允许发送)请求内核缓冲区中不可用的偏移量,因为用户空间没有提供足够的数据。

  • EBUSY:设备或资源正忙。例如,如果相同的会话已经处于活动状态并且堆栈无法从该状况中恢复,则会发生这种情况。

  • EACCES:权限被拒绝。例如,尝试发送广播消息但未配置 SO_BROADCAST 时,可能会发生此错误。

  • EADDRNOTAVAIL:地址不可用。在以下情况下会发生此错误

    • 尝试使用 getsockname(2) 检索对等方的地址,但套接字未连接。

    • 尝试将数据发送到 NAME 或从 NAME 发送数据,但 NAME 的地址声明未执行或未被堆栈检测到。

  • EBADFD:文件描述符状态不佳。如果出现以下情况,可能会发生此错误

    • 尝试将数据发送到未绑定的套接字。

    • 套接字已绑定但没有源名称,并且源地址为 J1939_NO_ADDR

    • can_ifindex 不正确。

  • EFAULT:地址错误。当堆栈无法复制自 sockptr 或复制到 sockptr、用户空间的数据不足或用户空间提供的缓冲区对于请求的数据来说不够大时,最常发生此错误。

  • EINTR:在传输任何数据之前发生了信号;请参阅 signal(7)

  • EINVAL:传递了无效参数。例如

    • msg->msg_namelen 小于 J1939_MIN_NAMELEN

    • addr->can_family 不等于 AF_CAN

    • 提供了不正确的 PGN。

  • ENODEV:没有这样的设备。当找不到提供的 can_ifindex 的 CAN 网络设备或者 can_ifindex 为 0 时,会发生这种情况。

  • ENOMEM:内存不足。通常与堆栈中的内存分配问题有关。

  • ENOPROTOOPT:协议不可用。如果请求的套接字选项不可用,则在使用 getsockopt(2)setsockopt(2) 时,可能会发生这种情况。

  • EDESTADDRREQ:需要目标地址。发生此错误

    • 对于 connect(2),如果 struct sockaddr *uaddrNULL

    • 对于 send*(2),如果尝试将 ETP 消息发送到广播地址。

  • EDOM:参数超出域。如果尝试将 TP 或 ETP 消息发送到保留用于 TP 或 ETP 操作的控制 PGN 的 PGN,则可能会发生此错误。

  • EIO:I/O 错误。如果为 TP 或 ETP 会话提供给套接字的数据量与会话的已公布数据量不匹配,则可能会发生这种情况。

  • ENOENT:没有这样的文件或目录。当堆栈尝试传输 CTS 或 EOMA 但找不到匹配的接收套接字时,可能会发生这种情况。

  • ENOIOCTLCMD:套接字层没有可用的 ioctl。

  • EPERM:操作不允许。例如,如果请求的操作需要 CAP_NET_ADMIN 权限,则可能会发生这种情况。

  • ENETUNREACH:网络无法访问。最有可能的是,当帧无法传输到 CAN 总线时,会发生这种情况。

  • ETIME:计时器已过期。如果在尝试发送简单消息时发生超时,例如,当未收到来自控制器的回声消息时,可能会发生这种情况。

  • EPROTO:协议错误。

    • 用于 J1939 中的各种协议级别错误,包括

      • 重复的序列号。

      • 意外的 EDPO 或 ECTS 数据包。

      • EDPO/ECTS 中的无效 PGN 或偏移量。

      • EDPO 数据包的数量超过了 CTS 允许的数量。

      • 任何其他协议级别错误。

  • EMSGSIZE: 消息过长。

  • ENOMSG: 没有可用的消息。

  • EALREADY: ECU 已经参与一个或多个连接管理会话,并且无法支持另一个。

  • EHOSTUNREACH: 发生超时,会话已中止。

  • EBADMSG: 在活动数据传输期间接收到 CTS (允许发送) 消息,导致中止。

  • ENOTRECOVERABLE: 达到最大重传请求限制,会话无法恢复。

  • ENOTCONN: 接收到意外的数据传输包。

  • EILSEQ: 接收到错误的序列号,软件无法恢复。