引言
CanFestival作为一个开源的CANopen架构,实现了CANopen通信规范DS301的基本协议,并且在不断升级[1]。本文结合CANopen开源协议栈CanFestival,分析CANopen的协议结构、对象字典和通信对象,实现了嵌入式操作系统μC/OSII在Freescale微控制器MC9S12XF512上的移植;进一步修改CAN总线的底层驱动程序,将CANopen从站协议嵌入到μC/OSII系统中,实现了一种CANopen从站平台的方法。
1 CANopen协议通信机制分析
CANopen通信以CAN总线为基础,物理层遵循IS11898标准,数据链路层遵循CAN2.0标准,在应用层上CANopen协议定义了一系列规范来实现高层通信,主要分为CANopen通信规范和CANopen设备规范。CANopen的通信规范主要包括DS301,DS301定义CANopen设备之间的通信方式和行为规范,所有设备必须实现DS301。 CANopen的设备规范则定义了特定类型设备进入CAN总线的通信方式和功能。
CANopen设备模型分为三个部分[2,5],如图1所示。通信接口实现CANopen四种通信对象的功能,来实现不同数据的收发,包括服务数据对象(SDO)、过程数据对象(PDO)、网络管理对象(NMT)以及一些特殊功能对象(SYNC、TIME和EMCY)。
图1 CANopen设备模型
CANopen设备最核心的部分是对象字典,它是一个有序的对象组,每个对象采用一个16位的索引值来寻址,同时为允许访问数据结构中的单个元素,定义一个8位的子索引[78]。对象字典中的对象可以是输入输出信号、设备功能以及网络变量。在应用程序中,CANopen主从站进行连接,通过SDO对CANopen设备对象字典进行配置,同时通过PDO进行实时数据的通信。
2 CANopen平台构建
2.1 CAN总线通信硬件实现
CANopen平台的构建使用飞思卡尔的芯片MC9S12XF512。CAN总线收发器采用TJA1040。如图2所示,在CANH和CANL之间并联两个60 Ω的电阻并通过一个30 pF的小电容接地,从而滤除CAN总线上的高频干扰。MSCAN控制器模块上的TXD、RXD引脚与TJA1040的TXD、RXD相互连接,并通过TJA1040的CANH和CANL引脚接入到CAN总线网络中。
图2 CAN总线接口硬件电路
2.2 CANopen协议栈软件设计
整个CANopen协议栈的软件设计遵循模块化的原则,并依据协议栈的分层结构按照以下三个相对独立的部分来进行设计,包括操作系统及硬件驱动接口层、CANopen通信协议层以及设备行规应用层,如图3所示。
图3 CANopen协议栈软件结构
2.2.1 操作系统及硬件驱动接口层
底层提供操作系统与CAN总线驱动接口,操作系统接口使得CANopen协议栈可以嵌入到不同的系统中,实现任务的调度。CAN总线驱动接口处理CAN总线的数据帧信息,获得其中的有效数据进行存储并提供给上层通信对象使用。基于MC9S12XF512芯片的CAN总线驱动程序主要包括CAN总线控制器初始化函数、CAN总线报文发送程序、CAN总线报文接收程序以及中断函数的设置,其中初始化的内容包括CAN总线波特率、滤波器的设置,CAN总线控制器时钟的选择。CAN总线驱动中定义结构体Message来存储CAN总线的信息帧:
typedef struct{
SHORT_CAN cobid;
UNS8rtr;
UNS8len;
UNS8data[8];
}Message
其中 32位的cob_id表示CAN帧ID值,兼容CAN2.0A/B;rtr表示是否为远程帧,0为数据帧,1为远程帧;len代表数据的长度,对应CAN总线帧里DLC的数值;8字节数组data存储CAN总线有效数据。 CAN总线驱动接口将CAN总线底层驱动的收发程序封装在对应于CanFestival的f_can_send和f_can_receive函数当中,二者分别读写CAN总线收发程序的缓冲区,获取其中CAN总线数据cob_id、rtr、len、data的数值并存入到结构体Message中。通过这种方式,在运行高层协议时,CANopen通信对象不需要关心底层驱动的实现过程及执行环境,从而将CANopen通信协议层和底层驱动分隔开来,便于移植。
2.2.2 CANopen通信协议层
CANopen通信协议层实现协议4种通信对象的功能,每个设备独立拥有一个对象字典,它作为通信对象与上层应用程序的接口,存放着设备的配置参数和过程参数,设备之间配置参数的通信通过SDO以客户机/服务器的方式来实现。SDO提供基于请求和应答的点对点通信,允许大于8字节的数据采用分段或分块的方式传输。过程参数的通信通过PDO采用生产者/消费者方式来实现,数据从一个生产者传到一个或者多个消费者,且数据长度在8字节以内。PDO在对象字典中通过通信参数规定该PDO所使用的cobid、传输类型、抑制时间等参数,通过映射参数设置映射到PDO中的对象字典里的具体对象。时间标记、同步、应急等预定义的报文则可以提高网络利用率,NMT用来实现节点状态的转移和错误的控制。它采用主从结构,主节点对从节点进行状态管理和节点保护,每个CANopen从节点以状态机的方式接受主节点NMT对其状态的切换。
图4 报文处理
根据CANopen预定义连接集的描述,cobid的高4位是用来区分不同通信对象的功能码[2],报文接收到以后,通过判断高4位功能码来区分所接收到的通信对象,并通过一个指向函数的指针来调用相应的函数对报文进行处理,如图4所示,通过接收中断,CAN总线信息写入到接收缓冲区后调用f_can_receive函数,将CAN总线帧中的有效数据存入Message结构体,而后通过ReceiveHandler函数读取cobid高4位功能码,并根据不同的功能码最终切换至4种通信对象的报文处理函数。
2.2.3 对象字典的建立
协议栈软件的顶层主要包括对象字典的建立和访问以及其他一些应用包括节点反馈、网络的设置等。由于CANopen对象字典采用主索引和子索引结构,二者形成天然的二维数组结构,因此协议栈对象字典的实现是基于数组和数组的变型。主索引和子索引不同的数组下标在内存中就代表索引数据距离对象字典首地址的偏移量,通过该偏移量,便可以访问到对象字典中的数据。一个对象的信息应该包括索引、子索引、数据、数据类型和访问类型。这里通过定义两个结构体来表示主索引和子索引:
typedef struct td_subindex{
enum e_accessAttributebAccessType;
UNS8bDataType;
UNS8size;
void*pObject;
} subindex;
typedef struct td_indextable{
subindex*pSubindex;
UNS8bSubCount;
} indextable;
其中bAccessType表示数据的访问类型,分为只读、只写和可读写,bDataType表示数据类型,指针*pObject指向所包含的数据,通过结构体indextable来定义主索引,其中bSubCount表示此索引下子索引的数目。以对象字典中索引为1001H的错误寄存器为例,其对象字典定义为:
UNS8 OBJNAME = 0x0;
subindex Index1001[] ={
{ RO, uint8, sizeof(UNS8), (void*)&OBJNAME }
};
3 μC/OSII移植及CANopen的嵌入
3.1 μC/OSII在MC9S12XF512上的移植
为了使μC/OSII系统内核能在某一特定的微控制器上运行,必须对与处理器相关的代码进行修改。MC9S12XF512单片机有512 KB的片内FLASH和 32 KB RAM[3],可以在占有资源相对较少的条件下,运行操作系统的实时内核。集成的开发环境CodeWarrior可以进行程序的编译、下载以及调试,提高了工作效率。由于μC/OSII系统具有较强的可移植性,其移植工作主要是修改与处理器相关的代码,包括OS_CPU.H、OS_CPU_C.C和OS_CPU_A.ASM[4],因为CodeWarrior支持C语言与汇编语言的混合编程,所以可将后两个文件合成一个OS_CPU_C.C文件,以下从与移植相关的声明和函数两方面进行代码的修改。
3.1.1 修改与移植相关的声明
在OS_CPU.H中定义了与编译器有关的数据类型、开关中断宏、堆栈的生长方向以及任务切换宏。在MC9S12中堆栈是按字节进行操作,因此定义堆栈数据类型OS_STK为8位[6]。MC9S12处理器的堆栈是由高地址向低地址增长的,所以常量OS_STK_GROWTH设置为1。任务切换宏OS_TASK_SW()是在μC/OSII从低优先级任务切换到最高优先级任务时被调用的,OS_TASK_SW()通过模拟一次中断过程,在中断返回的时候进行任务切换。MC9S12提供了软中断源和陷阱中断源,这两个中断源都可以使得处理器的寄存器状态保存到将被挂起任务的堆栈中。由于芯片中不存在监控程序,所以利用MC9S12XF512提供的软中断指令SWI来定义OS_TASK_SW(),并将其中断服务程序的入口点指向OSCtxSw()。
3.1.2 修改与处理器相关的函数
(1) 任务切换函数
任务级任务切换函数OSCtxSw()的任务是将当前的CPU的状态保存到正在运行任务的堆栈中,然后将堆栈指针保存到任务控制块中,之后运行OSTaskSwHook()钩子函数,而后将任务控制变量OSTCBCur和OSPrioCur的值更改为即将要运行的任务的属性,从任务控制块中得到将要运行任务的堆栈指针赋给SP寄存器,最后运行中断返回指令。对于中断级任务切换函数,由于中断服务函数已经将CPU寄存器和中断发生前正运行的任务堆栈指针保存过,因此OSIntCtxSw()即是OSCtxSw()在调用钩子函数的后半部分。在MC9S12中,当中断发生时芯片会将CPU寄存器推入堆栈,但是不会包括页面管理寄存器,因此需要用汇编语言加入其入栈和出栈的操作:
ldaa PPAGE
psha//PPAGE入栈
pula
staa PPAGE//PPAGE出栈
(2) 任务堆栈初始化函数
任务堆栈初始化函数OSTaskStkInit()在创建任务的时候调用,用来在任务堆栈中按照一定顺序初始化任务最初的数据。在MC9S12XF512芯片中,根据数据存放的顺序,从高到低位对opt参数、PC寄存器、Y寄存器、X寄存器、D寄存器、CCR寄存器和PPAGE寄存器进行初始化,并最后返回堆栈指针所指向的地址。其中PC的值设置为任务的入口地址并存放两次,第一个值是建立扩展任务所需的; D的值初始化为参数pdata的值用来传递任务参数;PPAGE则是用来存储页面寄存器的值。
(3) 时钟节拍中断服务函数
和其他中断服务程序一样,首先OSTickISR()在被中断任务堆栈中保存CPU的值,然后调用OSIntEnter(),使得中断嵌套层数全局变量OSIntNesting加1。随后通过调用OSTimeTick(),遍历任务控制链表中的所有任务控制块,把各自用来存放延时时限的OSTCBDly变量减1,若其计数值为0,则表明该任务进入就绪状态,最后调用OSIntExit()标志着时钟节拍中断服务子程序的结束。本次移植中时钟节拍由硬件产生,必须设置好实时时钟的控制寄存器,使得MC9S12芯片在产生相应中断后,调用处理程序。实时时钟控制寄存器设置如下:
RTICTL=0x49;//每秒产生100次中断
CRGINT|=0x80;//使能中断
μC/OSII系统经过上述修改后在MC9S12芯片中运行起来。μC/OSII的引入使得系统开发的效率得以提高,编写程序时,只要将相关的功能封装成任务,并根据任务的轻重缓急设定优先级,启动多任务环境后,由μC/OSII来管理这些任务。
3.2 μC/OSII系统中嵌入CANopen
状态机作为CANopen协议整个操作流程的核心,其从站状态机定义了4个基本状态:初始化、预操作、操作和停止,如图3所示。执行过程中,从站上电复位后由初始化状态跳转到预操作状态,等待主站控制命令的到来;主站是状态机的控制者,从站待命后,主站发送命令切换各个从站的状态,从而在各个状态下完成操作。为将CANopen协议嵌入到μC/OSII中,需要将状态机封装成一个任务:
void CANopenTask(void *pdata){
e_nodeState lastState=unknown_state;
pdata=pdata;
while(1){
switch(getState()){
case Initialisation:
……
case Pre_operational:
……
case Operational:
……
case Stopped:
……
}
}
}
在主函数中,操作系统完成初始化、创建任务和启动任务的过程,通过OSTaskCreate()函数创建封装好的CANopen从站任务,最后通过OSStart()函数,启动操作系统,运行任务。
4 CANopen从站通信测试
实验中将μC/OSII上运行的CANopen从站与主节点连接,按照以下流程进行通信实验。在系统上电完成初始化后,从节点首先向主节点发送Bootup报文,通知主节点已进入Pre_operational状态。从节点处于Pre_operational状态时,接收来自主节点的SDO报文对其对象字典进行配置。当从节点接收到主节点的NMT报文后,从节点进入operational状态,此时主节点和从节点通过PDO进行实时数据的交互。在operational状态下,将从节点的TPDO1~3映射到主节点的RPDO1~3,以时间触发和事件触发方式分别进行PDO传送。通信实验表明平台能够完成DS301通信规范所定义的网络管理、PDO和SDO报文收发以及同步报文发送等功能。