基于Linux C的CANopen移植


本节,我们学习如何在Linux下使用c语言调用CANopen(canfestival)。

本专题相关教程:

基于STM32F4的CANOpen移植教程

基于STM32F4的CANopen快速SDO通信

linux下CANopen for python的使用

基于Linux C的CANopen移植

CANopen补充–时间计算出错

CANopen补充–主站检测节点是否在线

本文相关资源:
链接:https://pan.baidu.com/s/1fdZr4sSCyebEcXTp5Sk18g?pwd=q0du
提取码:q0du
在这里插入图片描述

名字含义
CANopen_Linux移植好的代码,带心跳报文
CANopen_Linux_SDO移植好的代码,带SDO通信
Mongo-canfestival-3-asc-1a25f5151a8dcanfetival源码,Mongo分支(含stm32驱动)

本次移植,只需要一份源码。至于相关软件,在之前博客有资源和使用教程,这里不赘述。

备注:评论区有位大佬提供了一个不需要修改源码,只需要写cmakelist的方法,大家可以去看一下。 链接如下:CANfestival移植到imx8

0 基础要求

(1)你会驱动自己板子的CAN 。我们的Linux基本都是嵌入式板子(ARM)下的。如树莓派之类的,我个人使用的是beagle bone black。
(2)cmake与交叉编译器的环境已经配置好,并且会用。

1 新建工程

我们新建一个文件夹为CANopen_Linux,作为我们的工程目录。

在里面新建两个文件夹和一个CMakeLists文件,如下:

在这里插入图片描述

名字说明
CANopen用于存放所有与CANopen有关的代码。
main含main.c和main.h
CMakeLists.txtcmake文件

j进入CANopen文件夹,在里面新建几个子文件夹。和之前教程一样。

在这里插入图片描述

说明如下:

文件夹名说明
dictionary存放字典和其对应的.c /.h 文件
hardware外设的驱动文件,包括定时器,CAN,还有配置文件
inc由CANopen源代码移植过来的h文件
src由CANopen源代码移植过来的c文件

整体文件树如下:

在这里插入图片描述

1.1 h文件移植

进入源代码/include目录,先将该目录下19个h文件,都复制到新工程/CANopen/inc 里。

再把timers_unix(含一个文件)和unix(含两个文件)文件夹里的文件放在同一个文件夹里,取名linux。(其实名字随意)
在这里插入图片描述

在这里插入图片描述

进入源代码/examples/AVR/Slave目录,把config文件,移植到新工程/CANopen/hardware

在这里插入图片描述

并对config做一点修改。
在这里插入图片描述

1.2 c文件移植

进入源代码/src目录,将该目录下除了symbols.c之外的12个c文件,复制到新工程/CANopen/src 里。
在这里插入图片描述

删除dcf.c文件下第59、98行前面的“inline”关键字

在这里插入图片描述

在这里插入图片描述

2 建立自己的底层驱动文件

​ 在新工程/CANopen/hardware下新建timer、CAN的c/h文件。其中定时器用于时间获取,CAN是通信基础。

需要说明的是,CANopen源代码里含有timer.c 文件,为了命名不冲突,我这里起名加了后缀。

比如使用can0,就建立can0.c。

如图,我们使用了can0,timer0(其实用的不是定时器)。 config.h为之前移植的文件,不用管。
在这里插入图片描述

文件说明
can0每种板子的can配置都不太一样,这里仅供参考。包含CAN的初始化/接收/发送
timer0使用select阻塞的方式实现,定时间隔10ms。(要求与timerscfg.h里的配置一样),溢出满值为65535

我们对新工程/CANopen/inc/linux/timerscfg.h进行一些修改。使其和timer0.c里的配置一致。

至于为什么是10ms间隔,而之前STM32是10us,这个后边解释。
在这里插入图片描述

can0.c

// Includes for the Canfestival driver
#include "can0.h"
/**********************************can0.c**********************************/

pthread_t tid_canopen_can;//CANopen接收线程的句柄
void CAN_RX_Handler(void);//CAN数据接收的回调函数

int sockfd_can; // can的文件描述符

void CAN0_Init(void)
{
  /**************在开始下边之前,需要保证板子的CAN已经配置好*********************/
  sockfd_can = socket(PF_CAN, SOCK_RAW, CAN_RAW);
  printf("sockfd_can\r\n");

  if (sockfd_can < 0)
    perror("socket error");

  /******************将套接字与 CAN 设备进行绑定********************************/
  struct ifreq ifr = {0};
  struct sockaddr_can can_addr = {};

  //指定can0设备
  strcpy(ifr.ifr_name, "can0");
  ioctl(sockfd_can, SIOCGIFINDEX, &ifr);

  can_addr.can_family = AF_CAN; //填充数据
  can_addr.can_ifindex = ifr.ifr_ifindex;

  int ret = bind(sockfd_can, (struct sockaddr *)&can_addr, sizeof(can_addr)); //绑定
  if (ret < 0)
    perror("bind error");
  /**************************关闭回环功能*************************************/
  int loopback = 0; // 0:close 1:open
  setsockopt(sockfd_can, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
  //在本地回环功能开启的情况下,所有的发送帧都会被回环到与 CAN 总线接口对应的套接字上
}

void CANopen_CAN_Task(void)
{
  CAN0_Init();
  struct pollfd fds;
  fds.fd = sockfd_can; // can
  fds.events = POLLIN;
  fds.revents = 0; //
  while (1)
  {
    int ret = poll(&fds, 1, -1); //使用poll进行接收数据 1表示fds里元素个数为1 -1为一直阻塞
    if (ret < 0)
      perror("poll error");

    if (fds.revents & POLLIN) //接受到can数据
    {
      CAN_RX_Handler();
    }
  }
}

void CAN_RX_Handler(void)
{
  struct can_frame RxMessage;
  int ret = read(sockfd_can, &RxMessage, sizeof(struct can_frame)); //这里也有阻塞效果

  if (RxMessage.can_id & CAN_SFF_MASK) //只处理标准帧
  {
    Message rxm = {0};
    rxm.cob_id = RxMessage.can_id & CAN_SFF_MASK;
    rxm.len = RxMessage.can_dlc;
    for (int i = 0; i < rxm.len; i++)
      rxm.data[i] = RxMessage.data[i];

    /*****canopen的数据接收处理部分*******/
    canDispatch(&Master_Data, &rxm); // CANopen自身的处理函数
  }
}

// The driver send a CAN message passed from the CANopen stack CAN_PORT dont use
unsigned char canSend(CAN_PORT port, Message *m)
{
  int i;
  struct can_frame TxMessage = {0};
  TxMessage.can_id = m->cob_id; //默认标准帧。如为扩展帧则can_id = CAN_EFF_FLAG | id
  TxMessage.can_dlc = m->len;

  for (i = 0; i < m->len; i++)
    TxMessage.data[i] = m->data[i];

  int ret = write(sockfd_can, &TxMessage, sizeof(TxMessage)); // write frame
  if (ret != sizeof(TxMessage))
    return 0; // error
  else
    return 1; // success
}

can0.h

#ifndef __CAN0_H
#define __CAN0_H

#include "main.h"
#include "data.h"
#include "Master.h"

pthread_t tid_canopen_can;//CANopen接收线程的句柄
void CANopen_CAN_Task(void);//CANopen接收线程

#endif

timer0.c

#include "timer0.h"

pthread_t tid_canopen_timer;

TIMEVAL last_counter_val = 0;
TIMEVAL elapsed_time = 0;

UNS16 timer_tick = 0; //定时器计数 16位

void CANopen_Timer_Task(void)
{
    struct timeval t;
    TimeDispatch(); //先执行一遍定时任务
    while (1)
    {
        t.tv_sec = 0;
        t.tv_usec = 10000;               // 10ms。可减少0.1ms,因为下边运行需要时间
        select(0, NULL, NULL, NULL, &t); //每次阻塞10ms,模拟10ms一次中断

        timer_tick++;
        if (timer_tick == 65535)
        {
            elapsed_time = 0;
            TimeDispatch();
        }
    }
}


UNS16 get_Tick(void)
{
    return timer_tick;
}

void set_Tick(UNS16 num)
{
    timer_tick = num;
}


// 设置下一次定时任务
void setTimer(TIMEVAL value)
{
    UNS32 timer = get_Tick(); // 获取定时器时间
    elapsed_time += timer - last_counter_val;
    last_counter_val = 65535 - value;
    set_Tick(65535 - value);
    // printf("setTimer %lu, elapsed %lu\r\n", value, elapsed_time);
}

// Return the elapsed time to tell the Stack how much time is spent since last call.
TIMEVAL getElapsedTime(void)
{
    UNS32 timer = get_Tick(); // Copy the value of the running timer
    if (timer < last_counter_val)
        timer += 65535;
    TIMEVAL elapsed = timer - last_counter_val + elapsed_time;
    // printf("all elapsed:%lu-->timer:%lu last count:%lu this elapsed:%lu\r\n",
    //        elapsed, timer, last_counter_val, elapsed_time);
    return elapsed;
}

timer0.h

#ifndef __TIMER0_H
#define __TIMER0_H

#include "main.h"

#include <sys/time.h>
#include <sys/select.h>
#include <stdio.h>
#include "timerscfg.h"
#include "applicfg.h"
#include <stdbool.h>

pthread_t tid_canopen_timer;  //CANopen定时器线程句柄
void CANopen_Timer_Task(void);//CANopen定时器线程
#endif

2022年3月18记:
timer0.c里的时间相关函数存在缺陷,在超过1个功能使用到时间时会产生干涉,如果大家后续除了心跳报文发送之外,没有用到其他会使用时间的功能,就不用管了。详情可以根据CANopen补充–时间计算出错修改,会更加简单明了。

3 建立词典

在这里插入图片描述

打开objdictedit(使用在之前教程有)。我们起名字为Master,使用心跳管理,这样我们待会便可以通过心跳报文来判断移植成功与否。

在字典里设置心跳报文间隔为1000ms(0x3E8)。这样,它每隔1000ms就会发送一个心跳报文。
在这里插入图片描述

点击保存,将生成的.od文件放入新工程/CANopen/dictionnary文件夹。
在这里插入图片描述

再点击建立词典,同样将生成的.c文件放入CANopen/dictionnary文件夹。

在这里插入图片描述

效果如下:

在这里插入图片描述

文件说明
.od文件词典文件,用于配置,不会被工程调用
.c .h词典文件对应的c和h文件。需要被工程调用

4 main文件

对于main.c,我们只需要初始化好can和定时线程即可

/*********************main.c******************************/

#include "main.h"

void main(void)
{

    pthread_create(&tid_canopen_can, NULL, (void *)CANopen_CAN_Task, NULL);     //创建canopen接收线程
    pthread_create(&tid_canopen_timer, NULL, (void *)CANopen_Timer_Task, NULL); //创建canopen定时器线程

    sleep(1);
    unsigned char nodeID = 0x00; //节点ID
    setNodeId(&Master_Data, nodeID);
    setState(&Master_Data, Initialisation); //节点初始化
    setState(&Master_Data, Operational);
    while (1)
    {
        printf("hello aa\r\n");
        sleep(1);
    }
}

main.h 需要包含所需要的头文件

#ifndef _MAIN_H
#define _MAIN_H

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdbool.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <termios.h>

/*******CAN驱动**************/
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>

/*****CANopen 相关头文件********/
#include "can0.h"
#include "timer0.h"
#include "timer.h"
#include "data.h"
#include "Master.h"
#include "timerscfg.h"
#include "canfestival.h"
#endif

上边应该有一些文件是不需要的。没有影响。

5 CMakeLists编写

因为我们使用的交叉编译器,所以需要提前配置好。

#***************CMakeLists.txt*********************
set (CMAKE_C_COMPILER "arm-linux-gnueabihf-gcc")
#set (CMAKE_CXX_COMPILER "arm-linux-gnueabihf-g++") #c++编译使用这个
# CMake 最低版本号要求
cmake_minimum_required(VERSION 3.16)

# 设置工程名
project (CANopen_Linux)

set(CMAKE_C_FLAGS -lpthread) #编译命令后缀 因为使用了线程,需要添加-lpthread
#set(CMAKE_CXX_FLAGS -lpthread)#c++使用这个

include_directories (main CANopen/inc CANopen/inc/linux )#包含头文件目录
include_directories (CANopen/dictionary CANopen/hardware)#包含头文件目录


aux_source_directory(${PROJECT_SOURCE_DIR}/CANopen/src CANOPEN_SRC)#添加canopen源文件
aux_source_directory(${PROJECT_SOURCE_DIR}/CANopen/hardware CANOPEN_HARDWARE)#添加硬件
aux_source_directory(${PROJECT_SOURCE_DIR}/CANopen/dictionary CANOPEN_DICTIONARY)#添加词典

aux_source_directory(${PROJECT_SOURCE_DIR}/main MAIN_SRCS)#添加main
# 指定生成目标canopen_exe
add_executable(canopen_exe ${MAIN_SRCS} ${CANOPEN_HARDWARE} ${CANOPEN_SRC} ${CANOPEN_DICTIONARY})

 

6 编译以及运行(心跳报文实验)

我个人使用VM虚拟机下的Ubuntu18,开发环境为VScode。

将工程复制到虚拟机。并使用VScode打开。
在这里插入图片描述

如果想打开串口调试反馈。可以在CANopen/inc/linux/applicfg.h添加宏定义。它会在终端发送很多信息,方便调试。但是这个会降低运行速度。日常使用要关闭。
在这里插入图片描述

按照使用cmake的习惯。我们新建一个build文件夹。

在这里插入图片描述

进入build文件夹,使用cmake指令。

cd build
cmake .. 

在这里插入图片描述

再使用make指令

make

在这里插入图片描述

我们将其发送到板子上,并执行观察
在这里插入图片描述

终端没有问题,我们再看CAN数据

在这里插入图片描述
没有问题。1s一个心跳报文。
2022年3月18记:
如果各位需要主站检测节点心跳是否离线的功能,可以看CANopen补充–主站检测节点是否在线

7 定时器讨论

从上边截图可以发现,1s一个心跳报文。实际误差在10ms之内。那么能不能继续减少误差呢?实测不好弄。当然,也可能是我的板子水土不服。各位可以自己试试。

经过我一天的实验,使用过很多办法,如下:

使用方法解释
usleep定时间隔为10us,效果很差
setitimer绑定信号要么无法触发,要么和系统发生离奇的化学反应(比如本来终端1s打印一次hello,加了之后会直接刷屏)
POSIX Timer编译不通过,找不到文件
select使用很简单。
定时间隔为1ms:误差很大。
定时间隔10ms:误差10ms。
定时间隔100ms:误差不超过10ms。

综上所述,我最终使用了select(定时10ms),个人认为效果够用了。我们在工程里,基本上都不上过心跳报文~~~~

如果想更精确点,可以在timer0.c修改select定时间隔为9.9ms,误差会小一些。不过个人觉得没啥必要。

在这里插入图片描述
效果如下,还是可以的。

在这里插入图片描述

8 SDO实验

相关实验在STM32F4教程有说过,这里简单说一下。

8.1快速sdo介绍

首先,我们要了解快速SDO的过程。只要一次传输32位(4字节)以下,都用快速SDO即可。
在这里插入图片描述

就对象而言,主机要访问节点词典的数据,因此主机是client客户端,节点是server服务器。上传与下载是对服务器来说的(这点和常识有点不太一样)。因此,上传指的是服务器发送数据给客户端,下载是客户端给服务器数据。

我们这里要访问节点服务器2000位置的数据。假设这个数据是short int类型(2字节16位),内容为0x0003。

因此发送指令为: 40 00 20 00 00 00 00 00 。返回的信息不出意外应该是:4B 00 20 00 03 00 00 00 (读响应2字节,因为我们的变量test就是16位的,03是读取内容)。

发送帧(client->server,主机->节点)内容
40读取指令
00 20读取0x2000位置的数据
00子索引,这里无
00 00 00 00未用,补零
接收帧(server->client,节点->主机)内容
4B读响应两个字节
00 20表明该数据位置为0x2000
00子索引,这里无
03 00 00 00高位在后,即0x000003=3

8.2 工程配置

主站配置:一个SDO client (终端) 节点配置: 一个SDO server (服务器)。 这里,我们假设主站的id=0x00,节点ID=0x02

1) 主站词典配置

为了方便观察,我们把心跳关了(心跳间隔设置为0);

在这里插入图片描述

添加一个client
在这里插入图片描述

设置client->server 和server->client 的cob id。规则如下

cob id内容
client->server的cob id0x600+目标id
server->client 的cob id0x580+目标id

假设需要对接的服务器id为0x02,则

client->server 的cob id:0x600+0x02,

server->client 的cob id :0x580+0x02。 

在这里插入图片描述

主站的词典配置就成功了。点击保存,与建立词典即可。效果如下:
在这里插入图片描述

2)节点词典配置

这里直接使用之前STM32工程了,一模一样。不介绍了。 不用也行,可以只观察主站的发送帧,至少可以判断发送有没有问题。

8.3 快速SDO使用

我们打开主站工程,需要添加访问字节SDO的代码。

发送代码为

unsigned char get_test_datasend[8]={0x40,0x00,0x20,0x00,0x00,0x00,0x00,0x00};
sendSDO(&Master_Data,SDO_CLIENT,0,get_test_datasend);

我们在main.c里添加
在这里插入图片描述

每一秒发送一个SDO命令,用于获取0x2000地址的变量内容。

我们在CAN0.c中断里添加数据获取程序。

在这里插入图片描述

canDispatch为CANopen的接收处理函数,我们在它之前截取属于我们的信息。

然后我们就可以在上位机和串口调试助手观察效果啦。
在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WxoeZ2ax-1646749587854)(C:\Users\FEN\AppData\Roaming\Typora\typora-user-images\image-20220308192918626.png)]

效果与预测相符。

  • 10
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值