CAN(Controller Area Network)是一种广泛用于嵌入式系统、汽车和工业控制中的通信协议。Linux 支持 CAN 协议栈,并通过 SocketCAN 实现对 CAN 总线的访问。在这篇博客中,我们将深入讲解如何在 Linux 系统中配置和使用 CAN 通信,详细介绍配置环境、测试案例、代码实现以及如何使用 can-utils
工具和自定义代码进行测试。
can-utils
和自定义代码测试 CAN 通信。在 Linux 系统上使用 CAN 通信,首先需要安装一些必备的工具和库:
首先,确保你安装了所需的开发工具和库:
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 总线的发送和接收。
如果你没有物理 CAN 接口设备(如 USB-to-CAN 适配器),你可以使用虚拟 CAN 接口 vcan0
来进行测试。虚拟接口适用于不需要实际硬件的 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 总线通信,进行开发和测试。
如果你有物理 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 环境,无论是通过虚拟接口进行仿真,还是通过物理接口进行实际通信。
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 数据日志时非常有用。
我们将编写一个简单的代码示例,用于发送和接收 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
):
socket
创建原始 CAN 套接字,bind
绑定到指定的网络接口。主程序 (main
):
这种方式可以在 Linux 系统中使用 C++ 进行高效的 CAN 通信,实现消息的发送与接收,并且利用线程池提高并发性能。