Linux内核编程(十六)CAN总线驱动


  
在这里插入图片描述
   CAN设备属于网络设备,在开发板中,我们可以使用 ifconfig -a来查看CAN是否存在或开启。如果存在CAN设备can0,可以使用 ifconfig can0 up来启动can0,使用 ifconfig can0 down来关闭can0。如果需要设置can设备的波特率,则需要使用一些第三方的命令来设置,具体查看第八章的第4小节代码测试内容。

一、基础知识点

  1. 什么是CAN总线?
       CAN (Controller Area Network) 是博世公司于 1986 年开发的一种串行通信协议,主要应用于汽车领域。CAN 协议的诞生是为了减少汽车中线束的数量,使得汽车中的电子控制单元(ECU)能够互相通信。ECU 是汽车中的最小控制模块,通过 CAN 总线,各个 ECU 模块可以在同一个局域网内实现高效的数据传输和通信。CAN 总线就像是汽车电子的“局域网”,让不同模块之间能够无缝协作。

  2. CAN标准
       ISO 11898(高速 CAN):该标准定义了 CAN 的通信速率范围为 125 kbps 到 1 Mbps,主要用于需要高速数据传输的应用场景,如动力总成和汽车的关键控制系统。
       ISO 11519(低速 CAN):该标准定义了 CAN 的通信速率低于 125 kbps,适用于对传输速度要求不高、但对抗干扰和容错能力要求较高的应用场景,如车内的舒适性系统和传感器网络。

  3. CAN收发器
    CAN收发器主要功能是将CAN控制器的TTL信号转换成CAN总线的差分信号。
    在这里插入图片描述

  4. 两台设备使用CAN总线的硬件连接
    (1)CPU自带CAN控制器在这里插入图片描述
    (2)CPU不带CAN控制器
    在这里插入图片描述
       can 总线俩端 120欧姆为终端电阻,是为了消除总线上的信号反射。上图中当然可以一台设备的cpu带CAN控制器,一台设备不带CAN控制器也可以相互通信。

  5. 显性电平和隐形电平
    显性电平通常用字母D表示(代表Dominant),电平为0。
    隐性电平则用字母 R表示(代表Recessive),电平为1。它的优先级低于显性电平。当总线上没有显性电平时,隐性电平才会被读取为1。
    在通信协议中,如 CAN 总线,显性电平为0,隐性电平为1,当多个节点同时发送数据时,显性电平将占据总线。

  6. CAN的电气属性
    在这里插入图片描述
       使用差分信号,当 CANH(CAN 高线)和 CANL(CAN 低线)之间的电压相等(CANH = CANL = 2.5V),总线上处于隐形状态,这代表逻辑“1”。当 CANH 和 CANL 之间的电压相差 2V(CANH = 3.5V,CANL = 1.5V),总线进入显性状态,代表逻辑“0”。
       由于 CAN 使用差分信号传输,这种设计能够很好地抵抗电磁干扰。物理层通常采用双绞线作为传输介质,进一步增强了抗干扰能力,确保数据传输的稳定性和可靠性。

二、CAN协议

CAN 通信通过以下 5 种类型的帧进行:

类型作用
数据帧用于发送实际数据,包含要传输的消息内容。
遥控帧用于请求数据帧,没有实际数据,只是请求其他节点发送某个数据帧。
错误帧用于检测和报告传输过程中的错误,确保总线的通信完整性。
过载帧用于向总线发出过载信号,以延迟数据传输。
帧间隔用于区分相邻帧,提供帧之间的间隔。

   另外,数据帧和遥控帧有标准格式和扩展格式两种格式。标准格式有 11个位的标识符(称 ID),扩展格式有 29个位的ID。

1. 数据帧

(1)概念:CAN 通信主要通过数据帧来发送数据,数据帧负责携带数据,从发送设备传输到接收设备。
(2)类型:标准数据帧(常用):使用 11 位的标识符。扩展数据帧:使用 29 位的标识符。
在这里插入图片描述

(3)标准数据帧格式: 标准数据帧格式包含 7 个不同的位场。
在这里插入图片描述

位场作用
帧起始标识数据帧的开始。
仲裁字段用于决定总线的优先级,包含 11 位标识符和远程请求位。
控制字段用于表示数据长度(数据长度代码,DLC),决定数据字段的字节数。
数据字段实际传输的数据内容,长度为 0-8 字节。
CRC 字段用于传输过程中的错误检测,包含 CRC 校验值和 CRC 定界符。
ACK应答字段接收节点发送确认信号,表示成功接收到数据。
帧结束标识数据帧的结束,帧终止。

(4)帧格式分析
在这里插入图片描述
帧起始和帧结束
   帧起始:由1个显性位(逻辑“0”)组成,表示数据帧的开始。
   帧结束:由7个连续的隐性位(逻辑“1”)组成,表示数据帧的结束。

仲裁字段: 仲裁字段包括 11 位 ID 位,1位 RTR 位,共 12 位。
   ID 位:仲裁字段包括 11 位的标识符(ID 位),用于区分不同功能的数据帧,也用于确定优先级。根据 CAN 总线的仲裁机制,ID 值越小,优先级越高。当多个节点同时发送时,ID 小的数据帧会优先传输。禁止设置高7位为隐性(禁止设定 ID = 1111111xxXX),因为这种情况下仲裁会受到影响。
   RTR 位:远程传输请求(Remote Transmission Request,RTR)标志位,用于区分数据帧和遥控帧。数据帧的 RTR 位必须为显性 0(逻辑“0”)。遥控帧的 RTR 位必须为隐性 1(逻辑“1”)
   在仲裁规则中,若数据帧和遥控帧具有相同的 ID,则数据帧的优先级大于遥控帧。

控制字段:包含1 位 IDE 位、1 位 R0 保留位、4 位 DLC 位。
   IDE 位:用于区分数据帧类型。标准数据帧的 IDE 位固定为显性 0(逻辑“0”),扩展数据帧的 IDE 位为隐性 1(逻辑“1”)。
   R0 位:保留位,固定为显性 0(逻辑“0”),目前没有特定用途。
   DLC 位:4 位数据长度代码(DLC),用于表示数据字段中包含的数据字节数,范围为 0 到 8 字节。

数据字段:数据字段可以承载 0 到 64 位的数据,也就是 0 到 8 字节的数据。这是实际传输的数据内容。

CRC 校验字段:由 15 位 CRC 校验位和 1 位 CRC 界定符组成。
  前 15 位用于 CRC 校验,确保传输过程中的数据完整性。
  CRC 界定符:必须为隐性 1(逻辑“1”),用来分隔 CRC 校验位和后续字段。

ACK 字段:ACK 字段由 2 位组成:1 位确认槽位和 1 位确认界定符。
 &emsp确认槽位:接收方在此位置发送显性位(逻辑“0”)来表示成功接收数据。
 &emsp确认界定符:必须为隐性 1(逻辑“1”),用于确认信号的分隔。

2. 遥控帧(远程帧)

  遥控帧与数据帧的主要区别在于没有数据字段,因此遥控帧可以看作是去掉数据字段的数据帧。有些资料中也称遥控帧为远程帧。和数据帧一样,遥控帧也分为两种格式:标准格式和扩展格式。两种格式的遥控帧都包含以下 6 个部分:帧起始、仲裁字段、控制字段、CRC字段、应答字段、帧结尾。

●遥控帧用于接收设备主动请求数据。具体流程如下:
(1)发送方的正常广播:发送设备通过 CAN 总线广播数据帧,其他设备通过 ID 来识别并接收自己需要的数据。
(2)接收方的请求:如果接收设备需要的数据还没有被发送方广播,它可以通过发送一个遥控帧来请求该数据。遥控帧与数据帧类似,但没有携带实际数据,而是用相同的 ID 表示请求的内容。
(3)发送方响应:当其他设备(通常是拥有该数据的发送设备)接收到这个遥控帧后,它会识别出这是一个数据请求,并根据 ID 确定需要发送的数据内容。
(4)广播数据:发送设备在响应遥控帧后,会通过 CAN 总线广播该请求的数据帧,所有接收设备(包括发送遥控帧的接收方)都可以接收并使用这条数据。
  因此,遥控帧的作用是让接收设备在需要特定数据时,主动发出请求,触发发送设备广播该数据,确保接收方能够得到所需的数据。

在这里插入图片描述

3. 错误帧

  错误帧是在 CAN 总线通信中用于检测和报告错误的特殊帧。当某个节点检测到数据传输过程中发生错误时,会立即发送错误帧,通知其他节点停止当前数据传输,并重发数据帧。通过错误帧,CAN 总线确保了通信的可靠性,并且在发生错误时可以进行快速恢复和数据重发。

●错误帧由两个部分组成:错误标志(主动、被动)、错误定界符。
主动错误标志:由 6 个连续的显性位(逻辑“0”)组成,用于主动报告错误。主动错误标志由检测到错误的节点发送。
被动错误标志:由 6 个隐性位(逻辑“1”)组成,用于被动报告错误,表示该节点已经进入了“被动错误状态”。
错误定界符:由 8 个隐性位(逻辑“1”)组成,用于标识错误帧的结束。

4. 过载帧

  当接收节点准备好接收下一帧数据的时间不足时,它会发送过载帧(Overload Frame)来通知发送节点延迟下一次数据发送。这种机制确保了在接收节点负载较重时,数据传输能够得到有效管理,避免了数据丢失或通信冲突。

●过载帧格式:过载标志、过载界定符。
  过载标志:由 6 个连续的显性位(逻辑“0”)组成,与主动错误标志的构成相同。该标志用来指示接收节点需要额外的时间来处理接收到的数据,或准备接收下一帧数据。
  过载界定符:由 8 个隐性位(逻辑“1”)组成,与错误界定符的构成相同。该界定符标识过载帧的结束。

●过载帧的工作机制:
(1)当接收节点需要更多时间来准备接收下一帧数据时,它会立即发送一个过载帧。
(2)如果接收节点仍然无法准备好接收更多数据,它可以发送最多两条连续的过载帧来进一步延迟数据发送。
(3)发送节点在接收到过载帧后,会暂停当前的数据传输,并在接收到过载帧后的时间后重新尝试发送数据。

5. 帧间隔

帧间隔是用于分隔数据帧和遥控帧的帧。由三个隐性电平组成。过载帧和错误帧前不能插入帧间隔。
在这里插入图片描述

三、位填充

填充范围:从帧起始到 CRC 字段。
填充规则:在相同极性的5个连续位之后使用位填充。填充位与其前面的位极性相反。
在这里插入图片描述
在这里插入图片描述

四、位时间

  位时间是指在总线上传输一个二进制位所需的时间。它通常被分为四个阶段,每个阶段的时间长度由称为 Time Quantum (简称 TQ) 的最小时间单位决定。TQ 是决定总线传输速度和同步精度的基础单位。在典型的协议或系统中,这四个阶段可能代表了不同的功能或作用。
在这里插入图片描述

  1. 同步段(Sync Segment):该段用于同步发送端和接收端的时钟,通常是固定长度,代表一个位时间的开始。

  2. 传播段(Propagation Time Segment, Prop Seg):这个阶段用来补偿信号在总线上传播的延迟时间。延迟可能由线路长度或器件之间的延迟引起。

  3. 相位缓冲段 1(Phase Buffer Segment 1, Phase Seg1):用于处理信号抖动和其他不确定性。可以对这个阶段的时间进行调整,以便更好地同步发送端和接收端。

  4. 相位缓冲段 2(Phase Buffer Segment 2, Phase Seg2):与 Phase Seg1 类似,也用于调整和补偿信号传播中的不确定性。它出现在位时间的末尾,通常用于对接收到的信号进行最后的相位调整。

五、硬同步和再同步

1. 硬同步

  硬同步是在检测到一个特定信号边沿(通常是显性位到隐性位的过渡,或隐性位到显性位的过渡)时,强制将节点的时间同步到总线的位时间上。这个操作会直接将当前的时间调整到一个新的时刻,而不考虑当前已经计数的时间。因此,硬同步是一种瞬时的、强制性的同步方式,确保所有参与通信的节点在这个时刻重新校准它们的时钟。这种同步通常用于开始一个新的帧或重新校准时钟。

2. 再同步

  在 CAN 协议中,再同步是一种常见机制,用来确保节点之间的时钟偏差不会累积到影响数据传输的程度。当一个节点在总线上检测到显性位或隐性位的边沿,且这些边沿与本地时钟不同步时,就会触发再同步机制。
  再同步(Resynchronization)是指在通信过程中通过对时钟的微调来保持发送方和接收方之间的时序一致性。这种微调通常发生在信号传输过程中,通过对位时间的两个可调整阶段(相位缓冲段 1 和相位缓冲段 2)的长度进行修改来逐步校正时钟的相位偏差。
  在通信系统中,由于时钟频率的微小差异,节点间的时钟可能会逐渐偏移,从而导致接收错误。再同步的目的就是检测到总线上信号的边沿与本地时钟预期不同步时,进行调整,以便保持通信双方时钟同步,避免数据传输错误。

3. 两者的区别

硬同步:通常发生在帧的起始位,用来强制同步时钟,是一个瞬时的、强制性的过程。
再同步:在数据帧传输过程中,通过微调时钟来校正同步偏差,是一个逐步、细微的调整过程。

六、仲裁规则

在这里插入图片描述
  can总线上可以挂载多个通信结点,但是总线只有一条,同一时刻只能传输一个数据。那如何保证总线上的数据可以有序高效的传输呢。主要通过下面三种方式。这三种方法是用于多节点总线通信中的常见机制,尤其在像 CAN 总线这样允许多设备共享通信介质的协议中非常重要。这些方法能够确保多个节点在共享总线时能够有效地传输数据,而不会发生冲突。

  1. 非破坏性仲裁: 非破坏性仲裁机制的基本原理是通过显性和隐性位来决定优先级。显性位0的优先级高于隐性位1,而“线与机制”确保任何节点发送显性位时,总线状态为显性位。因此,当多个节点同时发送数据时,总线会显示优先级较高的数据。通过这个机制,多个节点可以在同一时间内开始发送数据,但只有优先级最高的节点最终占用总线,其他节点会自动退出。(即多个节点发送数据时,总线上的电平由多个节点的数据决定,全1为1,有0为0)
    (1)显性优先权规则:显性位优先级高于隐性位,即只要有一个节点发送显性0,总线状态就为0。而所有节点都发送隐性1时,总线状态才为1。
    (2)非破坏性:当多个节点同时发送数据时,低优先级节点会在仲裁过程中检测到自己的数据与总线状态不符,自动停止发送数据,而不会影响高优先级节点的数据传输,这就是“非破坏性仲裁”。

  2. 载波侦听: 载波侦听是为了避免数据冲突的一种手段。节点在发送数据前,会先侦听总线的状态,只有在检测到总线为空闲状态时,才允许节点开始发送数据。如果总线上已经有其他节点在通信,则该节点会等待总线变为空闲状态后再发送。通过侦听总线状态,可以确保只有在没有其他节点正在发送时,新的节点才能开始传输数据,这样可以有效避免多个节点同时发送数据造成的冲突。

  3. 回读机制: 回读机制允许节点在发送数据的同时,实时监控总线上的数据。节点在发送数据时,也会读取总线上的数据,并将其与自己发送的数据进行比较。通过这种方式,节点可以检测到是否发生了数据冲突(即自身发送的数据与总线状态不一致)。如果节点发现总线上的数据与自己发送的数据不一致,说明另一个节点的数据具有更高的优先级,节点就会停止发送数据,让优先级更高的节点继续传输。这种冲突检测是通过非破坏性仲裁来实现的。

举例:单元1和单元2在总线空闲时,同时发送数据,总线电平如何变化。
在这里插入图片描述

七、如何配置CAN?

将设备树的can总线的节点状态设置为使能,并且使用图形化界面将can总线驱动加载到镜像文件中即可。
在这里插入图片描述

八、CAN应用编程

详细的socket使用,查看这个文章。

1. 创建 CAN 设备的套接字

在使用 socket() 函数创建 CAN 设备的套接字时,确实有两种主要的方式,分别针对不同的通信需求。

(1) 原始套接字 (SOCK_RAW) 使用 CAN 原始协议 (CAN_RAW)
  这种方式使用 SOCK_RAWCAN_RAW,使得应用程序可以直接发送和接收原始的 CAN 帧,而无需关心 CAN 协议的具体实现细节。这种方式适用于需要完全控制 CAN 帧内容的情况,例如开发底层通信协议或测试 CAN 网络。

int socket_fd  = socket(PF_CAN, SOCK_RAW, CAN_RAW);
//PF_CAN:协议族,用于表示 CAN 网络。
//SOCK_RAW:套接字类型,表示原始套接字,用于直接发送/接收 CAN 帧。
//CAN_RAW:协议,表示使用 CAN 原始协议。

  这类套接字不处理高层协议,允许发送和接收任何合法的 CAN 帧。例如,它可以用于与特定 CAN 设备通信,或测试 CAN 网络的底层传输。

(2)数据报套接字 (SOCK_DGRAM) 使用 CAN 广播管理协议 (CAN_BCM)
  使用 SOCK_DGRAMCAN_BCM(广播管理协议,Broadcast Manager),这类套接字可以用来进行无连接的 CAN 网络通信,并且提供了更高层的抽象,允许应用程序发送和接收 CAN 广播消息。CAN_BCM 主要用于定期发送 CAN 帧或监听一组特定的 CAN 消息,并在接收到期望的消息时触发事件。

int socket_fd = socket(PF_CAN, SOCK_DGRAM, CAN_BCM);
/*
	PF_CAN:协议族,用于表示 CAN 网络。
	SOCK_DGRAM:套接字类型,表示数据报套接字,适用于无连接通信。
	CAN_BCM:协议,表示使用 CAN 广播管理协议。
*/

  CAN_BCM 适合在 CAN 网络中使用广播或周期性传输数据的应用。例如,在汽车中的控制器局域网络(CAN)环境中,这种协议可以用于周期性地发送传感器数据,或监听多个控制单元(ECU)的状态信息。

2. CAN 设备常用结构体

(1)struct sockaddr_can
  该结构体是CAN 套接字绑定时使用的地址结构体,包含了套接字所属的协议族以及 CAN 网络接口的信息。

struct sockaddr_can {
    sa_family_t can_family;   // 协议族,必须设置为 AF_CAN
    int can_ifindex;          // CAN 网络接口索引,通过 ioctl 获取的接口索引,用于标识哪个 CAN 接口(如 can0 或 can1)进行通信。
    union {
        struct { 
            canid_t rx_id, tx_id;  // CAN 接收ID和发送ID,CAN_ISOTP使用
        } tp;
    };
};

(2)struct ifreq
  该结构体是用于设备接口操作的结构体,常用于通过 ioctl() 系统调用配置网络设备,获取网络接口的配置等操作。在 CAN 套接字中,struct ifreq 主要用于获取 CAN 接口索引。

struct ifreq {
    char ifr_name[IFNAMSIZ];  // 网络接口名称(如 "can0")
    union {
        struct sockaddr ifr_addr;  // 套接字地址
        struct sockaddr ifr_dstaddr;  // 目的套接字地址
        struct sockaddr ifr_broadaddr;  // 广播地址
        struct sockaddr ifr_netmask;  // 网络掩码
        struct sockaddr ifr_hwaddr;  // 硬件地址
        short ifr_flags;  // 接口标志
        int ifr_ifindex;  // 接口索引
        int ifr_metric;  // 路由指标
        int ifr_mtu;  // 最大传输单元
        struct ifmap ifr_map;  // 设备映射(物理地址)
        char ifr_slave[IFNAMSIZ];  // 绑定设备名称
        char ifr_newname[IFNAMSIZ];  // 新的设备名称
        char *ifr_data;  // 特殊接口数据
    };
};

(3)struct can_frame
  是 Linux 中定义的结构体,用于表示一个 CAN 帧。它定义在 linux/can.h 头文件中,用于原始套接字 CAN(SOCK_RAW)通信。结构体包含了 CAN 帧的标识符、数据长度和数据内容。

struct can_frame {
    canid_t can_id;  // CAN 帧的 ID,32 位整型。包含帧类型和标志位。
    __u8    can_dlc; // 数据长度代码 (DLC),0~8,表示数据字节数。
    __u8    __pad;   // 填充位,确保结构体大小一致
    __u8    __res0;  // 保留位
    __u8    __res1;  // 保留位
    __u8    data[8]; // 实际的 CAN 帧数据,最大 8 字节。
};

帧类型:

#define CAN_EFF_FLAG 0x80000000U //(扩展帧标志)
#define CAN_RTR_FLAG 0x40000000U //(远程传输请求标志)
#define CAN_ERR_FLAG 0x20000000U//(错误帧标志)

3. 具体代码

can_send.c

#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <unistd.h>

int main() {
    int socket_fd, ret;
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame[3]; // CAN 帧数组
    unsigned char data[2] = {0x01, 0x02};  // 要发送的数据

    // 设置第一个 CAN 帧
    frame[0].can_id = 0x11;  // 标准帧 ID
    frame[0].can_dlc = 2;    // 数据长度代码 (DLC),2 字节
    memcpy(frame[0].data, data, frame[0].can_dlc);  // 复制数据

    // 设置第二个 CAN 帧,使用扩展帧格式(EFF)
    frame[1].can_id = 0x11 | CAN_EFF_FLAG;  // 扩展帧 ID,带扩展帧标志
    frame[1].can_dlc = 2;                   // 数据长度代码 (DLC),2 字节
    memcpy(frame[1].data, data, frame[1].can_dlc);  // 复制数据

    // 设置第三个 CAN 帧,使用远程传输请求(RTR)
    frame[2].can_id = 0x11 | CAN_RTR_FLAG;  // 标准帧 ID,带远程传输请求标志
    frame[2].can_dlc = 2;                   // 数据长度代码 (DLC),远程请求帧一般设置为请求的字节数

    // 1. 创建原始 CAN 套接字
    socket_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (socket_fd < 0) {
        printf("socket error\n");
        return -1;
    }

    // 2. 设置接口名称,"can0" 为接口名
    strcpy(ifr.ifr_name, "can0");
    
    // 3. 获取接口索引
    if (ioctl(socket_fd, SIOCGIFINDEX, &ifr) < 0) {
        printf("ioctl error\n");
        return -1;
    }

    // 4. 配置套接字地址结构
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;

    // 5. 绑定套接字到 CAN 接口
    if (bind(socket_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        printf("bind error\n");
        return -1;
    }

    // 6. 循环发送帧
    while (1) {
			        // 发送第一个帧
			        ret = write(socket_fd, &frame[0], sizeof(frame[0]));
			        if (ret != sizeof(frame[0])) {
			            printf("send frame[0] error\n");
			            break;
			        }
			
			        // 发送第二个帧
			        ret = write(socket_fd, &frame[1], sizeof(frame[1]));
			        if (ret != sizeof(frame[1])) {
			            printf("send frame[1] error\n");
			            break;
			        }
			
			        // 发送第三个帧
			        ret = write(socket_fd, &frame[2], sizeof(frame[2]));
			        if (ret != sizeof(frame[2])) {
			            printf("send frame[2] error\n");
			            break;
			        }
			
			        sleep(1);  // 每秒发送一次
   			 }

    // 7. 关闭套接字
    close(socket_fd);
    return 0;
}

can_recv.c

#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <unistd.h>

#define BUF_SIZ 128

int main() {
    int socket_fd, ret, err, i, n;
    char buf[BUF_SIZ];
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame;

    // 1. 创建原始 CAN 套接字
    socket_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (socket_fd < 0) {
        printf("socket error\n");
        return -1;
    }

    // 2. 设置接口名称,"can0" 为接口名
    strcpy(ifr.ifr_name, "can0");

    // 3. 获取接口索引
    if (ioctl(socket_fd, SIOCGIFINDEX, &ifr) < 0) {
        printf("ioctl error\n");
        return -1;
    }

    // 4. 配置套接字地址结构
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;

    // 5. 绑定套接字到 CAN 接口
    if (bind(socket_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        printf("bind error\n");
        return -1;
    }

    // 6. 进入接收 CAN 帧的循环
    while (1) {
			        ret = read(socket_fd, &frame, sizeof(frame)); // 读取 CAN 帧
			        if (ret > 0) {
				           // 如果是扩展帧 (EFF)
				            if (frame.can_id & CAN_EFF_FLAG)
				                n = snprintf(buf, BUF_SIZ, "<0x%08x> ", frame.can_id & CAN_EFF_MASK);  // 扩展帧 ID
				            else
				                n = snprintf(buf, BUF_SIZ, "<0x%03x> ", frame.can_id & CAN_SFF_MASK);  // 标准帧 ID
				            
				            // 添加帧的 DLC(数据长度代码)
				            n += snprintf(buf + n, BUF_SIZ - n, "[%d] ", frame.can_dlc);
				
				            // 输出数据字段(若非 RTR 帧)
				            for (i = 0; i < frame.can_dlc; i++) {
				                n += snprintf(buf + n, BUF_SIZ - n, " %02x ", frame.data[i]);
				            }
				
				            // 如果是远程请求帧(RTR)
				            if (frame.can_id & CAN_RTR_FLAG) {
				                snprintf(buf + n, BUF_SIZ - n, " remote request");
				            }
				
				            // 输出到标准输出
				            printf("%s\n", buf);
				
				            // 刷新输出
				            err = fflush(stdout);
				            if (err < 0) {
				                printf("fflush error\n");
				                break;
				            }
			        } else {
			            printf("read error\n");
			            break;
			        }
    }

    // 7. 关闭套接字
    close(socket_fd);
    return 0;
}

4. 代码测试

  通信的双方使用本文中硬件连接的方式进行连接,然后打开can设备并设置相同的波特率。如我们使用PC与开发板进行通信,PC端上使用软件ECan Tools来设置通信的波特率,开发板使用命令来打开can总线并设置相同波特率。其中开发板的命令我们可以使用iproute2canutils工具包来生成,然后拷贝到开发板上使用。详细使用视频链接在此。

(1)命令设置波特率

ip link set can0 type can bitrate 250000
/*
	ip:网络管理命令。
	link:指定要操作网络设备的子命令。在这里操作的是网络链路(设备)。注释:操作网络接口(链路)。
	set:表示要对网络接口进行配置或修改。
	can0:这是 CAN 网络接口的名称,类似于 eth0 这样的以太网接口名称。在这个例子中,can0 是第一个 CAN 设备的接口。
	type can:指定接口类型为 CAN。在这里,明确告诉系统要对一个 CAN 类型的接口进行配置。
	bitrate 250000:设置 CAN 总线的波特率为 250 kbps(250,000 位每秒)。CAN 网络常见的波特率有 125000、250000、500000、1000000 等,选择合适的波特率取决于设备和网络的要求。
*/

(2)启动can0设备:ifconfig can0 up
(3)运行代码。优化:可以使用线程来完成接收和发送的功能。

九、CAN过滤规则

  标准帧的帧 ID 长度是 11 位,帧 ID 的范围是 0-7FF。扩展帧的帧ID长度是29位,帧ID的范围是0-1FFFFFFF。

用于匹配的帧类型:

#define CAN_SFF_MASK 0x000007FFU /* 标准帧 (SFF) */
#define CAN_EFF_MASK 0x1FFFFFFFU /* 扩展帧 (EFF) */

用于屏蔽的帧标志:

#define CAN_EFF_FLAG 0x80000000U //(扩展帧标志)
#define CAN_RTR_FLAG 0x40000000U //(远程帧、遥控帧)
#define CAN_ERR_FLAG 0x20000000U//(错误帧标志)

1. 使用命令 candump 设置过滤规则

  在 CAN 总线上,可以使用 candump 命令来过滤和接收特定的 CAN 帧。以下是如何使用不同的过滤规则来指定标准帧和扩展帧的接收。举例:
(1)扩展帧

candump vcan2,12345678:1FFFFFFF
  作用:只会接收到 ID 为 12345678 的扩展帧以及远程帧。
  参数:12345678: 这是目标扩展帧的 ID。扩展帧的 ID 是 29 位的,因此 12345678 是在 00000000 到 1FFFFFFF 范围内的有效扩展帧 ID。1FFFFFFF: 掩码用于匹配所有的 29 位 ID。1FFFFFFF 是扩展帧的最大掩码,表示全位匹配。

(2)标准帧
candump vcan2,123:C00007FF
  作用:将接收到 ID 为 123 的 标准帧, 并且不会接收远程传输请求(RTR)帧,也不会接收扩展帧(EFF)。
  参数:123: 这是目标标准帧的 ID,范围在 000 到 7FF 之间。123 是一个有效的标准帧 ID。C00007FF: 掩码用于标准帧 和 扩展帧(EFF) 以及 远程帧(RTR) 标志位。其中C0000000 用于屏蔽扩展帧和远程帧的标志位(EFF 和 RTR),000007FF 匹配标准帧的 11 位 ID。

candump vcan2,123:800007FF
  作用:将接收到 ID 为 123 的 标准帧 和 远程帧(RTR)。不包括扩展帧(EFF)
  参数:123: 这是目标标准帧的 ID,范围在 000 到 7FF 之间。123 是一个有效的标准帧 ID。80000000: 掩码用于屏蔽扩展帧。7FF用于匹配标准帧。

●说明:对于标准帧中,掩码如何匹配?
  对于标准帧,例如使用掩码C00007FF,低位要为标准帧的最大范围7FF,而高位要为屏蔽的帧类型高位相加。例如屏蔽扩展帧(0x80000000U )和远程帧(0x40000000U),则8+4=c(十六进制),则高位填c就表示屏蔽这两个帧,其他同理。

2. 应用程序设置过滤规则

(1)结构体
  struct can_filter 是用于定义 CAN 帧过滤规则的结构体,应用程序可以通过它来设置接收 CAN 帧的过滤器。每个过滤器包含两个部分:can_id 和 can_mask,它们共同决定哪些 CAN 帧会被接收。

struct can_filter {
    canid_t can_id;    // CAN 帧 ID
    canid_t can_mask;  // CAN 帧掩码
};

(2)掩码宏定义

//用于匹配的
#define CAN_SFF_MASK 0x000007FFU /* 标准帧 (SFF) */
#define CAN_EFF_MASK 0x1FFFFFFFU /* 扩展帧 (EFF) */

//用于屏蔽的
#define CAN_EFF_FLAG 0x80000000U //(扩展帧标志)
#define CAN_RTR_FLAG 0x40000000U //(远程帧、遥控帧)
#define CAN_ERR_FLAG 0x20000000U//(错误帧标志)

(3)API函数

int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
/*
socket:这是需要设置选项的套接字描述符,即通过 socket() 创建的套接字。
level:指定要设置的选项属于哪个协议层。
		常见的选项:
		SOL_SOCKET: 通用套接字选项(影响套接字的通用行为)。
		SOL_CAN_RAW: 针对 CAN 的特定选项,常用于 CAN 原始套接字。
option_name:这是需要设置的选项名称,不同的协议和层有不同的选项名称。
		常见的选项:
		SO_REUSEADDR: 允许重新绑定已在使用的地址。
		SO_RCVBUF: 设置接收缓冲区的大小。
		SO_SNDBUF: 设置发送缓冲区的大小。
		CAN_RAW_FILTER: 在 CAN 协议中,用于设置过滤器。
option_value:这是一个指向需要设置的选项值的指针,通常是一个结构体或者整数变量的地址,具体取决于 option_name。
		例如,设置 CAN_RAW_FILTER 时,这里会指向一个 struct can_filter 数组。
option_len:这是 option_value 的大小,通常通过 sizeof() 来计算。例如,如果 option_value 是一个 struct can_filter 数组,option_len 就应该是 sizeof(struct can_filter) * 数组元素数量。
*/

(4)简单使用举例

struct can_filter rfilter[2]; //定制两条过滤规则

// 设置第一个过滤规则
rfilter[0].can_id = 0x123;  // 目标帧 ID
rfilter[0].can_mask = CAN_SFF_MASK | CAN_EFF_FLAG | CAN_RTR_FLAG;  // 匹配标准帧,屏蔽扩展帧、远程帧

// 设置第二个过滤规则
rfilter[1].can_id = 0x456;  // 目标帧 ID
rfilter[1].can_mask = CAN_EFF_MASK | CAN_RTR_FLAG;  // 匹配扩展帧,屏蔽远程帧

// 设置套接字过滤规则
setsockopt(socket_fd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

(5)完整使用代码

#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <unistd.h>

#define BUF_SIZ 128

int main() {
    int socket_fd, ret, err, i, n;
    char buf[BUF_SIZ];
    struct sockaddr_can addr;
    struct ifreq ifr;
    struct can_frame frame;
	struct can_filter rfilter[2];  //设置过滤规则
	
    // 1. 创建原始 CAN 套接字
    socket_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (socket_fd < 0) {
        printf("socket error\n");
        return -1;
    }

    // 2. 设置接口名称,"can0" 为接口名
    strcpy(ifr.ifr_name, "can0");

    // 3. 获取接口索引
    if (ioctl(socket_fd, SIOCGIFINDEX, &ifr) < 0) {
        printf("ioctl error\n");
        return -1;
    }

    // 4. 配置套接字地址结构
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;

    // 5. 绑定套接字到 CAN 接口
    if (bind(socket_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        printf("bind error\n");
        return -1;
    }

	// 6. 设置套接字过滤规则 
	//设置第一个过滤规则
	rfilter[0].can_id = 0x123;  // 目标帧 ID
	rfilter[0].can_mask = CAN_SFF_MASK | CAN_EFF_FLAG | CAN_RTR_FLAG;  // 匹配标准帧,屏蔽扩展帧、远程帧
	
	// 设置第二个过滤规则
	rfilter[1].can_id = 0x456;  // 目标帧 ID
	rfilter[1].can_mask = CAN_EFF_MASK | CAN_RTR_FLAG;  // 匹配扩展帧,屏蔽远程帧
	
	setsockopt(socket_fd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
	
    // 7. 进入接收 CAN 帧的循环
    while (1) {
			        ret = read(socket_fd, &frame, sizeof(frame)); // 读取 CAN 帧
			        if (ret > 0) {
				           // 如果是扩展帧 (EFF)
				            if (frame.can_id & CAN_EFF_FLAG)
				                n = snprintf(buf, BUF_SIZ, "<0x%08x> ", frame.can_id & CAN_EFF_MASK);  // 扩展帧 ID
				            else
				                n = snprintf(buf, BUF_SIZ, "<0x%03x> ", frame.can_id & CAN_SFF_MASK);  // 标准帧 ID
				            
				            // 添加帧的 DLC(数据长度代码)
				            n += snprintf(buf + n, BUF_SIZ - n, "[%d] ", frame.can_dlc);
				
				            // 输出数据字段(若非 RTR 帧)
				            for (i = 0; i < frame.can_dlc; i++) {
				                n += snprintf(buf + n, BUF_SIZ - n, " %02x ", frame.data[i]);
				            }
				
				            // 如果是远程请求帧(RTR)
				            if (frame.can_id & CAN_RTR_FLAG) {
				                snprintf(buf + n, BUF_SIZ - n, " remote request");
				            }
				
				            // 输出到标准输出
				            printf("%s\n", buf);
				
				            // 刷新输出
				            err = fflush(stdout);
				            if (err < 0) {
				                printf("fflush error\n");
				                break;
				            }
			        } else {
			            printf("read error\n");
			            break;
			        }
    }

    // 8. 关闭套接字
    close(socket_fd);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值