- CAN(Controller Area Network) 是一种用于实时应用的串行通信协议,具有高可靠性、抗干扰能力强等特点。 主要用于汽车内部电子控制单元(ECU)之间的通信。
- Linux 内核从 2.6.25 版本开始引入了 SocketCAN 子系统,它将 CAN 设备抽象成网络设备,使得我们可以使用类似网络套接字的 API 来操作 CAN 总线。
can 启动
不同工控机 can 启动方式会不同,这个需要参考厂家提供的手册。
以下列出通用的启动方式:
# 关闭
sudo ip link set down can0
# 设置 can0 的比特率为 500000
sudo ip link set can0 type can bitrate 500000
# 启动
sudo ip link set up can0
虚拟 CAN 接口(vcan)
如果没有物理 CAN 设备,可以用虚拟接口进行测试:
# 加载 vcan 内核模块
sudo modprobe vcan
# 创建虚拟 CAN 接口
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0
注意,如果使用docker容器可能没有对应的内核模块,虚拟机(如 vmware) 一般是有的。
检查 CAN 接口
- 通过
ifconfig -a检查can接口:ifconfig -a: 显示所有网络设备;ifconfig -a [devname]: 显示指定设备;
ifconfig -a vcan0
vcan0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
UP RUNNING NOARP MTU:16 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
can工具 can-utils
- 安装(ubuntu) :
sudo apt-get install can-utils - 终端常用指令工具
candump,cansend- candump:监听并显示 CAN 总线上的数据。
- cansend:向 CAN 总线发送一帧数据。
- 直接输入
candump,cansend可显示帮助描述。
candump 常用组合指令
| 描述 | 指令 |
|---|---|
| 查看所有数据 | candump canX |
| 查看指定帧0x181数据 | candump canX,181:7FF |
| 查看指定帧0x181数据,并显示时间戳 | candump canX,181:7FF -ta |
| 查看指定帧0x181,0x281数据 | candump canX,181:7FF,281:7FF |
| 查看CANopen节点为1 的 txpdo 所有数据 | candump canX,081:0ff |
| 查看CANopen节点为1 的 rxpdo 所有数据 | candump canX,001:0ff |
- canX: 这是一个占位符,表示CAN接口名称(例如
can0,can1或虚拟接口vcan0)。 - 过滤语法(如
181:7FF)用于指定CAN ID和掩码,以监听特定帧。 - 选项
-ta表示添加时间戳(ASCII格式)。
candump 示例
candump can42
# 输出:
can42 005 [4] 07 8F DF 75
can42 006 [2] 68 E6
can42 181 [6] 00 02 00 01 FF FE
can42 281 [6] 00 01 00 03 00 00
can42 381 [8] 49 03 01 05 00 07 69 20
can42 005 [4] 07 8F DF 8E
can42 006 [2] 68 E7
can42 181 [6] FF FF 00 03 FF F9
can42 281 [6] FF FF 00 00 00 00
can42 381 [8] 49 03 01 05 00 07 69 20
candump can42,181:7FF
# 输出:
can42 181 [6] 00 01 FF FE FF FD
can42 181 [6] FF FF FF FF 00 01
can42 181 [6] FF F9 00 02 FF FD
can42 181 [6] FF FA 00 01 FF FD
can42 181 [6] FF FD 00 03 FF FF
can42 181 [6] 00 01 00 04 FF FF
candump can42,181:7FF -ta
# 输出:
(1757998968.502590) can42 181 [6] 00 02 00 06 00 03
(1757998968.505086) can42 181 [6] 00 02 00 03 FF FF
(1757998968.507657) can42 181 [6] 00 04 00 03 FF FD
(1757998968.510075) can42 181 [6] 00 02 00 01 FF FD
(1757998968.512557) can42 181 [6] 00 01 00 06 FF FC
(1757998968.515106) can42 181 [6] 00 00 00 02 00 01
(1757998968.517700) can42 181 [6] 00 01 00 06 FF FB
candump can42,081:0FF
# 输出:
can42 381 [8] 49 23 01 0C FF F3 69 0A
can42 181 [6] FF FE 00 04 FF FE
can42 281 [6] 00 02 00 03 FF FF
can42 381 [8] 49 23 01 0C FF F3 69 0A
can42 181 [6] 00 03 00 01 FF FC
can42 281 [6] 00 00 00 02 00 02
can42 381 [8] 49 23 01 0C FF F3 69 0A
can42 181 [6] 00 00 00 04 FF FD
cansend 常用组合指令
| 描述 | 指令 |
|---|---|
| 启动CANopen 总线 | cansend canX 000#0100 |
| 发送0x201数据 | cansend canX 201#08000000 |
| 发送远程帧0x701数据 | cansend canX 701#R |
- 其他复杂指令(如CANopen sdo 读写),还是使用
CANopenNode之类的工具,或python脚本。
c++ 代码示例
- 正常按 socket 对象读写就可以。
#pragma once
#include <errno.h>
#include <fcntl.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <algorithm>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
class LinuxCAN {
public:
LinuxCAN(const std::string& name = "can0") : name_(name) {}
~LinuxCAN() { this->Close(); }
public:
bool Open() {
// 0.close previous
this->Close();
struct sockaddr_can addr;
struct ifreq ifr;
int ret = 0;
// 1.Create socket
fd_ = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (fd_ < 0) {
printf("socket PF_CAN failed .\n");
return false;
}
// 2.Specify can channel
strcpy(ifr.ifr_name, name_.c_str());
ret = ioctl(fd_, SIOCGIFINDEX, &ifr);
if (ret < 0) {
printf("ioctl SIOCGIFINDEX failed \n");
return false;
}
// 3.Bind the socket to can
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
ret = bind(fd_, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0) {
printf("bind failed \n");
return false;
}
///* 数据过滤配置参数 */
// const int size = this_config.filter_size;
// struct can_filter rfilter[20];
// /* 数据加入过滤器 */
// for (int i = 0; i < size; i++) {
// rfilter[i].can_id = this_config.rfilter[i];
// rfilter[i].can_mask = CAN_SFF_MASK;
// }
// setsockopt(fd_, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter,sizeof(rfilter));
}
bool Close() {
if (fd_ >= 0) {
close(fd_);
fd_ = -1;
}
return true;
}
public:
uint32_t Mask(uint32_t id, bool SFF = true, bool EFF = false) {
uint32_t ret = id;
if (EFF) {
ret &= CAN_EFF_MASK;
}
if (SFF) {
ret &= CAN_SFF_MASK;
}
return ret;
}
/// blocking , return 0 if success
int RecvOnce(uint32_t& id, std::vector<uint8_t>& data) {
if (fd_ < 0) {
printf("error : CAN not open \n");
return -1;
}
can_frame fr;
int nbytes;
nbytes = read(fd_, &fr, sizeof(can_frame));
if (nbytes < 0) {
printf("error : CAN read error : %d , restart... \n", errno);
return -1;
} else {
id = fr.can_id;
data.resize(fr.can_dlc);
for (int i = 0; i < fr.can_dlc; i++) {
data[i] = fr.data[i];
}
}
return 0;
}
/// blocking , return 0 if success
int SendOnce(uint32_t id, const std::vector<uint8_t>& data) {
if (fd_ < 0) {
printf("error : CAN not open \n");
return -1;
}
int nbytes;
struct can_frame fr;
memset(&fr, 0, sizeof(fr));
fr.can_id = id;
fr.can_dlc = std::min(data.size(), (size_t)8);
std::memcpy(fr.data, data.data(), fr.can_dlc);
nbytes = write(fd_, &fr, sizeof(fr));
if (nbytes < 0) {
printf("error : CAN send error : %d , restart... \n", errno);
return -1;
}
return 0;
}
private:
std::string name_;
int fd_ = -1;
};
实际使用遇到的问题
- 有的工控机can设备运行一段时间会报错,无法再收发数据 (可能是总线有干扰);
- 解决:通过
ip link重启can设备;
- 解决:通过
- No buffer space available 报错;
- 解决:由于缓冲队列空间不足,设置缓冲队列 :
echo 4096 > /sys/class/net/can0/tx_queue_len - 有的工控机 上述操作不管用,最后还是需要运行过程检查can设备状态并重启;
- 解决:由于缓冲队列空间不足,设置缓冲队列 :
- can总线上有多个波特率设备,或can总线没接好,无法收到数据;
- 解决:使用can卡检查can总线状态,排查问题点,检查哪个can设备有问题;
- 有的工控机can设备运行一段时间会报错,且无法通过
ip link重启can设备,或重启后依旧有问题,必须重启工控机才能使用;- 解决:工控机固件驱动的问题,需要联系工控机厂家去处理更新固件;