发送与接收 -- 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 0x05

2. 接收 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);
参数值说明
domainAF_CANCAN 协议族(类似 AF_INET 用于 TCP/IP)
typeSOCK_RAW原始套接字,直接访问 CAN 帧
protocolCAN_RAWCAN 原始协议

返回值:成功返回 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#11223344socket() → bind() → 构造 can_frame → write()
candump vcan0socket() → 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 ..
make

6.2 运行

# 终端 1:启动接收
./can_recv vcan0

# 终端 2:发送数据
./can_send vcan0 0x123 0x11 0x22 0x33 0x44

# 终端 1 输出:
# [接收] CAN ID: 0x123  DLC: 4  数据: 11 22 33 44

6.3 使用过滤器

# 只接收 ID = 0x123 的帧
./can_recv vcan0 0x123

7. 关键知识点总结

知识点要点
发送流程socket → bind → 构造帧 → write → close
接收流程socket → 设置过滤器 → bind → 循环 read → close
struct can_framecan_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 概览

如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕
Cygnus Yang 支付宝支付宝
Cygnus Yang 微信微信