CAN 总线与 Linux SocketCAN C 语言测试程序

一、CAN 总线简介

1. 什么是 CAN 总线

CAN(Controller Area Network,控制器局域网)是一种多主、实时、高可靠的串行通信总线,最初由德国博世(Bosch)公司为汽车电子设计,现已广泛应用于工业自动化、轨道交通、医疗设备、船舶等领域。

2. CAN 总线核心特点

  • 多主架构:网络中任意节点均可主动发送数据,无主从之分;
  • 实时性强:通过非破坏性总线仲裁(ID 优先级)保证高优先级数据优先传输;
  • 可靠性高:内置 CRC 校验、错误检测与重传机制,抗电磁干扰能力强;
  • 传输距离远:波特率 125kbps 时传输距离可达 500m,5kbps 时可达 10km;
  • 数据帧短:单帧最多传输 8 字节数据,适合传输控制指令与传感器数据。

3. CAN 协议分层

遵循 ISO/OSI 简化模型:

|-----------|----------------------------|-----------------------------|
| 层级 | 功能 | 对应规范 |
| 应用层 | 设备配置、数据交换(如 CANopen、J1939) | CiA 301、SAE J1939 |
| 数据链路层 | 帧传输、仲裁、错误检测 | CAN 2.0A(标准帧)、CAN 2.0B(扩展帧) |
| 物理层 | 电气特性、传输介质 | ISO 11898-2(差分总线,终端电阻 120Ω) |

4. CAN 数据帧结构

以标准数据帧为例(11 位 ID):

|-----------|--------|-------------------------------------|
| 字段 | 长度 | 说明 |
| 仲裁场 | 12 位 | 包含 11 位 ID(优先级)+ RTR 位(0=数据帧,1=远程帧) |
| 控制场 | 6 位 | 包含 IDE 位(0=标准帧)+ DLC(数据长度,0-8) |
| 数据场 | 0-8 字节 | 实际传输的数据 |
| CRC 场 | 16 位 | 循环冗余校验码 |
| ACK 场 | 2 位 | 应答位 |
| EOF 场 | 7 位 | 帧结束标志 |

二、Linux SocketCAN 环境准备

Linux 内核自 2.6.25 版本起集成了 SocketCAN 子系统,提供了标准的 socket 接口用于 CAN 总线通信,无需依赖第三方库。

1. 硬件与工具安装

  • 硬件:USB-CAN 适配器(如 PCAN-USB、USB-CAN Analyzer);
  • 安装 can-utils 工具包(用于调试):

|------------------------------------------------------|
| Bash sudo apt update && sudo apt install can-utils |

2. 配置 CAN 接口

假设 CAN 接口为 can0,波特率设为 500kbps(常用波特率):

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Bash # 加载 CAN 内核模块(如未自动加载) sudo modprobe can sudo modprobe can_raw # 配置 can0 波特率并启动 sudo ip link set can0 type can bitrate 500000 sudo ip link set can0 up # 验证接口状态(应显示 "UP, RUNNING") ip link show can0 |

3. 快速测试(可选)

打开两个终端,分别运行:

|---------------------------------------------------------------------------------------------------------|
| Bash # 终端1:监控 CAN 总线数据 candump can0 # 终端2:发送测试数据帧(ID=0x123,数据=01 02 03 04) cansend can0 123#01.02.03.04 |

若终端1 收到数据,说明硬件与接口配置正常。

三、Linux 下 C 语言 SocketCAN 测试程序

1. 关键数据结构与函数

SocketCAN 核心头文件:<linux/can.h>、<linux/can/raw.h>、<net/if.h>。

(1)CAN 帧结构 struct can_frame

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| C struct can_frame { canid_t can_id; // CAN ID(11位标准帧或29位扩展帧) __u8 can_dlc; // 数据长度(0-8) __u8 __pad; // 保留 __u8 __res0; // 保留 __u8 __res1; // 保留 __u8 data[8]; // 数据域 }; |

(2)CAN 地址结构 struct sockaddr_can

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| C struct sockaddr_can { sa_family_t can_family; // 协议族,固定为 PF_CAN int can_ifindex; // 网络接口索引(通过 if_nametoindex 获取) union { struct { canid_t rx_id; canid_t rx_mask; } filter; } can_addr; }; |

2. 测试 程序 can_ test .c

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| C #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <net/if.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <linux/can.h> #include <linux/can/raw.h> int main(int argc, char *argv[]) { int s; struct sockaddr_can addr; struct ifreq ifr; struct can_frame frame; const char *ifname = "can0"; // CAN 接口名称 // 1. 创建 SocketCAN 套接字 if ((s = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) { perror("socket 创建失败"); return EXIT_FAILURE; } // 2. 获取 CAN 接口索引 strcpy(ifr.ifr_name, ifname); if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl 获取接口索引失败"); close(s); return EXIT_FAILURE; } // 3. 绑定 CAN 接口 memset(&addr, 0, sizeof(addr)); addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind 绑定接口失败"); close(s); return EXIT_FAILURE; } // 4. 构造 CAN 数据帧 memset(&frame, 0, sizeof(frame)); frame.can_id = 0x123; // 标准帧 ID(11位) frame.can_dlc = 4; // 数据长度 4 字节 frame.data[0] = 0x01; // 数据字节 0 frame.data[1] = 0x02; // 数据字节 1 frame.data[2] = 0x03; // 数据字节 2 frame.data[3] = 0x04; // 数据字节 3 // 5. 发送数据帧 printf("发送 CAN 帧: ID=0x%X, DLC=%d, 数据=", frame.can_id, frame.can_dlc); for (int i = 0; i < frame.can_dlc; i++) { printf("%02X ", frame.data[i]); } printf("\n"); if (write(s, &frame, sizeof(struct can_frame)) != sizeof(struct can_frame)) { perror("write 发送失败"); close(s); return EXIT_FAILURE; } // 6. 关闭套接字并退出 close(s); return EXIT_SUCCESS; } |

3. 接收程序 can_receive.c

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| C #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <net/if.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <linux/can.h> #include <linux/can/raw.h> int main(int argc, char *argv[]) { int s; struct sockaddr_can addr; struct ifreq ifr; struct can_frame frame; const char *ifname = "can0"; // CAN 接口名称 // 1. 创建 SocketCAN 套接字 if ((s = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) { perror("socket 创建失败"); return EXIT_FAILURE; } // 2. 获取 CAN 接口索引 strcpy(ifr.ifr_name, ifname); if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl 获取接口索引失败"); close(s); return EXIT_FAILURE; } // 3. 绑定 CAN 接口 memset(&addr, 0, sizeof(addr)); addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind 绑定接口失败"); close(s); return EXIT_FAILURE; } // 4. (可选)设置 CAN ID 过滤器,仅接收 ID=0x123 的帧 struct can_filter rfilter; rfilter.can_id = 0x123; rfilter.can_mask = CAN_SFF_MASK; // 标准帧掩码 setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter)); printf("等待接收 CAN 帧(接口: %s)...\n", ifname); // 5. 循环接收数据帧 while (1) { int nbytes = read(s, &frame, sizeof(struct can_frame)); if (nbytes < 0) { perror("read 接收失败"); close(s); return EXIT_FAILURE; } else if (nbytes < sizeof(struct can_frame)) { fprintf(stderr, "错误: 接收到的帧不完整\n"); continue; } // 打印接收到的帧 printf("收到 CAN 帧: ID=0x%X, DLC=%d, 数据=", frame.can_id, frame.can_dlc); for (int i = 0; i < frame.can_dlc; i++) { printf("%02X ", frame.data[i]); } printf("\n"); } // 6. 关闭套接字(实际不会执行到这里) close(s); return EXIT_SUCCESS; } |

四、编译与运行

1. 编译程序

|------------------------------------------------------------------|
| Bash gcc can_send.c -o can_send gcc can_receive.c -o can_receive |

2. 运行步骤

  1. 配置 CAN 接口(见本文第二部分);
  1. 打开终端1,运行接收程序

|--------------------|
| Bash ./can_receive |

  1. 打开终端2,运行发送程序

|-----------------|
| Bash ./can_send |

3. 预期结果

  • 终端2 输出:

|------------------------------------------------------|
| Plain Text 发送 CAN 帧: ID=0x123, DLC=4, 数据=01 02 03 04 |

  • 终端1 输出:

|------------------------------------------------------------------------------|
| Plain Text 等待接收 CAN 帧(接口: can0)... 收到 CAN 帧: ID=0x123, DLC=4, 数据=01 02 03 04 |

五、 发送接收测试程序

#include <stdio.h>

#include <stdlib.h>

#include <stdint.h>

#include <string.h>

#include <unistd.h>

#include <net/if.h>

#include <sys/ioctl.h>

#include <sys/socket.h>

#include <linux/can.h>

#include <linux/can/raw.h>

#define CAN_INTERFACE "can0" // 可根据需要修改为 can1、vcan0 等

// 打开并绑定 CAN 接口,返回套接字描述符,失败返回 -1

static int can_open(const char *ifname)

{

int fd;

struct sockaddr_can addr;

struct ifreq ifr;

if ((fd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0) {

perror("socket");

return -1;

}

strcpy(ifr.ifr_name, ifname);

if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) {

perror("ioctl");

close(fd);

return -1;

}

addr.can_family = AF_CAN;

addr.can_ifindex = ifr.ifr_ifindex;

if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {

perror("bind");

close(fd);

return -1;

}

return fd;

}

// 发送一帧 CAN 数据,成功返回写入字节数,失败返回 -1

static int can_send(int fd, canid_t id, const uint8_t *data, uint8_t len)

{

struct can_frame frame;

if (len > CAN_MAX_DLEN)

len = CAN_MAX_DLEN;

frame.can_id = id;

frame.can_dlc = len;

memcpy(frame.data, data, len);

if (write(fd, &frame, sizeof(frame)) != sizeof(frame)) {

perror("write");

return -1;

}

return sizeof(frame);

}

// 阻塞接收一帧 CAN 数据,成功返回读取字节数,失败返回 -1

static int can_recv(int fd, struct can_frame *frame)

{

int nbytes = read(fd, frame, sizeof(*frame));

if (nbytes < 0) {

perror("read");

return -1;

}

if (nbytes < sizeof(*frame)) {

fprintf(stderr, "收到不完整的帧\n");

return -1;

}

return nbytes;

}

// 打印 CAN 帧内容

static void print_frame(const struct can_frame *frame, const char *prefix)

{

printf("%s: ID=0x%03X DLC=%d DATA=", prefix, frame->can_id, frame->can_dlc);

for (int i = 0; i < frame->can_dlc; i++)

printf("%02X ", frame->data[i]);

printf("\n");

}

int main(void)

{

int fd;

struct can_frame rx_frame;

uint8_t tx_data[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77};

fd = can_open(CAN_INTERFACE);

if (fd < 0) {

fprintf(stderr, "无法打开 CAN 接口 %s\n", CAN_INTERFACE);

return 1;

}

// 发送一帧

if (can_send(fd, 0x123, tx_data, sizeof(tx_data)) < 0) {

fprintf(stderr, "发送失败\n");

close(fd);

return 1;

}

// 等待接收一帧

if (can_recv(fd, &rx_frame) < 0) {

fprintf(stderr, "接收失败\n");

close(fd);

return 1;

}

print_frame(&rx_frame, "接收");

close(fd);

return 0;

}

相关推荐
Predestination王瀞潞2 小时前
4.3.3 存储->微软文件系统标准(微软,自有技术标准):VFAT(Virtual File Allocation Table)虚拟文件分配表系统
linux·microsoft·vfat
HalvmånEver2 小时前
Linux:socket套接字编程的基础概念
linux·运维·服务器
浅念-2 小时前
C ++ 智能指针
c语言·开发语言·数据结构·c++·经验分享·笔记·算法
二进制person2 小时前
JavaEE初阶 --网络初识
运维·服务器·网络
李&@杰2 小时前
《中小型企业网络完整项目方案(拓扑+配置+说明+验收清单)》
网络
IMPYLH2 小时前
Linux 的 cp 命令
linux·运维·服务器
@syh.3 小时前
【linux】多线程
linux
RisunJan3 小时前
Linux命令-man(查看Linux中的指令帮助)
linux·运维·服务器
REDcker3 小时前
CentOS 与主流 Linux 发行版:版本与时间表(年表)
linux·运维·centos