本文记录将CANopen开源库CANfestival移植到GD32F470单片机的过程。CANopen协议理解请参考博客:CANopen协议的理解-CSDN博客
CANfestival开源库下载链接
CSDN链接: https://download.csdn.net/download/heqiunong/89774627
官网链接:https://hg.beremiz.org/canfestival/file/de1fc3261f21
objdictedit字典工具下载链接
https://download.csdn.net/download/heqiunong/89774674
https://download.csdn.net/download/supcool/12492303
视频参考链接
1 canfestival移植_哔哩哔哩_bilibili
移植正文
上述各种链接都是些准备工作,主要有两个东西需要下载,一个是CANfestival库,一个是objdictedit对象字典工具。
1、CANfestival库文件预处理
下载好了CANfestval库后,里面有些文件是多余的,需要删除。
1.1 src文件夹下的文件删除,src重命名为source
左边红色框住的部分删除掉
1.2 include文件夹下的文件删除,AVR文件夹重命为gd32
1.3 gd32文件夹(原AVR文件夹)下的文件删除
2、将CANfestival库放入工程文件当中
先把预处理的CANfestivel库拷贝到keil的工程文件夹当中,具体拷到哪里由自己定。
2.1 将所有源文件添加进入项目
2.2 添加include路径
3、处理编译的报错
3.1 注释config.h文件中的部分内容
3.2 缺函数的问题
完成3.1后,编译工程会缺函数
3.3 处理前两个缺函数的报错
start_and_seek_node 和 start_node这两个函数,是有的,只是因为inline关键字没有被识别,去掉inline就可以了。
全局搜索一下,找到位置。(这两个函数定义是在dcf.c文件中)
删除这两个inline,现在就只缺下面3个函数了。
3.4 添加canSend函数以及CAN接收的处理
在添加canSend函数之前,我们需要对GD32F470的CAN进行外设层的配置,这部分配置完最好拿个USB-to-CAN的工具验证一下配置成功没有。 这部分工作可GD32F470提供的参考例程,本文的重点不在这里。
假设我们已经把CAN的外设部分都配置好了,下面来添加canSend函数。
#include "canfestival.h"
// This function is called by CANopen library
uint8_t canSend(CAN_PORT notused, Message *message)
{
uint8_t transmit_mailbox = 0;
uint32_t timeout = 0xFFFF;
transmitMessage.tx_dlen = message->len;
memcpy(transmitMessage.tx_data, message->data, message->len);
transmitMessage.tx_ff = CAN_FF_STANDARD;
transmitMessage.tx_sfid = message->cob_id;
// Check here if an accident occurs
transmitMessage.tx_ft = (message->rtr == 0) ? CAN_FT_DATA : CAN_FT_REMOTE;
// Transmit message
transmit_mailbox = can_message_transmit(CAN0, &transmitMessage);
// Waiting for transmit completed
timeout = 0xFFFF;
while((CAN_TRANSMIT_OK != can_transmit_states(CAN0, transmit_mailbox)) && (0 != timeout)){
timeout--;
}
return (timeout!=0) ? 0:1;
}
Message这个结构体是canfestival的库里面定义的,所以这里需要包含canfestival.h。我们需要做的是理解Message结构体里面的内容,然后把信息通过gd32的CAN对应的外设函数把Message发送出去。
★那么同样的道理,当CAN在接收外部发来的信息的时候,我们也要把接收到的信息,按照Message的格式,存到Message里面去。gd32的CAN接收是用中断来做的,下面给出代码参考一下。
void CAN0_RX0_IRQHandler(void)
{
// For CANopen communication
Message Rx_Message;
can_message_receive(CAN0, CAN_FIFO0, &receiveMessage);
Rx_Message.cob_id = receiveMessage.rx_sfid;
Rx_Message.rtr = (receiveMessage.rx_ft == CAN_FT_DATA) ? 0:1; // be careful
Rx_Message.len =receiveMessage.rx_dlen;
memcpy(Rx_Message.data, receiveMessage.rx_data, receiveMessage.rx_dlen);
// TODO we need objdictedit
// canDispatch(&GD32Master_Data,&Rx_Message);
}
★注意函数最后一行的// canDispatch(&GD32Master_Data,&Rx_Message);是需要添加完对象字典后,需要解开注释的,
可以从上述函数分析,CAN接收中断函数把接收到的信息,存到了一个Message类型的结构体变量里面,最后调用canDispatch(&GD32Master_Data,&Rx_Message)函数,把接收到的信息和对象字典两个变量传进canfestival库进行处理。
那么搞完3.4这一步,就只剩下两个错误了。
3.5 添加getElapsedTime和setTimer函数
canfestival库的运行是需要一个定时器的,这个定时器需要由单片机给它提供,因此我们需要配置一个gd32的定时器给canfestival库。关于GD32的定时器配置内容,不是本文的重点,这里直接给出代码供参考。
static void CanOpenTimerConfig(void)
{
timer_parameter_struct initPara;
rcu_periph_clock_enable(RCU_TIMER2); // Timer 0 1 3 4 7 has been used for other purposes
// TIMER2_CLK = 240MHz
timer_deinit(TIMER2);
// CANopen requires a 10us timer, which is 100kHz
initPara.prescaler = 240 - 1; // 240MHz -> 100kHz
initPara.period = TIMEVAL_MAX - 1;
initPara.alignedmode = TIMER_COUNTER_EDGE;
initPara.counterdirection = TIMER_COUNTER_UP;
initPara.clockdivision = TIMER_CKDIV_DIV1;
initPara.repetitioncounter = 0;
timer_init(TIMER2, &initPara);
timer_auto_reload_shadow_enable(TIMER2); // Auto-reload preload enable
timer_flag_clear(TIMER2, TIMER_FLAG_UP);
timer_interrupt_enable(TIMER2, TIMER_INT_UP); // Enable count up interrupt
nvic_irq_enable(TIMER2_IRQn, 1U, 1U); // Enable and set timer interrupt priority
timer_enable(TIMER2);
}
// This function is called by CANopen library
void setTimer(TIMEVAL value)
{
TIMER_CAR(TIMER2) = value;
}
// This function is called by CANopen library
TIMEVAL getElapsedTime(void)
{
return TIMER_CNT(TIMER2);
}
// TIMER2 is assigned as a CANopen timer
void TIMER2_IRQHandler(void)
{
if(SET == timer_interrupt_flag_get(TIMER2,TIMER_INT_UP))
{
TimeDispatch();
timer_interrupt_flag_clear(TIMER2,TIMER_INT_UP);
}
}
我们把定时器的计数频率配置成了1MHz。计数周期这里配置成TIMEVAL_MAX - 1;
★注意:canfestival库里面默认的频率是125kHz,所以canfestival库里面timerscfg.h文件几个定义需要改改
★注意:除了报错所要求我们添加的getElapsedTime和setTimer函数以外, 定时器的计数溢出中断里面,也调用了一个canfestival库里的函数TimeDispatch(); 而且canfestival库规定,计数中断周期是2ms。所以我们在配置定时器的时候才使用TIMEVAL_MAX-1来配置的。 如何理解#define TIMEVAL_MAX 2000的意思?定时器是1MHz,2000即表示2000*(1/1MHz)= 2000us = 2ms,这个细节需要去理解和注意的。
同理,比如我是100kHz(10us计数一次)定时器呢?那TIMEVAL_MAX这里就是 200了,200*(1/100kHz)= 2000us = 2ms。 一个计数值 = 10us, 下面这两个define应该这么改。
最需要注意的就是3.4和3.5,这里很容易出问题。
4、添加对象字典
如果前面的操作都没有问题,那么这时候编译工程是不会报错的啦,但是这时候移植是没有完成的。 我们还需要添加单片机主节点的CANopen对象字典。 这时候就要用到前面我们提到的objdictedit字典啦。Objdictedit这个工具可以保存你的配置到一个xxxxx.od文件中,在开发的过程中,你可以每次只修改一部分。然后保存到.od文件中。下一次再改呢,又把这个.od文件再打开。 我们keil工程里面,需要的不是.od文件,而是利用.od文件生成的.c和.h文件。所以我们每次修改完.od文件保存之后,同时,我们还要利用objdictedit来生成一次.c和.h文件,把更新后的.c和.h文件替换keil工程里面原来的对象字典.c和.h文件。
下面我们以配置一个1ms的同步报文为例,来举例说明objdictedit的使用过程。
把objdictedit生成的Master.c Master.h添加进入项目中。
将Master.c代码最底部的对象字典变量名拷贝一下
下面把代码贴出了,方便copy
// user_can.c
...
#include "canfestival.h"
extern CO_Data Master_Data;
...
...
...
void CAN0_RX0_IRQHandler(void)
{
// For CANopen communication
Message Rx_Message;
can_message_receive(CAN0, CAN_FIFO0, &receiveMessage);
Rx_Message.cob_id = receiveMessage.rx_sfid;
Rx_Message.rtr = (receiveMessage.rx_ft == CAN_FT_DATA) ? 0:1; // be careful
Rx_Message.len =receiveMessage.rx_dlen;
memcpy(Rx_Message.data, receiveMessage.rx_data, receiveMessage.rx_dlen);
// ★
canDispatch(&Master_Data,&Rx_Message);
}
...
另外,在main主函数这边也需要做一个CANopen的基本的初始化操作。
// main.c
...
#include "Master.h"
...
void main()
{
...
setNodeId(&Master_Data,0x00);
setState(&Master_Data,Initialisation);
setState(&Master_Data,Pre_operational);
setState(&Master_Data,Operational);
...
}
5、实验现象
温故而知新,写这篇文章的时候对3.4,3.5这部分内容理解又加深了。 CANfestival的移植相比于CANopen协议的理解还是要简单一些,后续应该会根据实际的项目,更新一些除了SYNC操作以外的其他操作,欢迎关注/阅订。