在 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 包含多个实用工具,例如 cansendcandump,可以用于测试 CAN 总线的发送和接收。

1.2 配置虚拟 CAN 接口(没有外设的情况)

如果你没有物理 CAN 接口设备(如 USB-to-CAN 适配器),你可以使用虚拟 CAN 接口 vcan0 来进行测试。虚拟接口适用于不需要实际硬件的 CAN 总线仿真和开发。

启用虚拟 CAN 接口
  1. 加载 vcan 驱动模块

    sudo modprobe vcan
    
  2. 创建虚拟 CAN 接口 vcan0

    sudo ip link add dev vcan0 type vcan
    sudo ip link set vcan0 up
    
  3. 测试虚拟接口

    使用 can-utils 工具测试虚拟 CAN 接口:

    • 发送一个 CAN 帧:

      cansend vcan0 123#deadbeef
      
    • 查看接收到的 CAN 数据:

      candump vcan0
      

这样,你就可以在没有实际硬件的情况下仿真 CAN 总线通信,进行开发和测试。

1.3 配置物理 CAN 接口(有外设的情况)

如果你有物理 CAN 外设(如 USB-to-CAN 适配器),你需要配置物理接口。

  1. 检查 CAN 适配器:首先,检查系统是否识别到了 CAN 适配器,运行以下命令:

    ip link show
    

    你应该看到类似 can0can1 的接口。如果没有,请插入设备并确认驱动已加载。

  2. 启用物理 CAN 接口

    假设你的物理接口为 can0,你可以通过以下命令启用接口,并设置传输速率(例如 500 kbps):

    sudo ip link set can0 up type can bitrate 500000
    
  3. 测试物理接口:同样,使用 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 帧。

  1. 创建线程池:我们将使用线程池来处理高并发的 CAN 数据接收。

  2. CAN 通信类:负责与 CAN 总线进行交互。

  3. 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;  // 程序正常退出
}

代码注释总结:

  1. 线程池 (ThreadPool)

    • 提供了一个用于并发执行任务的线程池,通过 enqueue 函数将任务放入队列,工作线程从队列中取出任务执行。
    • 使用 std::mutex 保护任务队列的访问,并使用 std::condition_variable 实现线程间的同步。
  2. CAN 通信 (CanCommunication)

    • 提供了通过套接字进行 CAN 消息的发送与接收功能。
    • 使用 socket 创建原始 CAN 套接字,bind 绑定到指定的网络接口。
    • 发送和接收消息时,通过多线程处理接收到的数据,以提高并发性能。
  3. 主程序 (main)

    • 创建线程池和 CAN 通信对象。
    • 启动接收线程并发送测试消息。
    • 主线程等待 5 秒以确保接收到的 CAN 消息被处理。

        这种方式可以在 Linux 系统中使用 C++ 进行高效的 CAN 通信,实现消息的发送与接收,并且利用线程池提高并发性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yy__xzz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值