• Linux SocketCAN 编程(C++,启用多线程接收)


    目录

    1、使用指令设置can参数

    2、使用 socket() 函数创建一个 socketcan 套接字

    3、使用 ioctl() 函数 将套接字与 can 设备绑定

    4、使用 setsockopt() 函数设置过滤规则(接收滤波器)

    5、CAN ID 过滤规则解析

    6、使用 write() 函数和 can_frame 结构体发送数据

    7、使用 read() 函数和 can_frame 结构体接收数据

    8、将接收代码放到线程中处理

    9、完整的初始化代码


    SocketCAN 采用常用的 Socket 网络编程接口来封装 CAN 协议,可以使开发人员几乎无压力地使用 CAN。SocketCAN 编程的思路与 Socket 网络编程几乎一样。

    SocketCAN 首先需要用到的头文件:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include

    1、使用指令设置can参数

    1. #define ip_cmd_set_can0_params "ip link set can0 type can bitrate 1000000 triple-sampling on"
    2. #define ip_cmd_can0_up "ifconfig can0 up"
    3. #define ip_cmd_can0_down "ifconfig can0 down"
    4. // 使用系统调用函数运行以上命令,也可以自行在终端中运行
    5. system(ip_cmd_set_can0_params); // 设置参数
    6. system(ip_cmd_can0_up); // 开启can0接口

    2、使用 socket() 函数创建一个 socketcan 套接字

    int can_fd = socket(AF_CAN, SOCK_RAW, CAN_RAW);

    socket() 函数返回一个 socketcan 的句柄,后续的操作都是基于这个句柄的。

    socket() 函数定义如下:

    int socket (int domain, int type, int protocol)

    第一个参数 domain 代表协议族,协议族定义了 socket 的地址类型,一般常用的 :

    • AF_INET 代表 IPv4(32位)与端口号的组合
    • AF_INET6 代表 IPv6
    • AF_CAN 就是 CAN 了

    第二个参数 type 指定了 socket 的类型。常用的有

    • SOCK_STREAM 表示流套接字, 提供顺序、可靠、双向、基于连接的字节流。 可以支持带外数据传输机制,例如:TCP协议、FTP协议
    • SOCK_DGRAM 表示数据包套接字, 支持数据报(无连接,不可靠的固定最大长度的消息)例如UDP协议
    • SOCK_RAW 表示原始套接字,使用原始套接字时候调用,原始套接字也就是链路层协议。CAN 就是用的这个类型

    第三个参数 protocol 表示指定的协议。 常用的协议有

    • IPPROTO_TCP,表示TCP传输协议
    • IPPTOTO_UDP,表示UDP传输协议
    • CAN_RAW,表示CAN传输协议

    3、使用 ioctl() 函数 将套接字与 can 设备绑定

    1. struct ifreq ifr; // ifreq 结构体用来保存某个接口的信息,定义在头文件 if.h 中
    2. strcpy(ifr.ifr_name, "can0");
    3. ioctl(can_fd, SIOCGIFINDEX, &ifr); // 指定 can0 设备,并获取设备索引
    4. struct sockaddr_can addr;
    5. addr.can_family = AF_CAN; // 指定协议族
    6. addr.can_ifindex = ifr.ifr_ifindex; // 设备索引
    7. // 将套接字与 can0 绑定
    8. int bind_res = bind(can_fd, (struct sockaddr *)&addr, sizeof(addr));

    4、使用 setsockopt() 函数设置过滤规则(接收滤波器)

    1. /********************* 过滤规则设置 *********************/
    2. // CAN_SFF_MASK 0x000007FFU 定义在 can.h 中 (SFF: standard frame format)
    3. struct can_filter rfilter[3]; // 定义过滤规则数组,结构体 can_filter 是库函数定义好的
    4. rfilter[0].can_id = Group1_ID1;
    5. rfilter[0].can_mask = CAN_SFF_MASK; // 标准帧 (SFF: standard frame format)
    6. rfilter[1].can_id = Group1_ID2;
    7. rfilter[1].can_mask = CAN_SFF_MASK; // 标准帧
    8. rfilter[2].can_id = Group1_ID3;
    9. rfilter[2].can_mask = CAN_SFF_MASK; // 标准帧
    10. setsockopt(can_fd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

    如上述代码所示,可以用 can_filter 数组的形式(最多512个)设置一组过滤规则。

    接下来看下代码中结构体 can_filter 的定义:

    1. struct can_filter {
    2. canid_t can_id;
    3. canid_t can_mask;
    4. };

    在这个结构体中定义了两个 canid_t (unsigned int) 型的变量,其中 can_id 的作用是过滤器 filter ,can_mask 用来做 filter 的掩码,匹配过滤器的规则如下:

    & mask == can_id & mask

    翻译一下就是接收数据帧的 ID(received_can_id) 与 mask 相与的结果要和 filter(can_id)与 mask 相与的结果相同。如果还没理解怎么办呢?接着往下看呗~

    5、CAN ID 过滤规则解析

    过滤规则由 filtermask 这两部分组成(都是 unsigned int 类型)。mask 用来决定接收到的数据帧 ID 中的哪些位会与 filter 进行比较,具体规则如下:

    • 如果 mask 的某个 bit 被设置为 0,则接收 ID 相同位置上的 bit 将会被接受,而不管这个位置的值是 0 还是 1
    • 如果 mask 的某个 bit 被设置为 1,则接收 ID 相同位置上的 bit 将会与 filter 对应位置上的 bit 进行对比,如果两者相同,则该帧数据将被接收,否则将被丢弃

    举几个例子:

    1、我们只接收 ID 为 0x00001234 的数据帧,则 filtermask 的设置如下:

    1. filter = 0x00001234;
    2. mask = 0x1FFFFFFF;

    由于 mask 的低 29 位全是 1,则到达的数据帧必须与 filter 逐位对比,所有 bit 都匹配的时候才会被接收,因此此时只能接收 ID 为 0x00001234 的数据帧。

    2、我们接收 ID 为 0x00001230 ~ 0x00001237 的数据帧,则 filtermask 的设置如下:

    1. filter = 0x00001230;
    2. mask = 0x1FFFFFF8;

    由于 mask 的低 3 位都是 0,则到达的数据帧的低 3 位将会被忽略对比,其他 bit 仍会参与与 filter 的对比,因此此时可以接收 ID 为 0x00001230 ~ 0x00001237 这 8 种类型的数据帧。

    3、接收所有帧

    1. filter = 0;
    2. mask = 0;

    6、使用 write() 函数和 can_frame 结构体发送数据

    1. struct can_frame frame; // 声明 can 帧结构体,can_frame 定义在头文件 can.h 中
    2. frame.data[0] = 0xFF; // 要发送的(最多)8个字节的数据
    3. frame.data[1] = 0xFF;
    4. frame.data[2] = 0xFF;
    5. frame.data[3] = 0xFF;
    6. frame.data[4] = 0xFF;
    7. frame.data[5] = 0xFF;
    8. frame.data[6] = 0xFF;
    9. frame.data[7] = 0xFC;
    10. /************ 写数据 ************/
    11. frame.can_dlc = 8; // 设置数据长度(CAN协议规定一帧最多有八个字节的有效数据)
    12. frame.can_id = 1; // 设置 ID 号,假设这里 ID 号为1,实际的 ID 号要根据是标准帧(11位)还是拓展帧(29)位来设置
    13. write(can_fd, &frame, sizeof(frame)); // 写数据

    7、使用 read() 函数和 can_frame 结构体接收数据

    1. struct can_frame status; // can_frame 结构体定义在头文件 can.h 中
    2. read(can_fd, &status, sizeof(status)); // 读取数据,读取到的有效数据保存在 status.data[] 数组中

    8、将接收代码放到线程中处理

    由于我们可能需要很频繁地接收数据,因此可以使用多线程的方法在线程中进行数据帧的接收。C++ 使用多线程需要使用到以下头文件:

    1. #include
    2. #include
    3. #include

    线程的创建与使用都听简单,如下:

    加入我封装了一个 can 接收函数如下:

    1. int can_get_status(int can_fd, struct get_params &status_now)
    2. {
    3. // ......
    4. }

    函数功能是将 can_fd 接收到的有限数据的数据位解析之后存放在结构体 status_now 中,则将该接收函数放入线程的方法如下:

    1. thread can0read_thread(can_get_status, can_fd, std::ref(status_now)); // 此句执行完线程即开始执行
    2. can0read_thread.detach(); // 将线程放到后台执行,此处不阻塞 or can0read_thread.join();

    thread 为线程类型,can0read_thread 为定义的线程名,第一个参数 can_get_status 是线程函数名,后面根的的参数是线程函数的参数。其中需要注意的是:对于引用传参必须要在参数上加上 std::ref() 进行转换。

    首先需要说明的是,线程在创建完之后会即刻开始执行。

    can0read_thread.detach() 代表将线程放到后台执行,这里的主线程会继续往下执行,适用于线程中有 while 循环等情况。

    can0read_thread.join() 代表主线程运行到这里的时候将等待这个子线程 can0read_thread 运行完之后才会继续往下执行,如果子线程里是一个 while(1) 死循环的话,这样做将会导致主线程一直阻塞在这里,因此需要用 detach 还是 join 需要是情况而定。

    另外一点需要注意的是:这里使用到了 C++ 的 thread 库,在编译时需要加上 pthread 库,要不然会报错,比如编译 socketcan_test.cpp 的文件,则需要以下命令:

    g++ socketcan_test.cpp -o test -l pthread

    -l 指令其实就是 library, 就是在编译时要加上 pthread 库的依赖。

    9、完整的初始化代码

    1. #define ip_cmd_set_can0_params "ip link set can0 type can bitrate 1000000 triple-sampling on"
    2. #define ip_cmd_can0_up "ifconfig can0 up"
    3. #define ip_cmd_can0_down "ifconfig can0 down"
    4. /*************************************
    5. * @brief 初始化 SocketCAN0
    6. * @param none
    7. * @return -1: error, 0:success
    8. ************************************/
    9. int can_init(void)
    10. {
    11. // /******************** 通过 ip 指令设置 can0 参数 ********************/
    12. // system(ip_cmd_set_can0_params); // 波特率 1Mbps
    13. // system(ip_cmd_can0_up); // 开启 can0
    14. /******************** 通过 ip 指令设置 can0 参数 ********************/
    15. // 创建socket can
    16. int can_fd = socket(AF_CAN, SOCK_RAW, CAN_RAW);
    17. if(can_fd < 0)
    18. {
    19. perror("socket can creat error!\n");
    20. return -1;
    21. }
    22. /********************* 绑定 can0 设备与 socket *********************/
    23. struct ifreq ifr; // if.h
    24. strcpy(ifr.ifr_name, "can0");
    25. ioctl(can_fd, SIOCGIFINDEX, &ifr); // 指定编号为 can0 的设备,获取设备索引
    26. struct sockaddr_can addr;
    27. addr.can_family = AF_CAN; // 指定协议族
    28. addr.can_ifindex = ifr.ifr_ifindex; // 设备索引
    29. // 将套接字与 can0 绑定
    30. int bind_res = bind(can_fd, (struct sockaddr *)&addr, sizeof(addr));
    31. if(bind_res < 0)
    32. {
    33. perror("bind error!");
    34. return -1;
    35. }
    36. /********************* 过滤规则设置 *********************/
    37. // CAN_SFF_MASK 0x000007FFU (SFF: standard frame format)
    38. // 此处设置三组过滤规则,只接收 ID 为 1、2、3 的三种数据帧
    39. struct can_filter rfilter[3];
    40. rfilter[0].can_id = 1;
    41. rfilter[0].can_mask = CAN_SFF_MASK; // 标准帧 (SFF: standard frame format)
    42. rfilter[1].can_id = 2;
    43. rfilter[1].can_mask = CAN_SFF_MASK; // 标准帧
    44. rfilter[2].can_id = 3;
    45. rfilter[2].can_mask = CAN_SFF_MASK; // 标准帧
    46. setsockopt(can_fd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
    47. return can_fd;
    48. }

  • 相关阅读:
    Node.js 入门教程 11 Node.js 从命令行接收参数
    什么是SQL优化中的谓词下推,看这一篇就够了
    全网最详细的自动化测试(Jenkins 篇)
    DB2 HADR+TSA运维,TSA添加资源组的命令
    Banana Pi BPI-W3(Armsom W3)RK3588开当板之调试UART
    第一章:最新版零基础学习 PYTHON 教程(第四节 - Python 3 基础知识)
    LabVIEW样式检查表1
    番茄钟的实现(基于Xilinx EGO1学习板)
    SpringBoot实现定时任务的三种方式
    【证明】二次型正定的充要条件是特征值全为正
  • 原文地址:https://blog.csdn.net/Flag_ing/article/details/126387114