0%

Linux 源码中的 CAN 总线

下面是学习 Linux 源码中与 CAN 总线相关部分的笔记,基于 Linux 5.10 版本。

CAN 总线

  • 多主控制
    • 总线空闲时所有单元都可以发送消息(CSMA)
    • 最先访问总线的单元可以获得发送权
    • 多个单元同时发送时,通过消息标识符来确定发送权(CD+AMP)
  • 没有 “地址”,通过标识符来区别消息,使得节点无需了解其他节点的状况,相对独立工作
  • 总线上的电平分显性电平(0)与隐性电平(1)
  • 广播形式,即在同一时刻网络上所有节点侦测的数据是一致的。

数据帧格式

  • 帧起始:一个显性位
  • 仲裁段:即标识符,11 位,加上一个 RTR 位用于区别数据帧和远程帧
  • 控制段:两个保留位,4 位数据长度码(DLC),表示数据载荷(0-8 字节)
  • 数据段:包含 0-8 字节数据
  • CRC 段:15 位 CRC 顺序 + 1 位 CRC 界定符
  • ACK 段:确认是否正常接收。1 位 ACK 槽 + 1 位 ACK 界定符
  • 帧结束:7 位隐性位

非破坏性仲裁

起始位 10 9 8 7 6 5 4 3 2 1 0 剩余部分
节点 1 0 0 0 0 0 0 0 0 1 1 1 1
节点 2 0 0 0 0 0 0 0 1 - - - -
总线 0 0 0 0 0 0 0 0 1 1 1 1

Linux 下的 CAN

Linux 下最早使用 CAN 的方法是基于字符设备来实现的,SocketCAN 则使用 socket 接口和 linux 网络协议栈,使得 CAN 设备驱动可通过网络接口调用。SocketCAN 接口设计接近 TCP/IP 协议,使程序员能比较容易的学习和使用。

SocketCAN 允许多个应用程序同时访问一个 CAN 设备,且单个应用程序可访问多个 CAN 网络,由于其构建于 linux 网络层上,可以直接使用已有的队列功能,CAN 控制器的设备驱动将自己作为一个网络设备注册进 linux 的网络层。

首先我们要打开一个套接字,由于 SocketCAN 实现了一个新的协议族,所以需要将 PF_CAN 作为第一个参数传递给 socket 系统调用。假如我们打开一个 CAN_RAW 协议的套接字:

1
2
s = socket (PF_CAN, SOCK_RAW, CAN_RAW);
//int socket (int family, int type, int protocol);

创建套接字后,我们一般使用 bind 系统调用去将其绑定到一个 CAN 接口上(与 TCP/IP 不同,因为 CAN 寻址机制不同)。然后我们就可以用 recv ()/send ()recvmsg ()/sendmsg ()recvfrom ()/sendto () 来读写数据了。

CAN 帧

5.10 版的 CAN 帧结构体定义如下:

1
2
3
4
5
6
7
8
9
/include/uapi/linux/can.h
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* frame payload length in byte (0 .. CAN_MAX_DLEN) */
__u8 __pad; /* padding */
__u8 __res0; /* reserved /padding */
__u8 __res1; /* reserved /padding */
__u8 data [CAN_MAX_DLEN] __attribute__((aligned (8)));
};

注意在 5.11 中,can_dlc 被弃用了,使用 len 来代替。实际上表示同一个东西,都表示有效负载长度,只是 can_dlc 的命名有误导性(Data Length Code)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//include/uapi/linux/can.h
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
union {
/* CAN frame payload length in byte (0 .. CAN_MAX_DLEN)
* was previously named can_dlc so we need to carry that
* name for legacy support
*/
__u8 len;
__u8 can_dlc; /* deprecated */
};
__u8 __pad; /* padding */
__u8 __res0; /* reserved /padding */
__u8 len8_dlc; /* optional DLC for 8 byte payload length (9 .. 15) */
__u8 data [CAN_MAX_DLEN] __attribute__((aligned (8)));
};

sockaddr_can

PF_PACKET 套接字类似,sockaddr_can 结构有一个接口编号,绑定在一个特定的接口上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//include/uapi/linux/can.h
struct sockaddr_can {
__kernel_sa_family_t can_family;
int can_ifindex;
union {
/* transport protocol class address information (e.g. ISOTP) */
struct { canid_t rx_id, tx_id; } tp;

/* J1939 address information */
struct {
/* 8 byte name when using dynamic addressing */
__u64 name;

/* pgn:
* 8 bit: PS in PDU2 case, else 0
* 8 bit: PF
* 1 bit: DP
* 1 bit: reserved
*/
__u32 pgn;

/* 1 byte address */
__u8 addr;
} j1939;

/* reserved for future CAN protocols address information */
} can_addr;
};

为了决定接口编号,需要使用 ioctl () 函数,例如我们想要将套接字绑定在 can0 设备上:

1
2
3
4
5
6
7
8
9
10
11
12
13
int s;
struct sockaddr_can addr;
struct ifreq ifr;

s = socket (PF_CAN, SOCK_RAW, CAN_RAW);

strcpy(ifr.ifr_name, "can0" );
ioctl (s, SIOCGIFINDEX, &ifr);

addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;

bind (s, (struct sockaddr *)&addr, sizeof(addr));

如果 can_ifindex 设为 0,那么将绑定到所有 CAN 接口上,此时套接字将接收来自所有 CAN 接口的 CAN 帧。如果要知道源 CAN 接口,需要使用 recvfrom 系统调用。如果想要发送,则需要使用 sendto 系统调用来指定传出接口。

读写

读示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct can_frame frame;

nbytes = read (s, &frame, sizeof(struct can_frame));

if (nbytes < 0) {
perror ("can raw socket read");
return 1;
}

/* paranoid check ... */
if (nbytes < sizeof(struct can_frame)) {
fprintf(stderr, "read: incomplete CAN frame\n");
return 1;
}

/* do something with the received CAN frame */

写也是类似的:

1
nbytes = write (s, &frame, sizeof(struct can_frame));

如果该套接字绑定了所有 CAN 接口(addr.can_ifindex == 0),可以使用 recvfrom 来获取源 CAN 接口的信息:

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_can addr;
struct ifreq ifr;
socklen_t len = sizeof(addr);
struct can_frame frame;

nbytes = recvfrom (s, &frame, sizeof(struct can_frame),
0, (struct sockaddr*)&addr, &len);

/* get interface name of the received CAN frame */
ifr.ifr_ifindex = addr.can_ifindex;
ioctl (s, SIOCGIFNAME, &ifr);
printf("Received a CAN frame from interface % s", ifr.ifr_name);

要在绑定到所有 CAN 接口的套接字上写 CAN 帧,必须明确指定传出接口:

1
2
3
4
5
6
7
strcpy(ifr.ifr_name, "can0");
ioctl (s, SIOCGIFINDEX, &ifr);
addr.can_ifindex = ifr.ifr_ifindex;
addr.can_family = AF_CAN;

nbytes = sendto (s, &frame, sizeof(struct can_frame),
0, (struct sockaddr*)&addr, sizeof(addr));

从套接字读取消息后,可通过 ioctl 调用获取准确时间戳:

1
2
struct timeval tv;
ioctl (s, SIOCGSTAMP, &tv);

CAN FD

一般来说 CAN FD(CAN flexible data rate)的处理和前面描述的示例类似,CAN FD 的 CAN 控制器支持 CAN FD 帧仲裁阶段和有效负载阶段的两种不同比特率,以及高达 64 字节的有效负载。这样破坏了内核接口(ABI),因为它们依赖于具有定长(8 字节)的有效载荷的 CAN 帧。因此 CAN_RAW 套接字支持一个新的套接字选项 CAN_RAW_FD_FRAMES,它可将套接字切换到能同时支持 CAN FD 帧和经典 CAN 帧的处理的模式。

1
2
3
4
5
6
7
8
9
//include/uapi/linux/can.h
struct canfd_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 len; /* frame payload length in byte */
__u8 flags; /* additional flags for CAN FD */
__u8 __res0; /* reserved /padding */
__u8 __res1; /* reserved /padding */
__u8 data [CANFD_MAX_DLEN] __attribute__((aligned (8)));
};

CAN 帧过滤器

使用 can_filter 可以过滤一些我们不关心的 CAN 帧,仅接受我们需要的帧:

1
2
3
4
struct can_filter {
canid_t can_id;
canid_t can_mask;
};

过滤器匹配当且仅当

1
<received_can_id> & mask == can_id & mask

使用示例:

1
2
3
4
5
6
7
8
struct can_filter rfilter [2];

rfilter [0].can_id = 0x123;
rfilter [0].can_mask = CAN_SFF_MASK;
rfilter [1].can_id = 0x200;
rfilter [1].can_mask = 0x700;

setsockopt (s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof (rfilter));

安全钩函数

SocketCAN 本身并未打钩函数,但是由于 SocketCAN 走的是 linux 的套接字的机制,而套接字是有钩函数的,例如:

  • socket_create:创建套接字时的权限检查
  • socket_post_create:为套接字创建一个安全结构体
  • socket_socketpair:创建套接字对的权限检查
  • socket_bind:bind 前的检查
  • socket_connect:connect 前的检查
  • socket_listen:listen 前的检查
  • socket_accept:接受一个新连接前的检查
  • socket_sendmsg:发消息前的检查
  • socket_recvmsg:收消息前的检查
  • socket_getsockname:获取本地套接字地址(名字)前的检查
  • socket_getpeername:获取远端套接字地址(名字)前的检查
  • socket_getsockopt:获取套接字选项前的检查
  • socket_setsockopt:设置套接字选项前的检查
  • socket_shutdown:关闭前的检查
  • socket_sock_rcv_skb:检查传入网络数据包的权限
  • sk_alloc_security
  • sk_free_security
  • sk_clone_security
  • sk_getsecid