CAN 是一种多主方式的串行通讯总线,基本设计规范要求有高的位速率,高抗电磁干扰性,而且能够检测出产生的任何错误。经过几十年的发展,现在,CAN
的高性能、高可靠性以及高实时性已被认同,并被广泛地应用于工业自动化、船舶、医疗设备、工业设备等方面。
以汽车电子为例,汽车上有空调、车门、发动机、大量传感器等,这些部件、模块都是通过 CAN
总线连在一起形成一个网络,车载网络构想图如下所示:
CAN 的电气属性
CAN 总线使用两根线来连接各个单元:
CAN_H
和
CAN_L
,
CAN
控制器通过判断这两根线上的电位差来得到总线电平,CAN
总线电平分为显性电平和隐性电平两种。
1)显性电平表示逻辑“
0
”,此时
CAN_H
电平比 CAN_L
高,分别为
3.5V
和
1.5V
,电位差为
2V
。
2)隐形电平表示逻辑“
1
”,此时
CAN_H
和
CAN_L
电压都为 2.5V
左右,电位差为
0V
。
CAN
总线就通过显性和隐形电平的变化来将具体的数据发送出去,如下图所示:
![](https://img-blog.csdnimg.cn/6ea6cc265c504f00a33ad028c881c632.png)
CAN 网络拓扑
CAN 是一种分布式的控制总线,CAN 总线作为一种控制器局域网,和普通的以太网一样,它的网络由很多的 CAN 节点构成,其网络拓扑结构如下图所示
![](https://img-blog.csdnimg.cn/3ede594d3fbb4de2962119d02d00f789.png)
CAN 网络的每个节点非常简单,均由一个
MCU
(微控制器)、一个
CAN
控制器和一个
CAN
收发器构成,然后通过 CAN_H
和
CAN_L
这两根线连接在一起形成一个
CAN
局域网络。
CAN
能够使用多种物理介质,例如双绞线、光纤等。最常用的就是双绞线。信号使用差分电压传送,两条信号线被称为“CAN_H
” 和“CAN_L
”,在我们的开发板上,
CAN
接口使用了这两条信号线,
CAN
接口也只有这两条信号线。
由此可知,CAN
控制器局域网和普通的以太网一样,每一个
CAN
节点就相当于局域网络中的一台主机。
途中所有的 CAN
节点单元都采用
CAN_H
和
CAN_L
这两根线连接在一起,
CAN_H
接
CAN_H
、
CAN_L 接 CAN_L
,
CAN
总线两端要各接一个
120
Ω的端接电阻,用于匹配总线阻抗,吸收信号反射及回拨,提高数据通信的抗干扰能力以及可靠性。
CAN 总线传输速度可达
1Mbps/S
,最新的
CAN-FD
最高速度可达
5Mbps/S
,甚至更高,
CAN-FD
不在本章讨论范围,感兴趣的可以自行查阅相关资料。CAN
传输速度和总线距离有关,总线距离越短,传输速度越快。
CAN 总线通信模型
CAN 总线传输协议参考了 OSI 开放系统互连模型,也就是前面所介绍的 OSI 七层模型(具体详情参考29.2 小节)。虽然 CAN 传输协议参考了 OSI 七层模型,但是实际上 CAN 协议只定义了“传输层”、“数据链路层”以及“物理层”这三层,而应用层协议可以由 CAN 用户定义成适合特别工业领域的任何方案。已在工业控制和制造业领域得到广泛应用的标准是 DeviceNet,这是为 PLC 和智能传感器设计的。在汽车工业,许多制造商都有他们自己的应用层协议标准。
![](https://img-blog.csdnimg.cn/65907881cfe046edbdeaac541921c2c9.png)
CAN 帧的种类
SocketCan 应用编程
由于 Linux
系统将
CAN
设备作为网络设备进行管理,因此在
CAN
总线应用开发方面,
Linux
提供了 SocketCAN 应用编程接口,使得
CAN
总线通信近似于和以太网的通信,应用程序开发接口更加通用,也更加灵活。
SocketCAN 中大部分的数据结构和函数在头文件
linux/can.h
中进行了定义,所以,在我们的应用程序 中一定要包含<linux/can.h>
头文件
操作一 创建 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 函数在
30.2.1
小节中给大家详细介绍过,第一个参数用于指定通信域,在
SocketCan
中,通常将其设置为PF_CAN
,指定为
CAN
通信协议;第二个参数用于指定套接字的类型,通常将其设置为
SOCK_RAW
;第三个参数通常设置为 CAN_RAW
。
操作二 将套接字与 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);
}
操作三 设置过滤规则
在我们的应用程序中,如果没有设置过滤规则,应用程序默认会接收所有 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()
函数的第
4
个参数设置为
NULL
,将第
5
个参数设置为
0
,如下所示:
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
操作四 数据发送/接收
在数据收发的内容方面,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
位。 can_id 的第
29
、
30
、
31
位是帧的标志位,用来定义帧的类型,定义如下:
/* 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
,可采用如下方法进行发送:
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)
、数据接收
数据接收使用
read()
函数来实现,如下所示:
struct can_frame frame;
int ret = read(sockfd, &frame, sizeof(frame));
(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 */
......
......
操作五 回环功能设置
在默认情况下,CAN 的本地回环功能是开启的,可以使用下面的方法关闭或开启本地回环功能,在本地回环功能开启的情况下,所有的发送帧都会被回环到与
CAN
总线接口对应的套接字上。
int loopback = 0; //0 表示关闭,1 表示开启(默认)
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
CAN 应用编程实战
本小节我们来编写简单地 CAN
应用程序。在
Linux
系统中,
CAN
总线设备作为网络设备被系统进行统一管理。在控制台下,CAN
总线的配置和以太网的配置使用相同的命令。
使用 ifconfig
命令查看
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};
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
数据接收实例
#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);
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)