本文是《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;
}