Linux SocketCAN 编程(C++,启用多线程接收)
SocketCAN 采用常用的 Socket 网络编程接口来封装 CAN 协议,可以使开发人员几乎无压力地使用 CAN。SocketCAN 编程的思路与 Socket 网络编程几乎一样。socket() 函数返回一个 socketcan 的句柄,后续的操作都是基于这个句柄的。第一个参数第二个参数指定了 socket 的类型。常用的有第三个参数表示指定的协议。常用的协议有。......
目录
2、使用 socket() 函数创建一个 socketcan 套接字
3、使用 ioctl() 函数 将套接字与 can 设备绑定
4、使用 setsockopt() 函数设置过滤规则(接收滤波器)
6、使用 write() 函数和 can_frame 结构体发送数据
7、使用 read() 函数和 can_frame 结构体接收数据
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 过滤规则解析
过滤规则由 filter 和 mask 这两部分组成(都是 unsigned int 类型)。mask 用来决定接收到的数据帧 ID 中的哪些位会与 filter 进行比较,具体规则如下:
- 如果 mask 的某个 bit 被设置为 0,则接收 ID 相同位置上的 bit 将会被接受,而不管这个位置的值是 0 还是 1
- 如果 mask 的某个 bit 被设置为 1,则接收 ID 相同位置上的 bit 将会与 filter 对应位置上的 bit 进行对比,如果两者相同,则该帧数据将被接收,否则将被丢弃
举几个例子:
1、我们只接收 ID 为 0x00001234 的数据帧,则 filter 和 mask 的设置如下:
filter = 0x00001234;
mask = 0x1FFFFFFF;
由于 mask 的低 29 位全是 1,则到达的数据帧必须与 filter 逐位对比,所有 bit 都匹配的时候才会被接收,因此此时只能接收 ID 为 0x00001234 的数据帧。
2、我们接收 ID 为 0x00001230 ~ 0x00001237 的数据帧,则 filter 和 mask 的设置如下:
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;
}
更多推荐
所有评论(0)