在Linux中配置和使用CAN通信的详细指南
作者:yy__xzz
引言
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 <iostream>              // 包含输入输出流,用于打印日志或调试信息
#include <string>                // 包含 string 类的定义,用于字符串操作
#include <cstring>               // 包含 C 风格字符串操作函数的定义
#include <unistd.h>              // 包含 POSIX 系统调用,例如 close, read, write
#include <net/if.h>              // 包含网络接口相关的定义
#include <sys/ioctl.h>           // 包含 I/O 控制相关的函数定义,例如 SIOCGIFINDEX
#include <fcntl.h>               // 包含文件控制相关的定义
#include <linux/can.h>           // 包含 CAN 协议相关的定义
#include <linux/can/raw.h>       // 包含原始 CAN 套接字定义
#include <sys/socket.h>          // 包含 socket 套接字的相关定义
#include <thread>                // 包含多线程支持的定义
#include <atomic>                // 包含原子操作的定义,用于线程安全
#include <mutex>                 // 包含互斥量的定义,用于线程同步
#include <vector>                // 包含 vector 容器的定义
#include <queue>                 // 包含队列容器的定义
#include <functional>            // 包含函数对象的定义,用于队列任务
#include <condition_variable>    // 包含条件变量定义,用于线程同步
#include <chrono>                // 包含时间相关定义,用于控制线程等待时间
#include <iostream>              // 包含 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<void()> task) {
        {
            std::lock_guard<std::mutex> lock(queueMutex);  // 锁住队列,避免多线程访问冲突
            tasks.push(task);  // 将任务放入队列
        }
        condVar.notify_one();  // 唤醒一个等待的线程
    }
 
private:
    // 线程池中的工作线程函数
    void workerLoop() {
        while (!stop) {  // 当 stop 为 false 时,线程继续工作
            std::function<void()> task;  // 定义一个任务对象
            {
                // 锁住队列,线程安全地访问任务队列
                std::unique_lock<std::mutex> 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<std::thread> workers;           // 线程池中的所有线程
    std::queue<std::function<void()>> tasks;    // 任务队列,存储待处理的任务
    std::mutex queueMutex;                      // 互斥锁,用于保护任务队列
    std::condition_variable condVar;            // 条件变量,用于通知线程执行任务
    std::atomic<bool> 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<bool> stopReceiving;  // 原子标志,表示是否停止接收数据
    std::mutex parseMutex;  // 解析数据时的互斥锁
 
    // 解析 CAN 消息
    void parseCanMessage(const can_frame &frame) {
        std::lock_guard<std::mutex> 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 通信,实现消息的发送与接收,并且利用线程池提高并发性能。
以上就是在Linux中配置和使用CAN通信的详细指南的详细内容,更多关于Linux CAN通信的资料请关注脚本之家其它相关文章!
