发送与接收 -- C 语言 SocketCAN API 实战
目录
1. 发送 CAN 帧的完整流程
发送一个 CAN 帧需要 5 个步骤:
创建 socket → 绑定设备 → 构造帧 → 发送 → 关闭1.1 完整代码
参见 examples/02-can-send-receive/can_send.c。核心流程如下:
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
int main(void) {
/* 步骤 1:创建 CAN 原始套接字 */
int sock = socket(AF_CAN, SOCK_RAW, CAN_RAW);
/* 步骤 2:绑定到 vcan0 设备 */
struct ifreq ifr;
strcpy(ifr.ifr_name, "vcan0");
ioctl(sock, SIOCGIFINDEX, &ifr);
struct sockaddr_can addr = {
.can_family = AF_CAN,
.can_ifindex = ifr.ifr_ifindex
};
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
/* 步骤 3:构造 CAN 帧 */
struct can_frame frame;
frame.can_id = 0x123; /* CAN ID */
frame.can_dlc = 4; /* 4 字节数据 */
frame.data[0] = 0x11;
frame.data[1] = 0x22;
frame.data[2] = 0x33;
frame.data[3] = 0x44;
/* 步骤 4:发送 */
write(sock, &frame, sizeof(frame));
/* 步骤 5:关闭 */
close(sock);
return 0;
}1.2 参数化版本
can_send.c 的完整版本支持命令行参数:
# 发送标准帧
./can_send vcan0 0x123 0x11 0x22 0x33 0x44
# 发送 NMT 启动命令(COB-ID 0x000)
./can_send vcan0 0x000 0x01 0x052. 接收 CAN 帧的完整流程
接收与发送类似,但多了过滤器和信号处理:
创建 socket → 设置过滤器 → 绑定设备 → 循环接收 → 关闭2.1 完整代码
参见 examples/02-can-send-receive/can_recv.c。核心流程如下:
#include <signal.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
/* 信号处理 -- Ctrl+C 优雅退出 */
static volatile int running = 1;
static void signal_handler(int sig) {
(void)sig;
running = 0;
}
int main(void) {
signal(SIGINT, signal_handler);
/* 创建并绑定 socket */
int sock = socket(AF_CAN, SOCK_RAW, CAN_RAW);
/* ... bind 代码与发送端相同 ... */
/* 可选:设置过滤器 */
struct can_filter rfilter = {
.can_id = 0x123,
.can_mask = CAN_SFF_MASK
};
setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FILTER,
&rfilter, sizeof(rfilter));
/* 循环接收 */
struct can_frame frame;
while (running) {
int nbytes = read(sock, &frame, sizeof(frame));
if (nbytes > 0) {
printf("ID: 0x%03X DLC: %d Data:",
frame.can_id & CAN_SFF_MASK, frame.can_dlc);
for (int i = 0; i < frame.can_dlc; i++) {
printf(" %02X", frame.data[i]);
}
printf("\n");
}
}
close(sock);
return 0;
}3. 关键 API 详解
3.1 socket()
int socket(int domain, int type, int protocol);| 参数 | 值 | 说明 |
|---|---|---|
| domain | AF_CAN | CAN 协议族(类似 AF_INET 用于 TCP/IP) |
| type | SOCK_RAW | 原始套接字,直接访问 CAN 帧 |
| protocol | CAN_RAW | CAN 原始协议 |
返回值:成功返回 socket 文件描述符(>= 0),失败返回 -1。
3.2 bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);将 socket 绑定到具体的 CAN 设备。can_ifindex 是内核中 CAN 设备的唯一标识。
3.3 write() / read()
ssize_t write(int sockfd, const struct can_frame *frame, size_t len);
ssize_t read(int sockfd, struct can_frame *frame, size_t len);write()是阻塞调用,发送完成后返回read()默认是阻塞调用,收到帧后返回- 返回值是读写的字节数,< 0 表示错误
3.4 setsockopt() – 设置过滤器
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &filter, sizeof(filter));过滤器规则:
- 不设置过滤器 → 接收所有帧
- 设置一个过滤器 → 只接收匹配的帧
- 可以设置多个过滤器(传入数组)
/* 接收 ID = 0x100 或 ID = 0x200 的帧 */
struct can_filter filters[2] = {
{ .can_id = 0x100, .can_mask = CAN_SFF_MASK },
{ .can_id = 0x200, .can_mask = CAN_SFF_MASK }
};
setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FILTER, filters, sizeof(filters));4. 错误处理
4.1 常见错误码
| 错误码 | 含义 | 常见原因 |
|---|---|---|
ENETDOWN | 网络不可达 | CAN 设备未激活 |
EMSGSIZE | 消息过大 | CAN FD 帧传给了非 FD socket |
ENOBUFS | 缓冲区满 | 发送速率超过总线处理能力 |
4.2 CAN 错误帧
CAN 控制器检测到总线错误时会生成错误帧。SocketCAN 可以通过以下方式接收错误帧:
/* 启用错误帧接收 */
can_err_mask_t err_mask = CAN_ERR_MASK;
setsockopt(sock, SOL_CAN_RAW, CAN_RAW_ERR_FILTER,
&err_mask, sizeof(err_mask));错误帧的 can_id 包含 CAN_ERR_FLAG,可以通过此标志判断是否为错误帧:
if (frame.can_id & CAN_ERR_FLAG) {
/* 这是一个错误帧 */
}5. 与命令行工具的对比
理解 C 代码与 can-utils 命令的对应关系:
| can-utils 命令 | 等效 C 代码操作 |
|---|---|
cansend vcan0 123#11223344 | socket() → bind() → 构造 can_frame → write() |
candump vcan0 | socket() → bind() → 循环 read() → 打印 |
candump vcan0,123 | 同上 + setsockopt(CAN_RAW_FILTER) |
关键理解:can-utils 工具本质上就是调用 SocketCAN API 的 C 程序。理解了 API,你就理解了 can-utils 的底层原理。
6. 编译与运行
6.1 编译
cd examples/02-can-send-receive
mkdir build && cd build
cmake ..
make6.2 运行
# 终端 1:启动接收
./can_recv vcan0
# 终端 2:发送数据
./can_send vcan0 0x123 0x11 0x22 0x33 0x44
# 终端 1 输出:
# [接收] CAN ID: 0x123 DLC: 4 数据: 11 22 33 446.3 使用过滤器
# 只接收 ID = 0x123 的帧
./can_recv vcan0 0x1237. 关键知识点总结
| 知识点 | 要点 |
|---|---|
| 发送流程 | socket → bind → 构造帧 → write → close |
| 接收流程 | socket → 设置过滤器 → bind → 循环 read → close |
| struct can_frame | can_id + can_dlc + data[8] |
| 过滤器 | can_filter { can_id, can_mask },内核级过滤 |
| 扩展帧 | `can_id |
| 错误帧 | can_id & CAN_ERR_FLAG 判断 |
★ Insight ─────────────────────────────────────
- SocketCAN 的
read()是阻塞的 —— 这意味着如果没有收到帧,线程会被挂起。在实际产品中,通常会结合select()/poll()或使用非阻塞 I/O 来实现超时处理。 - 过滤器的内核级实现是关键性能优化 —— 如果总线负载很高(如 500kbps 下每秒数千帧),用户态程序如果不设置过滤器,会被
read()淹没。 write()返回值检查很重要 —— 在 CAN 总线关闭(Bus Off)状态下,write()会返回ENETDOWN。不检查的话,你的程序会"以为"帧已发送。─────────────────────────────────────────────────
上一步 | 下一步
← 04-vcan-setup.md —— 虚拟 CAN 设置 → 06-canopen-overview.md —— CANOpen 概览
如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕
支付宝
微信