Linux应用层例程7 CAN 应用编程基础
CAN 是一种多主方式的串行通讯总线,基本设计规范要求有高的位速率,高抗电磁干扰性,而且能够检测出产生的任何错误。经过几十年的发展,现在,CAN的高性能、高可靠性以及高实时性已被认同,并被广泛地应用于工业自动化、船舶、医疗设备、工业设备等方面。以汽车电子为例,汽车上有空调、车门、发动机、大量传感器等,这些部件、模块都是通过 CAN总线连在一起形成一个网络,车载网络构想图如下所示:CAN 总线使用两
·
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
总线就通过显性和隐形电平的变化来将具体的数据发送出去,如下图所示:
CAN 网络拓扑
CAN 是一种分布式的控制总线,CAN 总线作为一种控制器局域网,和普通的以太网一样,它的网络由很多的 CAN 节点构成,其网络拓扑结构如下图所示
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 和智能传感器设计的。在汽车工业,许多制造商都有他们自己的应用层协议标准。
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);
}
更多推荐
已为社区贡献1条内容
所有评论(0)