编写c++程序分别在x86和arm架构的ubuntu下访问CAN 接口设备

发布于:2025-06-30 ⋅ 阅读:(19) ⋅ 点赞:(0)

我们项目有一台电源,支持通过CANBus协议与外部通讯,我需要在程序里读取它的数据,并且对它进行控制。而程序最终是要部署到一台工控机上,但该工控机内存只有4G,内置了一个被厂家修改过的ubuntu20.04,在上面开发调试,又卡又不方便。我的计划是,先在我本机的虚拟机上开发,虚拟机装的也是ubuntu,ubuntu24.04,跑通了再移植到工控机。

这里面有一些步骤需要处理。尤其是对我这个以前很少接触硬件的程序员来说,有许多困难。首先第一步是要将电源与电脑连接;第二步是在虚拟机上写c++程序通过CANBus协议与电源通信;第三步是把电源改为与工控机连接;第四步把虚拟机的c++程序移植到工控机。

为什么会有这么多步骤呢,因为我的开发机只是普通的笔记本电脑,除了接显示器,基本只有USB口,如何跟电源相连是个问题,而工控机自带了许多端子,其中就有2个CAN口,可以直接自己找一些线与电源的引脚相连;其次是笔记本电脑的CPU架构是x86,而工控机是ARM,这个对使用的库有影响。

以下是详细介绍。

一、笔记本电脑上的虚拟机与电源相连

上面提到,我的笔记本电脑基本上只有USB口,它怎么跟电源连接呢?此前我对硬件几乎一片空白,厂家其实是代理商,他也一知半解,真正的厂家爱答不理。最后通过研读使用手册,搜索资料,特别是请教集团嵌入式开发大牛,才有了一点思路。最后是买了一个CAN转接USB口的这么一个东西连上了。
在这里插入图片描述
在这里插入图片描述
就两根线。

二、虚拟机上的程序

CAN转接USB口是个很成熟的产品了,提供了丰富翔实的资料,和供开发的库,所以开发起来并没有太多障碍。主要就是通过CAN转接USB口与电源通讯。在我们的程序中,CAN转接USB口称为Device(设备),它上面有两个CAN口,那么每个CAN口就有个编号(CANInd),第一个CANInd就是0,第二个就是1。程序运行的时候,首先要打开Device,然后初始化指定的CAN口,就可以给电源(实际上是给CAN转接USB口)发送指令,然后接收电源返回的数据了。

CANBus是很成熟的协议了,应用广泛,其指令有固定的格式。CAN转接USB口厂家做了一些封装,按照其格式填写,使用很方便。其实在工控机上采用比较原始的库写代码,也都差不多。摘录我在虚拟机上的部分源代码如下:

1、CAN转接USB口提供的库

1)数据结构

// 2.定义CAN信息帧的数据类型。
typedef struct _VCI_CAN_OBJ {
  UINT ID;
  UINT TimeStamp;
  BYTE TimeFlag;
  BYTE SendType;
  BYTE RemoteFlag; // 是否是远程帧
  BYTE ExternFlag; // 是否是扩展帧
  BYTE DataLen;
  BYTE Data[8];
  BYTE Reserved[3];
} VCI_CAN_OBJ, *PVCI_CAN_OBJ;

// 3.定义初始化CAN的数据类型
typedef struct _INIT_CONFIG {
  DWORD AccCode;
  DWORD AccMask;
  DWORD Reserved;
  UCHAR Filter;
  UCHAR Timing0;
  UCHAR Timing1;
  UCHAR Mode;
} VCI_INIT_CONFIG, *PVCI_INIT_CONFIG;

2)函数

#ifdef __cplusplus
#define EXTERN_C extern "C"
#else
#define EXTERN_C
#endif

EXTERN_C DWORD VCI_OpenDevice(DWORD DeviceType, DWORD DeviceInd,
                              DWORD Reserved);
EXTERN_C DWORD VCI_CloseDevice(DWORD DeviceType, DWORD DeviceInd);
EXTERN_C DWORD VCI_InitCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd,
                           PVCI_INIT_CONFIG pInitConfig);
EXTERN_C DWORD VCI_StartCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd);
EXTERN_C DWORD VCI_ResetCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd);

EXTERN_C ULONG VCI_Transmit(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd,
                            PVCI_CAN_OBJ pSend, UINT Len);
EXTERN_C ULONG VCI_Receive(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd,
                           PVCI_CAN_OBJ pReceive, UINT Len, INT WaitTime);

调用顺序就是
VCI_OpenDevice
VCI_InitCAN
VCI_StartCAN
VCI_Transmit,发送指令
VCI_Receive,接收返回数据

三、工控机与电源相连

工控机跟普通电脑稍有点不一样,本身就提供了许多插口,我们买的是一种基本款,CAN口就有2个,RS485口4个,还有许多乱七八糟的口,可能是供自定义的。所以电源跟工控机直连就可以了。问题是,我接了线之后发现无法通讯。查阅资料说,H和L两个接口间需要接一个120欧姆的电阻,以防止形成通讯回路。

怪不得,之前在用CAN转接USB口的时候,一定要将电阻开关至少打开一个才行。

在这里插入图片描述
在这里插入图片描述
我也不知道电阻是啥,请教搞嵌入式开发的同事,他一听就懂。由于手头没有120欧的电阻,他就找了2个220欧姆的,并联接在H和L之间,说并联就是110欧,近似120欧,应该也可以。结果真的就是,接上电阻以后,马上就能收到返回数据了。我真是佩服得五体投地。
在这里插入图片描述

四、工控机上的程序

那工控机上的程序该怎么写呢?现有的程序依赖CAN转接USB口厂家提供的库,但工控机不能使用。首先是工控机并不需要接CAN转接USB口;其次工控机的CPU是ARM架构的,现有的这个库不行。

如果专门为工控机新写一套代码,简单粗暴,但不利于维护。我的思路是,尽量使用同一套代码,数据结构采用目前的,自己实现那几个与CAN操作相关的函数。在CMakeLists.txt上做区分,当系统运行在x86架构下,就用厂家的库;而在arm架构下,使用自己实现的函数。
在这里插入图片描述

1、自己实现CAN操作相关函数

#include "controlcan.h"

#include <cstring>
#include <iostream>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>

const int OK = 1;
const int FAIL = 0;

std::string getCanName(DWORD CANInd) { return "can" + std::to_string(CANInd); }

void convert_to_vci_obj(const struct can_frame *src, PVCI_CAN_OBJ dst) {
  // ID 处理(判断是否为扩展帧 & 远程帧)
  dst->ID = src->can_id;

  dst->ExternFlag = (src->can_id & CAN_EFF_FLAG) ? 1 : 0; // 扩展帧标志位
  dst->RemoteFlag = (src->can_id & CAN_RTR_FLAG) ? 1 : 0; // 远程帧标志位

  if (dst->ExternFlag) {
    dst->ID &= CAN_EFF_MASK; // 取出 29 位扩展帧 ID
  } else {
    dst->ID &= CAN_SFF_MASK; // 取出 11 位标准帧 ID
  }

  dst->DataLen = src->can_dlc;                // 数据长度
  memcpy(dst->Data, src->data, src->can_dlc); // 拷贝数据内容

  // 其他字段赋默认值或根据需要填充
  dst->TimeStamp = 0;                              // 如果你使用 timeval 或其他时间戳机制可填入
  dst->TimeFlag = 0;                               // 可选标志
  dst->SendType = 0;                               // 通常用于发送时控制,接收时无意义
  memset(dst->Reserved, 0, sizeof(dst->Reserved)); // 预留字段清零
}

// 只需声明为 extern "C",防止 C++ 名称改编
extern "C" {

DWORD VCI_OpenDevice(DWORD DeviceType, DWORD DeviceInd, DWORD Reserved) {
  int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
  if (s < 0) {
    perror("Socket creation failed");
  }
  return s;
}

DWORD VCI_CloseDevice(DWORD DeviceType, DWORD Device) {
  close(Device);
  return OK;
}

DWORD VCI_InitCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_INIT_CONFIG pInitConfig) {
  std::string ifname = getCanName(CANInd);
  std::string cmd_down = "ip link set " + ifname + " down";
  std::string cmd_set = "ip link set " + ifname + " up type can bitrate " + std::to_string(pInitConfig->Reserved);

  if (system(cmd_down.c_str()) != 0) {
    std::cerr << "[ERROR] Failed to bring down " << ifname << std::endl;
    return FAIL;
  }

  if (system(cmd_set.c_str()) != 0) {
    std::cerr << "[ERROR] Failed to set bitrate on " << ifname << std::endl;
    return FAIL;
  }

  std::cout << "[INFO] ✔ " << ifname << " initialized with bitrate " << pInitConfig->Reserved << " bps" << std::endl;
  return OK;
}

DWORD VCI_StartCAN(DWORD DeviceType, DWORD Device, DWORD CANInd) {
  std::string ifname = getCanName(CANInd);

  struct ifreq ifr;
  strcpy(ifr.ifr_name, ifname.c_str());

  if (ioctl(Device, SIOCGIFINDEX, &ifr) < 0) {
    perror("[ERROR] Getting interface index failed");
    return FAIL;
  }

  struct sockaddr_can addr;
  addr.can_family = AF_CAN;
  addr.can_ifindex = ifr.ifr_ifindex;

  if (bind(Device, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
    perror("[ERROR] Bind failed");
    return FAIL;
  }

  std::cout << "[INFO] ✔ Socket bound to " << ifname << std::endl;
  return OK;
}

DWORD VCI_ResetCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd) {
  std::string ifname = getCanName(CANInd);
  std::string cmd_down = "ip link set " + ifname + " down";
  if (system(cmd_down.c_str()) != 0) {
    std::cerr << "[ERROR] Failed to bring down " << ifname << std::endl;
    return FAIL;
  }

  return OK;
}

DWORD VCI_Transmit(DWORD DeviceType, DWORD Device, DWORD CANInd, PVCI_CAN_OBJ pSend, UINT Len) {
  int ok = OK;

  // 转换为 can_frame 格式
  struct can_frame frame;
  frame.can_id = pSend->ID; // 支持扩展帧自动识别(高位非标准)
  if (pSend->ExternFlag) {
    frame.can_id |= CAN_EFF_FLAG; // 设置扩展帧标志
  }
  frame.can_dlc = pSend->DataLen;

  for (int i = 0; i < frame.can_dlc; ++i) {
    frame.data[i] = pSend->Data[i];
  }

  // 发送帧
  int nbytes = write(Device, &frame, sizeof(struct can_frame));
  if (nbytes == -1) {
    ok = FAIL;
    perror("Write");
  } else {
    std::cout << "CAN message sent successfully." << std::endl;
  }

  return ok;
}

ULONG VCI_Receive(DWORD DeviceType, DWORD Device, DWORD CANInd, PVCI_CAN_OBJ pReceive, UINT Len, INT WaitTime) {
  struct can_frame received_frame;
  int nbytes = read(Device, &received_frame, sizeof(struct can_frame));
  if (nbytes < 0) {
    perror("Read");
    return 0;
  }

  // 判断是否为错误帧(重点过滤)
  if (received_frame.can_id & CAN_ERR_FLAG) {
    printf("Received error frame, ignored.\n");
    return 0;
  }

  // 判断是否为远程帧(可选过滤)
  if (received_frame.can_id & CAN_RTR_FLAG) {
    printf("Received remote frame, ignored.\n");
    return 0;
  }

  convert_to_vci_obj(&received_frame, pReceive);

  return 1;
}
}

2、CMakeLists.txt中根据情况是加载厂家库还是自己实现的库

# 检测当前架构
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
    if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64")
        set(PLATFORM_X86_64 TRUE)
    elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64")
        set(PLATFORM_ARM64 TRUE)
    endif()
endif()

set (SOURCES
    "UnderWtConn.cpp"  
。。。
    "config/ConfigManager.cpp"
    "lib/canClient.cpp"
)
if(PLATFORM_ARM64)
    list(APPEND SOURCES "lib/controlcanArm.cpp") #加载自己的库
endif()
add_executable(UnderwtConn ${SOURCES})

if(PLATFORM_X86_64)
    set(CONTROLCAN_LIB_PATH ${CMAKE_SOURCE_DIR}/lib/libcontrolcan.so) #加载厂家库
endif()

3、效果

好处就是原有代码基本不需要更改,两边都能工作。唯一的小缺陷,就是为了迁就CAN转USB口厂家库,有些函数的参数有点勉强。比如

EXTERN_C DWORD VCI_OpenDevice(DWORD DeviceType, DWORD DeviceInd,DWORD Reserved);

for 工控机的话,DeviceType没有意义,DeviceInd原先是指CAN转USB口设备,但在工控机这里,其实指的是socket。只能多加一点注释,解析一番。


网站公告

今日签到

点亮在社区的每一天
去签到