当前位置: 代码网 > 服务器>服务器>Linux > 在Linux中配置和使用CAN通信的详细指南

在Linux中配置和使用CAN通信的详细指南

2025年08月03日 • Linux •我要评论
引言can(controller area network)是一种广泛用于嵌入式系统、汽车和工业控制中的通信协议。linux 支持 can 协议栈,并通过 socketcan 实现对 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 <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通信的资料请关注代码网其它相关文章!

赞 (0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com