CAN 应用编程基础-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板
CAN 应用编程基础
CAN 基础知识
什么是 CAN?
-
车载网络构想图
-
概述CAN协议
-
CAN是Controller Area Network的缩写,中文称为控制器局域网络
-
它是一种ISO国际标准化的串行通信协议
-
-
CAN总线的起源
-
最初由德国电气商博世公司开发
-
开发动机是解决现代汽车中电子控制系统之间的通讯问题,减少信号线的数量
-
通过设计一个单一的网络总线,所有外围器件可以连接到该总线上
-
-
汽车工业中的应用
-
现代汽车对安全性、舒适性、方便性、低公害、低成本的要求推动了各种电子控制系统的发展
-
电子控制系统包括发动机管理、变速箱控制、汽车仪表、空调、车门控制、灯光控制、气囊控制、转向控制、胎压监测、制动系统、雷达、自适应巡航、电子防盗系统等
-
随着电子控制系统的增加,系统之间通信所需的数据类型和可靠性要求不同,多条总线结构和线束数量随之增加
-
-
CAN协议的发展
-
为适应减少线束数量和实现大量数据高速通信的需求,博世公司在1986年开发了面向汽车的CAN通信协议
-
此后,CAN通过ISO11898和ISO11519进行了标准化
-
在欧洲,CAN已成为汽车网络的标准协议,并成为应用最广泛的现场总线之一
-
-
CAN的技术特点
-
CAN是一种多主方式的串行通讯总线
-
设计规范要求高的位速率、高抗电磁干扰性,并且能够检测出任何产生的错误
-
经过几十年的发展,CAN的高性能、高可靠性和高实时性已被广泛认可
-
-
CAN的应用领域
-
被广泛应用于工业自动化、船舶、医疗设备、工业设备等领域
-
以汽车电子为例,车上的空调、车门、发动机、大量传感器等部件和模块通过CAN总线连接,形成一个网络
-
CAN 的特点
-
多主控制
-
所有单元在总线空闲时都可以开始发送消息
-
最先访问总线的单元获得发送权(采用CSMA/CA方式)
-
多个单元同时发送时,发送高优先级ID消息的单元获得发送权
-
-
消息发送
-
所有消息以固定格式发送
-
总线空闲时,所有连接单元可以开始发送新消息
-
根据消息ID决定优先级,ID表示消息优先级而非目的地址
-
通过逐位仲裁比较ID,仲裁获胜的单元继续发送,失败的单元停止发送并转为接收
-
-
系统灵活性
- 连接单元没有“地址”信息,增加单元时不需要改变其他单元的软硬件及应用层
-
通信速度
-
根据网络规模设定合适的通信速度
-
同一网络中所有单元必须使用统一的通信速度
-
不同网络间可以有不同的通信速度
-
-
远程数据请求
- 通过发送“遥控帧”请求其他单元发送数据
-
错误检测与恢复
-
所有单元可以检测错误
-
检测到错误的单元会通知其他所有单元
-
发送单元检测到错误会强制结束发送,并反复重发直到成功
-
-
故障封闭
-
CAN能区分暂时数据错误和持续数据错误
-
发生持续数据错误时,可将故障单元从总线上隔离
-
-
连接能力
-
CAN总线可连接多个单元,理论上连接单元数无限制
-
实际连接数受总线时间延迟和电气负载限制,通信速度与连接单元数成反比
-
CAN 的电气属性
-
CAN 电气属性
-
CAN总线连接
-
使用两根线:CAN_H和CAN_L
-
通过判断这两根线上的电位差来确定总线电平
-
-
电平类型
-
CAN总线电平分为显性电平和隐性电平两种
-
显性电平
-
表示逻辑“0”
-
CAN_H电平为3.5V
-
CAN_L电平为1.5V
-
电位差为2V
-
-
隐性电平
-
表示逻辑“1”
-
CAN_H和CAN_L电压都为2.5V左右
-
电位差为0V
-
-
-
数据传输方式
- 通过显性和隐性电平的变化发送数据
-
总线空闲状态
- 当总线上没有节点传输数据时,总线处于隐性状态,即总线空闲时电平为隐性
CAN 网络拓扑
-
CAN 网络拓扑图
-
CAN总线简介
-
CAN是一种分布式控制总线,类似于以太网
-
CAN网络由多个CAN节点组成
-
-
节点结构
-
每个CAN节点非常简单,包括
-
一个MCU(微控制器)
-
一个CAN控制器
-
一个CAN收发器
-
-
通过CAN_H和CAN_L两根线连接在一起,形成CAN局域网络
-
-
物理介质
-
CAN可以使用多种物理介质,如双绞线、光纤等
-
最常用的是双绞线
-
-
信号传输
-
信号使用差分电压传送,线对称为CAN_H和CAN_L
-
开发板上的CAN接口使用这两条信号线,接口也只有这两条信号线
-
-
网络拓扑
-
所有CAN节点通过CAN_H和CAN_L连接在一起
-
CAN_H接CAN_H
-
CAN_L接CAN_L
-
-
CAN总线两端各接一个120Ω的端接电阻,用于匹配总线阻抗,吸收信号反射及回拨,提高抗干扰能力和可靠性
-
-
传输速度
-
CAN总线传输速度可达1Mbps
-
最新的CAN-FD最高速度可达5Mbps,甚至更高
-
传输速度与总线距离有关,总线距离越短,传输速度越快
-
CAN 总线通信模型
-
OSI 七层模型和 CAN 协议
-
CAN 总线协议与 OSI 模型的关系
-
CAN 总线传输协议参考了 OSI 七层模型
-
实际上,CAN 协议只定义了“传输层”、“数据链路层”以及“物理层”这三层
-
-
应用层协议
-
应用层协议可由 CAN 用户根据特定工业领域需求自行定义
-
常见的工业控制和制造业应用
- DeviceNet:适用于 PLC 和智能传感器
-
汽车工业应用
- 各制造商有各自的应用层协议标准
-
-
不同的应用层协议
-
用于自动化技术的现场总线标准:DeviceNet
-
工业控制:CanOpen
-
乘用车的诊断协议:OBD、UDS(统一诊断服务,ISO14229)
-
商用车的 CAN 总线协议:SAE J1939
-
-
数据链路层
-
分为 MAC 子层和 LLC 子层
-
MAC 子层是 CAN 协议的核心部分
-
主要功能
-
将物理层收到的信号组织成有意义的消息
-
提供错误控制和传输控制的流程
-
具体功能包括消息的帧化、仲裁、应答、错误的检测或报告
-
-
通常在 CAN 控制器的硬件中执行
-
-
物理层
-
定义信号的实际发送方式、位时序、位的编码方式及同步的步骤
-
具体未定义的内容(需用户根据系统需求确定)
-
信号电平
-
通信速度
-
采样点
-
驱动器和总线的电气特性
-
连接器的形态
-
-
CAN 帧的种类
-
CAN通信协议报文帧类型
-
定义了5种类型的报文帧
-
通信通过这5种帧进行
-
帧的种类及用途
-
-
数据帧的构成
-
数据帧和遥控帧格式
-
数据帧
-
使用最多的帧类型
-
数据帧和遥控帧有标准格式和扩展格式
-
标准格式:11位标识符(ID)
-
扩展格式:29位标识符(ID)
-
-
数据帧由 7 个段构成
-
帧起始
:表示数据帧开始的段 -
仲裁段:表示该帧优先级的段
-
控制段
:表示数据的字节数及保留位的段 -
数据段:
数据的内容,可发送0~8个字节的数据 -
CRC段
:检查帧的传输错误的段 -
ACK段
:表示确认正常接收的段 -
帧结束
:表示数据帧结束的段
-
-
-
-
帧结构符号
-
D:显性电平(0)
-
R:隐性电平(1)
-
D/R:显性或隐性(0或1)
-
-
更加详细的内容可以参考瑞萨电子编写的《CAN入门教程》
SocketCan 应用编程
概要
-
CAN设备管理
- 在Linux系统中,CAN设备被作为网络设备进行管理
-
SocketCAN应用编程接口
-
Linux提供了SocketCAN应用编程接口
-
使得CAN总线通信近似于以太网通信
-
提高了应用程序开发接口的通用性和灵活性
-
-
头文件
-
SocketCAN中的大部分数据结构和函数在头文件linux/can.h中定义
-
应用程序中必须包含<linux/can.h>头文件
-
创建 socket 套接字
-
CAN总线套接字的创建
-
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);
}
- 在创建套接字时,如果返回值小于0,表示创建失败- 使用perror函数输出错误信息,并调用exit(EXIT_FAILURE)退出程序- socket函数参数- 第一个参数:通信域- 在SocketCAN中,通常设置为PF_CAN,指定为CAN通信协议- 第二个参数:套接字类型- 通常设置为SOCK_RAW,表示原始套接字类型- 第三个参数:协议类型- 通常设置为CAN_RAW,指定CAN原始协议
将套接字与 CAN 设备进行绑定
-
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
-
用于指定网络接口名称和获取接口索引
-
定义在<net/if.h>头文件中
-
-
struct sockaddr_can
-
用于指定CAN协议族和接口索引
-
定义在<linux/can.h>头文件中
-
设置过滤规则
-
默认接收所有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));
- can_filter结构体成员- struct can_filter结构体中有两个成员- can_id- can_mask- 仅发送数据,不接收报文- 如果应用程序不接收所有报文(仅发送数据),可以在内核中省略接收队列,以减少CPU资源的消耗- setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);- 这里setsockopt函数的第4个参数设置为NULL,第5个参数设置为0
数据发送/接收
-
CAN总线与标准套接字通信的差异
-
每次通信都采用struct can_frame结构体将数据封装成帧
-
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位
-
第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;
- struct can_frame frame; //定义一个 can_frame 变量
-
-
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 */
…
…
- /* error class (mask) in can_id /
-
回环功能设置
-
在默认情况下,CAN的本地回环功能是开启的
-
使用setsockopt函数设置本地回环功能
- int loopback = 0; //0 表示关闭,1 表示开启(默认)
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
-
本地回环功能开启的情况下
- 所有的发送帧都会被回环到与CAN总线接口对应的套接字上
CAN 应用编程实战
测试
- 配置 CAN 设备,使用 cansend 命令发送数据,使用 candump 命令接收数据,利用 CAN 分析仪进行测试
CAN 数据发送实例
- 每隔 1 秒中通过 can0 发送一帧数据,一次发送 6 个字节数据,帧 ID 为 0x123
#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}; // 定义网络接口请求结构体,并初始化为0struct sockaddr_can can_addr = {0}; // 定义CAN套接字地址结构体,并初始化为0struct can_frame frame = {0}; // 定义CAN帧结构体,并初始化为0int sockfd = -1; // 定义套接字描述符,并初始化为-1int ret; // 定义返回值变量/* 打开套接字 */sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW); // 创建原始CAN套接字if(0 > sockfd) {perror("socket error");exit(EXIT_FAILURE);}/* 指定can0设备 */strcpy(ifr.ifr_name, "can0"); // 设置网络接口名称为can0ioctl(sockfd, SIOCGIFINDEX, &ifr); // 获取can0的接口索引can_addr.can_family = AF_CAN; // 设置地址族为AF_CANcan_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套接字:使用socket函数创建一个原始CAN套接字,并检查是否成功
-
指定CAN设备:通过strcpy函数设置网络接口名称为can0,然后使用ioctl函数获取can0的接口索引,并设置CAN套接字地址结构体
-
绑定套接字:使用bind函数将套接字绑定到指定的CAN接口can0,并检查绑定是否成功
-
设置过滤规则:通过setsockopt函数设置套接字选项,使得套接字不接收任何报文,仅用于发送数据
-
准备发送数据:设置要发送的CAN帧数据,包括数据字节、数据长度码(DLC)和帧ID
-
循环发送数据:在一个无限循环中,使用write函数发送CAN帧数据,并检查发送是否成功。如果发送失败,程序会跳转到out标签处
-
间隔发送数据:在每次发送数据后,程序会调用sleep函数,使得数据每隔一秒钟发送一次
-
关闭套接字并退出:在程序结束时,使用close函数关闭套接字,并调用exit函数以成功状态退出程序
-
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}; // 定义网络接口请求结构体,并初始化为0struct sockaddr_can can_addr = {0}; // 定义CAN套接字地址结构体,并初始化为0struct can_frame frame = {0}; // 定义CAN帧结构体,并初始化为0int sockfd = -1; // 定义套接字描述符,并初始化为-1int 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"); // 设置网络接口名称为can0ioctl(sockfd, SIOCGIFINDEX, &ifr); // 获取can0接口索引can_addr.can_family = AF_CAN; // 设置地址族为AF_CANcan_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))) { // 读取CAN帧数据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套接字:使用socket函数创建一个原始CAN套接字,并检查是否成功
-
指定CAN设备:通过strcpy函数设置网络接口名称为can0,然后使用ioctl函数获取can0的接口索引,并设置CAN套接字地址结构体
-
绑定套接字:使用bind函数将套接字绑定到指定的CAN接口can0,并检查绑定是否成功
-
接收数据
-
使用无限循环持续接收CAN帧
-
使用 read 函数读取CAN帧数据,如果读取失败,输出错误信息并退出循环
-
校验接收到的帧是否为错误帧,如果是,输出错误信息并退出循环
-
校验帧格式(扩展帧或标准帧)并打印帧ID
-
校验帧类型(数据帧或远程请求帧),如果是远程请求帧,输出信息并继续下一次循环
-
打印帧数据长度
-
打印帧数据的每个字节
-
-
关闭套接字并退出:在程序结束时,使用close函数关闭套接字,并调用exit函数以成功状态退出程序