一、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. 运行步骤
- 配置 CAN 接口(见本文第二部分);
- 打开终端1,运行接收程序:
|--------------------|
| Bash ./can_receive |
- 打开终端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;
}