《STM32F4上的CANOpen移植:超详细指南》

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有关的代码。

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

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

    说明如下:

    文件夹名 说明
    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_InitTypeDef  GPIO_InitStructure;
      NVIC_InitTypeDef  NVIC_InitStructure;
      CAN_InitTypeDef        CAN_InitStructure;
      CAN_FilterInitTypeDef  CAN_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频率。剩下都配置为500K
      	CAN_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 stack
    unsigned 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;
    	else
      		TxMessage.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; 	// error
        return 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;
    }
    
    /**
    
      * @brief  This function handles CAN1 RX0 interrupt request.
      * @param  None
      * @retval None
        */
        void CAN1_RX0_IRQHandler(void)
        {
        int i;
        CanRxMsg RxMessage = {0};
        Message rxm = {0};
        CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
        // Drop extended frames
        if(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_CLK                    RCC_APB1Periph_CAN1
    #define CAN_RX_PIN                 GPIO_Pin_11
    #define CAN_TX_PIN                 GPIO_Pin_12
    #define CAN_GPIO_PORT              GPIOA
    #define CAN_GPIO_CLK               RCC_AHB1Periph_GPIOA
    #define CANx_RX0_IRQn               CAN1_RX0_IRQn
    
    #define GPIO_AF_CANx               GPIO_AF_CAN1
    #define CAN_RX_SOURCE              GPIO_PinSource11
    #define CAN_TX_SOURCE              GPIO_PinSource12
    
    unsigned 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 zero
    void TIM3_Init(void)
    {
    	NVIC_InitTypeDef NVIC_InitStructure;
    	TIM_TimeBaseInitTypeDef  TIM_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 timer
    	elapsed_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 timer
    	if(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的同学可以通过串口调试助手来观察。

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

    3.45 程序启动

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

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

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

    下载,启动!

    使用软件观察。

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

    4 末尾

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

    物联沃分享整理
    物联沃-IOTWORD物联网 » 《STM32F4上的CANOpen移植:超详细指南》

    发表评论