Linux底软开发—对CAN发送接收详细操作
文章目录
- Linux底软开发—对CAN发送接收详细操作
- 1.保证多条CAN数据发送的周期性
- 2.解析CAN报文数据
- 3.CAN总线异常机制应对
- 4.对CAN报文进行过滤操作
- 5.完整的接收报文代码(过滤,心跳检测,解析)
1.保证多条CAN数据发送的周期性
如果想同时发送多条CAN,在Linux下可以使用多线程操作,一条线程对应一条CAN报文
// 启动线程发送CAN消息
for (int i = 0; i < 3; ++i)
{
pthread_create(&tid[i], NULL, send_data, (void *)&data_args[i]);
}
// 等待所有线程结束
for (int i = 0; i < 3; ++i)
{
pthread_join(tid[i], NULL);
}
保证每条CAN报文的周期性,可以使用Linux的时间函数—usleep() ,精确到微秒,同样每个线程可以接受时间间隔参数
示例代码:循环发送多条CAN报文,自定义时间周期,每一条CAN报文都可以自定义自己的周期
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <pthread.h>
#define CAN_INTERFACE "can0"
#define CAN_ID_1 0x123
#define CAN_ID_2 0x124
#define CAN_ID_3 0x125
#define CAN_DATA_LEN 8
struct send_data_args
{
int sockfd;
struct can_frame frame;
int64_t interval; // 发送周期
};
void send_can_message(int sockfd, struct can_frame *frame)
{
int nbytes;
nbytes = write(sockfd, frame, sizeof(struct can_frame));
if (nbytes < 0)
{
perror("Write failed");
exit(EXIT_FAILURE);
}
}
void *send_data(void *args)
{
struct send_data_args *data_args = (struct send_data_args *)args;
while (1)
{
send_can_message(data_args->sockfd, &(data_args->frame));
printf("Sent CAN message with ID 0x%X\n", data_args->frame.can_id);
usleep(data_args->interval);
}
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
struct sockaddr_can addr;
struct ifreq ifr;
int sockfd;
int64_t time_stval; // 时间间隔周期
if (argc < 3)
{
// 如果未提供时间周期参数,则默认为100毫秒
time_stval = 100 * 1000;
}
else
{
char *str = argv[2]; // 一个数字字符串
int64_t result;
// 使用 strtoll 函数将 char* 转换为 int64_t
char *endptr;
result = strtoll(str, &endptr, 10);
// 检查转换是否成功
if (*endptr != '\0')
{
printf("Conversion failed. Not a valid number.\n");
return 1;
}
time_stval = result;
}
printf("调试信息如下:\n");
printf("接收参数个数:%d\n", argc);
printf("can接口为:%s\n", argv[1]);
printf("时间间隔周期为:%ld\n", time_stval);
pthread_t tid[3];
struct send_data_args data_args[3];
// 创建socket
if ((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0)
{
perror("Socket creation failed");
return EXIT_FAILURE;
}
// 设置CAN接口 从外部输入指定
strcpy(ifr.ifr_name, argv[1]);
ioctl(sockfd, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("Binding failed");
close(sockfd);
return EXIT_FAILURE;
}
// 准备CAN消息
data_args[0].sockfd = sockfd;
data_args[0].frame.can_id = CAN_ID_1;
data_args[0].frame.can_dlc = CAN_DATA_LEN;
memset(data_args[0].frame.data, 0, CAN_DATA_LEN); // 清空数据
data_args[0].interval = 200 * 1000; // 第一个CAN报文的发送周期为200毫秒
data_args[1].sockfd = sockfd;
data_args[1].frame.can_id = CAN_ID_2;
data_args[1].frame.can_dlc = CAN_DATA_LEN;
memset(data_args[1].frame.data, 0, CAN_DATA_LEN); // 清空数据
data_args[1].interval = 300 * 1000; // 第二个CAN报文的发送周期为300毫秒
data_args[2].sockfd = sockfd;
data_args[2].frame.can_id = CAN_ID_3;
data_args[2].frame.can_dlc = CAN_DATA_LEN;
memset(data_args[2].frame.data, 0, CAN_DATA_LEN); // 清空数据
data_args[2].interval = 400 * 1000; // 第三个CAN报文的发送周期为400毫秒
// 启动线程发送CAN消息
for (int i = 0; i < 3; ++i)
{
pthread_create(&tid[i], NULL, send_data, (void *)&data_args[i]);
}
// 等待所有线程结束
for (int i = 0; i < 3; ++i)
{
pthread_join(tid[i], NULL);
}
close(sockfd);
return EXIT_SUCCESS;
}
测试结果
上图中定义0x123 每隔200ms 发送数据,时间误差为0.0001s,
2.解析CAN报文数据
CAN报文的数据格式通常由CAN帧的数据域(Data Field)和数据长度码(Data Length Code,DLC)组成。具体格式如下:
- CAN标识符(CAN Identifier):用于标识CAN消息的ID。在标准CAN帧中,ID为11位;在扩展CAN帧中,ID为29位。ID可以表示消息的优先级、消息类型等信息。
- 远程传输请求位(Remote Transmission Request,RTR):用于标识消息是数据帧还是远程帧。数据帧包含实际的数据,而远程帧则不包含数据,仅用于请求数据。RTR位为0表示数据帧,为1表示远程帧。
- 数据长度码(Data Length Code,DLC):指示了CAN帧数据域中包含的数据字节数。DLC的取值范围通常为0到8。
- 数据域(Data Field):包含了CAN消息的实际数据。数据域的大小由DLC决定,最大为8个字节。
- CRC校验码(Cyclic Redundancy Check,CRC):用于检测CAN帧在传输过程中的错误。CRC通常由CAN控制器自动生成和验证。
- 确认位(ACK):用于确认CAN消息是否被成功接收。CAN总线上的所有节点都可以接收CAN消息,并通过ACK位来确认消息是否被正确接收。
- 结束位(End of Frame,EOF):指示了CAN帧的结束。
--------------------------------------------------------------------
| Bit Position | 0-1 | 2-12 | 13 | 14-17 | 18-25 | 26-31 |
|--------------|-------|--------|-------|---------|---------|--------|
| Field | SOF | ID | RTR | DLC | Data | CRC |
--------------------------------------------------------------------
在LinuxC编程中,接收CAN报文,系统API 已经封装好了结构体,对于开发者来讲,取到数据之后,读取can_frame结构体即可
结构体定义如下:
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
__u8 can_dlc; /* frame payload length in byte (0 .. CAN_MAX_DLEN) */
__u8 __pad; /* padding */
__u8 __res0; /* reserved / padding */
__u8 __res1; /* reserved / padding */
__u8 data[CAN_MAX_DLEN] __attribute__((aligned(8)));
};
读取示例:
printf("Received CAN frame:\n");
printf("ID: %03X\n", frame.can_id);
printf("Length: %d\n", frame.can_dlc);
printf("Data: ");
for (int i = 0; i < frame.can_dlc; i++)
{
printf("%02X ", frame.data[i]);
}
printf("\n");
具体功能需要解析frame.data 数据,根据通信矩阵,十六进制字节数据转为二进制 逐一分析即可
3.CAN总线异常机制应对
解决方案:引入心跳机制,即固定发送时间间隔的固定CAN_ID ,每一次得到CAN数据之后,就更新最后一次获取时间,引入心跳检测线程,一直判断是否超时,如果超时,说明CAN发送异常,数据未及时发送,或者掉线等其他原因,超时之后,再根据业务进行捕获异常。
示例代码: 在接受线程中,检测ID是否为心跳包,如果是心跳包,更新最后一次接收时间
// 检查是否为心跳包
if (frame.can_id == HEARTBEAT_ID)
{
// 更新心跳包接收时间
gettimeofday(&last_heartbeat_time, NULL);
}
注意:需要将last_heartbeat_time设置为全局变量,方便检测线程获取最新的时间
// 心跳包检测线程函数
void *heartbeat_checker(void *arg)
{
struct timeval current_time;
double elapsed_time;
while (1)
{
// 获取当前时间
gettimeofday(¤t_time, NULL);
// 计算与上次心跳包的时间间隔
elapsed_time = difftime(current_time.tv_sec, last_heartbeat_time.tv_sec);
// 检查是否超时
if (elapsed_time > TIMEOUT_SEC)
{
online = -1;
printf("CAN node is offline.\n");
// 清空接收缓冲区 实现逻辑
// ioctl(can_socket, SIOCINQ, 0);
}
// 休眠1秒
sleep(1);
}
return NULL;
}
4.对CAN报文进行过滤操作
可以设置过滤表,将需要的ID放入过滤表中,并且设置当前状态是否可用
例如在运行目录读取过滤表设置文件
filter_table.ini 文件内容
#ID STATUS 录入数据请用空格隔开
0x123 enable
0x122 enable
0x111 disable
在程序初始化之后,读取过滤表,一定要在读取CAN报文线程之前运行。判断哪些可以接收并放入数组中,为下一步过滤做判断
代码实现:
//得到过滤表中可用的ID
void getEnableIds()
{
FILE *file;
char line[MAX_LINE_LENGTH];
FilterEntry entries[MAX_IDS];
int count = 0;
int ptr = 0;
// 打开过滤表文件
file = fopen("filter_table.ini", "r");
if (file == NULL)
{
printf("filter_table.ini");
perror("文件不存在,请检查路径\n");
return 1;
}
// 逐行读取文件内容
while (fgets(line, sizeof(line), file) != NULL)
{
// 跳过以 "#" 开头的注释行
if (line[0] == '#')
{
continue;
}
// 解析每行,提取CAN信号ID和状态
unsigned int id;
char status[10];
if (sscanf(line, "%x %s", &id, status) == 2)
{
// 将CAN信号ID和状态存储到数组中
entries[count].id = id;
entries[count].enabled = (strcmp(status, "enable") == 0) ? 1 : 0;
if (strcmp(status, "enable") == 0)
{
// 可用ID
enable_id[ptr] = id;
ptr++;
enable_count++;
}
count++;
if (count >= MAX_IDS)
{
printf("Maximum number of entries reached. Aborting.\n");
break;
}
}
}
// 关闭文件
fclose(file);
printf("Read enable %d filter table entries:\n", ptr);
for (int i = 0; i < ptr; i++)
{
printf("Entry %d: ID = 0x%X\n", i + 1, enable_id[i]);
}
}
在读取过程中,判断报文是否可用,是否存在可用数组中。
// 判断是否是可用的ID 不是可用的直接pass
if(isIdExists(frame.can_id,enable_id,enable_count)==-1) continue;
// 打印接收到的CAN帧数据
printf("Received CAN frame:\n");
printf("ID: %03X\n", frame.can_id);
printf("Length: %d\n", frame.can_dlc);
printf("Data: ");
for (int i = 0; i < frame.can_dlc; i++)
{
printf("%02X ", frame.data[i]);
}
printf("\n");
5.完整的接收报文代码(过滤,心跳检测,解析)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <sys/time.h>
#define CAN_INTERFACE "can0" // 替换为你的CAN接口名称
#define HEARTBEAT_ID 0x166 // 心跳包的CAN消息ID
#define TIMEOUT_SEC 3 // 等待时长
struct timeval last_heartbeat_time; // 设为全局变量
int can_socket;
int online = 1;
// 过滤表相关 定义
#define MAX_IDS 100 // 最大支持的CAN信号ID数量
#define MAX_LINE_LENGTH 100 // 最大行长度
typedef struct
{
unsigned int id;
int enabled; // 0为disable,1为enable
} FilterEntry;
unsigned int enable_id[MAX_IDS];
int enable_count=0;
//得到过滤表中可用的ID
void getEnableIds()
{
FILE *file;
char line[MAX_LINE_LENGTH];
FilterEntry entries[MAX_IDS];
int count = 0;
int ptr = 0;
// 打开过滤表文件
file = fopen("filter_table.ini", "r");
if (file == NULL)
{
printf("filter_table.ini");
perror("文件不存在,请检查路径\n");
return 1;
}
// 逐行读取文件内容
while (fgets(line, sizeof(line), file) != NULL)
{
// 跳过以 "#" 开头的注释行
if (line[0] == '#')
{
continue;
}
// 解析每行,提取CAN信号ID和状态
unsigned int id;
char status[10];
if (sscanf(line, "%x %s", &id, status) == 2)
{
// 将CAN信号ID和状态存储到数组中
entries[count].id = id;
entries[count].enabled = (strcmp(status, "enable") == 0) ? 1 : 0;
if (strcmp(status, "enable") == 0)
{
// 可用ID
enable_id[ptr] = id;
ptr++;
enable_count++;
}
count++;
if (count >= MAX_IDS)
{
printf("Maximum number of entries reached. Aborting.\n");
break;
}
}
}
// 关闭文件
fclose(file);
printf("Read enable %d filter table entries:\n", ptr);
for (int i = 0; i < ptr; i++)
{
printf("Entry %d: ID = 0x%X\n", i + 1, enable_id[i]);
}
}
// 判断十六进制数值是否存在于数组中的函数
int isIdExists(unsigned int id, unsigned int *enable_id, int size) {
for (int i = 0; i < size; i++) {
if (enable_id[i] == id) {
return 1; // 存在
}
}
return -1; // 不存在
}
// 心跳包检测线程函数
void *heartbeat_checker(void *arg)
{
struct timeval current_time;
double elapsed_time;
while (1)
{
// 获取当前时间
gettimeofday(¤t_time, NULL);
// 计算与上次心跳包的时间间隔
elapsed_time = difftime(current_time.tv_sec, last_heartbeat_time.tv_sec);
// 检查是否超时
if (elapsed_time > TIMEOUT_SEC)
{
online = -1;
printf("CAN node is offline.\n");
// 清空接收缓冲区 实现逻辑
// ioctl(can_socket, SIOCINQ, 0);
}
// 休眠1秒
sleep(1);
}
return NULL;
}
// CAN数据接收线程函数
void *can_receiver(void *arg)
{
struct can_frame frame;
int nbytes;
while (1)
{
nbytes = read(can_socket, &frame, sizeof(struct can_frame));
if (nbytes < 0)
{
perror("read");
break;
}
else if (nbytes < sizeof(struct can_frame))
{
fprintf(stderr, "read: incomplete CAN frame\n");
break;
}
// 检查是否为心跳包
if (frame.can_id == HEARTBEAT_ID)
{
// 更新心跳包接收时间
gettimeofday(&last_heartbeat_time, NULL);
}
// 判断是否是可用的ID 不是可用的直接pass
if(isIdExists(frame.can_id,enable_id,enable_count)==-1) continue;
// 打印接收到的CAN帧数据
printf("Received CAN frame:\n");
printf("ID: %03X\n", frame.can_id);
printf("Length: %d\n", frame.can_dlc);
printf("Data: ");
for (int i = 0; i < frame.can_dlc; i++)
{
printf("%02X ", frame.data[i]);
}
printf("\n");
}
return NULL;
}
int main(int argc, char *argv[])
{
int s;
struct sockaddr_can addr;
struct ifreq ifr;
pthread_t checker_thread;
pthread_t receiver_thread;
getEnableIds();
printf("正在等待%s的数据.....\n", argv[1]);
// 创建socket
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (s == -1)
{
perror("socket");
return 1;
}
can_socket = s; // 将socket赋给全局变量
// 绑定CAN接口
strcpy(ifr.ifr_name, argv[1]);
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
perror("bind");
close(s);
return 1;
}
// 创建心跳包检测线程
if (pthread_create(&checker_thread, NULL, heartbeat_checker, NULL) != 0)
{
perror("pthread_create");
close(s);
return 1;
}
// 创建CAN数据接收线程
if (pthread_create(&receiver_thread, NULL, can_receiver, NULL) != 0)
{
perror("pthread_create");
close(s);
return 1;
}
// 等待线程结束
pthread_join(checker_thread, NULL);
pthread_join(receiver_thread, NULL);
// 关闭socket
close(s);
return 0;
}
测试效果: