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:虚拟终端(扩展传输协议)

动机

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

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

  • 动态寻址: J1939 中的地址声明是时间关键的。此外,在地址协商期间应正确处理数据传输。将此功能放入内核消除了_每个_通过 J1939 通信的用户空间进程的需求。这导致具有正确寻址的稳定 J1939 总线。

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

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

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

J1939 概念

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_family & can_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。返回的数据是 uint8_t 用于 prioritydst_addr,以及 uint64_t 用于 dst_name

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;
        }
}

动态寻址

必须区分使用已声明的地址和执行地址声明。要使用已声明的地址,必须填写 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 分配一起保留。从那时起,任何绑定到该名称的套接字都可以发送数据包。

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

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

发送示例

静态寻址

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

绑定

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));