摘要:本文对CANopen从站协议在stm32f103zet6单片机上的实现做了分析和说明。介绍了CANopen协议的SDO(服务数据对象),PDO(过程数据对象)等报文处理的工作和实现原理,实现了向主站发送数据及处理主站报文等功能。本文中,做了一个从站与一个主站进行数据交互的实现,实验表明CANopen从站协议可以正常使用在stm32f103zet6单片机上,并且可以与CANopen主站进行数据传输等交互操作。
关键词:CANopen;从站协议;服务数据对象;过程数据对象;单片机
1引言
图1 CANOPEN连接示意图
CANopen是一种架构在控制局域网路(Control Area Network, CAN)上的高层通讯协定,其协议在嵌入式系统及单片机上广泛使用,是工业控制常用到的一种现场总线。依靠CANopen协议集的支持,可以对不同的从站设备通过总线进行配置和系统重构。相信在不久的将来随着国内对CANopen协议的研究深入,CANopen协议会在各个领域有广泛的应用。
CANopen 是OSI模型中的网络层以上(包括网络层)的协定。CANopen 支持网络管理、设备监控及节点间的通讯,其中包括一个简易的传输层,可处理资料的传送。数据的传输和接收都基于CAN总线。如图1,通常多个从站设备靠CANopen网络传输数据给一个CANopen主站设备。
CANopen需要有对象字典,SDO(服务数据对象)处理功能,PDO(过程数据对象)处理功能,定时器,NMT(网络管理)处理功能等。本文着重介绍了CANopen协议的各个功能以及CANopen协议在单片机上stm32f103zet6的设计与实现。
2 CANopen对象字典介绍及设计
对象字典(od:object dictionary)是CANopen协议的核心。对象字典(od:object dictionary)是一个有序的对象组;每个对象采用一个16位的索引值来寻址,为了允许访问数据结构中的单个元素,同时定义了一个8位的子索引。通过接收主站发送的SDO(服务数据对象)报文,可以设置从站的对象字典,主要对象字典请参见表1。从站在做事件处理时通常会读取对象字典,根据对象字典里的数据进行事件处理。
譬如从站的1017索引是记录从站发送心跳包的时间间隔。当从站程序运行时并且从站是准备、停止、运行状态时,程序会查找1017索引的0号子索引里的数据进行处理。如果里面有数据的话(假设数据为2000),程序则会根据数据所设置的时间通过定时器判断来每2000毫秒发送心跳包。
表1 从站主要对象字典介绍
索引 | 作用 |
1017 | 设置从站发送心跳包的时间间隔。 |
1200 | 设置对主站节点ID的识别。 |
1800-1802 | 设置发送数据的ID及发送数据类型和发送哪个索引内的数据。 |
2000 | 存储要同步传输,异步传输的数据 |
6000,6001 | 存储要传输的错误代码 |
对象字典的元素定义如下:
索引:对象 16 位元的位址。数据的类型:一个代表对象的类型,可以是阵列、纪录或只是一个变量。类型:变量的类型。属性:提供此是否可读/可写的资料,有下列四种:可读/写、只读、唯写、只读常数。
以下是建立6003索引的代码案例,并且里面的数据是(2.78593)*100000的代码片段:
UNS32 AIdata=(2.78593)*100000;//要写入索引数据
static UNS8 highestSubIndex_6003 = 0; // 子索引为:1个(从0开始计数)
subindex Index6003[] =
{
{RW,uint32,sizeof(UNS32),(void*)&AIdata}
};//建立索引的读写属性,数据类型,数据大小,索引数据
3 NMT节点管理介绍及设计
NMT(网络管理, Network management)会定义(设备内部)从站的状态变更命令(如启动设备或停止设备)、侦测远端设备故障情形等。通常从站都由主机通过NMT报文来启动、停止和重启。每一个从站还必须配有一个单独的设备标识符,即从站节点ID。(从站节点ID一般在程序启动后,节点初始化状态时设置。)
节点可分为4种状态,初始化状态(Initialisatio),预操作状态(Pre_operational),操作状态(Operational)和停止状态(Stopped)。主站发送来的NMT格式一般为00 00 01 05,其中00 00是NMT功能码,代表主站发来的是NMT报文,01是要将从站节点设定为操作状态(参考表2),05是从站节点的ID。
表2 状态码表
状态 | 设定状态码 |
操作状态 | 01 (NMT_Start_Node) |
停止状态 | 02(NMT_Stop_Node) |
预操作状态 | 80(NMT_Enter_PreOperational) |
复位状态 | 81,,82(NMT_Reset_Node) |
当程序启动或者复位,一开始都是初始化状态,此状态会进行节点内部设置,如设置从站节点ID及一些索引数据的初始化,完成这些操作后,程序进入预操作状态,在预操作状态下,主站通常会通过SDO报文设置从站对象字典,包括心跳时间的设定,同步功能设置,数据存储映射设置等,当设置完毕后,会发送NMT节点管理报文将从站设为操作状态,此时从站节点如果已经设置了同步功能,当从站节点收到主站发送过来的同步报文后(通常是收到的报文是00 80 ,需要从站根据1005对象字典内的数据来确定可识别的同步报文ID,若数据为00 80,那么收到00 80的报文后发送从站状态数据)会返回目前从站的状态数据。
NMT主要涉及的代码为:
proceedNMTstateChange(Message *m)函数:
此函数主要功能是根据主站发送的设定状态码来设置从站节点的状态,代码片段如下:
if( ( (*m).data[1] == 0 ) || ( (*m).data[1] == bDeviceNodeId ) ){//判断报文是否是发给本从站
switch( (*m).data[0]){ //解析报文 case NMT_Start_Node:
if ( (nodeState == Pre_operational) || (nodeState == Stopped) )
nodeState = Operational;
break;
case NMT_Stop_Node:
if ( nodeState == Pre_operational ||nodeState == Operational )
nodeState = Stopped;
break;
case NMT_Enter_PreOperational:
if ( nodeState == Operational || nodeState == Stopped )
nodeState= Pre_operational;
break;
case NMT_Reset_Node:
nodeState = Initialisation;
break;
4 心跳功能介绍及设计
所谓“心跳”,指的是主站/从站之间的一种通信。采用心跳机制的好处在于,如果从站设备发生故障(如断电,重启等),会停止发送心跳报文,若主站一段时间内没有收到心跳报文,主站设备就会检测到从站发生了故障。(心跳报文:CANopen设备将根据主站给从站设置心跳时间间隔参数(索引1017h)的周期发送心跳报文。)
Stm32f103zet6单片机内置定时器用于计算心跳报文发送间隔,假设对象字典设置的数据报文发送间隔为2000毫秒,就可以将定时器设定为每500毫秒累计一次数值501,当累加数值大于2000时则发送心跳报文。心跳报文数据格式为07 05 7F。07 05根据协议计算可以知道是从站节点ID为5。(根据CANopen协议设定,心跳报文格式为”Communication Object Identifier”COB-ID+NODE-ID+1位状态码,心跳报文的COB-ID为0x700,0705等于0x700+0x05,则NODE-ID等于0x05),7F代表从站状态为预操作状态(详见表3)。当发送心跳报文后,数值清0,数值再次每500毫秒累加一次数值501,当累加到大于2000数值时则发送心跳报文,以此无限循环。
图2 心跳报文及定时器工作流程图
表3 心跳报文状态码对照表
描述 | 状态 |
从站为初始化状态 | 00 |
从站为预操作状态 | 7F |
从站为操作状态 | 05 |
从站为停止状态 | 04 |
心跳功能主要涉及的函数代码为:
定时器设置函数TIM3_Configuration:
此函数主要功能为每500毫秒触发一次定时器函数,代码片段:
void TIM3_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 9999;
TIM_TimeBaseStructure.TIM_Prescaler = 3599;//公式(1+3599)/72M*(1+9999)=0.5
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
触发的定时器函数则进行数值累加,代码片段:
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
/* Clear TIM3 update interrupt */
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
GPIOF->ODR ^= GPIO_Pin_7;//每次让连接PF7管脚的LED灯亮进行状态切换
a=a+501;//每次累加数值501
}
}
另外涉及的heartbeatMGR心跳函数:
此函数功能是对定时器累加的数值做判断,当累加的数值达到对象字典内的数值时则发送心跳报文,代码片段:
if ( should_time )//这个变量为1017索引内的值,通过读取对象字典函数getODentry()获取
{
if( ( a >= should_time ) )
{
msg.cob_id.w= bDeviceNodeId+ 0x700;
msg.len = (UNS8)0x01;
msg.rtr = 0;
msg.data[0] = nodeState; can_send(&msg);//发送心跳报文
a=0;//发送完毕后将定时器累加的数值清0
}
}
5 SDO服务数据对象介绍及设计
SDO服务数据对象可用来设置及读取远端节点的对象字典其中的资料。当主站要设置从站的对象字典的数据时,需要先发送一个SDO报文,当从站(stm32f103zet6)接收到报文后,会进行报文解析进而形成一个响应报文反馈给主站。参考以下流程图:
图3 从站处理主站SDO流程图
CANopen发送的SDO报文包括 11 位元的 ID、远端传输请求(RTR)位元及大小不超过8位元的资料。
表4报文介绍
描述 | 长度 |
功能码 | 4位元 |
节点ID | 7位元 |
传输请求 | 1位元 |
资料长度 | 4位元 |
资料内容 | 0-8字节 |
参考下图:
图3 处理写入报文示例
图4处理读取报文示例
从图3看出,从站先接收到主站发送过来的报文06 05 2B 17 00 D0 07 00 00;其中06 05是主站的设置05从站节点的ID(根据CANopen协议设定,主站发送的SDO报文格式为,COB-ID+NODE-ID+8位DATA,主站SDO报文的COB-ID为0x600,0605等于0x600+0x05,则NODE-ID等于0x05,即设定要设置的从站节点为5号节点),2B是代表设置的数据长度为2个字节(2F代表设置的数据长度为1个字节,27代表设置的数据长度为2个字节,23代表设置的数据长度为4个字节),17 10则代表要设置10 17索引,后面的00代表设置0号索引,D0 07 00 00代表设置的2个数据为D0 07。(因为只设置2个字节数据长度,所以后面的00 00 可以忽略。)如图3所示,当经过SDO报文处理后,程序会反馈一个SDO响应报文 05 85 60 17 00 00 00 00 00 00;其中05 85是代表此数据为5号从站返回的数据(从站发送SDO响应报文格式为COB-ID+NODE-ID+8位数据,从站SDO响应报文的COB-ID为0x580,0585等于0x580+0x05,则NODE-ID等于0x05,即此数据为5号从站返回的数据),60 代表响应写入成功命令字(若是80的话则是写入错误),17 10代表是对10 17索引的反馈,00 00 00 00代表反馈响应数据成功(若返回失败则返回错误码如:00 00 02 06,则代表是对象字典不存在,此错误代码可以通过代码去设置)。图3是处理设置报文(download)示例。图4是处理读取报文(upload)示例,如图主站发送的报文的第三个字节是40,代表请求读取数据命令字。17 10代表是读取1017索引的数据,从站的响应报文4B代表索引内的数据为2个字节(4F代表索引内的数据为1个字节,47代表索引内的数据为3个字节,43代表索引内的数据为4个字节),数据为D0 07。
SDO功能主要涉及的函数代码为:proceedSDO(message *m)函数:
此函数主要功能是处理SDO报文并将其封装成响应SDO报文,代码片段:
if((nodeState == Operational) || (nodeState == Pre_operational))
{
sdo.nodeId = (UNS8) (GET_NODE_ID((*m)));
MSG_WAR1(0x3A19, "Received SDO for nodeId : ", sdo.nodeId);
sdo.len = (*m).len;
if (sdo.len > 0)
sdo.body.SCS = m->data[0]; //封装SDO报文
for (i = 1 ; i < sdo.len ; i++)
sdo.body.data[i - 1] = m->data[i];
报文通过sendSDO(s_SDO sdo)函数发送:
此函数主要功能是将封装后的SDO报文发送,代码片段:
m.cob_id.w = *pwCobId;
m.rtr = DONNEES;
m.len = 8;
if (sdo.len > 0)
m.data[0] = sdo.body.SCS;
for (i = 1 ; i < sdo.len ; i++) {
m.data[i] = sdo.body.data[i - 1];
}
for (i = sdo.len ; i < 8; i++)m.data[i]=0;//将封装的报文转换成CAN协议帧发送
return can_send(&m);
6 PDO过程数据对象及设计
过程数据对象 (PDO) 协定可用来在节点之间交换即时的资料。PDO分为两种:传送用的TPDO及接收用的RPDO。一个节点的TPDO是将资料由此节点传输到其他节点,而RPDO则是接收由其他节点传输的资料。从站(stm32f103zet6)目前只设置了TPDO用于传输从站的数据及传输错误信息,TPDO和RPDO都可以通过代码实现添加和配置。
从站可通过一个TPDO传送最多 8 字节资料给一设备。一个PDO可以由对象字典中几个不同索引的资料组成,规划方式则是透过对象字典中对应PDO映射及PDO参数的索引。
PDO可以用同步或异步的方式传送:同步的PDO是从站接收到主站发送的SYNC讯息后触发,而异步的PDO是由从站内部达到一定条件或其他外部条件触发。同时从站报错事件也是由PDO发送。
图5 从站同步传输数据
图5是从站同步传输数据的报文,当从站收到主站发送的00 80报文时,则进入同步传输数据函数,从站会将之前主站设置好的索引内的数据发送给主站,01 81是由主站设置的同步传输标识ID,02 67 6F则是3个索引里的数值,传换成十进制数为157551。
同步传输涉及的函数代码为:
Proceedsync(Message *m),此函数功能是封装同步传输PDO报文,代码片段为:
if(index==0x2000&&subIndex==1)//将2000索引的1号子索引的数值赋给pdo的报文
{
*p=x;
}
if(index==0x2000&&subIndex==3)// //将2000索引的3号子索引的数值赋给pdo的报文 {
*p=y;
}
if(index==0x2000&&subIndex==4)// //将2000索引的4号子索引的数值赋给pdo的报文 {
*p=z;
}
if( objDict == OD_SUCCESSFUL ){
MSG_WAR1(0x3011, "Mapped data found size bytes : ", *pSize);
if (sizeData != *pSize) {
MSG_WAR1(0x2052, "Size of data different than (size in mapping / 8) : ", sizeData);
}
memcpy(&process_var.data[offset], pMappedAppObject, sizeData);//封装pdo报文
图6 从站发送错误事件
图6是一个从站发送错误事件的报文,当从站内部程序出现异常时(譬如取不到索引数据等),从站会进行报错,这里错误码和内容可以自己配置。01 83是由主站设置的发送错误事件标识ID,55 1F 00 00代表错误码,CD AB 00 00代表错误的数据。
发送错误事件涉及的函数代码为:
MSG_ERR(num, str, val) ,此函数功能是将错误码和错误数值存入由主站设置的特定索引并封装成PDO报文发送(封装由sendPDOevent函数完成),代码片段为:
MSG_ERR(0x1F55, "Error For Test", 0xABCD);//可发送图6的错误报文
MSG_ERR(num, str, val)//函数原型
{
if(nodeState == Operational ){
canopenErrNB=num;
canopenErrVAL=val; sendPDOevent(0,&canopenErrNB); //发送pdo报文
}
}
图7 从站异步传输数据
图7是一个异步传输数据报文,当内部达到一定条件是,会自动发送PDO报文,笔者将条件设定为如果时间在每小时56分时则发送PDO报文,01 82是主站设置给从站发送异步数据的标识ID,数据为当前时间戳,(0x86转换成十进制是56。)
异步传输数据所涉及函数也是sendPDOevent函数,此函数功能是将数据封装成PDO报文进行发送,代码片段:
if(minutes==56)
{
sendPDOevent( 0, &minutes );
}
7 CANOPEN从站协议在STM32F103ZET6单片机上的实现
7.1 硬件设置
首先需要对管脚进行设置,由原理图(见图8)可以看出CAN数据传输端CANTX接PB9管脚,CAN数据接收端CANRX接PB8管脚。需要编写gpio_config()函数进行配置,代码片段:
/* Configure CAN pin: RX */
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;//输入模式
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* Configure CAN pin: TX */
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//输出模式
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_PinRemapConfig(GPIO_Remap1_CAN1 , ENABLE);//重映射
图8 STM32F103ZET6 CAN原理图
硬件方面设置完毕后,对程序进行设计,程序主要分为两大部分。
7.2主程序介绍及设计
第一部分为main函数部分:程序运行时,进行初始化操作(设置节点ID、中断服务、管脚、发送邮箱、接收邮箱和波特率等),完成后进入对节点对状态进行检测和处理的无限循环程序:从站启动运行后会有四个状态,分别是初始化、预运行、运行、停 止。代码中分别用initialization、pre_operational、operational、stopped四种状态来表示,四个状态功能函数分别是 initialization(),preoperational(),operational(),stopped(),这些功能函数的主要作用是发送当前状态的心跳报文。用switch语句实现不同状态之间的切换及函数处理。
以下是主程序流程图:
图9 主程序流程图
代码框架大致为:
While(1)
{
switch(nodestatus){
case initialization:
initialization();
break;
case pre_operational:
pre_operational();
break;
case operational:
operational();
break;
case stopped:
stopped();
break;
}
}
7.3 中断服务程序介绍及设计
第二部分为中断服务程序:从节点上电启动后,中断等待来自主节点的管理报文,当报文到来的时候,基于中断的接收报文机制产生中断,中断由stm32的硬件进入void usb_lp_can_rx0_irqhandler(void)处理接收报文。can接收中断触发了CAN报文接收函数canreceive(&m)。然后通过不同功能码来实现报文的解析和处理。功能函数包括:proceednmtstatechange()函数,处理主站NMT报文,改变从节点状态;proceedsync()函数,用于接收同步报文;proceedpdo()函数,处理 pdo报文;proceedsdo()函数,处理sdo报文。通过功能函数的解析后来执行相应的处理。从而来实现对节点的控制。
以下是中断服务程序流程图:
图10 中断服务程序流程图
代码框架大致为:
CAN_Receive(CAN1,CAN_FIFO0,&RxMessage);
if(RxMessage.StdId==0x600+bDeviceNodeId)//检查是否是发给本站的报文
{
if((RxMessage.Data[0]==0x2F)||(RxMessage.Data[0]==0x2B)||(RxMessage.Data[0]==0x27)||(RxMessage.Data[0]==0x23)||(RxMessage.Data[0]==0x40)|| (RxMessage.Data[0]==0x60))//检查功能码
{
proceedSDO(0,&m);//处理SDO报文
}
if (RxMessage.StdId==0x0000)//检查功能码
{
proceedNMTstateChange(&m);//处理NMT报文
}
if (RxMessage.StdId==0x0080) 检查功能码
{
proceedSYNC(0,&m); //处理PDO同步报文
}
8 结束语
CANopen协议作为一种非常有竞争力标准总线,目前在欧洲已被广泛应用,在中国,随着工业时代的发展,CANopen协议也将会被广泛应用。本文介绍了CANopen在stm32f103zet6单片机上实现的思路及软件框架。实现了canopen301协议的所有功能,经过测试,已经可以成功的将多个单片机上的数据同时传输到主站设备。