CAN 是目前应用非常广泛的现场总线之一,主要应用于汽车电子工业领域,尤其是汽车领域,汽车上大量的传感器与模块都是通过 CAN 总线连接起来的。CAN 总线目前是自动化领域发展的热点技术之一,由于其高可靠性,CAN 总线目前广泛的应用于工业自动化、船舶、汽车、医疗和工业设备等方面。

目录

1.CAN 基础知识

1.1 协议层
1.2 编程实战

2.SocketCan 应用编程

2.1 创建 socket 套接字
2.2 将套接字与 CAN 设备进行绑
2.3 设置过滤规则
2.4 数据发送/接收

2.4.1 数据发送
2.4.2 数据发送
2.2.3 错误处理

2.5 回环功能设置

3.CAN 应用编程实战

3.1 设备测试

3.1.1 发送功能测试
3.1.2 接收功能测试

3.2 CAN 数据发送实战
3.3 CAN 数据接收实战

1. CAN 基础知识

1.1 协议层

在学习 CAN 应用编程之前,需学习CAN 相关的基础知识,个人建议,过一遍就好,某些细节不清楚没关系,请移步文首附件《CAN 入门教程》或作者博文,都详细讲解了CAN协议层的理论知识

【CAN总线协议】CAN通信入门总览:常见协议优劣、CAN应用、协议组成与标准、传输原理的实现、仲裁机制、传输与时序初探

【CAN总线协议】CAN接收报文(协议层:帧的五个种类、仲裁机制、错误的种类、位填充与位时序、同步方法;)

【CAN总线协议】CAN 协议架构及标准规格

【CAN总线协议】错误状态与计数值

【CAN总线协议】CAN通信的特点

1.2 编程实战

协议层了解差不多,直接来应用编程实战就好了,CAN实操学习步骤大致如下(个人见解,欢迎讨论,小白有问题可私信):

  1. CANdb++Editor安装包及教程移步:
    【安装手册】CANdb++ Editor
  2. DBC文件如何看懂,怎么使用?做一个就知道了:
    【使用手册】CANdb++Editor:自制一个DBC文件
  3. 雅特力AT32 CAN使用入门:
    【雅特力AT32 CAN】 MCU CAN入门使用指南(超详细)
  4. can数据收发实战:
    【CAN 数据收发实战】上位机ZCANPRO发送+USART打印DBC文件发送的报文信息——以雅特力AT32为例
    【以雅特力AT32为例】CAN过滤器及其原理与邮箱配置
  5. CAN报文解析:
    【DBC报文矩阵分析】读懂.DBC文件报文矩阵,信号矩阵数据解析思路(源码见链接)
    【数据存储】大/小端存储与字节顺序转换函数详解
  6. 解析代码实现移步:
    【CAN报文数据解析】矩阵信号Intel与Motorola格式(C语言)代码
  7. SocketCan 应用编程与Socket网络编程及其相似:
    【CAN 应用编程基础】SocketCan实战
  8. 学完QT,尝试试自己做一个CAN上位机软件,基本掌握CAN;
    【自制 CAN上位机】MY_CAN Tool V1.0 (GitHub开源)

学完这些就基本掌握CAN啦,不过我们的征途是星辰大海,要学的还很多,咱直接看企业需求:

在这里插入图片描述


2.SocketCan 应用编程

​ 由于 Linux 系统将 CAN 设备作为网络设备进行管理,因此在 CAN 总线应用开发方面,Linux 提供了

SocketCAN 应用编程接口,使得 CAN 总线通信近似于和以太网的通信,应用程序开发接口更加通用,也更

加灵活。

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

中一定要包含<linux/can.h>头文件

2.1 创建 socket 套接字

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

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

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。、

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

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

......
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>头文件中

2.3 设置过滤规则

如果没有设置过滤规则,应用程序默认会接收所有 ID 的报文;

如果应用程序只需要接收某些特定 ID 的报文(亦或者不接受所有报文,只发送报文),则可以通过 setsockopt 函数

设置过滤规则:

/* 譬如某应用程序只接收 ID 为 0x60A 和 0x60B 的报文帧 */

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(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
/* setsockopt()函数的第 4 个参数设置为 NULL,将第 5 个参数设置为 0 */
2.4 数据发送/接收

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

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 位。

/* 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 */
2.4.1 数据发送

对于数据发送,使用 write()函数来实现:

/* 譬如要发送的数据帧包含了三个字节数据 0xA0、0xB0 以及 0xC0,帧 ID 为 123,可采用如下方法进行发送: */
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),可采用如下方法进行发送: */
struct can_frame frame;
frame.can_id = CAN_RTR_FLAG | 123;
write(sockfd, &frame, sizeof(frame));
2.4.2 数据发送

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

struct can_frame frame;
int ret = read(sockfd, &frame, sizeof(frame));
2.2.3 错误处理

应用程序接收到一帧数据之后,可以通过判断 can_id 中的 CAN_ERR_FLAG 位判是否为错误帧

如果为错误帧,可以通过 can_id 的其他符号位来判断错误的具体原因。

/* 错误帧的符号位在头文件<linux/can/error.h>中定义: */
/* 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 */
......
......
2.5 回环功能设置

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

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

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


3. CAN 应用编程实战

以正点原子I.MX6U开发板为例,在 Linux 系统中,CAN 总线设备作为网络设备被系统进行统一管理

在控制台下,CAN 总线的配置和以太网的配置使用相同的命令,使用 ifconfig 命令查看 CAN 设备,如下所示:

在这里插入图片描述

开发板上只有一个 CAN 接口,即 can0:

在这里插入图片描述

3.1 设备测试

​ 想要进行测试,可以使用两块开发板或者CAN测试设备,譬如 CAN 分析仪、CANFDD200U板卡,这里笔者使用 CAN 分析仪;购买CAN 测试设备,就直接使用它进行测试即可,简单好用,配套全面(上班去公司都有)!

使用:
	在测试之前,使用 CAN 分析仪或者其它测试 CAN 的设备连接到 ALPHA|Mini 开发板底板上的 CAN 接口处,CANH 连接到仪器的 CANH、CANL 连接到 CAN 仪器的 CANL;
	打开 CAN 分析仪配套的上位机软件,笔者使用的是创芯科技的推出的一款 CAN 分析仪,其配套的上位机软件界面如下所示:

注:学完QT可以试试自己做一个CAN上位机软件,以下为创芯科技推出的CAN 分析仪及配套上位机。
在这里插入图片描述

设备连接好之后,通过上位机软件启动 CAN 分析仪,接下来我们进行测试。

在进行测试之前需要对开发板上的 can 设备进行配置,执行以下命令:

注意,CAN 分析仪设置的波特率要和开发板 CAN 设备的波特率一致

ifconfig can0 down #先关闭 can0 设备
ip link set can0 up type can bitrate 1000000 triple-sampling on #设置波特率为 1000000
3.1.1 发送功能测试

上述配置完成,可以使用 cansend 命令发送数据:

cansend can0 123#01.02.03.04.05.06.07.08

在这里插入图片描述

#”号前面的 123 表示帧 ID,后面的数字表示要发送的数据,此时上位机便会接收到开发板发送过来的数据,如下所示:

在这里插入图片描述

3.1.2 接收功能测试

接着测试开发板接收 CAN 数据,首先在开发板上执行 candump 命令

candump -ta can0

接着通过 CAN 分析仪上位机软件,向开发板发送数据,如下所示:

在这里插入图片描述

此时开发板便能接收到上位机发送过来的数据,如下所示:

在这里插入图片描述

测试完成之后,按 Ctrl + C 退出 candump 程序,关于 CAN 的测试就到这里。

3.2 CAN 数据发送实战
						/* 示例代码 CAN 数据发送示例代码 */
#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};				// 配置或获取网络接口信息,结合ioctl与网络接口交互
    struct sockaddr_can can_addr = {0};	// 专门为 CAN 网络协议定义的套接字地址结构体
    struct can_frame frame = {0};		// CAN数据帧
    int sockfd = -1;
    int ret;
    
    /* 打开套接字,协议族/地址族: CAN 总线协议、套接字类型:原始套接字、协议:CAN */
    sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if(0 > sockfd) 
    {
        perror("socket error");
        exit(EXIT_FAILURE);
 	}
    
     /* 指定 can0 设备,获取 can0 接口的索引值 */
     strcpy(ifr.ifr_name, "can0");
     ioctl(sockfd, SIOCGIFINDEX, &ifr);
     /* 设置 CAN 地址族 */
     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_write 拷贝到开发板 Linux 系统/home/root 目录下,然后运行该程序;

在这里插入图片描述

现象

每隔 1 秒中通过 can0 发送一帧数据,一次发送 6 个字节数据,帧 ID 为 0x123,此时 CAN 上位机便会接收到开发板发送过来的数据,如下所示:

在这里插入图片描述

3.3 CAN 数据接收实战
							/* 示例代码 31.3.2 CAN 数据接收示例代码 */
#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);
}

编译

在这里插入图片描述

现象

编译得到的可执行文件 can_read 拷贝到开发板 Linux 系统/home/root 目录下,然后执行程序,接着通过 CAN 上位机向开发板发送数据,此时开发板便会接收到上位机发送过来的数据,如下所示:
在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐