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_ADDR
、 SCM_J1939_DEST_NAME
或 SCM_J1939_PRIO
。返回的数据是 uint8_t
用于 priority
和 dst_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));