一 问题
一般来说,使用SocketCAN在CAN总线上发送/接受报文,都是使用read/write函数,下面是一个完整的使用read/write函数进行操作的代码,
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <thread>
void receiveThread(int sock)
{
struct can_frame frame;
int nbytes = read(sock, &frame, sizeof(struct can_frame));
if (nbytes < 0)
{
std::cout << "Error: can raw socket read.\n";
return;
}
if (nbytes < sizeof(struct can_frame))
{
std::cout << "Error: read incomplete CAN frame.\n";
return;
}
printf("Receive OK\n");
// 打印接收到的报文数据
printf("ID=0x%X DLC=%d Data: ", frame.can_id, frame.can_dlc);
for (uint8_t i = 0; i < frame.can_dlc; ++i)
{
printf("0x%02X ", frame.data[i]);
}
printf("\n");
}
int main()
{
int nbytes;
struct sockaddr_can vcan0Addr;
struct ifreq ifr;
struct can_frame frame = {0};
int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
// 获取vcan0的接口索引
strcpy(ifr.ifr_name, "vcan0");
ioctl(s, SIOCGIFINDEX, &ifr);
vcan0Addr.can_family = AF_CAN;
vcan0Addr.can_ifindex = ifr.ifr_ifindex; // 把vcan0的接口索引赋值给vcan0Addr
// 把vcan0Addr绑定到socket上
bind(s, (struct sockaddr *)&vcan0Addr, sizeof(vcan0Addr));
// 设置过滤,只接收cob-id是0x590的CAN报文
struct can_filter rfilter;
rfilter.can_id = 0x590;
rfilter.can_mask = CAN_SFF_MASK;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
// 开启接收回复的线程
std::thread t(receiveThread, s);
// 准备数据
frame.can_id = 0x610; // cob-id
frame.can_dlc = 8; // 数据长度为8个字节
frame.data[0] = 0x40;
frame.data[1] = 0x01;
frame.data[2] = 0x10;
frame.data[3] = 0x00;
frame.data[4] = 0x00;
frame.data[5] = 0x00;
frame.data[6] = 0x00;
frame.data[7] = 0x00;
// 发送数据,发送完后CAN设备会有一个回应
nbytes = write(s, &frame, sizeof(frame));
if (nbytes != sizeof(frame))
{
std::cout << "Error\n";
}
t.join(); // 等待接收结束
return 0;
}
注意,代码里发送的是一个CANOpen SDO报文,使用“candump vcan0”可以观察到如下报文,
可以看出一个socket对应一个CAN接口,如果有多个CAN接口,按照这种方式就需要多个socket,那么如何只使用一个socket操作多个CAN接口呢?
二 单个socket操作多路CAN总线
在linux的官方文档里给了答案,https://www.kernel.org/doc/html/v4.17/networking/can.html,文档里有句话如下,
To bind a socket to all(!) CAN interfaces the interface index must be 0 (zero). In this case the socket receives CAN frames from every enabled CAN interface. To determine the originating CAN interface the system call recvfrom(2) may be used instead of read(2). To send on a socket that is bound to ‘any’ interface sendto(2) is needed to specify the outgoing interface.
意思是给socket绑定地址时,其接口索引要为0,然后改用recvfrom/sendto来代替read/write。说的挺简单,但文档里没给详细的例子,本人调试并思考了很久才弄明白…
下面就直接上代码,假设有2个can接口 — vcan0和vcan1,我们使用一个socket在这2给接口上发送/接收报文,
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <thread>
void receiveThread(int sock)
{
while (true)
{
struct can_frame frame;
struct sockaddr_can addr;
struct ifreq ifr;
socklen_t len = 0;
recvfrom(sock, &frame, sizeof(struct can_frame), 0,
(struct sockaddr*)&addr, &len);
printf("Receive OK\n");
// 打印接收到的报文数据
printf("ID=0x%X DLC=%d Data: ", frame.can_id, frame.can_dlc);
for (uint8_t i = 0; i < frame.can_dlc; ++i)
{
printf("0x%02X ", frame.data[i]);
}
printf("\n");
}
}
int main(int, char**) {
int nbytes = 0;
struct sockaddr_can sockCan0, sockCan1;
struct ifreq ifr;
struct can_frame frame[2] = {{0}, {0}};
int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
struct sockaddr_can addr;
addr.can_family = AF_CAN;
addr.can_ifindex = 0; // 关键点, 接口索引为0 !!!
bind(s, (struct sockaddr *)&addr, sizeof(addr));
// 获取vcan0的接口索引
strcpy(ifr.ifr_name, "vcan0");
ioctl(s, SIOCGIFINDEX, &ifr);
sockCan0.can_family = AF_CAN;
sockCan0.can_ifindex = ifr.ifr_ifindex;
// 获取vcan1的接口索引
strcpy(ifr.ifr_name, "vcan1");
ioctl(s, SIOCGIFINDEX, &ifr);
sockCan1.can_family = AF_CAN;
sockCan1.can_ifindex = ifr.ifr_ifindex;
// 清除过滤设置,下面会重新设置
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
// 重新设置过滤规则,只接收cob-id是0x590和0x591的CAN报文
struct can_filter rfilter[2];
rfilter[0].can_id = 0x590;
rfilter[0].can_mask = CAN_EFF_MASK;
rfilter[1].can_id = 0x591;
rfilter[1].can_mask = CAN_EFF_MASK;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, rfilter, sizeof(rfilter));
// 开启接收线程
std::thread t(receiveThread, s);
// 准备发送到vcan0的报文数据
frame[0].can_id = 0x610;
frame[0].can_dlc = 8;
frame[0].data[0] = 0x40;
frame[0].data[1] = 0x01;
frame[0].data[2] = 0x10;
frame[0].data[3] = 0x00;
frame[0].data[4] = 0x00;
frame[0].data[5] = 0x00;
frame[0].data[6] = 0x00;
frame[0].data[7] = 0x00;
// 准备发送到vcan1的报文数据
frame[1].can_id = 0x611;
frame[1].can_dlc = 8;
frame[1].data[0] = 0x40;
frame[1].data[1] = 0x01;
frame[1].data[2] = 0x10;
frame[1].data[3] = 0x00;
frame[1].data[4] = 0x00;
frame[1].data[5] = 0x00;
frame[1].data[6] = 0x00;
frame[1].data[7] = 0x00;
// 向vcan0发送数据,注意使用的地址是sockCan0
nbytes = sendto(s, &frame[0], sizeof(struct can_frame),
0, (struct sockaddr*)&sockCan0, sizeof(sockCan0));
if (nbytes < sizeof(struct can_frame))
{
printf("Error: sendto vcan0\n");
}
// 向vcan1发送数据,注意使用的地址是sockCan1
nbytes = 0;
nbytes = sendto(s, &frame[1], sizeof(struct can_frame),
0, (struct sockaddr*)&sockCan1, sizeof(sockCan1));
if (nbytes < sizeof(struct can_frame))
{
printf("Error: sendto vcan1\n");
}
t.join();
close(s);
return 0;
}
这样就实现了一个socket向多个CAN接口发送/接收数据。特别注意代码里有3个类型为struct sockaddr_can的变量,其中一个专门用来实现接口索引为0这个条件的。
使用candump观测到的vcan0和vcan1上的报文如下,
接收线程这边打印如下,