草庐IT

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

地球被支点撬走啦 2023-08-01 原文

目录

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 首先需要用到的头文件:

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

1、使用指令设置can参数

#define ip_cmd_set_can0_params "ip link set can0 type can bitrate 1000000 triple-sampling on"
#define ip_cmd_can0_up         "ifconfig can0 up"
#define ip_cmd_can0_down       "ifconfig can0 down"

// 使用系统调用函数运行以上命令,也可以自行在终端中运行
system(ip_cmd_set_can0_params); // 设置参数
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 设备绑定

struct ifreq ifr;  // ifreq 结构体用来保存某个接口的信息,定义在头文件 if.h 中
strcpy(ifr.ifr_name, "can0");
ioctl(can_fd, SIOCGIFINDEX, &ifr); // 指定 can0 设备,并获取设备索引

struct sockaddr_can addr;
addr.can_family = AF_CAN;  // 指定协议族
addr.can_ifindex = ifr.ifr_ifindex;  // 设备索引
// 将套接字与 can0 绑定
int bind_res = bind(can_fd, (struct sockaddr *)&addr, sizeof(addr));

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

/********************* 过滤规则设置 *********************/
// CAN_SFF_MASK 0x000007FFU 定义在 can.h 中 (SFF: standard frame format)
struct can_filter rfilter[3];  // 定义过滤规则数组,结构体 can_filter 是库函数定义好的
rfilter[0].can_id = Group1_ID1;
rfilter[0].can_mask = CAN_SFF_MASK; // 标准帧 (SFF: standard frame format)
    
rfilter[1].can_id = Group1_ID2;
rfilter[1].can_mask = CAN_SFF_MASK; // 标准帧

rfilter[2].can_id = Group1_ID3;
rfilter[2].can_mask = CAN_SFF_MASK; // 标准帧

setsockopt(can_fd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

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

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

struct can_filter {
	canid_t can_id;
	canid_t can_mask;
};

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

<received_can_id> & 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 的设置如下:

filter = 0x00001234;
mask = 0x1FFFFFFF; 

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

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

filter = 0x00001230;
mask = 0x1FFFFFF8; 

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

3、接收所有帧

filter = 0;
mask = 0; 

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

struct can_frame frame;  // 声明 can 帧结构体,can_frame 定义在头文件 can.h 中

frame.data[0] = 0xFF;  // 要发送的(最多)8个字节的数据
frame.data[1] = 0xFF;
frame.data[2] = 0xFF;
frame.data[3] = 0xFF;
frame.data[4] = 0xFF;
frame.data[5] = 0xFF;
frame.data[6] = 0xFF;
frame.data[7] = 0xFC;

/************ 写数据 ************/
frame.can_dlc = 8;  // 设置数据长度(CAN协议规定一帧最多有八个字节的有效数据)
frame.can_id = 1;    // 设置 ID 号,假设这里 ID 号为1,实际的 ID 号要根据是标准帧(11位)还是拓展帧(29)位来设置
write(can_fd, &frame, sizeof(frame));  // 写数据

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

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

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

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

#include <thread>
#include <condition_variable>
#include <mutex>

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

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

int can_get_status(int can_fd, struct get_params &status_now)
{
   // ......
}

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

thread can0read_thread(can_get_status, can_fd, std::ref(status_now));  // 此句执行完线程即开始执行
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、完整的初始化代码

#define ip_cmd_set_can0_params "ip link set can0 type can bitrate 1000000 triple-sampling on"
#define ip_cmd_can0_up         "ifconfig can0 up"
#define ip_cmd_can0_down       "ifconfig can0 down"
/*************************************
 * @brief 初始化 SocketCAN0
 * @param none
 * @return -1: error, 0:success
 ************************************/
int can_init(void)
{
    // /******************** 通过 ip 指令设置 can0 参数 ********************/
    // system(ip_cmd_set_can0_params); // 波特率 1Mbps
    // system(ip_cmd_can0_up);  // 开启 can0

    /******************** 通过 ip 指令设置 can0 参数 ********************/
    // 创建socket can
    int can_fd = socket(AF_CAN, SOCK_RAW, CAN_RAW);
    if(can_fd < 0)
    {
        perror("socket can creat error!\n");
        return -1;
    }

    /********************* 绑定 can0 设备与 socket *********************/
    struct ifreq ifr;  // if.h
    strcpy(ifr.ifr_name, "can0");
    ioctl(can_fd, SIOCGIFINDEX, &ifr); // 指定编号为 can0 的设备,获取设备索引

    struct sockaddr_can addr;
    addr.can_family = AF_CAN;  // 指定协议族
    addr.can_ifindex = ifr.ifr_ifindex;  // 设备索引
    // 将套接字与 can0 绑定
    int bind_res = bind(can_fd, (struct sockaddr *)&addr, sizeof(addr));
    if(bind_res < 0)
    {
        perror("bind error!");
        return -1;
    }

    /********************* 过滤规则设置 *********************/
    // CAN_SFF_MASK 0x000007FFU  (SFF: standard frame format)
    // 此处设置三组过滤规则,只接收 ID 为 1、2、3 的三种数据帧
    struct can_filter rfilter[3];
    rfilter[0].can_id = 1;
    rfilter[0].can_mask = CAN_SFF_MASK; // 标准帧 (SFF: standard frame format)
    
    rfilter[1].can_id = 2;
    rfilter[1].can_mask = CAN_SFF_MASK; // 标准帧

    rfilter[2].can_id = 3;
    rfilter[2].can_mask = CAN_SFF_MASK; // 标准帧

    setsockopt(can_fd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

    return can_fd;
}

有关Linux SocketCAN 编程(C++,启用多线程接收)的更多相关文章

  1. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  2. ruby-on-rails - RSpec:避免使用允许接收的任何实例 - 2

    我正在处理旧代码的一部分。beforedoallow_any_instance_of(SportRateManager).toreceive(:create).and_return(true)endRubocop错误如下:Avoidstubbingusing'allow_any_instance_of'我读到了RuboCop::RSpec:AnyInstance我试着像下面那样改变它。由此beforedoallow_any_instance_of(SportRateManager).toreceive(:create).and_return(true)end对此:let(:sport_

  3. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  4. ruby-on-rails - 如何使用 Rack 接收 JSON 对象 - 2

    我有一个非常简单的RubyRack服务器,例如:app=Proc.newdo|env|req=Rack::Request.new(env).paramspreq.inspect[200,{'Content-Type'=>'text/plain'},['Somebody']]endRack::Handler::Thin.run(app,:Port=>4001,:threaded=>true)每当我使用JSON对象向服务器发送POSTHTTP请求时:{"session":{"accountId":String,"callId":String,"from":Object,"headers":

  5. ruby-on-rails - 启用 Rack::Deflater 时 ETag 发生变化 - 2

    在启用Rack::Deflater来gzip我的响应主体时偶然发现了一些奇怪的东西。也许我遗漏了一些东西,但启用此功能后,响应被压缩,但是资源的ETag在每个请求上都会发生变化。这会强制应用程序每次都响应,而不是发送304。这在没有启用Rack::Deflater的情况下有效,我已经验证页面源没有改变。我正在运行一个使用thin作为Web服务器的Rails应用程序。Gemfile.lockhttps://gist.github.com/2510816有没有什么方法可以让我从Rack中间件获得更多的输出,这样我就可以看到发生了什么?提前致谢。 最佳答案

  6. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

  7. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  8. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

  9. ruby - 如何在 ruby​​ 中运行后台线程? - 2

    我是ruby​​的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp

  10. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

随机推荐