【CAN 应用编程】SocketCan实战
由于 Linux 系统将 CAN 设备作为网络设备进行管理,因此在 CAN 总线应用开发方面,Linux 提供了SocketCAN 应用编程接口,使得 CAN 总线通信近似于和以太网的通信,应用程序开发接口更加通用,也更加灵活。SocketCAN 中大部分的数据结构和函数在头文件 linux/can.h 中进行了定义,所以,在我们的应用程序中一定要包含头文件。
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接收报文(协议层:帧的五个种类、仲裁机制、错误的种类、位填充与位时序、同步方法;)
1.2 编程实战
协议层了解差不多,直接来应用编程实战就好了,CAN实操学习步骤大致如下(个人见解,欢迎讨论,小白有问题可私信):
- CANdb++Editor安装包及教程移步:
【安装手册】CANdb++ Editor - DBC文件如何看懂,怎么使用?做一个就知道了:
【使用手册】CANdb++Editor:自制一个DBC文件 - 雅特力AT32 CAN使用入门:
【雅特力AT32 CAN】 MCU CAN入门使用指南(超详细) - can数据收发实战:
【CAN 数据收发实战】上位机ZCANPRO发送+USART打印DBC文件发送的报文信息——以雅特力AT32为例
【以雅特力AT32为例】CAN过滤器及其原理与邮箱配置 - CAN报文解析:
【DBC报文矩阵分析】读懂.DBC文件报文矩阵,信号矩阵数据解析思路(源码见链接)
【数据存储】大/小端存储与字节顺序转换函数详解 - 解析代码实现移步:
【CAN报文数据解析】矩阵信号Intel与Motorola格式(C语言)代码 - SocketCan 应用编程与Socket网络编程及其相似:
【CAN 应用编程基础】SocketCan实战 - 学完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 上位机向开发板发送数据,此时开发板便会接收到上位机发送过来的数据,如下所示:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)