在 Linux 中使用 CAN 通信:从配置到测试与代码实现
引言
CAN(Controller Area Network)是一种广泛用于嵌入式系统、汽车和工业控制中的通信协议。Linux 支持 CAN 协议栈,并通过 SocketCAN 实现对 CAN 总线的访问。在这篇博客中,我们将深入讲解如何在 Linux 系统中配置和使用 CAN 通信,详细介绍配置环境、测试案例、代码实现以及如何使用 can-utils
工具和自定义代码进行测试。
本文内容
- 环境配置:包括有外设和没有外设两种情况。
- 测试案例:如何使用
can-utils
和自定义代码测试 CAN 通信。 - 代码实现:编写一个高效且线程安全的 CAN 通信代码,并详细注释每一部分。
- 调试和测试:如何进行调试以及常见问题的解决方法。
1. 环境配置
1.1 安装和配置必备工具
在 Linux 系统上使用 CAN 通信,首先需要安装一些必备的工具和库:
- SocketCAN 驱动程序:这是 Linux 内核中实现 CAN 协议栈的模块,通常在大多数 Linux 发行版中已经默认启用。
- can-utils 工具:一个用于测试和调试 CAN 总线通信的工具集。
- 编译器和开发工具:用于编译 C++ 代码的工具。
安装依赖
首先,确保你安装了所需的开发工具和库:
sudo apt update
sudo apt install build-essential
sudo apt install can-utils # 安装 can-utils 工具包
sudo apt install libsocketcan-dev # 如果需要安装 SocketCAN 开发库
can-utils
包含多个实用工具,例如 cansend
和 candump
,可以用于测试 CAN 总线的发送和接收。
1.2 配置虚拟 CAN 接口(没有外设的情况)
如果你没有物理 CAN 接口设备(如 USB-to-CAN 适配器),你可以使用虚拟 CAN 接口 vcan0
来进行测试。虚拟接口适用于不需要实际硬件的 CAN 总线仿真和开发。
启用虚拟 CAN 接口
-
加载
vcan
驱动模块:sudo modprobe vcan
-
创建虚拟 CAN 接口
vcan0
:sudo ip link add dev vcan0 type vcan sudo ip link set vcan0 up
-
测试虚拟接口:
使用
can-utils
工具测试虚拟 CAN 接口:-
发送一个 CAN 帧:
cansend vcan0 123#deadbeef
-
查看接收到的 CAN 数据:
candump vcan0
-
这样,你就可以在没有实际硬件的情况下仿真 CAN 总线通信,进行开发和测试。
1.3 配置物理 CAN 接口(有外设的情况)
如果你有物理 CAN 外设(如 USB-to-CAN 适配器),你需要配置物理接口。
-
检查 CAN 适配器:首先,检查系统是否识别到了 CAN 适配器,运行以下命令:
ip link show
你应该看到类似
can0
或can1
的接口。如果没有,请插入设备并确认驱动已加载。 -
启用物理 CAN 接口:
假设你的物理接口为
can0
,你可以通过以下命令启用接口,并设置传输速率(例如 500 kbps):sudo ip link set can0 up type can bitrate 500000
-
测试物理接口:同样,使用
can-utils
发送和接收数据:-
发送数据:
cansend can0 123#deadbeef
-
查看数据:
candump can0
-
现在,你已经成功配置了 CAN 环境,无论是通过虚拟接口进行仿真,还是通过物理接口进行实际通信。
2. 测试案例
2.1 使用 can-utils
工具测试
can-utils
提供了一些常用的命令行工具,可以快速地测试 CAN 总线的发送和接收。
-
cansend
:用于向 CAN 总线发送数据。发送一个数据帧:
cansend vcan0 123#deadbeef
这会向
vcan0
接口发送一个带有 ID 为0x123
,数据为deadbeef
的 CAN 帧。 -
candump
:用于查看 CAN 总线上的数据。查看所有 CAN 总线接口的数据:
candump vcan0
你将看到类似下面的输出,显示收到的数据帧:
vcan0 123 [4] dead
-
canplayer
:用于回放保存的 CAN 数据文件。回放一个 CAN 数据文件:
canplayer -I can_logfile.log
这个工具在处理实际的 CAN 数据日志时非常有用。
2.2 使用代码测试
代码测试:发送和接收 CAN 数据
我们将编写一个简单的代码示例,用于发送和接收 CAN 帧。
-
创建线程池:我们将使用线程池来处理高并发的 CAN 数据接收。
-
CAN 通信类:负责与 CAN 总线进行交互。
-
main
函数:启动接收线程并发送数据。
#include // 包含输入输出流,用于打印日志或调试信息
#include // 包含 string 类的定义,用于字符串操作
#include // 包含 C 风格字符串操作函数的定义
#include // 包含 POSIX 系统调用,例如 close, read, write
#include // 包含网络接口相关的定义
#include // 包含 I/O 控制相关的函数定义,例如 SIOCGIFINDEX
#include // 包含文件控制相关的定义
#include // 包含 CAN 协议相关的定义
#include // 包含原始 CAN 套接字定义
#include // 包含 socket 套接字的相关定义
#include // 包含多线程支持的定义
#include // 包含原子操作的定义,用于线程安全
#include // 包含互斥量的定义,用于线程同步
#include // 包含 vector 容器的定义
#include // 包含队列容器的定义
#include // 包含函数对象的定义,用于队列任务
#include // 包含条件变量定义,用于线程同步
#include // 包含时间相关定义,用于控制线程等待时间
#include // 包含 I/O 相关功能
// 线程池类,用于管理多个线程,执行异步任务
class ThreadPool {
public:
// 构造函数,初始化线程池,启动 numThreads 个线程
ThreadPool(size_t numThreads) : stop(false) {
// 创建并启动工作线程
for (size_t i = 0; i < numThreads; ++i) {
workers.push_back(std::thread([this]() { workerLoop(); }));
}
}
// 析构函数,停止线程池中的所有线程
~ThreadPool() {
stop = true; // 设置停止标志
condVar.notify_all(); // 通知所有线程退出
for (std::thread &worker : workers) {
worker.join(); // 等待所有线程结束
}
}
// 向线程池队列中添加一个任务
void enqueue(std::function task) {
{
std::lock_guard lock(queueMutex); // 锁住队列,避免多线程访问冲突
tasks.push(task); // 将任务放入队列
}
condVar.notify_one(); // 唤醒一个等待的线程
}
private:
// 线程池中的工作线程函数
void workerLoop() {
while (!stop) { // 当 stop 为 false 时,线程继续工作
std::function task; // 定义一个任务对象
{
// 锁住队列,线程安全地访问任务队列
std::unique_lock lock(queueMutex);
condVar.wait(lock, [this]() { return stop || !tasks.empty(); }); // 等待任务或停止信号
// 如果 stop 为 true 且队列为空,退出循环
if (stop && tasks.empty()) {
return;
}
task = tasks.front(); // 获取队列中的第一个任务
tasks.pop(); // 从队列中移除该任务
}
task(); // 执行任务
}
}
std::vector workers; // 线程池中的所有线程
std::queue> tasks; // 任务队列,存储待处理的任务
std::mutex queueMutex; // 互斥锁,用于保护任务队列
std::condition_variable condVar; // 条件变量,用于通知线程执行任务
std::atomic stop; // 原子变量,用于控制线程池的停止
};
// CAN 通信类,用于发送和接收 CAN 消息
class CanCommunication {
public:
// 构造函数,初始化 CAN 通信
CanCommunication(const std::string &interfaceName) : stopReceiving(false) {
sock = socket(PF_CAN, SOCK_RAW, CAN_RAW); // 创建原始 CAN 套接字
if (sock < 0) { // 如果套接字创建失败,输出错误并退出
perror("Error while opening socket");
exit(EXIT_FAILURE);
}
struct ifreq ifr; // 网络接口请求结构体
strncpy(ifr.ifr_name, interfaceName.c_str(), sizeof(ifr.ifr_name) - 1); // 设置接口名
ioctl(sock, SIOCGIFINDEX, &ifr); // 获取网络接口的索引
struct sockaddr_can addr; // CAN 地址结构体
addr.can_family = AF_CAN; // 设置地址族为 CAN
addr.can_ifindex = ifr.ifr_ifindex; // 设置接口索引
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { // 绑定套接字到指定的 CAN 接口
perror("Error while binding socket");
exit(EXIT_FAILURE);
}
}
// 析构函数,关闭 CAN 套接字
~CanCommunication() {
if (sock >= 0) {
close(sock); // 关闭套接字
}
}
// 发送 CAN 消息
void sendCanMessage(const can_frame &frame) {
if (write(sock, &frame, sizeof(frame)) != sizeof(frame)) { // 写入套接字发送数据
perror("Error while sending CAN message");
}
}
// 接收 CAN 消息
void receiveCanMessages(ThreadPool &threadPool) {
while (!stopReceiving) { // 如果没有接收停止信号,继续接收数据
can_frame frame; // 定义一个 CAN 帧
int nbytes = read(sock, &frame, sizeof(frame)); // 从套接字中读取数据
if (nbytes < 0) { // 如果读取失败,输出错误信息
perror("Error while receiving CAN message");
continue;
}
// 将解析任务提交到线程池
threadPool.enqueue([this, frame]() {
this->parseCanMessage(frame); // 解析 CAN 消息
});
}
}
// 停止接收数据
void stopReceivingData() {
stopReceiving = true; // 设置停止接收标志
}
private:
int sock; // 套接字描述符
std::atomic stopReceiving; // 原子标志,表示是否停止接收数据
std::mutex parseMutex; // 解析数据时的互斥锁
// 解析 CAN 消息
void parseCanMessage(const can_frame &frame) {
std::lock_guard lock(parseMutex); // 锁住互斥量,确保解析数据时的线程安全
std::cout << "Received CAN ID: " << frame.can_id << std::endl; // 打印 CAN ID
std::cout << "Data: ";
for (int i = 0; i < frame.can_dlc; ++i) { // 遍历 CAN 数据字节
std::cout << std::hex << (int)frame.data[i] << " "; // 打印每个字节的十六进制表示
}
std::cout << std::endl;
}
};
// 主函数
int main() {
ThreadPool threadPool(4); // 创建一个有 4 个线程的线程池
CanCommunication canComm("vcan0"); // 创建一个 CanCommunication 对象,使用虚拟 CAN 接口 "vcan0"
// 启动一个线程来接收 CAN 消息
std::thread receiverThread(&CanCommunication::receiveCanMessages, &canComm, std::ref(threadPool));
// 创建并发送一个 CAN 消息
can_frame sendFrame;
sendFrame.can_id = 0x123; // 设置 CAN ID 为 0x123
sendFrame.can_dlc = 8; // 设置数据长度为 8 字节
for (int i = 0; i < 8; ++i) {
sendFrame.data[i] = i; // 填充数据
}
canComm.sendCanMessage(sendFrame); // 发送 CAN 消息
std::this_thread::sleep_for(std::chrono::seconds(5)); // 等待 5 秒,以便接收和处理消息
canComm.stopReceivingData(); // 停止接收数据
receiverThread.join(); // 等待接收线程结束
return 0; // 程序正常退出
}
代码注释总结:
-
线程池 (
ThreadPool
):- 提供了一个用于并发执行任务的线程池,通过
enqueue
函数将任务放入队列,工作线程从队列中取出任务执行。 - 使用
std::mutex
保护任务队列的访问,并使用std::condition_variable
实现线程间的同步。
- 提供了一个用于并发执行任务的线程池,通过
-
CAN 通信 (
CanCommunication
):- 提供了通过套接字进行 CAN 消息的发送与接收功能。
- 使用
socket
创建原始 CAN 套接字,bind
绑定到指定的网络接口。 - 发送和接收消息时,通过多线程处理接收到的数据,以提高并发性能。
-
主程序 (
main
):- 创建线程池和 CAN 通信对象。
- 启动接收线程并发送测试消息。
- 主线程等待 5 秒以确保接收到的 CAN 消息被处理。
这种方式可以在 Linux 系统中使用 C++ 进行高效的 CAN 通信,实现消息的发送与接收,并且利用线程池提高并发性能。