CAN Linux C应用编程

由于 Linux 系统将 CAN 设备作为网络设备进行管理,因此在 CAN 总线应用开发方面, Linux 提供了SocketCAN 应用编程接口,使得 CAN 总线通信近似于和以太网的通信,应用程序开发接口更加通用,也更加灵活。

SocketCAN 中大部分的数据结构和函数在头文件 linux/can.h 中进行了定义,所以,在我们的应用程序中一定要包含<linux/can.h>头文件。

创建socket套接字

CAN 总线套接字的创建采用标准的网络套接字操作来完成, 网络套接字在头文件<sys/socket.h>中定义。

创建 CAN 套接字的方法如下:

c 复制代码
int sockfd=-1;
/* 创建套接字 */
sockfd=socket(PF_CAN,SOCK_RAW,CAN_RAW);
if(0>sockfd){
	perror("socket error");
	exit(EXIT_FAILURE);
}

socket函数中第一个参数用于指定通信域,在 SocketCan 中,通常将

其设置为PF_CAN,指定为CAN通信协议;第二个参数用于指定套接字的类型,通常将其设置为SOCK_RAW;第三个参数通常设置为 CAN_RAW。

将套接字与 CAN 设备进行绑定

将创建的套接字与 can0 进行绑定,示例代码如下所示:

c 复制代码
......
struct ifreq ifr = {0};
struct sockaddr_can can_addr = {0};
int ret;
......
strcpy(ifr.ifr_name, "can0"); //指定名字
ioctl(sockfd, SIOCGIFINDEX, &ifr);
can_addr.can_family = AF_CAN; //填充数据
can_addr.can_ifindex = ifr.ifr_ifindex;
/* 将套接字与 can0 进行绑定 */
ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
if (0 > ret) {
	perror("bind error");
	close(sockfd);
	exit(EXIT_FAILURE);
}

这里出现了两个结构体: struct ifreq 和 struct sockaddr_can,其中 struct ifreq 定义在<net/if.h>头文件中,而 struct sockaddr_can 定义在<linux/can.h>头文件中。

设置过滤规则

在我们的应用程序中,如果没有设置过滤规则,应用程序默认会接收所有 ID 的报文;如果我们的应用程序只需要接收某些特定 ID 的报文 (亦或者不接受所有报文,只发送报文),则可以通过 setsockopt 函数设置过滤规则, 譬如某应用程序只接收 ID 为 0x60A 和 0x60B 的报文帧,则可将其它不符合规则的帧全部给过滤掉, 示例代码如下所示:

c 复制代码
struct can_filter rfilter[2]; //定义一个 can_filter 结构体对象
// 填充过滤规则,只接收 ID 为(can_id & can_mask)的报文
rfilter[0].can_id = 0x60A;
rfilter[0].can_mask = 0x7FF;
rfilter[1].can_id = 0x60B;
rfilter[1].can_mask = 0x7FF;
// 调用 setsockopt 设置过滤规则
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

struct can_filter 结构体中只有两个成员, can_id 和 can_mask。

如果应用程序不接收所有报文, 在这种仅仅发送数据的应用中,可以在内核中省略接收队列,以此减少CPU 资源的消耗。 此时可将 setsockopt()函数的第 4 个参数设置为 NULL,将第 5 个参数设置为 0,如下所示:

c 复制代码
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);

数据发送/接收

在数据收发的内容方面, CAN 总线与标准套接字通信稍有不同,每一次通信都采用 struct can_frame 结构体将数据封装成帧。结构体定义如下:

c 复制代码
struct can_frame {
canid_t can_id; /* CAN 标识符 */
__u8 can_dlc; /* 数据长度(最长为 8 个字节) */
__u8 __pad; /* padding */
__u8 __res0; /* reserved / padding */
__u8 __res1; /* reserved / padding */
__u8 data[8]; /* 数据 */
};

can_id 为帧的标识符,如果是标准帧,就使用 can_id 的低 11 位;如果为扩展帧,就使用 0~28 位。

can_id 的第 29、 30、 31 位是帧的标志位,用来定义帧的类型,定义如下:

c 复制代码
/* special address description flags for the CAN_ID */
#define CAN_EFF_FLAG 0x80000000U /* 扩展帧的标识 */
#define CAN_RTR_FLAG 0x40000000U /* 远程帧的标识 */
#define CAN_ERR_FLAG 0x20000000U /* 错误帧的标识,用于错误检查 */
/* mask */
#define CAN_SFF_MASK 0x000007FFU /* <can_id & CAN_SFF_MASK>获取标准帧 ID */
#define CAN_EFF_MASK 0x1FFFFFFFU /* <can_id & CAN_EFF_MASK>获取标准帧 ID */
#define CAN_ERR_MASK 0x1FFFFFFFU /* omit EFF, RTR, ERR flags */

(1)、数据发送

对于数据发送,使用 write()函数来实现,譬如要发送的数据帧包含了三个字节数据 0xA0、 0xB0 以及

0xC0,帧 ID 为 123,可采用如下方法进行发送:

c 复制代码
struct can_frame frame; //定义一个 can_frame 变量
int ret;
frame.can_id = 123;//如果为扩展帧,那么 frame.can_id = CAN_EFF_FLAG | 123;
frame.can_dlc = 3; //数据长度为 3
frame.data[0] = 0xA0; //数据内容为 0xA0
frame.data[1] = 0xB0; //数据内容为 0xB0
frame.data[2] = 0xC0; //数据内容为 0xC0
ret = write(sockfd, &frame, sizeof(frame)); //发送数据
if(sizeof(frame) != ret) //如果 ret 不等于帧长度,就说明发送失败
perror("write error");

如果要发送远程帧(帧 ID 为 123),可采用如下方法进行发送:

c 复制代码
struct can_frame frame;
frame.can_id = CAN_RTR_FLAG | 123;
write(sockfd, &frame, sizeof(frame));

(2)、数据接收

数据接收使用 read()函数来实现,如下所示:

c 复制代码
struct can_frame frame;
int ret = read(sockfd, &frame, sizeof(frame));

(3)、错误处理

当应用程序接收到一帧数据之后,可以通过判断 can_id 中的 CAN_ERR_FLAG 位来判断接收的帧是否为错误帧。如果为错误帧,可以通过 can_id 的其他符号位来判断错误的具体原因。错误帧的符号位在头文

件<linux/can/error.h>中定义。

c 复制代码
/* error class (mask) in can_id */
#define CAN_ERR_TX_TIMEOUT 0x00000001U /* TX timeout (by netdevice driver) */
#define CAN_ERR_LOSTARB 0x00000002U /* lost arbitration / data[0] */
#define CAN_ERR_CRTL 0x00000004U /* controller problems / data[1] */
#define CAN_ERR_PROT 0x00000008U /* protocol violations / data[2..3] */
#define CAN_ERR_TRX 0x00000010U /* transceiver status / data[4] */
#define CAN_ERR_ACK 0x00000020U /* received no ACK on transmission */
#define CAN_ERR_BUSOFF 0x00000040U /* bus off */
#define CAN_ERR_BUSERROR 0x00000080U /* bus error (may flood!) */
#define CAN_ERR_RESTARTED 0x00000100U /* controller restarted */
......

回环功能设置

在默认情况下, CAN 的本地回环功能是开启的,可以使用下面的方法关闭或开启本地回环功能:

c 复制代码
int loopback = 0; //0 表示关闭, 1 表示开启(默认)
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));

在本地回环功能开启的情况下,所有的发送帧都会被回环到与 CAN 总线接口对应的套接字上。

CAN 数据发送实例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
int main(void)
{
	struct ifreq ifr = {0};
	struct sockaddr_can can_addr = {0};
	struct can_frame frame = {0};
	int sockfd = -1;
	int ret;
	/* 打开套接字 */
	sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
	if(0 > sockfd) {
		perror("socket error");
		exit(EXIT_FAILURE);
	}
	/* 指定 can0 设备 */
	strcpy(ifr.ifr_name, "can0");
	ioctl(sockfd, SIOCGIFINDEX, &ifr);
	can_addr.can_family = AF_CAN;
	can_addr.can_ifindex = ifr.ifr_ifindex;
	/* 将 can0 与套接字进行绑定 */
	ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
	if (0 > ret) {
		perror("bind error");
		close(sockfd);
		exit(EXIT_FAILURE);
	}
	/* 设置过滤规则:不接受任何报文、仅发送数据 */
	setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
	/* 发送数据 */
	frame.data[0] = 0xA0;
	frame.data[1] = 0xB0;
	frame.data[2] = 0xC0;
	frame.data[3] = 0xD0;
	frame.data[4] = 0xE0;
	frame.data[5] = 0xF0;
	frame.can_dlc = 6; //一次发送 6 个字节数据
	frame.can_id = 0x123;//帧 ID 为 0x123,标准帧
	for ( ; ; ) {
	ret = write(sockfd, &frame, sizeof(frame)); //发送数据
	if(sizeof(frame) != ret) { //如果 ret 不等于帧长度,就说明发送失败
		perror("write error");
		goto out;
	}
		sleep(1); //一秒钟发送一次
	}
	out:
		/* 关闭套接字 */
		close(sockfd);
		exit(EXIT_SUCCESS);
}

CAN 数据接收实例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
int main(void)
{
	struct ifreq ifr = {0};
	struct sockaddr_can can_addr = {0};
	struct can_frame frame = {0};
	int sockfd = -1;
	int i;
	int ret;
	/* 打开套接字 */
	sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
	if(0 > sockfd) {
		perror("socket error");
		exit(EXIT_FAILURE);
	}
	/* 指定 can0 设备 */
	strcpy(ifr.ifr_name, "can0");
	ioctl(sockfd, SIOCGIFINDEX, &ifr);
	can_addr.can_family = AF_CAN;
	can_addr.can_ifindex = ifr.ifr_ifindex;
	/* 将 can0 与套接字进行绑定 */
	ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
	if (0 > ret) {
		perror("bind error");
		close(sockfd);
		exit(EXIT_FAILURE);
	}
	/* 设置过滤规则 */
	//setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
	/* 接收数据 */
	for ( ; ; ) {
		if (0 > read(sockfd, &frame, sizeof(struct can_frame))) {
			perror("read error");
			break;
		}
		/* 校验是否接收到错误帧 */
		if (frame.can_id & CAN_ERR_FLAG) {
			printf("Error frame!\n");
			break;
		}
		/* 校验帧格式 */
		if (frame.can_id & CAN_EFF_FLAG) //扩展帧
			printf("扩展帧 <0x%08x> ", frame.can_id & CAN_EFF_MASK);
		else //标准帧
			printf("标准帧 <0x%03x> ", frame.can_id & CAN_SFF_MASK);
		/* 校验帧类型:数据帧还是远程帧 */
		if (frame.can_id & CAN_RTR_FLAG) {
			printf("remote request\n");
		continue;
	}
	/* 打印数据长度 */
	printf("[%d] ", frame.can_dlc);
	/* 打印数据 */
	for (i = 0; i < frame.can_dlc; i++)
		printf("%02x ", frame.data[i]);
		printf("\n");
	}
	/* 关闭套接字 */
	close(sockfd);
	exit(EXIT_SUCCESS);
}
相关推荐
Lary_Rock1 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
热爱跑步的恒川2 小时前
【论文复现】基于图卷积网络的轻量化推荐模型
网络·人工智能·开源·aigc·ai编程
云飞云共享云桌面3 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮4 小时前
Linux 使用中的问题
linux·运维
hikktn5 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
音徽编程5 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
观音山保我别报错5 小时前
C语言扫雷小游戏
c语言·开发语言·算法
dsywws5 小时前
Linux学习笔记之vim入门
linux·笔记·学习