02-CAN应用编程.md 37 KB


title: CAN应用编程 tags: [

嵌入式Linux,
Linux应用编程,
CAN,
SocketCAN,
CAN总线,
can_frame,
can_filter,
can-utils,
IMX6ULL,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十一章 CAN应用编程基础"

CAN应用编程

💡 关联知识:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]];延伸阅读:[[嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/04-CAN驱动开发]]

CAN(Controller Area Network,控制器局域网络)是目前应用非常广泛的现场总线之一,主要应用于汽车电子和工业领域。汽车上大量传感器与模块都通过 CAN 总线连接。Linux 把 CAN 设备当网络设备管理,因此 CAN 应用编程近似于以太网 socket 通信——内核提供了 SocketCAN 接口。本篇先讲 CAN 总线原理(差分信号、仲裁、帧格式),再系统讲解 SocketCAN 编程 API 与 can-utils 工具,最后给出两个完整可编译运行的程序(发送 / 接收)。


1. 什么是 CAN

CAN 是 ISO 国际标准化的串行通信协议:

时间/标准 内容
1986 年 德国博世公司开发出面向汽车的 CAN 通信协议,动机是解决汽车中庞大电子控制系统之间的通讯、减少信号线束
ISO11898 / ISO11519 CAN 标准化
现状 欧洲汽车网络标准协议,当前应用最广泛的现场总线之一

CAN 是一种多主方式的串行通讯总线,基本设计规范要求高比特率、高抗电磁干扰性,并且能检测产生的任何错误。经过几十年发展,其高性能、高可靠性和高实时性已被广泛认可,应用于工业自动化、船舶、医疗设备、工业设备等方面。汽车上的空调、车门、发动机、大量传感器等部件、模块都通过 CAN 总线连成一个网络。


2. CAN 的特点

特点 说明
多主控制 总线空闲时所有单元都可开始发送(CSMA/CA);最先访问总线者获得发送权;多个单元同时发送时,高优先级 ID 的单元获得发送权
消息发送 所有消息以固定格式发送;两个以上单元同时发送时,根据标识符 ID 决定优先级;ID 不表示目的地址,而表示访问总线的消息优先级。逐位仲裁比较,仲裁获胜者继续发送,失利者立即停止发送转为接收
系统柔软性 连接在总线上的单元没有类似「地址」的信息;增加单元时其它单元的软硬件及应用层无需改变
通信速度 可按网络规模设定合适速度;同一网络中所有单元必须设定统一速度,否则会输出错误信号妨碍整个网络;不同网络间可不同
远程数据请求 可通过发送「遥控帧」请求其它单元发送数据
错误检测/通知/恢复 所有单元都可检测错误;检测到错误的单元立即通知其它所有单元;发送单元检测出错误会强制结束当前发送并反复重发直到成功
故障封闭 可判断是总线暂时性数据错误(外部噪声)还是持续性错误(单元内部故障、驱动器故障、断线);持续错误时可将故障单元从总线隔离
连接 可同时连接多个单元,理论上数量无限制;实际受总线时间延迟及电气负载限制。降低速度可增加单元数,提高速度则减少

3. CAN 的电气属性

CAN 总线用两根线连接各单元:CAN_HCAN_L。CAN 控制器通过判断两根线上的电位差得到总线电平,分为显性电平隐性电平

电平 逻辑值 CAN_H CAN_L 电位差
显性电平 逻辑 0 约 3.5V 约 1.5V 2V
隐性电平 逻辑 1 约 2.5V 约 2.5V 0V

CAN 总线上没有节点传输数据时一直处于隐性状态(总线空闲即隐性)。CAN 总线就通过显性、隐性电平的变化把数据发送出去。显性电平覆盖隐性电平,这是仲裁机制的基础。

flowchart LR
    X["显性电平(逻辑 0)<br/>CAN_H=3.5V, CAN_L=1.5V<br/>压差 2V"] -->|"覆盖"| Y["隐性电平(逻辑 1)<br/>CAN_H=2.5V, CAN_L=2.5V<br/>压差 0V"]
    Y -.->|"总线空闲时保持隐性"| Z["空闲状态"]

4. CAN 网络拓扑

CAN 是分布式控制总线,与以太网一样由许多 CAN 节点构成。每个节点都非常简单:

flowchart TB
    subgraph N1["CAN 节点 1"]
        M1["MCU 微控制器"] --> C1["CAN 控制器"] --> T1["CAN 收发器"]
    end
    subgraph N2["CAN 节点 2"]
        M2["MCU 微控制器"] --> C2["CAN 控制器"] --> T2["CAN 收发器"]
    end
    T1 ===|"CAN_H"| T2
    T1 ===|"CAN_L"| T2
    R1["120Ω 端接电阻"] --- T1
    R2["120Ω 端接电阻"] --- T2
  • 每个节点 = MCU + CAN 控制器 + CAN 收发器三部分;
  • 通过 CAN_H、CAN_L 两条线连接成 CAN 局域网络;
  • CAN 可使用多种物理介质(双绞线、光纤等),最常用双绞线,信号使用差分电压传送;
  • CAN 总线两端各接一个 120Ω 端接电阻,用于匹配总线阻抗、吸收信号反射及回拨,提高抗干扰能力与可靠性;
  • 传输速度可达 1Mbps,最新 CAN-FD 最高可达 5Mbps 甚至更高;速度与总线距离有关,距离越短速度越快。

每个 CAN 节点相当于局域网络中的一台主机。


5. CAN 总线通信模型

CAN 传输协议参考了 OSI 开放系统互连模型,但实际只定义了传输层、数据链路层、物理层三层,应用层协议可以由用户定义成适合特定工业领域的方案。

CAN 的内容
应用层 用户自定义;常见标准:DeviceNet(工业控制/制造业,为 PLC 和智能传感器设计)、CANopen(工业控制)、OBD / UDS(统一诊断服务, ISO14229,乘用车诊断)、SAEJ1939(商用车)
数据链路层 分 LLC 子层和 MAC 子层,MAC 子层是 CAN 协议的核心;负责把物理层信号组织成有意义的消息,提供传送错误控制等流程:消息的帧化、仲裁、应答、错误的检测或报告,通常在 CAN 控制器硬件中执行
物理层 定义信号实际的发送方式、位时序、位的编码方式及同步步骤;信号电平、通信速度、采样点、驱动器/总线电气特性、连接器形态等由用户根据系统需求确定
flowchart TB
    A["应用层:DeviceNet / CANopen / OBD / UDS / SAEJ1939(用户自定义)"] --> B["数据链路层:LLC 子层 + MAC 子层(CAN 核心)"]
    B --> C["物理层:位时序 / 位编码 / 同步"]

6. CAN 帧的种类

CAN 定义了 5 种类型的报文帧:

用途
数据帧 发送单元向接收单元传送数据的帧
遥控帧 接收单元向具有相同 ID 的发送单元请求数据的帧
错误帧 检测出错误时向其它单元通知错误的帧
过载帧 接收单元通知其尚未做好接收准备的帧
间隔帧 将数据帧及遥控帧与前面的帧分离开来的帧

其中数据帧和遥控帧都有标准格式(11 位标识符 ID)和扩展格式(29 位标识符 ID)。数据帧是使用最多的帧类型。

6.1 数据帧的结构

数据帧由 7 个段构成:

说明
帧起始 表示数据帧开始的段
仲裁段 表示该帧优先级的段
控制段 表示数据的字节数及保留位的段
数据段 数据的内容,可发送 0~8 个字节
CRC 段 检查帧的传输错误的段
ACK 段 表示确认正常接收的段
帧结束 表示数据帧结束的段
flowchart LR
    SOF["帧起始<br/>SOF 1位<br/>显性"] --> ARB["仲裁段<br/>ID + RTR"]
    ARB --> CTRL["控制段<br/>IDE + r0 + DLC 4位"]
    CTRL --> DATA["数据段<br/>0~8 字节"]
    DATA --> CRC["CRC段<br/>CRC 15位 + 界定符"]
    CRC --> ACK["ACK段<br/>ACK槽 + 界定符"]
    ACK --> EOF["帧结束<br/>EOF 7位 隐性"]

⚠️ 来源说明:以下位级字段细节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书仅给出「7 个段」的概述,并指引读者参考瑞萨电子《CAN 入门教程》。

标准帧与扩展帧的位级构成对比:

字段 标准帧 扩展帧 说明
帧起始 SOF 1 位 1 位 显性,标志帧开始
标识符 ID 11 位 11 位基本 ID 决定优先级,ID 越小优先级越高
SRR / IDE IDE(1 位,显性) SRR(1 位,隐性)+ IDE(1 位,隐性) 区分标准/扩展格式
扩展 ID 18 位 与基本 ID 组成 29 位扩展 ID
RTR 1 位 1 位 数据帧为显性,遥控帧为隐性
控制段 IDE + r0 + DLC(4 位) 同左 DLC 表示数据字节数 0~8
数据段 0~8 字节 0~8 字节 实际数据
CRC 段 15 位 + 界定符 15 位 + 界定符 校验传输错误
ACK 段 ACK 槽 + 界定符 同左 发送方发隐性,接收方正确接收后拉为显性
帧结束 EOF 7 位 7 位 隐性

6.2 仲裁机制

⚠️ 来源说明:位级仲裁原理不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

当多个节点同时开始发送时,CAN 对消息 ID 的每个位进行逐个仲裁比较

  1. 每个发送节点在发送每一位的同时监听总线电平;
  2. 发出隐性(1)却检测到显性(0)的节点,判定仲裁失利,立即停止发送转为接收;
  3. 仲裁获胜的节点继续发送,无需重发。

由于显性覆盖隐性,ID 二进制值越小(前面显性位越多)优先级越高。这种机制称为非破坏性仲裁(Non-destructive Arbitration),不会浪费已发送的报文,保证高优先级消息的实时性。

6.3 位时序

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

CAN 是异步串行总线,收发双方靠位时序同步。一个位时间(Bit Time)被划分为若干时间段,每段由若干个最小时间单位 Tq(Time Quantum) 组成:

作用
同步段 SYNC_SEG 1 个 Tq,用于硬同步,期望信号跳变发生在此段
传播时间段 PROP_SEG 补偿总线上的物理传播延迟和收发器延迟
相位缓冲段 1 PHASE_SEG1 补偿正相位误差,可通过重同步延长
相位缓冲段 2 PHASE_SEG2 补偿负相位误差,可通过重同步缩短

采样点通常位于 PHASE_SEG1 结束处,大致在位时间的 75%~87.5% 位置(常用 87.5%)。配置 bitrate 时,采样点、波特率分频(brp)、各段长度需匹配晶振与网络延迟。

位时间 = SYNC_SEG + PROP_SEG + PHASE_SEG1 + PHASE_SEG2
波特率 = 1 / 位时间
采样点 = (SYNC_SEG + PROP_SEG + PHASE_SEG1) / 位时间 × 100%

7. SocketCAN 应用编程

Linux 将 CAN 设备作为网络设备统一管理,提供 SocketCAN 应用编程接口,使 CAN 通信近似于以太网通信。SocketCAN 中大部分数据结构和函数在头文件 linux/can.h 中定义,应用程序必须包含:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>

7.1 创建 CAN 套接字

CAN 套接字采用标准网络套接字操作,第一个参数设为 PF_CAN,类型设为 SOCK_RAW,协议设为 CAN_RAW

int sockfd = -1;
sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (0 > sockfd) {
    perror("socket error");
    exit(EXIT_FAILURE);
}
参数 取值 说明
domain PF_CAN 指定 CAN 通信协议
type SOCK_RAW 原始套接字类型
protocol CAN_RAW CAN 原始协议

7.2 将套接字与 CAN 设备绑定

需要用 ioctl(SIOCGIFINDEX) 根据接口名(如 can0)取得接口索引,再填充 struct sockaddr_canbind()

struct ifreq ifr = {0};
struct sockaddr_can can_addr = {0};
int ret;

strcpy(ifr.ifr_name, "can0");          // 指定设备名
ioctl(sockfd, SIOCGIFINDEX, &ifr);     // 获取接口索引

can_addr.can_family = AF_CAN;          // 填充数据
can_addr.can_ifindex = ifr.ifr_ifindex;

ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
if (0 > ret) {
    perror("bind error");
    close(sockfd);
    exit(EXIT_FAILURE);
}
结构体 头文件 关键成员
struct ifreq <net/if.h> ifr_name(接口名)、ifr_ifindex(接口索引)
struct sockaddr_can <linux/can.h> can_familyAF_CAN)、can_ifindex(接口索引)

⚠️ 来源说明:以下 struct sockaddr_can 完整定义不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识(原书未展开该结构体)。

struct sockaddr_can {
    __kernel_sa_family_t can_family;  /* AF_CAN */
    int                  can_ifindex; /* CAN 网络设备接口索引 */
    union {
        struct { canid_t rx_id, tx_id; } tp;  /* 传输协议地址(ISO-TP) */
    } can_addr;
};

7.3 设置过滤规则

不设置过滤规则时,应用程序默认接收所有 ID 的报文。若只接收特定 ID,可用 setsockopt 设置 CAN_RAW_FILTERstruct can_filter 只有两个成员 can_idcan_mask

struct can_filter rfilter[2];          // 定义一个 can_filter 结构体对象

// 填充过滤规则:只接收 ID 为 (can_id & can_mask) 的报文
rfilter[0].can_id = 0x60A;
rfilter[0].can_mask = 0x7FF;
rfilter[1].can_id = 0x60B;
rfilter[1].can_mask = 0x7FF;

// 调用 setsockopt 设置过滤规则
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

规则说明:内核把收到报文的 can_idcan_mask 按位与,再与 can_id 比较,相等则接收。can_mask 中为 1 的位参与比较,为 0 的位忽略。例如 can_id=0x60A, can_mask=0x7FF 表示精确匹配标准帧 ID 0x60A

如果应用只发送、不接收任何报文,可在内核中省略接收队列以减少 CPU 消耗,此时把第 4 个参数设为 NULL、第 5 个参数设为 0

setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);

7.4 数据发送与接收

CAN 每次通信都采用 struct can_frame 把数据封装成帧:

struct can_frame {
    canid_t can_id; /* CAN 标识符 */
    __u8    can_dlc;/* 数据长度(最长为 8 个字节) */
    __u8    __pad;  /* padding */
    __u8    __res0; /* reserved / padding */
    __u8    __res1; /* reserved / padding */
    __u8    data[8];/* 数据 */
};
  • 标准帧使用 can_id 的低 11 位
  • 扩展帧使用 0~28 位
  • can_id 的第 29、30、31 位是帧的标志位,用于定义帧类型。

    /* special address description flags for the CAN_ID */
    #define CAN_EFF_FLAG 0x80000000U  /* 扩展帧的标识 */
    #define CAN_RTR_FLAG 0x40000000U  /* 远程帧的标识 */
    #define CAN_ERR_FLAG 0x20000000U  /* 错误帧的标识,用于错误检查 */
    
    /* mask */
    #define CAN_SFF_MASK 0x000007FFU  /* <can_id & CAN_SFF_MASK> 获取标准帧 ID */
    #define CAN_EFF_MASK 0x1FFFFFFFU  /* <can_id & CAN_EFF_MASK> 获取扩展帧 ID */
    #define CAN_ERR_MASK 0x1FFFFFFFU  /* omit EFF, RTR, ERR flags */
    

数据发送使用 write()

struct can_frame frame;
int ret;

frame.can_id = 123;         // 标准帧 ID;扩展帧写作 CAN_EFF_FLAG | 123
frame.can_dlc = 3;          // 数据长度为 3
frame.data[0] = 0xA0;
frame.data[1] = 0xB0;
frame.data[2] = 0xC0;

ret = write(sockfd, &frame, sizeof(frame));  // 发送数据
if (sizeof(frame) != ret)                    // ret 不等于帧长度说明发送失败
    perror("write error");

发送遥控帧(远程帧):

frame.can_id = CAN_RTR_FLAG | 123;   // 帧 ID 为 123 的远程帧
write(sockfd, &frame, sizeof(frame));

数据接收使用 read()

struct can_frame frame;
int ret = read(sockfd, &frame, sizeof(frame));

7.5 错误处理

接收到一帧数据后,可通过判断 can_id 中的 CAN_ERR_FLAG 位判断是否为错误帧;若为错误帧,可通过 can_id 的其它符号位判断具体原因。错误帧符号位在 <linux/can/error.h> 中定义:

/* error class (mask) in can_id */
#define CAN_ERR_TX_TIMEOUT  0x00000001U /* TX timeout (by netdevice driver) */
#define CAN_ERR_LOSTARB     0x00000002U /* lost arbitration    / data[0] */
#define CAN_ERR_CRTL        0x00000004U /* controller problems / data[1] */
#define CAN_ERR_PROT        0x00000008U /* protocol violations / data[2..3] */
#define CAN_ERR_TRX         0x00000010U /* transceiver status  / data[4] */
#define CAN_ERR_ACK         0x00000020U /* received no ACK on transmission */
#define CAN_ERR_BUSOFF      0x00000040U /* bus off */
#define CAN_ERR_BUSERROR    0x00000080U /* bus error (may flood!) */
#define CAN_ERR_RESTARTED   0x00000100U /* controller restarted */

7.6 回环功能设置

默认情况下 CAN 的本地回环功能是开启的。可用 CAN_RAW_LOOPBACK 关闭或开启:

int loopback = 0;   // 0 表示关闭,1 表示开启(默认)
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));

回环开启时,所有发送帧都会被回环到与 CAN 总线接口对应的套接字上,因此单块开发板也能自收自发做测试

7.7 其它 SocketCAN 套接字选项

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

选项 层级 作用
CAN_RAW_FILTER SOL_CAN_RAW 设置接收过滤规则
CAN_RAW_ERR_FILTER SOL_CAN_RAW 设置要接收的错误帧类型掩码
CAN_RAW_LOOPBACK SOL_CAN_RAW 本地回环开关,默认开启
CAN_RAW_RECV_OWN_MSGS SOL_CAN_RAW 是否接收自己发送的报文(回环开启后本 socket 是否收到)
CAN_RAW_JOIN_FILTERS SOL_CAN_RAW 多个过滤器之间改为「或」的并集关系
CAN_RAW_FD_FRAMES SOL_CAN_RAW 使能 CAN-FD 帧(struct canfd_frame

8. can-utils 工具

开发板出厂系统提供了一些 CAN 测试工具。在 Linux 中 CAN 设备作为网络设备管理,配置命令与以太网相同。

8.1 查看与配置 CAN 设备

# 查看 CAN 设备(ALPHA 板上只有 can0)
ifconfig -a | grep can
ifconfig can0

# 配置波特率:先关闭设备,再以 1000000 波特率启用(triple-sampling 三次采样)
ifconfig can0 down
ip link set can0 up type can bitrate 1000000 triple-sampling on

# 查看详细参数(扩展)
ip -details link show can0

CAN 分析仪等测试设备的波特率必须和开发板 CAN 设备一致,否则通信失败。

8.2 发送与接收

# 发送:# 前是帧 ID,后面是数据(十六进制,用 . 分隔)
cansend can0 123#01.02.03.04.05.06.07.08

# 接收:显示 CAN 报文,-ta 表示显示绝对时间戳
candump -ta can0

# 按 Ctrl+C 退出

⚠️ 来源说明:以下工具不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

candump can0                 # 列出报文,Ctrl+C 退出
cangen can0 -g 10 -I 123     # 以 10ms 间隔生成 ID=0x123 的随机数据帧
canbusload can0@1000000      # 统计总线负载率
cansniffer can0              # 动态过滤、按 ID 归类显示变化中的报文
candump can0,123:7FF         # 只显示 ID=0x123 的报文(过滤)

9. CAN 应用编程实战

9.1 CAN 数据发送实例(can_write.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>

int main(void)
{
    struct ifreq ifr = {0};
    struct sockaddr_can can_addr = {0};
    struct can_frame frame = {0};
    int sockfd = -1;
    int ret;

    /* 打开套接字 */
    sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (0 > sockfd) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    /* 指定 can0 设备 */
    strcpy(ifr.ifr_name, "can0");
    ioctl(sockfd, SIOCGIFINDEX, &ifr);
    can_addr.can_family = AF_CAN;
    can_addr.can_ifindex = ifr.ifr_ifindex;

    /* 将 can0 与套接字进行绑定 */
    ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
    if (0 > ret) {
        perror("bind error");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    /* 设置过滤规则:不接受任何报文、仅发送数据 */
    setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);

    /* 发送数据 */
    frame.data[0] = 0xA0;
    frame.data[1] = 0xB0;
    frame.data[2] = 0xC0;
    frame.data[3] = 0xD0;
    frame.data[4] = 0xE0;
    frame.data[5] = 0xF0;
    frame.can_dlc = 6;      // 一次发送 6 个字节数据
    frame.can_id = 0x123;   // 帧 ID 为 0x123,标准帧

    for ( ; ; ) {
        ret = write(sockfd, &frame, sizeof(frame)); // 发送数据
        if (sizeof(frame) != ret) {                 // ret 不等于帧长度说明发送失败
            perror("write error");
            goto out;
        }
        sleep(1);                                   // 一秒钟发送一次
    }

out:
    /* 关闭套接字 */
    close(sockfd);
    exit(EXIT_SUCCESS);
}

逐段解释:

代码段 作用
socket(PF_CAN, SOCK_RAW, CAN_RAW) 创建 CAN 原始套接字
strcpy(ifr.ifr_name, "can0") 指定 CAN 设备名为 can0
ioctl(SIOCGIFINDEX, &ifr) 取得 can0 的接口索引,存入 ifr.ifr_ifindex
can_addr.can_family/can_ifindex 填充 CAN 套接字地址
bind() 把套接字与 can0 绑定
CAN_RAW_FILTER, NULL, 0 只发不收,省略内核接收队列
frame.can_id = 0x123 标准帧 ID,低 11 位有效
frame.can_dlc = 6 数据长度 6 字节
write() 发送一帧,返回值为 sizeof(frame) 表示成功
sleep(1) 每秒发送一次

9.2 CAN 数据接收实例(can_read.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>

int main(void)
{
    struct ifreq ifr = {0};
    struct sockaddr_can can_addr = {0};
    struct can_frame frame = {0};
    int sockfd = -1;
    int i;
    int ret;

    /* 打开套接字 */
    sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (0 > sockfd) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }

    /* 指定 can0 设备 */
    strcpy(ifr.ifr_name, "can0");
    ioctl(sockfd, SIOCGIFINDEX, &ifr);
    can_addr.can_family = AF_CAN;
    can_addr.can_ifindex = ifr.ifr_ifindex;

    /* 将 can0 与套接字进行绑定 */
    ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
    if (0 > ret) {
        perror("bind error");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    /* 设置过滤规则 */
    //setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);

    /* 接收数据 */
    for ( ; ; ) {
        if (0 > read(sockfd, &frame, sizeof(struct can_frame))) {
            perror("read error");
            break;
        }

        /* 校验是否接收到错误帧 */
        if (frame.can_id & CAN_ERR_FLAG) {
            printf("Error frame!\n");
            break;
        }

        /* 校验帧格式 */
        if (frame.can_id & CAN_EFF_FLAG)    // 扩展帧
            printf("扩展帧 <0x%08x> ", frame.can_id & CAN_EFF_MASK);
        else                                // 标准帧
            printf("标准帧 <0x%03x> ", frame.can_id & CAN_SFF_MASK);

        /* 校验帧类型:数据帧还是远程帧 */
        if (frame.can_id & CAN_RTR_FLAG) {
            printf("remote request\n");
            continue;
        }

        /* 打印数据长度 */
        printf("[%d] ", frame.can_dlc);

        /* 打印数据 */
        for (i = 0; i < frame.can_dlc; i++)
            printf("%02x ", frame.data[i]);
        printf("\n");
    }

    /* 关闭套接字 */
    close(sockfd);
    exit(EXIT_SUCCESS);
}

逐段解释:

代码段 作用
socket() / ioctl() / bind() 与发送端完全相同的创建与绑定流程
注释掉的 setsockopt 若打开,则「不接受任何报文」;默认不设置即接收所有 ID
read() 阻塞读取一帧 CAN 报文,存入 frame
can_id & CAN_ERR_FLAG 判断是否为错误帧,是则退出
can_id & CAN_EFF_FLAG 判断扩展帧,用 CAN_EFF_MASK 取出 29 位 ID
can_id & CAN_SFF_MASK 标准帧取 11 位 ID
can_id & CAN_RTR_FLAG 判断是否为远程帧(遥控帧)
can_dlc + data[] 循环 按长度打印数据字节

9.3 编译、部署与测试步骤

# 交叉编译(Ubuntu 上执行)
arm-linux-gnueabihf-gcc -o can_write can_write.c
arm-linux-gnueabihf-gcc -o can_read  can_read.c

# 拷贝到开发板
scp can_write can_read root@192.168.1.10:/home/root/

# 开发板上配置 CAN 设备(波特率必须与对端一致)
ifconfig can0 down
ip link set can0 up type can bitrate 1000000 triple-sampling on

方式一:单板回环测试(CAN 默认开启本地回环)

# 终端 1:运行接收程序
./can_read
# 终端 2:运行发送程序
./can_write
# can_read 会不断打印:标准帧 <123> [6] a0 b0 c0 d0 e0 f0

方式二:双板通信(两块开发板 CAN_H↔CAN_H、CAN_L↔CAN_L 对接,两端各接 120Ω 端接电阻)

# A 板运行 can_read,B 板运行 can_write(或反之)
# B 板每 1 秒发送一帧 ID=0x123 的 6 字节数据,A 板打印接收结果

方式三:配合 CAN 分析仪 / can-utils

# 开发板发,上位机(或另一块板)收
./can_write
# 上位机 candump 或分析仪可见 ID=0x123 的报文

# 上位机发,开发板收
cansend can0 123#01.02.03.04.05.06.07.08
./can_read

9.4 调试方法

现象 可能原因 排查手段
socket error 内核不支持 SocketCAN 或未加载 CAN 驱动 ifconfig -a 看是否有 can0;dmesg 看驱动加载日志
bind error 设备名写错或 can0 未 up ifconfig can0 检查状态,ip link set can0 up ...
收不到数据 双端波特率不一致、接线错误、缺端接电阻 ip -details link show can0 核对 bitrate;检查 CAN_H/CAN_L;两端接 120Ω
收到 Error frame! 总线错误 / bus-off 查看 dmesg 内核 CAN 报错,检查布线、终端电阻、波特率
write error 接口 down 或被过滤 确认 ifconfig can0 up,返回值是否等于 sizeof(frame)
单板回环收不到 回环被关闭 CAN_RAW_LOOPBACK 设为 1(默认即 1)

10. 跨平台对比:Linux SocketCAN vs STM32 vs 裸机

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

维度 Linux SocketCAN(本教程) STM32(HAL/裸机) 说明
接口层次 标准 socket(PF_CAN 直接操作 bxCAN 寄存器 Linux 把 CAN 当网络设备
发送 write(sockfd, &frame, sizeof(frame)) 填充发送邮箱 / HAL_CAN_AddTxMessage 数据都封装为帧
接收 read() + can_frame 中断/轮询接收邮箱 Linux 由内核管理接收队列
过滤 can_filter + CAN_RAW_FILTER 配置筛选器组(Filter Bank) 硬件/内核过滤
帧格式 struct can_framecan_id/can_dlc/data[] CAN_TxHeaderTypeDef 字段概念一致
配置波特率 ip link set can0 up type can bitrate ... 配置位时序寄存器 都需匹配对端
工具 candump / cansend 无,需自行实现 SocketCAN 生态完善

11. 面试精选

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

Q1:CAN 为什么使用差分信号?显性电平和隐性电平分别对应什么逻辑值?

:CAN 通过 CAN_H、CAN_L 两根线之间的电位差判断总线电平,差分传输抗共模干扰能力强、可靠性高。显性电平表示逻辑 0,此时 CAN_H 约 3.5V、CAN_L 约 1.5V,电位差约 2V;隐性电平表示逻辑 1,此时两线都约 2.5V,电位差约 0V。总线空闲时保持隐性状态,且显性电平会覆盖隐性电平,这是仲裁和 ACK 的基础。

Q2:CAN 的仲裁机制是怎样的?为什么 ID 越小优先级越高?

:多个节点同时发送时,CAN 对 ID 的每一位逐位比较:节点在发送每一位的同时监听总线,若它发出隐性(1)却检测到显性(0),说明有更高优先级节点在发送,该节点立即退出仲裁转为接收,获胜者继续发送,这种非破坏性仲裁不浪费报文。由于显性覆盖隐性,ID 二进制值越小(高位显性位越多)优先级越高,因此需要实时响应的消息应分配更小的 ID。

Q3:数据帧由哪 7 个段组成?标准帧与扩展帧的区别是什么?

:数据帧由帧起始、仲裁段、控制段、数据段、CRC 段、ACK 段、帧结束 7 个段构成,数据段可携带 0~8 字节数据。标准帧使用 11 位标识符,扩展帧使用 29 位标识符(11 位基本 ID + 18 位扩展 ID);两者通过 IDE 位区分,扩展帧还多了 SRR 位。遥控帧(远程帧)与数据帧结构类似,通过 RTR 位区分,用于请求具有相同 ID 的发送单元发送数据。

Q4:SocketCAN 中创建并绑定一个 CAN 套接字的流程是什么?

:先 socket(PF_CAN, SOCK_RAW, CAN_RAW) 创建套接字;然后用 strcpy(ifr.ifr_name, "can0") 指定设备名,ioctl(sockfd, SIOCGIFINDEX, &ifr) 获取接口索引;接着填充 struct sockaddr_cancan_family = AF_CANcan_ifindex = ifr.ifr_ifindex;最后 bind() 把套接字与 can0 绑定。收发数据分别用 write()/read() 配合 struct can_frame,过滤规则通过 setsockopt(SOL_CAN_RAW, CAN_RAW_FILTER, ...) 设置。

Q5:struct can_framestruct can_filter 各有什么作用?CAN_RAW_LOOPBACK 有什么意义?

struct can_frame 用于封装一帧 CAN 数据,包含 can_id(标识符及 EFF/RTR/ERR 标志位)、can_dlc(数据长度)、data[8](数据),发送和接收都通过它。struct can_filtercan_idcan_mask,用于设置只接收哪些 ID 的报文,内核将收到报文的 can_id & can_maskcan_id 比较,相等则接收;把 CAN_RAW_FILTER 的第四、五参数设为 NULL, 0 表示只发不收。CAN_RAW_LOOPBACK 默认开启,发送帧会回环到对应 CAN 接口的套接字,因此单块开发板即可自收自发完成测试。


延伸阅读

  • CAN 驱动原理:[[嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/04-CAN驱动开发]]
  • 本库相关:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]]

内容来源:《I.MX6U嵌入式Linux C应用编程指南》第三十一章 CAN应用编程基础(基础内容参考瑞萨电子《CAN 入门教程》);例程源码 31_can/can_write.c31_can/can_read.c