在 Linux 中使用 CAN 通信:从配置到测试与代码实现

文章转载链接www.51testing.com/html/36/n-7...

引言

CAN(Controller Area Network)是一种广泛用于嵌入式系统、汽车和工业控制中的通信协议。Linux 支持 CAN 协议栈,并通过 SocketCAN 实现对 CAN 总线的访问。在这篇博客中,我们将深入讲解如何在 Linux 系统中配置和使用 CAN 通信,详细介绍配置环境、测试案例、代码实现以及如何使用 can-utils 工具和自定义代码进行测试。

1. 环境配置

1.1 安装和配置必备工具

在 Linux 系统上使用 CAN 通信,首先需要安装一些必备的工具和库:

· SocketCAN 驱动程序:这是 Linux 内核中实现 CAN 协议栈的模块,通常在大多数 Linux 发行版中已经默认启用。

· can-utils 工具:一个用于测试和调试 CAN 总线通信的工具集。

· 编译器和开发工具:用于编译 C++ 代码的工具。

安装依赖

首先,确保你安装了所需的开发工具和库:

bash 复制代码
  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 接口

1.加载 vcan 驱动模块:

复制代码
  sudo modprobe vcan

2.创建虚拟 CAN 接口 vcan0:

bash 复制代码
  sudo ip link add dev vcan0 type vcan

  sudo ip link set vcan0 up

3.测试虚拟接口:

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

发送一个 CAN 帧:

arduino 复制代码
  cansend vcan0 123#deadbeef

查看接收到的 CAN 数据:

复制代码
  candump vcan0

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

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

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

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

bash 复制代码
  ip link show

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

启用物理 CAN 接口:

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

bash 复制代码
  sudo ip link set can0 up type can bitrate 500000

测试物理接口:同样,使用 can-utils 发送和接收数据:

发送数据:

arduino 复制代码
  cansend can0 123#deadbeef

查看数据:

复制代码
  candump can0

现在,你已经成功配置了 CAN 环境,无论是通过虚拟接口进行仿真,还是通过物理接口进行实际通信。

2. 测试案例

2.1 使用 can-utils 工具测试

can-utils 提供了一些常用的命令行工具,可以快速地测试 CAN 总线的发送和接收。

cansend:用于向 CAN 总线发送数据。

发送一个数据帧:

arduino 复制代码
  cansend vcan0 123#deadbeef

这会向 vcan0 接口发送一个带有 ID 为 0x123,数据为 deadbeef 的 CAN 帧。

candump:用于查看 CAN 总线上的数据。

查看所有 CAN 总线接口的数据:

复制代码
  candump vcan0

你将看到类似下面的输出,显示收到的数据帧:

css 复制代码
  vcan0  123   [4]  dead

canplayer:用于回放保存的 CAN 数据文件。

回放一个 CAN 数据文件:

c 复制代码
  canplayer -I can_logfile.log

这个工具在处理实际的 CAN 数据日志时非常有用。

2.2 使用代码测试

代码测试:发送和接收 CAN 数据

我们将编写一个简单的代码示例,用于发送和接收 CAN 帧。

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

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

main 函数:启动接收线程并发送数据。

arduino 复制代码
  #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 通信,实现消息的发送与接收,并且利用线程池提高并发性能。

相关推荐
测试员周周3 小时前
【AI测试功能4】别再用传统等价类设计 AI测试用例了——语义覆盖的四种变体方法
人工智能·python·测试
努力进修1 天前
抽奖系统---测试报告
测试
老神在在0012 天前
测试方法与使用场景
单元测试·测试
Maỿbe3 天前
测试的基本认知
测试
humors2214 天前
十款顶级跑分与排名软件全解析
电脑·内存·测试·cpu·gpu·笔记本·硬盘
狼爷5 天前
JMeter 全指南:从性能测试入门到架构级实战
jmeter·测试
测试员周周6 天前
【AI测试系统】第5篇:从 Archon 看 AI 工程化落地:为什么"确定性编排+AI 弹性智能"是终局?
人工智能·python·测试
EulerBlind6 天前
接口自测-1777696985
测试
测试员周周7 天前
【AI测试系统】第4篇:告别硬编码!基于 Markdown + Python 的 Skill 引擎设计:让 AI 测试系统拥有无限扩展的“灵魂”
人工智能·python·测试