本文是《CAN总线开发一本全(6) - CANopenNode组件》的补充其中一个小节。
总结在微控制器平台上移植CANopenNode,需要根据具体硬件条件,适配2个源文件:
- CANopenNode-1.3/stack/drvTemplate/CO_driver.c 文件。
- CANopenNode-1.3/example/main.c 文件。
- 创建并配置硬件定时器周期中断服务,以1ms为周期,调用CANopenNode的定时器周期执行线程的函数。
接下来,将以集成了FlexCAN外设模块的MM32F0140微控制器为例,实现对CANopenNode v1.3的适配过程。
目前灵动官方的软件开发平台MindSDK已经适配了CANopenNode协议栈,并创建了一系列样例工程:
为了描述适配CANopenNode的过程,这里仍然从零开始,展现完整的移植开发过程。
准备微控制器基本工程
首先从灵动MindSDK的网站上(https://mindsdk.mindmotion.com.cn/)获取到POKT-F0140(使用MM32F0140主控)开发板的flexcan驱动样例工程,flexcan_loopback,作为模板工程。这个模板工程里包含了MM32F0140微控制器正常工作的所有必要源码,包括芯片头文件、启动程序、中断向量表、以及一系列初始化硬件电路板到进入main()函数的源码,以及flexcan外设模块的驱动程序。
- 将模板工程的工程名改为
fthr-f1040_canopen_demo_mdk
- 将CANopenNode组件的源码包
CANopenNode-v1.3
复制到canopen_demo
工程的根目录下 - 将其中
stack/drvTemplate
目录下的CO_driver.c
和CO_driver_target.h
文件复制到canopen_demo
工程的board
目录下 - 将其中
example
目录下的的CO_OD.c
、CO_OD.h
和main.c
文件复制到canopen_demo
工程的application
目录下
在Keil MDK环境中打开canopen_demo工程。添加源文件和包含路径到工程中,如图x所示。
- 添加
CANopenNode-v1.3
目录下,CANopenNode-v1.3/stack
目录下所有的C源文件到工程 - 添加
CANopenNode-v1.3
目录和CANopenNode-v1.3/stack
目录到工程包含路径 - 添加
application
目录下新增文件CO_OD.c
、CO_OD.h
和main.c
,和board
目录下新增文件CO_driver.c
和CO_driver_target.h
,到canopen_demo工程中。
figure-canopen-demo-proj-settings
图x canopen_demo工程中包含CANopenNode源码整理好文件之后,试着编译一下工程,没有警告和错误,可以正常使用。如图x所示。
figure-canopen-demo-proj-build-log
图x canopen_demo工程编译正确此时的canopen_demo工程中,包含了CANopenNode的所有源码、FlexCAN外设模块的驱动,以及使用MM32F0140微控制器的所有必要的源文件,并且可以通过编译器验证编写程序代码的正确性。后续进行适配工作过程中,将通过开发者自行编码,将CANopenNode和FlexCAN外设模块绑定起来,并可实时编译工程验证编码内容。
在微控制器上适配CANopenNode
CANopeoNode组件中自带main.c文件,约定了整个CANopen协议栈的运行框架。在CANopenNode中的main.c
文件中,定义了应用程序入口main()
函数,以及定时器中断服务程序入口和CAN外设模块中断服务程序入口。在本例的移植工程中,定时器相关的程序被置于main.c文件中,而具体微控制器平台上的CAN外设模块相关的配置程序代码则位于CO_driver.c
文件中。
配置电路板的时钟和引脚 board_init.c
- 配置时钟
这里需要至少启用硬件定时器TIM2模块(产生1ms周期中断)和FlexCAN模块,另外,POKT-F0140开发板使用PB8和PB9作为CAN接口引脚,也需要启用对应IO端口的时钟。
在clock_init.c文件中更新BOARD_InitBootClocks()
源码:
void BOARD_InitBootClocks(void)
{
CLOCK_ResetToDefault();
CLOCK_BootToHSE72MHz();
/* TIM2.*/
RCC_EnableAPB1Periphs(RCC_APB1_PERIPH_TIM2, true);
RCC_ResetAPB1Periphs(RCC_APB1_PERIPH_TIM2);
/* FLEXCAN. */
RCC_EnableAPB1Periphs(RCC_APB1_PERIPH_FLEXCAN, true);
RCC_ResetAPB1Periphs(RCC_APB1_PERIPH_FLEXCAN);
...
/* GPIOB. */
RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOB, true); /* PB8 - CAN1_RX, PB9 - CAN1_TX. */
RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOB);
}
- 配置引脚
FTHR-F0140开发板使用PB8和PB9作为CAN接口引脚,需要配置引脚的复用功能为CAN服务。
在pin_init.c文件中更新BOARD_InitPins()
源码:
void BOARD_InitPins(void)
{
...
/* fthr-f0140. */
/* PA9 - FLEXCAN_RX. */
gpio_init.Pins = GPIO_PIN_9;
gpio_init.PinMode = GPIO_PinMode_In_Floating;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_PinAFConf(GPIOA, gpio_init.Pins, GPIO_AF_8);
/* PA10 - FLEXCAN_TX. */
gpio_init.Pins = GPIO_PIN_10;
gpio_init.PinMode = GPIO_PinMode_AF_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio_init);
GPIO_PinAFConf(GPIOA, gpio_init.Pins, GPIO_AF_8);
}
配置板子的BOARD_InitBootClocks()函数和BOARD_InitPins()函数,将在board_init.c文件中被BOARD_Init()函数调用,
void BOARD_Init(void)
{
BOARD_InitBootClocks();
BOARD_InitPins();
BOARD_InitDebugConsole();
}
BOARD_Init()
函数最终将在main.c文件中被调用,实现对电路板的初始化工作。
/* main ***********************************************************************/
int main (void){
CO_NMT_reset_cmd_t reset = CO_RESET_NOT;
/* Configure microcontroller. */
BOARD_Init();
...
}
准备硬件定时器 main.c
CANopenNode的三个线程之一,定时器周期执行线程,以1ms为间隔周期执行。例如,可以配置硬件定时器TIM2产生周期为1ms的中断服务,并在定时器的中断服务程序中嵌入CANopenNode提供main.c
文件中的tmrTask_thread()
函数。
在main.c
文件中编写brd_tim_init()
函数,配置TIM2硬件定时器,并在main()
函数中调用:
#include "board_init.h"
void BOARD_TIM_Init(void);
/* main ***********************************************************************/
int main (void)
{
...
/* Configure Timer interrupt function for execution every 1 millisecond */
BOARD_TIM_Init();
...
}
/* Setup the hardware timer. */
void BOARD_TIM_Init(void)
{
/* Set the counter counting step. */
TIM_Init_Type tim_init;
tim_init.ClockFreqHz = BOARD_TIM_FREQ;
tim_init.StepFreqHz = BOARD_TIM_UPDATE_STEP; /* 1ms. */
tim_init.Period = BOARD_TIM_UPDATE_PERIOD - 1u;
tim_init.EnablePreloadPeriod = false;
tim_init.PeriodMode = TIM_PeriodMode_Continuous;
tim_init.CountMode = TIM_CountMode_Increasing;
TIM_Init(BOARD_TIM_PORT, &tim_init);
/* Enable interrupt. */
TIM_EnableInterrupts(BOARD_TIM_PORT, TIM_INT_UPDATE_PERIOD, true);
NVIC_EnableIRQ(BOARD_TIM_IRQn);
/* Start the timer. */
TIM_Start(BOARD_TIM_PORT);
}
其中,关于硬件定时器的配置参数的定义统一放置于board_init.h
文件。
/* TIM1. */
#define BOARD_TIM_PORT (TIM_Type *)TIM2
#define BOARD_TIM_IRQn TIM2_IRQn
#define BOARD_TIM_IRQHandler TIM2_IRQHandler
#define BOARD_TIM_FREQ CLOCK_SYS_FREQ
#define BOARD_TIM_UPDATE_STEP 1000000u
#define BOARD_TIM_UPDATE_PERIOD 1000u
在main()
函数中调用了BOARD_TIM_Init()
函数,配置硬件定时器TIM2产生1ms为周期的中断,并启动定时器。此时,对应在硬件定时器的中断服务程序中调用CANopenNode的定时器线程函数tmrTask_thread()
,并在其中清硬件定时器中断的标志位。
/* timer thread executes in constant intervals ********************************/
void tmrTask_thread(void)
{
INCREMENT_1MS(CO_timer1ms);
if (CO- >CANmodule[0]- >CANnormal)
{
bool_t syncWas;
/* Process Sync */
syncWas = CO_process_SYNC(CO, TMR_TASK_INTERVAL);
/* Read inputs */
CO_process_RPDO(CO, syncWas);
/* Further I/O or nonblocking application code may go here. */
/* Write outputs */
CO_process_TPDO(CO, syncWas, TMR_TASK_INTERVAL);
/* verify timer overflow */
if (TIM_STATUS_UPDATE_PERIOD == (TIM_GetInterruptStatus(BOARD_TIM_PORT) & TIM_STATUS_UPDATE_PERIOD) )
{
CO_errorReport(CO- >em, CO_EM_ISR_TIMER_OVERFLOW, CO_EMC_SOFTWARE_INTERNAL, 0u);
TIM_ClearInterruptStatus(BOARD_TIM_PORT, TIM_STATUS_UPDATE_PERIOD);
}
}
}
/* Timer interrupt function ***************************************************/
void BOARD_TIM_IRQHandler(void)
{
TIM_ClearInterruptStatus(BOARD_TIM_PORT, TIM_STATUS_UPDATE_PERIOD);
tmrTask_thread();
}
这里要注意,CANopenNode原生的tmrTask_thread()函数的实现模板中,停用了“/* verify timer overflow */”之后的代码。这些被停用的代码,原本可以用来验证tmrTask_thread()
函数内部操作,例如处理同步过程、读接收PDO和写发送PDO,在清了上一次1ms中断的硬件标志位后的1ms中是否能够执行完毕。如果tmrTask_thread()
函数的处理时间过长,超出了一个周期任务的执行时间,此时检测到1ms定时器中断标志位再次置位,即出现超时。在1ms周期任务超时之后,CANopen协议栈会认为这是一个可能产生风险的任务,因此可调用CO_errorReport()
函数将错误情况上报给CANopen协议栈。
对接CAN驱动 CO_driver.c & main.c
CO_driver.c文件中定义了大量的具体微控制器平台的CAN外设硬件模块相关的驱动函数,但在最基础的移植过程中,仅需重点关注4个函数即可:
- CO_CANmode_init() - 初始化CAN外设模块,并配置好比特率、消息帧过滤器,以及收发中断等。
- CO_CANsend() - 将消息缓冲区中的数据搬运至CAN外设模块的硬件发送缓冲区中,即将发送CAN消息帧到CAN总线上。
- CO_CANinterrupt() - 绑定到CAN外设模块的硬件中断的服务程序,主要实现CAN硬件的接收过程,即将CAN外设模块从CAN总线上捕获下来的CAN消息帧数据转存到CANopenNode组件的接收缓冲区中,供协议栈进一步处理。当使用中断方式发送CAN消息帧时,也需要在CO_CANinterrupt()函数中调用CO_CANsend()函数发送CAN消息帧。
- CO_CANverifiyErrors() - 查看CAN外设模块的硬件错误。因为CAN总线上的消息帧需要经过仲裁才能上线,所以这里查错函数主要检查的是发送消息帧超时的情况。
原生CANopenNode组件包中的CO_driver.c文件中的函数已经实现了绝大部分同协议栈交互的业务逻辑,在具体微控制器平台上是适配时,仅需要将少量同硬件相关的步骤绑定到微控制器硬件的操作上即可。
CO_CANmodule_init()
在CO_driver.c
文件中向CO_CANmodule_init()
函数嵌入初始化FlexCAN外设模块的代码,包括初始化FlexCAN的通信引擎,配置好过滤器等(本移植工程未启硬件过滤器功能,由CANopenNode的软件过滤器处理)。
#include "board_init.h"
/******************************************************************************/
CO_ReturnError_t CO_CANmodule_init(
CO_CANmodule_t *CANmodule,
void *CANdriverState,
CO_CANrx_t rxArray[],
uint16_t rxSize,
CO_CANtx_t txArray[],
uint16_t txSize,
uint16_t CANbitRate)
{
uint16_t i;
/* verify arguments */
if(CANmodule==NULL || rxArray==NULL || txArray==NULL){
return CO_ERROR_ILLEGAL_ARGUMENT;
}
/* Configure object variables */
CANmodule- >CANdriverState = CANdriverState;
CANmodule- >rxArray = rxArray;
CANmodule- >rxSize = rxSize;
CANmodule- >txArray = txArray;
CANmodule- >txSize = txSize;
CANmodule- >CANnormal = false;
CANmodule- >useCANrxFilters = false;/* microcontroller dependent */
CANmodule- >bufferInhibitFlag = false;
CANmodule- >firstCANtxMessage = true;
CANmodule- >CANtxCount = 0U;
CANmodule- >errOld = 0U;
CANmodule- >em = NULL;
for(i=0U; i< rxSize; i++){
rxArray[i].ident = 0U;
rxArray[i].mask = 0xFFFFU;
rxArray[i].object = NULL;
rxArray[i].pFunct = NULL;
}
for(i=0U; i< txSize; i++){
txArray[i].bufferFull = false;
}
/* Configure CAN timing */
FLEXCAN_TimConf_Type flexcan_tim_conf;
flexcan_tim_conf.EnableExtendedTime = false;
flexcan_tim_conf.PhaSegLen1 = 5u;
flexcan_tim_conf.PhaSegLen2 = 1u;
flexcan_tim_conf.PropSegLen = 2u;
flexcan_tim_conf.JumpWidth = 1u;
/* Configure CAN module registers */
FLEXCAN_Init_Type flexcan_init;
flexcan_init.MaxXferNum = BOARD_FLEXCAN_XFER_MaxNum; /* The max mb number to be used. */
flexcan_init.ClockSource = FLEXCAN_ClockSource_Periph; /* Use peripheral clock. */
flexcan_init.BitRate = CANbitRate * 1000u; /* Set bitrate. */
flexcan_init.ClockFreqHz = BOARD_FLEXCAN_CLOCK_FREQ; /* Set clock frequency. */
flexcan_init.SelfWakeUp = FLEXCAN_SelfWakeUp_BypassFilter; /* Use unfiltered signal to wake up flexcan. */
flexcan_init.WorkMode = FLEXCAN_WorkMode_Normal; /* Normal workmode, can receive and transport. */
flexcan_init.Mask = FLEXCAN_Mask_Global; /* Use global mask for filtering. */
flexcan_init.EnableSelfReception = false; /* Not receiving mb frame sent by self. */
flexcan_init.EnableTimerSync = true; /* Every tx or rx done, refresh the timer to start from zero. */
flexcan_init.TimConf = &flexcan_tim_conf; /* Set timing sychronization. */
FLEXCAN_Init(BOARD_FLEXCAN_PORT, &flexcan_init);
/* Set tx mb. */
FLEXCAN_ResetMb(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_TX_MB_CH);
FLEXCAN_SetMbCode(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_TX_MB_CH, FLEXCAN_MbCode_TxInactive);
/* Set rx mb. */
FLEXCAN_RxMbConf_Type flexcan_mb_conf;
flexcan_mb_conf.Id = 0x000u; /* Id for filtering with mask and receiving. */
flexcan_mb_conf.MbType = FLEXCAN_MbType_Data; /* Only receive standard data frame. */
flexcan_mb_conf.MbFormat = FLEXCAN_MbFormat_Standard;
FLEXCAN_SetRxMb(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_RX_MB_CH, &flexcan_mb_conf);
FLEXCAN_SetMbCode(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_RX_MB_CH, FLEXCAN_MbCode_RxEmpty);/* Set for receiving. */
/* Configure CAN module hardware filters */
/* CAN module filters are not used, all messages with standard 11-bit */
/* identifier will be received */
/* Configure mask 0 so, that all messages with standard identifier are accepted */
FLEXCAN_RxMbMaskConf_Type mb_mask_conf;
mb_mask_conf.MbType = FLEXCAN_MbType_Data;
mb_mask_conf.MbFormat = FLEXCAN_MbFormat_Standard;
mb_mask_conf.IdMask = 0x000u;
FLEXCAN_EnableFreezeMode(BOARD_FLEXCAN_PORT, true);
FLEXCAN_SetGlobalMbMaskConf(BOARD_FLEXCAN_PORT, &mb_mask_conf);
FLEXCAN_EnableFreezeMode(BOARD_FLEXCAN_PORT, false);
/* configure CAN interrupt registers */
FLEXCAN_EnableMbInterrupts(BOARD_FLEXCAN_PORT, BOARD_FLEXCAN_RX_MB_INT | BOARD_FLEXCAN_TX_MB_INT, true);
NVIC_EnableIRQ(BOARD_FLEXCAN_IRQn);
return CO_ERROR_NO;
}