title: CAN应用编程 tags: [
嵌入式Linux,
Linux应用编程,
CAN,
SocketCAN,
CAN总线,
can_frame,
can_filter,
can-utils,
IMX6ULL,
] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[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 工具,最后给出两个完整可编译运行的程序(发送 / 接收)。
CAN 是 ISO 国际标准化的串行通信协议:
| 时间/标准 | 内容 |
|---|---|
| 1986 年 | 德国博世公司开发出面向汽车的 CAN 通信协议,动机是解决汽车中庞大电子控制系统之间的通讯、减少信号线束 |
| ISO11898 / ISO11519 | CAN 标准化 |
| 现状 | 欧洲汽车网络标准协议,当前应用最广泛的现场总线之一 |
CAN 是一种多主方式的串行通讯总线,基本设计规范要求高比特率、高抗电磁干扰性,并且能检测产生的任何错误。经过几十年发展,其高性能、高可靠性和高实时性已被广泛认可,应用于工业自动化、船舶、医疗设备、工业设备等方面。汽车上的空调、车门、发动机、大量传感器等部件、模块都通过 CAN 总线连成一个网络。
| 特点 | 说明 |
|---|---|
| 多主控制 | 总线空闲时所有单元都可开始发送(CSMA/CA);最先访问总线者获得发送权;多个单元同时发送时,高优先级 ID 的单元获得发送权 |
| 消息发送 | 所有消息以固定格式发送;两个以上单元同时发送时,根据标识符 ID 决定优先级;ID 不表示目的地址,而表示访问总线的消息优先级。逐位仲裁比较,仲裁获胜者继续发送,失利者立即停止发送转为接收 |
| 系统柔软性 | 连接在总线上的单元没有类似「地址」的信息;增加单元时其它单元的软硬件及应用层无需改变 |
| 通信速度 | 可按网络规模设定合适速度;同一网络中所有单元必须设定统一速度,否则会输出错误信号妨碍整个网络;不同网络间可不同 |
| 远程数据请求 | 可通过发送「遥控帧」请求其它单元发送数据 |
| 错误检测/通知/恢复 | 所有单元都可检测错误;检测到错误的单元立即通知其它所有单元;发送单元检测出错误会强制结束当前发送并反复重发直到成功 |
| 故障封闭 | 可判断是总线暂时性数据错误(外部噪声)还是持续性错误(单元内部故障、驱动器故障、断线);持续错误时可将故障单元从总线隔离 |
| 连接 | 可同时连接多个单元,理论上数量无限制;实际受总线时间延迟及电气负载限制。降低速度可增加单元数,提高速度则减少 |
CAN 总线用两根线连接各单元:CAN_H 和 CAN_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["空闲状态"]
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
每个 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["物理层:位时序 / 位编码 / 同步"]
CAN 定义了 5 种类型的报文帧:
| 帧 | 用途 |
|---|---|
| 数据帧 | 发送单元向接收单元传送数据的帧 |
| 遥控帧 | 接收单元向具有相同 ID 的发送单元请求数据的帧 |
| 错误帧 | 检测出错误时向其它单元通知错误的帧 |
| 过载帧 | 接收单元通知其尚未做好接收准备的帧 |
| 间隔帧 | 将数据帧及遥控帧与前面的帧分离开来的帧 |
其中数据帧和遥控帧都有标准格式(11 位标识符 ID)和扩展格式(29 位标识符 ID)。数据帧是使用最多的帧类型。
数据帧由 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 位 | 隐性 |
⚠️ 来源说明:位级仲裁原理不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
当多个节点同时开始发送时,CAN 对消息 ID 的每个位进行逐个仲裁比较:
由于显性覆盖隐性,ID 二进制值越小(前面显性位越多)优先级越高。这种机制称为非破坏性仲裁(Non-destructive Arbitration),不会浪费已发送的报文,保证高优先级消息的实时性。
⚠️ 来源说明:本节不属于《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%
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>
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 原始协议 |
需要用 ioctl(SIOCGIFINDEX) 根据接口名(如 can0)取得接口索引,再填充 struct sockaddr_can 后 bind():
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_family(AF_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;
};
不设置过滤规则时,应用程序默认接收所有 ID 的报文。若只接收特定 ID,可用 setsockopt 设置 CAN_RAW_FILTER。struct can_filter 只有两个成员 can_id 和 can_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_id 与 can_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);
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 位;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));
接收到一帧数据后,可通过判断 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 */
默认情况下 CAN 的本地回环功能是开启的。可用 CAN_RAW_LOOPBACK 关闭或开启:
int loopback = 0; // 0 表示关闭,1 表示开启(默认)
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
回环开启时,所有发送帧都会被回环到与 CAN 总线接口对应的套接字上,因此单块开发板也能自收自发做测试。
⚠️ 来源说明:本节不属于《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) |
开发板出厂系统提供了一些 CAN 测试工具。在 Linux 中 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 设备一致,否则通信失败。
# 发送:# 前是帧 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 的报文(过滤)
#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) |
每秒发送一次 |
#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[] 循环 |
按长度打印数据字节 |
# 交叉编译(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
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
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) |
⚠️ 来源说明:本节不属于《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_frame(can_id/can_dlc/data[]) |
CAN_TxHeaderTypeDef 等 |
字段概念一致 |
| 配置波特率 | ip link set can0 up type can bitrate ... |
配置位时序寄存器 | 都需匹配对端 |
| 工具 | candump / cansend |
无,需自行实现 | SocketCAN 生态完善 |
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
答:CAN 通过 CAN_H、CAN_L 两根线之间的电位差判断总线电平,差分传输抗共模干扰能力强、可靠性高。显性电平表示逻辑 0,此时 CAN_H 约 3.5V、CAN_L 约 1.5V,电位差约 2V;隐性电平表示逻辑 1,此时两线都约 2.5V,电位差约 0V。总线空闲时保持隐性状态,且显性电平会覆盖隐性电平,这是仲裁和 ACK 的基础。
答:多个节点同时发送时,CAN 对 ID 的每一位逐位比较:节点在发送每一位的同时监听总线,若它发出隐性(1)却检测到显性(0),说明有更高优先级节点在发送,该节点立即退出仲裁转为接收,获胜者继续发送,这种非破坏性仲裁不浪费报文。由于显性覆盖隐性,ID 二进制值越小(高位显性位越多)优先级越高,因此需要实时响应的消息应分配更小的 ID。
答:数据帧由帧起始、仲裁段、控制段、数据段、CRC 段、ACK 段、帧结束 7 个段构成,数据段可携带 0~8 字节数据。标准帧使用 11 位标识符,扩展帧使用 29 位标识符(11 位基本 ID + 18 位扩展 ID);两者通过 IDE 位区分,扩展帧还多了 SRR 位。遥控帧(远程帧)与数据帧结构类似,通过 RTR 位区分,用于请求具有相同 ID 的发送单元发送数据。
答:先 socket(PF_CAN, SOCK_RAW, CAN_RAW) 创建套接字;然后用 strcpy(ifr.ifr_name, "can0") 指定设备名,ioctl(sockfd, SIOCGIFINDEX, &ifr) 获取接口索引;接着填充 struct sockaddr_can 的 can_family = AF_CAN 和 can_ifindex = ifr.ifr_ifindex;最后 bind() 把套接字与 can0 绑定。收发数据分别用 write()/read() 配合 struct can_frame,过滤规则通过 setsockopt(SOL_CAN_RAW, CAN_RAW_FILTER, ...) 设置。
struct can_frame 与 struct can_filter 各有什么作用?CAN_RAW_LOOPBACK 有什么意义?答:struct can_frame 用于封装一帧 CAN 数据,包含 can_id(标识符及 EFF/RTR/ERR 标志位)、can_dlc(数据长度)、data[8](数据),发送和接收都通过它。struct can_filter 含 can_id 和 can_mask,用于设置只接收哪些 ID 的报文,内核将收到报文的 can_id & can_mask 与 can_id 比较,相等则接收;把 CAN_RAW_FILTER 的第四、五参数设为 NULL, 0 表示只发不收。CAN_RAW_LOOPBACK 默认开启,发送帧会回环到对应 CAN 接口的套接字,因此单块开发板即可自收自发完成测试。
内容来源:《I.MX6U嵌入式Linux C应用编程指南》第三十一章 CAN应用编程基础(基础内容参考瑞萨电子《CAN 入门教程》);例程源码 31_can/can_write.c、31_can/can_read.c