基于Linux C的CANopen移植
本节,我们学习如何在Linux下使用c语言调用CANopen(canfestival)。
本专题相关教程:
本文相关资源:
链接:https://pan.baidu.com/s/1fdZr4sSCyebEcXTp5Sk18g?pwd=q0du
提取码:q0du
名字 | 含义 |
---|---|
CANopen_Linux | 移植好的代码,带心跳报文 |
CANopen_Linux_SDO | 移植好的代码,带SDO通信 |
Mongo-canfestival-3-asc-1a25f5151a8d | canfetival源码,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.txt | cmake文件 |
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 id | 0x600+目标id |
server->client 的cob id | 0x580+目标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的接收处理函数,我们在它之前截取属于我们的信息。
然后我们就可以在上位机和串口调试助手观察效果啦。
效果与预测相符。