引言
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通信的资料请关注代码网其它相关文章!
发表评论