1 概述
CanFestival是一个免费而且开源的CANopen协议栈,较为适合于对CANopen协议栈功能完备性和工作性能要求不高的应用场合。对于功能和性能要求较高的应用,也有很多第三方的商用CANopen协议栈可供购买使用。目前,CanFestival在嵌入式控制(PLC、单片机、ARM等)领域中使用较多。
本文讲解了CanFestival库的基本架构和原理,针对ZYNQ平台详细介绍了裸机移植的原理和过程。最后通过测试实例,介绍了CanFestival中主要常用功能及相关API函数的使用方法。
2 CanFestival
2.1 CanFestival介绍
CanFestival能够运行于多种类型的平台。其源代码基于ANSI-C编写,驱动和例程的编译情况仅取决于具体的编译工具。在目前最新的版本中,提供了适用于多种硬件平台的底层驱动。此外,CanFestival可以在任意类Unix系统下编译和运行,如Linux和FreeBSD。
CanFestival协议功能完整,完全符合CANopen标准。CanFestival完全支持2002年2月发布的CIA DS-301 V4.02标准,并支持CiA DS302中的简明DFC协议和DS305规范。
此外,CanFestival为开发者提供了许多工具,以提高开发的便利性。例如,用于生成节点对象字典源代码的对象字典编辑器,以及便于开发者自由配置编译选项的配置脚本。
CanFestival库所支持的CANopen协议栈功能包括:
2.2 源代码获取
CanFestival开源库网址为:https://canfestival.org/index.html.en,
源码下载地址:https://canfestival.org/code.html.en,
其中官方主分支:https://hg.beremiz.org/。
另一个非官方分支则由后续的人在此基础之上不断更新完善:https://bitbucket.org/Mongo/canfestival-3-asc。
2.3 源代码概述
通过上述地址所下载的CanFestival的源码解压后的目录如下表所示:
路径 |
说明 |
./src |
与处理器无关的CANopen协议栈C源码 |
./include |
CANopen协议栈C源码对应的头文件,以及针对不同处理器或操作系统移植时所需要的头文件 |
./drivers |
针对不同处理器或操作系统的移植时的底层驱动(timer和CAN接口) |
./example |
CanFestival移植到不同处理器或操作系统的实例程序 |
./objdictgen |
使用python编写的图形化对象字典编辑工具,用于为基于CanFestival的具体应用设计相应的对象字典 |
./doc |
说明文档 |
2.3.1 CANopen协议栈
./src中包含了CANopen协议栈实现的源代码,如下图所示。
包括时间调度管理(timer.c),节点管理(对象字典访问objacces.c、状态机state.c),CANopen协议(服务数据对象sdo.c、过程数据对象pdo.c、同步对象sync.c、紧急事件emcy.c、心跳及节点守护lifegrd.c、自动波特率对象lss.c、网络管理nmtMaster.c和nmtSlave.c、设备配置文件dcf.c)等。
./include中包含了与./src 中相对应CANopen协议栈源文件的头文件,如下图所示。
上述的./src和./include中的文件在移植时不需要作修改。
上述的源文件和头文件的对应关系及功能描述如下表所示:
./src源文件 |
./include头文件 |
功能说明 |
备注 |
dcf.c |
dcf.h |
实现CANopen设备配置文件DCF访问机制 |
一般不需要使用 |
emcy.c |
emcy.h |
实现紧急报文协议 |
|
lifegrd.c |
lifegrd.h |
实现心跳报文、节点守护通信协议 |
|
lss.c |
lss.h |
实现LSS底层配置协议 |
用于设置节点号和波特率,一般不需要使用 |
nmtMaster.c |
nmtMaster.h |
实现主站NMT网络管理通信机制 |
|
nmtSlave.c |
nmtSlave.h |
实现从站NMT网络管理通信机制 |
|
objacces.c |
objacces.h |
实现对象字典访问 |
|
pdo.c |
pdo.h |
实现PDO通信协议 |
|
sdo.c |
sdo.h |
实现SDO通信协议 |
|
states.c |
states.h |
实现CANopen节点状态切换和调度 |
|
sync.c |
sync.h |
实现同步报文通信协议 |
|
timer.c |
timer.h |
实现CANopen协议所涉及的所有定时器的调度和管理 |
|
symbols.c |
|
linux系统导出各函数 |
只用于linux |
|
data.h |
定义了协议栈工作时所使用的数据结构 |
|
|
def.h |
定义与CANopen协议相关的宏定义 |
|
|
objdictdef.h |
定义对象字典相关的参数 |
|
|
sysdep.h |
定义字节大小端顺序的宏定义 |
|
|
can.h |
定义CANopen报文相关结构体、宏定义等 |
|
|
can_driver.h |
当在PC的操作系统中移植时需要 |
|
|
timers_driver.h |
当在PC的操作系统中移植时需要 |
|
2.3.2 移植所需的.h文件
除此之外,./include中还包含了移植到不同处理器或操作系统所需要的头文件,如下图所示。
针对不同的处理器或操作系统,各自的头文件内容都不一样,移植时需要针对不同的处理器或操作系统进行修改。针对单片机、ARM这类嵌入式处理器而言,其中,有3个头文件在移植时是必须使用的,其空模板位于./none文件夹中,如下图所示。
用户需要在applicfg.h中定义CanFestival库中各种整型、浮点型变量类型,以及调试信息输出函数;在timerfg.h定义CanFestival所需要使用的时间参数;在canfestival.h中定义CanFestival所需要使用的CAN接口函数的原型声明。
除此之外,用户还需要对CanFestival中的CANopen协议栈的运行参数进行配置。可以将这部分内容合并放到applicfg.h中,也可以单独新建1个.h文件来放置,并将该.h文件包含到applicfg.h中。建议采用单独的.h文件,例如./AT91、./AVR、./win32中的config.h。
2.3.3 底层驱动实例
./drivers中包含了针对不同处理器和操作系统在移植CanFestival时所使用的CAN接口和timer接口的驱动实例。如下图所示。其中包含了单片机、ARM、windows、linux等移植时使用的驱动程序,对于用户设计底层驱动有很好的参考价值。
2.3.4 应用实例
./example中包含了在不同处理器和操作系统中使用CanFestival进行主站、从站等实际应用开发的例子,如下图所示。这些实例对于如何使用CanFestival的API函数实现CANopen通信具有很大的参考价值。
2.3.5 对象字典生成工具
对象字典对于一个CANopen节点设备来说是非常重要的,它定义了节点的各种参数和内容。由于对象字典内容较多,./objdictgen中提供了使用python编写的脚本,便于用户通过图形化界面快速设计并生成所需要的对象字典。
2.3.6 说明文档
./doc包含了CanFestival的一些说明文档,个人感觉不是很详细。
3 CanFestival移植指南
3.1 概述
利用CanFestival库设计并实现PC、嵌入式控制器中的CANopen主/从站的示意图如下所示。图中绿色方框部分为CanFestival库。
从上图中可以看出,要利用CanFestival构建CANopen主站或者从站,除了CanFestival库本身,还需要设计目标接口(timer驱动、操作系统接口)、CAN接口驱动以及主/从节点的具体应用。这3部分也是CanFestival移植和使用过程中的主要工作。
对于嵌入式处理器,且在不使用嵌入式实时操作系统(μCOS 、FreeRTOS等等)进行裸机开发的条件下,移植CanFestival需要完成CAN接口驱动、timer驱动以及节点应用设计3部分内容。
其中,节点应用设计又包括:
CanFestival CANopen协议栈为timer驱动和CAN驱动分别提供了1个接口函数,TimeDispatch和canDispatch,如下图所示。
canDispatch函数负责将从CAN接口接收到的数据送入CANopen协议栈进行后续处理。TimeDispatch函数用于调度和管理CANopen协议栈中使用的各软件定时器,并执行与定时器相关的操作。
canDispatch和TimeDispatch在运行过程中都会调用CanFestival协议栈内部的函数。由于CanFestival内部的某些函数不支持可重入式的调用,就是指在未结束运行时不能被打断且被再次调用运行。因此,canDispatch和TimeDispatch两个函数的调用执行必须严格互斥且不能相互打断抢占,一方必须在另一方运行结束后才能开始运行。
在嵌入式处理器中进行裸机开发时,CAN数据接收和timer触发往往都使用中断的方式来实现,从而可将canDispatch函数放入CAN数据接收中断里调用,将TimeDispatch函数放入timer中断里调用。因此,需要保证两个中断互斥,不能互相嵌套或抢占。比较合理的方式就是将两个中断设置成相同的优先级。
3.2 CanFestival源代码移植
复制./src文件夹下除symbols.c以外的所有源文件,复制./include 文件夹中除can_driver.h和timers_driver.h以外的头文件。
因为,当time=0时表示没有启用Heartbeat timer,所以不需要设置Heartbeat timer的闹钟。增加对于time是否为0的判断,如下所示:
3.3 Timer驱动设计
在CANopen协议中存在很多需要进行计时或定时的地方。例如,周期性发送同步报文、心跳报文,SDO超时,PDO发送禁止时间,PDO事件定时器触发时间等等,所以1个CANopen节点必须能实现时间调度和管理的功能。
3.3.1 CanFestival timer管理调度原理
在CanFestival中时间调度是一个比较复杂的过程,需要设定一系列定时器来完成CANopen协议中所需要计时或定时的工作。CanFestival在timer.c中实现了多个软件定时器的调度和管理,只使用一个基准定时器(外部硬件或软件定时器)来模拟产生许多软件定时器。这样做的目的一方面是为了节省定时器资源,另一方面为了防止定时器中断过多造成互相抢占,导致协议栈内部某些函数被重入式调用从而造成数据异常。这也是CanFestival在移植过程中的一个重点和难点。
CanFestival中将软件定时器描述为Alarm,可以理解为闹钟,CanFestival协议栈建立了一个Alarm表,用于保存所有软件定时器。软件定时器的结构体定义如下图所示:
其中各个参数的含义如下表所示:
参数 |
说明 |
state |
软件定时器的状态 |
d |
CANopen协议栈结构体指针 |
id |
软件定时器的ID |
callback |
软件定时器计时数满的回调函数 |
val |
软件定时器剩余的计时数 |
interval |
周期型软件定时器的计时周期,对于非周期的恒为0 |
CanFestival在timer.h中声明了5个函数,其中有3个函数在timer.c中予以实现,分别为:
另外2个函数与具体的基准定时器的驱动相关,所以需要在用户的定时器驱动程序中予以实现,这是CanFestival库移植过程中timer接口驱动设计的一部分。这2个函数分别为:
3.3.1.1 SetAlarm函数
3.3.1.1.1 工作原理
SetAlarm函数在CanFestival协议栈中被用于分配和设置处于空闲状态的(TIMER_FREE)软件定时器,同时设置基准定时器的下次最近触发时间。SetAlarm函数执行的具体原理和流程如下:
遍历软件定时器列表,找到索引最小的处于空闲状态的(TIMER_FREE)的软件定时器。根据输入参数对其进行设置。然后判断所需要设置的软件定时器的触发时间是否大于基准定时器的最大计时范围,取两者的最小值。若该最小值小于当前已知的基准定时器的下一次剩余触发时间,表示当前的软件定时器会在更近的时间被触发,则调用SetTimer函数将该最小值作为基准定时器的下一次触发时间对其进行重置。若该最小值大于基准定时器的下一次触发时间,表示当前的软件定时器不会在更近的时间触发,则不调用SetTimer函数更新基准定时器触发时间。
3.3.1.1.2 重要参数释义
SetAlarm和TimeDispatch函数共享3个全局变量:
另外,SetAlarm函数中有几个重要的局部变量,其含义如下:
3.3.1.2 TimeDispatch函数
TimeDispatch是目标移植处理器的timer中断与CanFestival库的接口函数,在整个协议栈的时间管理和调度过程中至关重要。
3.3.1.2.1 工作原理
在上面已经提到,TimeDispatch负责对所有的处于计时状态(TIMER_ARMED)的软件定时器进行调度和管理。对计时完成需要触发软件定时器进行触发,并执行相应的回调函数。同时设置基准定时器的下次最近触发时间。TimeDispatch函数执行的具体原理和流程如下:
首先,遍历所有被使用的软件定时器,找出其中处于计时状态(TIMER_ARMED)的,依次判断其计时期是否已满或超出。
若计时期已满或超出,则需要对相应的软件定时器进行触发(TIMER_TRIG/ TIMER_TRIG_PERIOD)。同时,统计所有处于计时状态但不需要进行触发的软件定时器剩余触发时间的最小值(找到下一个最近触发的软件定时器),然后与基准定时器的最大计时范围作比较,取两者的最小值来重置外部基准定时器的触发时间,若无任何软件定时器则按照基准定时器的最大计时范围进行设置。使外部基准定时器按照该最短触发时间来触发中断来执行下一次的TimeDispatch函数。
最后,执行需要进行触发(TIMER_TRIG/ TIMER_TRIG_PERIOD)的软件定时器的回调函数,随后释放已经触发过的(TIMER_FREE)非周期触发(TIMER_TRIG)的软件定时器,保留周期性触发(TIMER_TRIG_PERIOD)的软件定时器。
3.3.1.2.2 重要参数释义
如3.2.1.1.2节所述TimeDispatch和SetAlarm函数共享3个全局变量:
另外,TimeDispatch函数中有几个重要的局部变量,其含义如下:
3.3.1.3 软件定时器调度原理
结合下图可对CanFestival中timer对软件定时器的调度和管理方式进行分析。
图中红线表示基准定时器固有的计时周期,代表基准定时器的最大计时范围。一般情况下,选用嵌入式处理器中的硬件定时器或是操作系统的软件定时器作为基准定时器默认都是周期型计数的触发方式。如果协议栈中没有任何软件定时器被使用时,则TimeDispatch函数将在基准定时器中断里被周期性的调用,就如上图中红线周期起伏。当协议栈中有软件定时器被使用时,则基准定时器的周期性触发方式将被改变。每个软件定时器都由SetAlarm函数创建,而SetAlarm函数则会根据软件定时器的触发时间调用SetTimer函数对基准定时器的下一次触发时间进行重置,例如上图中的t1、t3~t8。基准定时器触发后则调用TimeDispatch函数。
在上图中,设置了2个软件定时器A和B,其中B为周期触发型。满足A的触发时间tA>基准定时器的最大计时范围Tb>B的触发时间tB,并且t1=Tb,t3=t4=t5=t6+t7=t8=tB,t0、t3~t8< t1。由于tA>Tb,在这种情况下A的计时将会被分段进行。
假设在t0结束时,协议栈调用SetAlarm函数分配和设置软件定时器A。由于A的计时值的大于Tb,当基准定时器完成最大计时触发时,A也不会被触发,因而SetAlarm函数会调用SetTimer将基准定时器的下一个触发前时间设置为其原本的计时周期Tb(t1)。
因此,在t1结束时,基准定时器触发调用的TimeDispatch函数中也不会触发A,且同样会通过SetTimer将基准定时器的下一个触发时间设置为Tb。由于t2
t3结束时,基准定时器触发调用TimeDispatch触发B并执行B的回调函数,由于B为周期型,所以将基准定时器的触发时间设为tB。t4同理。在t5结束时,由于t6 3.3.2 CanFestival所使用的软件定时器 CanFestival里所使用的软件定时器有: 3.3.3 基准定时器设计 CanFestival中所有的软件定时器都由同一个基准定时器衍生模拟而来。因此,基准定时器的设计是CanFestival移植中timer驱动设计的一个重要环节。 timerscfg.h文件的设计与基准定时器相关。 3.3.3.1 timerscfg.h 在timerscfg.h中共需要定义4个与CanFestival中timer相关的宏定义。 这4个宏定义都与基准定时器相关。基准定时器的选取需要考虑TIMEVAL_MAX对应多长的实际时间,这需要根据具体应用进行权衡。如果软件定时器使用较少或软件定时器的触发时间较长。此时如果TIMEVAL_MAX值太小则会造成基准定时器无用的触发过多,TimeDispatch函数被频繁调用,额外占用系统资源。另外,需要考虑基准定时器的计时精度问题,US_TO_TIMEVAL(us) 宏将计时精度精确到1μs,由于定时器计数值都是整数,小于1的小数都会被认为是0。因此,用户需要确定基准定时器的每个计数单位对应多少μs或ms。这个也需要根据实际情况来确定。 3.3.3.2 ZYNQ定时器驱动设计 ZYNQ定时器驱动程序包含在timer_zynq.c,timer_zynq.h中。 在ZYNQ中采用PS的私有硬件定时器SCU timer,作为基准定时器。首先需要考虑的是SCU timer的计时范围,和最大计时值。由于SCU timer是采用递减计数方式,即从设置值逐渐递减直至0。 以米联客Miz701N-7010开发板为例,主频为650M,SCU timer的初始时钟频率为650/2=325M,分频系数设为24,则分频后的时钟为325/(24+1)=13M。计算公式如下图所示。 3.3.3.2.1 timerscfg.h设计 定义定时器计数时间的数据类型如下,为32位的无符号整型变量。 最大计数值设为0x00FFFFFF,这样SCU timer的1个计时周期为1.29s。当然,用户可以根据具体应用进行调整。但需要注意的是,由于计数器计数的数据类型为32bit,SCU timer计数器也为32bit。为了防止溢出timer.c中TimeDispatch和SetAlarm函数在计算计数值时产生溢出,TIMEVAL_MAX的最高位应该恒为0。 由于SCU timer则分频后的时钟为13M。这样1μs对应的计数值13,1ms对应的计数值为13000。 3.3.3.2.2 SetTimer函数实现 SetTimer函数具体实现如下: 通过XScuTimer_SetCounterReg函数直接将SCU timer的计数器值置为需要的计时数,计数器递减至0后则会触发SCU timer产生中断。 3.3.3.2.3 getElapsedTime函数实现 getElapsedTime函数具体实现如下: 因为SCU timer是递减计数,为了遵循CanFestival的递增的计数习惯,所以用TIMEVAL_MAX减去SCU timer的计数器值作为时刻的参考。 3.3.3.2.4 中断函数 SCU timer中断函数实现如下所示: 将CanFestival的软件定时器调度函数TimeDispatch函数放入该中断函数里被调用。 3.4 CAN接口驱动设计 3.4.1 CAN接口初始化 需要初始化CanFestival结构体变量指针。 3.4.2 CAN发送 CAN接口发送函数需要在canfestival.h中进行原型声明,且函数原型已由CanFestival固定,不能进行更改。如下: 其中,CAN帧对应的Message结构体在CanFestival中的can.h头文件中定义。如下所示。 3.4.3 CAN接收 将CanFestival中的canDispatch函数放入CAN接收中断中,将接收到的CAN帧送入CanFestival CANopen协议栈中进行处理。 3.5 CanFestival协议栈配置 CanFestival协议栈的配置参数在config.h中进行定义。 控制协议栈调试信息输出的宏定义: 若定义DEBUG_ERR_CONSOLE_ON则开启错误调试信息输出功能,注释则关闭;若定义DEBUG_WAR_CONSOLE_ON则开启警告调试信息输出功能,注释则关闭。 宏定义REPEAT_SDO_MAX_SIMULTANEOUS_TRANSFERS_TIMES、REPEAT_NMT_MAX_NODE_ID_TIMES、REPEAT_EMCY_MAX_ERRORS_TIMES都起到了用于重复某个参数的作用,如下图所示。 这些宏定义主要在data.h中被CANOPEN_NODE_DATA_INITIALIZER宏定义使用,用于初始化CanFestival结构体。 需要注意的是,REPEAT_SDO_MAX_SIMULTANEOUS_TRANSFERS_TIMES中重复的次数需要与SDO_MAX_SIMULTANEOUS_TRANSFER定义的数量一致。REPEAT_NMT_MAX_NODE_ID_TIMES中重复的次数需要与NMT_MAX_NODE_ID定义的数量一致。REPEAT_EMCY_MAX_ERRORS_TIMES中重复的次数需要与EMCY_MAX_ERRORS定义的数量一致。 3.6 applicfg.h文件设计 在applicfg.h文件中定义CanFestival中所需要使用并且适用于当前处理器的整型、浮点型变量类型,以及调试信息输出函数。 3.6.1 变量类型 在ZYNQ平台移植CanFestival所需的各种整型、浮点型变量类型定义如下所示: 3.6.2 调试信息输出 CanFestival中字符串信息输出函数宏定义为MSG,在ZYNQ平台可以使用xil_printf函数将信息通过串口输出,如下所示: 在CanFestival中调试信息函数又分为警告和错误两种,其中错误信息输出函数定义为MSG_ERR: 该函数使用宏定义DEBUG_ERR_CONSOLE_ON作为条件编译,用于控制是否开启错误信息输出功能。DEBUG_ERR_CONSOLE_ON在config.h中定义。 警告信息输出函数定义为MSG_WAR: 该函数使用宏定义DEBUG_WAR_CONSOLE_ON作为条件编译,用于控制是否开启错误信息输出功能。DEBUG_ WAR_CONSOLE_ON在config.h中定义。 3.6.3 其他参数 CAN接口指针,用于指向CAN接口底层驱动的结构体,定义如下: 在ZYNQ中,该指针指向PS端CAN接口驱动程序中的结构体XCanPs。 3.7 canfestival.h 提供CAN接口驱动CAN发送函数的原型声明。 3.8 回调函数设计 在CanFestival协议栈中,更多地方都需要使用回调函数,这些回调函数都需要用户自己进行设计。 在程序中需要设计的回调函数如下图所示。 这些回调函数可以用可不用,但必须进行定义,若不需要使用则定义为空函数即可。 3.9 对象字典设计 这个比较复杂,可参考官方的资料。 4 CanFestival协议栈API函数的使用 4.1 PDO发送 writeLocalDict函数,将映射到PDO的对象字典的内容进行改变,然后通过sendPDOevent函数将改变的对象字典的内容值通过PDO包发送出去。 例如,通过TPDO将函数调用如下所示: TPDO1和TPDO2的映射关系由ObjDict.c中的0x1A00,0x1A01决定,如下所示: TPDO1和TPDO2的参数设置由ObjDict.c中的0x1800,0x1801决定,如下所示。因为是做主站,所以TPDO和RPDO的COBID正好相反。 对象字典0x2100和0x2101的内容如下所示: 通过CANpro软件抓到的包如下: PDO1和PDO2发送1次数据的SDK的打印信息如下所示: 4.2 PDO接收 无API函数,接收到的PDO包直接根据相应内容刷新对象字典。 RPDO1的参数由ObjDict.c中的0x1400决定。如下图所示: RPDO1的映射关系由ObjDict.c中的0x1600决定: 对象字典0x2000的内容如下: 使用CANpro软件发送PDO包,如下图所示: SDK打印信息: 4.3 PDO请求 发送PDO远程帧通过函数sendPDOrequest实现,如下所示。函数里需要填入的是RPDO参数在对象字典的索引,例如0x1400。 调用该函数后,PS便向外发出PDO1远程帧,SDK打印信息如下所示: CANpro软件抓包结果如下所示: 此时通过CANpro软件发送PDO1包, SDK打印信息如下所示: PDO1包的内容直接更新到对象字典索引:0x2000,子索引:0x0中。 4.4 SDO下载 SDO下载通过writeNetworkDict或者writeNetworkDictCallBack函数实现。同时需要设置SDO的超时时间。例如,将config.h文件中的SDO超时时间设为8s,如下所示。 例如,调用writeNetworkDict发送SDO下载,如下所示: SDK打印信息如下: CANpro软件在8s之内发送SDO下载应答,设置如下: SDK打印信息如下: CANpro软件的抓包结果如下所示。 SDO下载应答超过8s没有接收到,则PS将会发送超时错误包,表示终止该次SDO传输。SDK打印信息如下所示: CANpro软件的抓包结果如下所示。 4.5 SDO上传 SDO上传通过readNetworkDict或者readNetworkDictCallback函数实现。例如,调用readNetworkDictCallback发送SDO上传包。 SDK打印信息如下所示: CANpro软件在8s之内发送SDO上传应答,设置如下: SDK打印信息如下所示: CANpro软件抓包结果如下: 若8s内未收到上传的SDO包,将产生SDO上传请求超时报错,则PS将会发送超时错误包,表示终止该次SDO传输。SDK打印信息如下所示: SDO上传请求超时报错。CANpro软件抓包结果如下: 4.6 心跳报文 作为主站可能需要接收从站发送而来的心跳报文,作为心跳报文的消费者。对象字典里0x1016是关于消费者心跳报文的设置。如下设置表示关闭心跳报文接收功能。 若将心跳报文的周期设为1500ms,如下所示。 此时,当PS超过1500ms没有接收到外部的心跳报文时,就会产生超时错误。 SDK打印信息如下所示: 设置CANpro软件以1200ms周期发送心跳报文: SDK打印信息如下所示: 4.7 网络管理报文 作为主站向从站发送网络管理包通过masterSendNMTstateChange函数实现,例如向编号为1的节点,发送5种状态管理包如下所示。 SDK打印信息如下所示: CANpro软件抓包结果如下所示: 4.8 同步报文发送 作为主站向从站发送SYNC同步报文通过startSYNC函数实现。 SYNC包发送的周期由对象字典中0x1006的内容决定,如下所示周期为1000ms。 SDK打印信息如下所示: CANpro软件抓包结果如下所示: 4.9 紧急事件报文 当从站发送错误或者故障,会向主站发送紧急报文。例如,使用CANpro发送一个紧急事件,如下所示: CANpro软件抓包结果如下: SDK打印信息如下:
void setTimer(TIMEVAL value)
{
XScuTimer_SetCounterReg((&TimerInstance)->Config.BaseAddr, value);
}
TIMEVAL getElapsedTime(void)
{
TIMEVAL timer =TIMEVAL_MAX - XScuTimer_GetCounterValue(&TimerInstance);
if(timer < last_time_set)
timer += TIMEVAL_MAX;
TIMEVAL elapsed_time = timer - last_time_set;
return elapsed_time;
}
void TimerIntrHandler(void *CallBackRef)
{
XScuTimer *TimerInstancePtr = (XScuTimer *) CallBackRef;
XScuTimer_ClearInterruptStatus(TimerInstancePtr);
last_time_set = TIMEVAL_MAX - XScuTimer_GetCounterValue(&TimerInstance);
TimeDispatch();
}