CANopen移植到STM32F4平台

  • 前言
  • 1 物品准备
  • 2 相关软件安装
    • 2.1 CAN上位机
    • 2.2 对象字典生成工具objdictedit环境配置
  • 3 将CANopen移植到STM32F407
    • 3.1 基础代码移植
      • 3.11 h文件移植
      • 3.12 c文件移植
    • 3.2 建立自己的底层驱动文件
    • 3.3 建立词典
    • 3.4工程配置
      • 3.41 c文件添加
      • 3.42 头文件路径添加
      • 3.43 c99标准选择
      • 3.44 调试串口设置
      • 3.45 程序启动
  • 4 末尾

本专题相关教程:
基于STM32F4的CANOpen移植教程

基于STM32F4的CANopen快速SDO通信

linux下CANopen for python的使用

基于Linux C的CANopen移植

CANopen补充–时间计算出错

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

前言

为了在STM32F4上能够运行CANopen(CanFestival),跟着网上的教程操作,发现总是不够详细。主要是配置和代码运行部分基本没有解释。为了后来者能够少走弯路,便有了这篇教程。关于CANopen协议本身本文不做过多介绍,主要是介绍如何使用软件和代码修改。

本文配套资料下载地址:https://pan.baidu.com/s/1FMp7xuJ1r3gJTPB0wf3Yrw?pwd=osxs
提取码:osxs

废话不多说,GOGOGO。

1 物品准备

名称用途
USB-CAN模块/USB-CAN盒子用以监听数据(如实在没有的话,代码里添加串口反馈也勉强能测试)
Canfestival- 3 源代码CANopen源代码 /本文资料里有
STM32F4 裸工程移植目标平台代码,这里用正点原子的空白工程即可 /本文资料里有
CANopen轻松入门.pdf-周立功pdf书籍,用以学习CANopen协议 /本文资料里有

USB-CAN模块,比如下面这个,买啥都行,有这个功能就ok。

USB-CAN盒子,如下,相比模块,多了一些功能(我用的就这个,不过好像多的功能我并没有用上)

CANopen轻松入门.pdf-周立功链接 下载地址

2 相关软件安装

2.1 CAN上位机

如果使用USB-CAN盒子,找店家要上位机资料即可。比如我用的这款资料如下:

驱动:驱动下载
驱动安装教程:驱动安装视频

上位机软件:上位机下载地址


打开设备-选择设备-选择对应波特率即可。

如果是普通的USB-CAN模块,找店家应该也有资料。使用CANpro协议平台分析软件即可,这个网上搜很容易搜得到。附一个我随便找的链接:CANPro协议分析平台官方下载

同理,启动-选择设备(不对就反复选)-选择波特率。

2.2 对象字典生成工具objdictedit环境配置

​ CANopen需要使用到字典,路径:源代码/objdictgen/objdictedit.py。这是个基于python2.7才能运行的程序,因此我们先装环境。

安装环境,遇到了很多坑。主要是网上教程很多偏老,跟着操作,各种bug。最终成功的一个搭配是

软件名字备注
python-2.7.15.amd64.msi
wxPython3.0-win64-3.0.2.0-py27.exe使用2.8会导致在安装下边软件的时候,提示包缺失
Gnosis_Utils-1.2.2.zip

安装教程参考:CanFestival中对象字典编辑器objdictedit的正确打开环境_lei_g的博客-CSDN博客_canfestival中对象字典编辑器的打开

备注:python2.7和自己之前安装的如python3.7是不冲突的。

要使用objdictedit,可以使用这个方式固定到任务栏。方便以后打开。

选择默认程序–>更多应用–>在这台电脑上查找其他应用–>选择python2.7文件夹里的python.exe


当打开下边程序的时候,在桌面任务栏选择:固定到任务栏。那么以后都可以右键这个图标,点击上边的objdictedit.py即可打开软件。

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

3 将CANopen移植到STM32F407

首先,这是我给大家准备的礼物,截图如下:

名称说明
CanFestival-3源码内含1个官方源码与3个分支,我们使用Mongo-canfestival,因为它有对于cm4内核的支持
CANopen裸工程本教程所使用的空白代码
CANopen最终移植代码本教程所使用的移植好的最终代码
USBCAN调试软件USB-CAN盒子的上位机
字典工具安装字典工具安装所需文件

3.1 基础代码移植

打开我们的空白工程,界面如图,空空如也。需要说明的是,个人喜欢把所有头文件放入main.h,这样其他外设文件只用包含main.h即可。

文件夹如下:

我们新建一个文件夹,名为CANopen,用于存放所有与CANopen有关的代码。

里面再新建几个子文件夹。

说明如下:

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

3.11 h文件移植

进入源代码/include目录,先将该目录下19个h文件,都复制到新工程/CANopen/inc 里,再复制cm4文件夹(内含3个h文件)更名为stm32。如下:

修改一下stm32/canfestival.h文件,添加3行语句,防止递归调用。

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

并对config做一点修改。

3.12 c文件移植

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

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

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

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

需要说明的是,CANopen源代码里含有timer.c 文件,为了命名不冲突,我这里起名加了后缀。比如使用定时器3,就建立timer3.c。

如图,我们使用了can1,timer2, config.h为之前移植的文件,不用管。

文件说明
can1中断优先级为1(无所谓);波特率设置为1M(1M或者500K都行,要与config.h一致)
timer2中断优先级1(无所谓);时钟84M,分频840,即基础频率为100K(要求与timerscfg.h里的配置即可),重装载值为65535,即0.65s一次溢出中断

can与timer的代码移植自源代码/drivers/cm4。cm4是基于stm32F3的,因此有些代码需要修改

cm4.c里面包含can1与timer3的初始化代码以及一些封装好的代码。我们将其各自复制到can1.c和timer3.c。并根据板子情况做修改。

大家可以到移植成功的工程里看看有啥修改。

can1.c

#include "can1.h"static CO_Data *co_data = NULL;//Initialize the CAN hardware unsigned char CAN1_Init(CO_Data * d, uint32_t bitrate){GPIO_InitTypeDefGPIO_InitStructure;NVIC_InitTypeDefNVIC_InitStructure;CAN_InitTypeDefCAN_InitStructure;CAN_FilterInitTypeDefCAN_FilterInitStructure;/* save the canfestival handle */co_data = d;/* CAN GPIOs configuration **************************************************//* Enable GPIO clock */RCC_AHB1PeriphClockCmd(CAN_GPIO_CLK, ENABLE);/* Connect CAN pins to AF7 */GPIO_PinAFConfig(CAN_GPIO_PORT, CAN_RX_SOURCE, GPIO_AF_CANx);GPIO_PinAFConfig(CAN_GPIO_PORT, CAN_TX_SOURCE, GPIO_AF_CANx); /* Configure CAN RX and TX pins */GPIO_InitStructure.GPIO_Pin = CAN_RX_PIN | CAN_TX_PIN;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;GPIO_InitStructure.GPIO_PuPd= GPIO_PuPd_UP;GPIO_Init(CAN_GPIO_PORT, &GPIO_InitStructure);/* NVIC configuration *******************************************************/NVIC_InitStructure.NVIC_IRQChannel = CANx_RX0_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure);/* CAN configuration ********************************************************//* Enable CAN clock */RCC_APB1PeriphClockCmd(CAN_CLK, ENABLE);/* CAN register init */CAN_DeInit(CANx);CAN_StructInit(&CAN_InitStructure);/* CAN cell init */CAN_InitStructure.CAN_TTCM = DISABLE;CAN_InitStructure.CAN_ABOM = DISABLE;CAN_InitStructure.CAN_AWUM = DISABLE;CAN_InitStructure.CAN_NART = DISABLE;CAN_InitStructure.CAN_RFLM = DISABLE;CAN_InitStructure.CAN_TXFP = DISABLE;CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;/* CAN Baudrate (CAN clocked at 42 MHz)42e6 / ( 6 * (1+BS1+BS2))*/CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;if(bitrate == 1000000){CAN_InitStructure.CAN_BS1 = CAN_BS1_3tq;CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;}else {//除去1M频率。剩下都配置为500KCAN_InitStructure.CAN_BS1 = CAN_BS1_6tq;CAN_InitStructure.CAN_BS2 = CAN_BS2_7tq;}CAN_InitStructure.CAN_Prescaler = 6;CAN_Init(CANx, &CAN_InitStructure);/* CAN filter init */CAN_FilterInitStructure.CAN_FilterNumber = 0;CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0;CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;CAN_FilterInit(&CAN_FilterInitStructure);/* Enable FIFO 0 message pending Interrupt */CAN_ITConfig(CANx, CAN_IT_FMP0, ENABLE);return 1;}// The driver send a CAN message passed from the CANopen stackunsigned char canSend(CAN_PORT notused, Message *m){int i, res;CanTxMsg TxMessage = {0};TxMessage.StdId = m->cob_id;TxMessage.IDE = CAN_ID_STD;if(m->rtr)TxMessage.RTR = CAN_RTR_REMOTE;elseTxMessage.RTR = CAN_RTR_DATA;TxMessage.DLC = m->len;for(i=0 ; i<m->len ; i++)TxMessage.Data[i] = m->data[i]; res = CAN_Transmit(CANx, &TxMessage);if(res == CAN_TxStatus_NoMailBox)return 0; // errorreturn 1;// succesful}//The driver pass a received CAN message to the stack/*unsigned char canReceive(Message *m){}*/unsigned char canChangeBaudRate_driver( CAN_HANDLE fd, char* baud){return 0;}/*** @briefThis function handles CAN1 RX0 interrupt request.* @paramNone* @retval None*/void CAN1_RX0_IRQHandler(void){int i;CanRxMsg RxMessage = {0};Message rxm = {0};CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);// Drop extended framesif(RxMessage.IDE == CAN_ID_EXT) //不处理扩展帧return;rxm.cob_id = RxMessage.StdId;if(RxMessage.RTR == CAN_RTR_REMOTE)//远程帧rxm.rtr = 1;rxm.len = RxMessage.DLC;for(i=0 ; i<rxm.len ; i++) rxm.data[i] = RxMessage.Data[i];canDispatch(co_data, &rxm);//CANopen自身的处理函数,因为快速SDO不需要反馈,所以在上边处理后就不需要调用这步了}

can1.h

#ifndef __CAN1_H#define __CAN1_H#include "sys.h"#include "main.h"#include "data.h"// CAN bus defines for cortex-M4 STM32F407#define CANx CAN1#define CAN_CLKRCC_APB1Periph_CAN1#define CAN_RX_PIN GPIO_Pin_11#define CAN_TX_PIN GPIO_Pin_12#define CAN_GPIO_PORTGPIOA#define CAN_GPIO_CLK RCC_AHB1Periph_GPIOA#define CANx_RX0_IRQn CAN1_RX0_IRQn#define GPIO_AF_CANx GPIO_AF_CAN1#define CAN_RX_SOURCEGPIO_PinSource11#define CAN_TX_SOURCEGPIO_PinSource12unsigned char CAN1_Init(CO_Data * d, uint32_t bitrate);#endif 

timer3.c

#include "timer3.h"TIMEVAL last_counter_val = 0;TIMEVAL elapsed_time = 0;// Initializes the timer, turn on the interrupt and put the interrupt time to zerovoid TIM3_Init(void){NVIC_InitTypeDef NVIC_InitStructure;TIM_TimeBaseInitTypeDefTIM_TimeBaseStructure;/* TIM3 clock enable */RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);/* Enable the TIM3 gloabal Interrupt */NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure);/* Compute the prescaler value */uint16_t PrescalerValue =840-1; //84M频率/840为100k(与timerscfg.h配置一致即可),即10us间隔/* Time base configuration */TIM_TimeBaseStructure.TIM_Period = 65535;TIM_TimeBaseStructure.TIM_Prescaler = PrescalerValue;TIM_TimeBaseStructure.TIM_ClockDivision = 0;TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);TIM_ClearITPendingBit(TIM3, TIM_SR_UIF); /* TIM3 enable counter *///这里需要启动定时器TIM_Cmd(TIM3, ENABLE);/* Preset counter for a safe start */TIM_SetCounter(TIM3, 1);/* TIM Interrupts enable */TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);}//Set the timer for the next alarm.void setTimer(TIMEVAL value){uint32_t timer = TIM_GetCounter(TIM3);// Copy the value of the running timerelapsed_time += timer - last_counter_val;last_counter_val = 65535-value;TIM_SetCounter(TIM3, 65535-value);TIM_Cmd(TIM3, ENABLE);//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){uint32_t timer = TIM_GetCounter(TIM3);// Copy the value of the running timerif(timer < last_counter_val)timer += 65535;TIMEVAL elapsed = timer - last_counter_val + elapsed_time;//printf("elapsed %lu - %lu %lu %lu\r\n", elapsed, timer, last_counter_val, elapsed_time);return elapsed;}// This function handles Timer 3 interrupt request.void TIM3_IRQHandler(void){//printf("--\r\n");if(TIM_GetFlagStatus(TIM3, TIM_SR_UIF) == RESET)return;last_counter_val = 0;elapsed_time = 0;TIM_ClearITPendingBit(TIM3, TIM_SR_UIF);TimeDispatch();}

timer3.h

#ifndef __TIMER3_H#define __TIMER3_H#include "sys.h"#include "main.h"void TIM3_Init(void);#endif 

2022年3月18日记:
定时器实现函数存在缺陷,当超过一个功能需要调用时间时,会存在干涉。各位如果除了心跳报文发送之外,没用到其他需要时间的功能(节点掉线检测/pdo之类),那么可以忽略。不然可以看一下这个CANopen补充–时间计算出错。

3.3 建立词典


我们起名字为Master,使用心跳管理,这样我们待会便可以通过心跳报文来判断移植成功与否。

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

点击保存,将生成的.od文件放入CANopen/dictionnary文件夹。

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

效果如下:

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

3.4工程配置

文件都弄好了,我们打开keil软件,将这些文件都加入到工程。

3.41 c文件添加

在Groups里新建两个文件夹。需要说明的时候,为了美观,这里把词典文件和外设驱动文件放在一起了。

文件夹说明
CANopen含CANopen/src
CANopen_Driver含CANopen/hardware 和CANopen/dictionary。

3.42 头文件路径添加


3.43 c99标准选择

由于源码很多地方,把定义语句放在赋值语句之后,这只在C99标准之后允许,因此勾选C99模式。

3.44 调试串口设置

​ 使用工程自带的USART1。

警告,在项目中正常运行后,一定要关闭调试功能,不然串口发送数据会严重降低相应速度!!!!!

我们打开applicfg.h ,如果找不到,直接全局搜索:MSG(…) 便可定位到啦。

第一,添加debug的定义 再次警告,在项目中正常运行后,记得关闭(把定义注释掉);第二,把打印函数里的\n 改成\r\n。

如图是串口反馈的效果,还是挺直观的。没有USB-CAN的同学可以通过串口调试助手来观察。

3.45 程序启动

首次,在main.h里添加相关头文件

main函数添加canopen初始化。包含定时器3、串口1、can1的初始化

#include "sys.h"#include "main.h"int main(void){ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(168);//初始化延时函数TIM3_Init();USART1_Init(115200);CAN1_Init(&Master_Data,1000000);unsigned char nodeID = 0x00; //主站IDsetNodeId(&Master_Data, nodeID);setState(&Master_Data, Initialisation); //节点初始化setState(&Master_Data, Operational);while(1){delay_ms(1000);}}

下载,启动!

使用软件观察。

心跳没有问题,nice
如果大家有需要让主站检测节点是否掉线的需要,可以看CANopen补充–主站检测节点是否在线。

4 末尾

​ 到这里便移植成功啦。下一篇教程基于STM32F4的CANopen快速SDO通信(超级详细)