一、背景
CAN組網就必須得要應用層協議,原因就在於
* 便於網絡管理與控制
* 確認數據的收發
* 發送大於8個字節的數據塊(CAN每幀數據傳輸大小為8字節)
* 為不同節點分配不同的報文標識符
* 定義幀報文的內容及含義(這在我看來是最主要的原因)
* 網絡的監控,節點故障的診斷與標識
CAN上層協議有許多,用大家都公認的,便於產品的兼容,因此,CANopen成為備選項。
CANopen有個開源協議棧【CANFestival】,同時有一位大神已經做了移植並記錄,在此就厚着臉皮轉載過來以做備份。
轉載地址:http://www.cnblogs.com/tdyizhen1314/p/4348725.html
二、正文:
前段時間學習了CanOpen協議,到網上下載的CanFestival3-10源碼,移植到VC、QT、STM32等平台,由於網上的資源較少,走了不少彎路,移植好使用過程中才逐漸暴露出各種問題,比如OD字符串傳輸、心跳時間不准確等等,現在已經解決了遇到的所有問題,移植出來的工程能夠完好支持CanOpen協議,花了點時間,整理出一個簡單易用的移植方法說明,也寫了一些比較實用的調試工具,本來還想整理SDO、PDO、EDS文件裝載等相關知識的,可惜比較忙,等什么時候有空了再整理其他的吧!先把移植的貼上來,希望能幫到大家。
如果是第一次,整個移植過程還比較麻煩,需要有耐心,按照下面說的一步步來肯定可以的,移植成功一次后,再移植到其他平台就非常輕松了。
到網上下載CanFestival源碼CanFestival-3-1041153c5fd2,解壓出來,並將文件夾名字改為CanFestival-3-10,我們移植需要用到的源文件在CanFestival-3-10\src目錄下,頭文件在CanFestival-3-10\include目錄下。
CanFestival-3-10\src下的文件如下圖所示:
CanFestival-3-10\include下的文件如下圖所示:
接下來開始移植: 步驟一: 在新建好的工程目錄下新建文件夾CanFestival,再在CanFestival下新建文件夾driver、inc和src,再在inc文件夾下面新建 stm32文件夾(我這里主要以移植到stm32為例說明,如果是移植到VC或其他平台下,這里也可以命名為其他名字,如vc)。 步驟二: 將CanFestival-3-10\src目錄下的dcf.c、emcy.c、lifegrd.c、lss.c、nmtMaster.c、nmtSlave.c、objacces.c、pdo.c、sdo.c、states.c、sync.c、timer.c共12個文件拷貝到CanFestival\src目錄下; 將CanFestival-3-10\include目錄下的所有.h文件共19個文件全部拷貝到CanFestival\inc目錄下, 再把CanFestival-3-10\examples\AVR\Slave目錄下的ObjDict.h文件拷貝過來,一共20個; 將CanFestival-3-10\include\AVR目錄下的applicfg.h、canfestival.h、config.h、timerscfg.h共4個頭文件拷貝到canfestival\inc\stm32目錄下; 將CanFestival-3-10\examples\TestMasterSlave目錄下的TestSlave.c、TestSlave.h、TestMaster.h、TestMaster.c拷貝到canfestival\driver目錄下,並在該目錄下新建stm32_canfestival.c文件。 步驟三: 將CanFestival\src目錄下的所有.c文件添加到工程;將canfestival\driver目錄下的stm32_canfestival.c文件添加到工程; 如果實現的是從設備,再將canfestival\driver目錄下的TestSlave.c文件添加到工程,如果實現的是主設備,則將TestMaster.c文件添加到工程; 步驟四: 將文件目錄canfestival\inc、canfestival\inc\stm32、canfestival\driver等路徑添加到工程包含路徑。 步驟五: 在stm32_canfestival.c中包含頭文件#include "canfestival.h",並定義如下函數: void setTimer(TIMEVAL value) { } TIMEVAL getElapsedTime(void) { return 1; } unsigned char canSend(CAN_PORT notused, Message *m) { return 1; } 可以先定義一個空函數,等到編譯都通過了之后,再往里面添加內容,這幾個函數都是定義來供canfestival源碼調用的,如果找不到這幾個函數編譯就會報錯。 步驟六:通過以上幾步,所有的文件都弄齊了,但是編譯一定會出現報錯,注釋或刪除掉config.h文件中的如下幾行就能編譯通過: #include <inttypes.h> #include <avr\io.h> #include <avr\interrupt.h> #include <avr/pgmspace.h> #include <avr\sleep.h> #include <avr\wdt.h> 如果還有其他報錯,那有可能是因為不同源碼版本、不同平台、不同人遇到的錯誤也會不相同,這里的過程只能做一定的參考,不一定完全相同,解決這些錯誤需要有一定的調試功底,需要根據編譯出錯提示來進行修改對應地方,一般都是有些函數沒聲明或者某個頭文件沒有包含或者包含了一些不必要的頭文件而該文件不存在或者是一些變量類型不符合需定義之類的,如果能夠擺平所有的編譯出錯,那么移植就算成功了,如果你被編譯出錯擺平了,那么游戲就結束,沒得玩了。 步驟七: 解決了所有的編譯錯誤后,接下來實現剛才定義的3個空函數: 函數void setTimer(TIMEVAL value)主要被源碼用來定時的,時間到了就需要調用一下函數TimeDispatch(), 函數TIMEVAL getElapsedTime(void)主要被源碼用來查詢距離下一個定時觸發還有多少時間, 函數unsigned char canSend(CAN_PORT notused, Message *m)主要被源碼用來發一個CAN包的,需要調用驅動來將一個CAN包發出去。 我們在stm32_canfestival.c文件里定義幾個變量如下: unsigned int TimeCNT=0;//時間計數 unsigned int NextTime=0;//下一次觸發時間計數 unsigned int TIMER_MAX_COUNT=70000;//最大時間計數 static TIMEVAL last_time_set = TIMEVAL_MAX;//上一次的時間計數 setTimer和getElapsedTime函數實現如下: //Set the next alarm // void setTimer(TIMEVAL value) { NextTime=(TimeCNT+value)%TIMER_MAX_COUNT; } // Get the elapsed time since the last occured alarm // TIMEVAL getElapsedTime(void) { int ret=0; ret = TimeCNT> last_time_set ? TimeCNT - last_time_set : TimeCNT + TIMER_MAX_COUNT - last_time_set; last_time_set = TimeCNT; return ret; } 另外還要開一個1毫秒的定時器,每1毫秒調用一下下面這個函數。 void timerForCan(void) { TimeCNT++; if (TimeCNT>=TIMER_MAX_COUNT) { TimeCNT=0; } if (TimeCNT==NextTime) { TimeDispatch(); } } can發包函數canSend跟CAN驅動有關,CAN通道可以使用真實的CAN總線,也可以使用虛擬的CAN通道(如文件接口、網絡通道等等)。 啟動時初始化: 在初始化的文件里(比如main.c)添加以下幾行代碼 #include "TestSlave.h" unsigned char nodeID=0x21; extern CO_Data TestSlave_Data; 在調用函數(比如main函數)里調用以下代碼初始化 setNodeId(&TestSlave_Data, nodeID); setState(&TestSlave_Data, Initialisation); // Init the state 其中T estSlave_Data在TestSlave.c中定義 然后開啟調用TimerForCan()的1毫秒定時器,在接收CAN數據那里調用一下源碼函數canDispatch(&TestSlave_Data, &m); canfestival源碼就可以跑了,如果需要跟主設備聯調,還要實現canSend函數,這個與平台的Can驅動相關。 Stm32平台下的驅動實現: 開啟一個1毫秒定時器,可參考如下代碼,調用一下函數TIM4_start();即可: /* TIM4 configure */ static void TIM4_Configuration(void) { /* 時鍾及分頻設置 */ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; /* Time Base configuration */ /* 72M / 72 = 1us */ // 這個就是預分頻系數,當由於為0時表示不分頻所以要減1 TIM_TimeBaseStructure.TIM_Prescaler =72-1; //72000 - 1; //計數模式:向上計數 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //這個就是自動裝載的計數值,由於計數是從0開始的 //TIM_TimeBaseStructure.TIM_Period =0xffff;// TIM_TimeBaseStructure.TIM_Period =0x03e8;//1ms TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //重新計數的起始值 TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); // TIM IT enable TIM_ITConfig(TIM4, TIM_IT_CC1, ENABLE); //打開溢出中斷 // TIM enable counter TIM_Cmd(TIM4, ENABLE);//計數器使能,開始工作 } static void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3); /* Enable the TIM4 global Interrupt */ NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } static void RCC_Configuration(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); /* TIM4 clock enable */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); /* clock enable */ RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA ,ENABLE); } void TIM4_start(void) { RCC_Configuration(); /* configure TIM4 for remote and encoder */ NVIC_Configuration(); TIM4_Configuration(); } void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_CC1) != RESET) { //printf("enter tim4"); TIM_ClearITPendingBit(TIM4, TIM_IT_CC1); } TimerForCan(); } canSend函數實現如下: unsigned char canSend(CAN_PORT notused, Message *m) { uint32_t i; CanTxMsg *ptx_msg=&TxMessage; ptx_msg->StdId = m->cob_id; if(m->rtr) { ptx_msg->RTR = CAN_RTR_REMOTE; } else { ptx_msg->RTR = CAN_RTR_DATA; } ptx_msg->IDE = CAN_ID_STD; ptx_msg->DLC = m->len; for(i = 0; i < m->len; i++) { ptx_msg->Data = m->data; } if( CAN_Transmit( CAN1, ptx_msg )==CAN_NO_MB) { return 0xff; } else { return 0x00; } } 其中CAN_Transmit為stm32提供的庫函數,在stm32f10x_can.c中定義。 在使用stm32之前需要初始化一下CAN void CAN_Config(void) { /* CAN register init */ CAN_DeInit(CAN1); CAN_DeInit(CAN2); CAN_StructInit(&CAN_InitStructure); /* CAN1 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; //Fpclk=72M/2/CAN_Prescaler //BitRate=Fpclk/((CAN_SJW+1)*((CAN_BS1+1)+(CAN_BS2+1)+1)); //1M /*CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_3tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_5tq; CAN_InitStructure.CAN_Prescaler = 4;*/ //125K CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_7tq; CAN_InitStructure.CAN_Prescaler = 18; CAN_Init(CAN1, &CAN_InitStructure); CAN_Init(CAN2, &CAN_InitStructure); /* CAN1 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); /* CAN2 filter init */ CAN_FilterInitStructure.CAN_FilterNumber = 14; CAN_FilterInit(&CAN_FilterInitStructure); } Can 接收中斷實現: void CAN1_RX0_IRQHandler(void) { CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); //接收處理 m.cob_id=RxMessage.StdId; if(RxMessage.RTR == CAN_RTR_REMOTE) m.rtr=1; else if(RxMessage.RTR == CAN_RTR_DATA) m.rtr=0; m.len=RxMessage.DLC; for(i = 0; i < RxMessage.DLC; i++) m.data=RxMessage.Data; canDispatch(&TestSlave_Data, &m); } 移植到VC或其他C++平台說明: 由於源碼全是c文件,如果要移植到C++平台,需要將以上所有涉及的.c文件改成.cpp文件, 如果是移植到MFC,則還要在cpp文件中包含頭文件 #include "stdafx.h" 移植到VC等一些比較牛的編譯器下面時,由於檢查得更嚴格,所以編譯還會出現一些指針不匹配的問題,如:pdo.cpp文件的145、216、332行就會報錯, 只要強制轉換一下指針就能通過,如將 pwCobId = d->objdict[offset].pSubindex[1].pObject; 改成 pwCobId = (unsigned long *)d->objdict[offset].pSubindex[1].pObject; 即可通過。 還有407行由於代碼跨平台出現些亂碼錯誤,將 MSG_ERR (0x1948, " Couldn't build TPDO n�", numPdo); 改成 MSG_ERR (0x1948, " Couldn't build TPDO \n", numPdo); 即可。 這時編譯還不能通過,需修改除了dcf.h和canfestival.h以外的所有頭文件,在開頭加上 #ifdef __cplusplus extern "C" { #endif 頭文件結尾加上 #ifdef __cplusplus }; #endif 例如:data.c改成data.cpp后,data.h中添加位置如下: #ifndef __data_h__ #define __data_h__ #ifdef __cplusplus extern "C" { #endif //省略掉中間內容 #ifdef __cplusplus }; #endif #endif /* __data_h__ */ 另外,源碼文件文件還有一個錯誤,這個錯誤在keil里表現不出來,在VC里就會導致出錯,花了些時間才找到這些錯誤。如下: 文件dcf.cpp第40行,將 extern UNS8 _writeNetworkDict (CO_Data* d, UNS8 nodeId, UNS16 index, UNS8 subIndex, UNS8 count, UNS8 dataType, void *data, SDOCallback_t Callback, UNS8 endianize); 改成 extern UNS8 _writeNetworkDict (CO_Data* d, UNS8 nodeId, UNS16 index, UNS8 subIndex, UNS32 count, UNS8 dataType, void *data, SDOCallback_t Callback, UNS8 endianize); 移植到VC或QT時,由於電腦沒有CAN接口,這時要么用USB-CAN,要么得使用虛擬的CAN總線通道,Linux下面有虛擬的CAN總線, windows下沒有,只能通過走文件接口或網口來虛擬CAN總線。
記錄地點:深圳WZ
記錄時間:2016年7月29日
