2. CANOpen协议

CANOpen资料中有《can入门教程》、《CANOpen轻松入门》,了解CAN和CANOpen基础概念才能更好的理解程序。

2.1. 协议简介

CANopen是一种架构在控制局域网络(Controller Area Network, CAN)上的高层通信协议,包括通信子协议及设备子协议,常在嵌入式系统中使用,也是工业控制常用到的一种现场总线。

2.2. PDO

PDO 属于过程数据,即单向传输,无需接收节点回应CAN 报文来确认,从通讯术语上来说是属于“生产消费”模型。

2.3. SDO

SDO 属于服务数据,有指定被接收节点的地址(Node-ID),并且需要指定的接收节点回应 CAN 报文来确认已经接收,如果超时没有确认,则发送节点将会重新发送原报文。

2.4. 例程介绍

这里介绍的例程是基于F407霸天虎开发板,例程分SDO主机、SDO从机、PDO主机、PDO从机、及框架主机和框架从机,它们的CAN驱动配置是一样的,所以这里就单独列出来。

STM32F407具有CAN1和CAN2,根据开发板原理图这里使用CAN2,对应的引脚RX为B12,TX为B13。需要注意的是STM32的CAN1和CAN2共用的一个SRAM,CAN1是主bxCAN的,用于管理从bxCAN的之间的通信512字节的SRAM存储器, CAN2是从bxCAN的,没有直接访问SRAM存储器。即使只用到CAN2,也需要开启CAN1的时钟

bsp_can.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 #define CANx                       CAN2

 /* 注:CAN1和CAN2公用一个SRAM,所以即使只使用CAN2,也需要把CAN1的时钟初始化 */
 #define CAN_CLK_ENABLE()           __CAN1_CLK_ENABLE();__CAN2_CLK_ENABLE()
 #define CAN_RX_IRQ                 CAN2_RX0_IRQn
 #define CAN_RX_IRQHandler          CAN2_RX0_IRQHandler

 #define CAN_RX_PIN                 GPIO_PIN_12
 #define CAN_TX_PIN                 GPIO_PIN_13
 #define CAN_TX_GPIO_PORT           GPIOB
 #define CAN_RX_GPIO_PORT           GPIOB
 #define CAN_TX_GPIO_CLK_ENABLE()   __GPIOB_CLK_ENABLE()
 #define CAN_RX_GPIO_CLK_ENABLE()   __GPIOB_CLK_ENABLE()
 #define CAN_AF_PORT                GPIO_AF9_CAN2

CAN_GPIO_Config函数是can的引脚初始化函数,两个引脚的设置相同,模式设置为复用推挽输出,GPIO速度设置为高速,GPIO上拉。

CAN_GPIO_Config
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 static void CAN_GPIO_Config(void)
 {
     GPIO_InitTypeDef GPIO_InitStructure;

     /* 使能引脚时钟 */
     CAN_TX_GPIO_CLK_ENABLE();
     CAN_RX_GPIO_CLK_ENABLE();

     /* 配置CAN发送引脚 */
     GPIO_InitStructure.Pin = CAN_TX_PIN;   //
     GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;
     GPIO_InitStructure.Speed = GPIO_SPEED_FAST;
     GPIO_InitStructure.Pull  = GPIO_PULLUP;
     GPIO_InitStructure.Alternate =  CAN_AF_PORT;
     HAL_GPIO_Init(CAN_TX_GPIO_PORT, &GPIO_InitStructure);

     /* 配置CAN接收引脚 */
     GPIO_InitStructure.Pin = CAN_RX_PIN ;
     HAL_GPIO_Init(CAN_RX_GPIO_PORT, &GPIO_InitStructure);
 }

CAN_Mode_Config是CAN的模式配置,首先需要开启CAN的时钟,关闭时间触发通信模式使能,使能自动离线管理,使能自动唤醒模式, 禁止报文自动重传,失能接收FIFO锁定模式,失能发送FIFO优先级,设置为正常工作模式,重新同步跳跃宽度为1个时间单元, BTR-TS1 时间段1 占用了3个时间单元,BTR-TS2 时间段2 占用了3个时间单元,BTR-BRP为波特率分频器,定义了时间单元的时间长度 42/(1+3+3)/6=1 Mbps。

CAN_Mode_Config
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 static void CAN_Mode_Config(void)
 {

     /************************CAN通信参数设置**********************************/
     /* 使能CAN时钟 */
     CAN_CLK_ENABLE();       //注:CAN1和CAN2公用一个SRAM,所以即使只使用CAN2,也需要把CAN1的时钟初始化

     Can_Handle.Instance = CANx;
     Can_Handle.pTxMsg = &TxMessage;
     Can_Handle.pRxMsg = &RxMessage;
     /* CAN单元初始化 */
     Can_Handle.Init.TTCM=DISABLE;                      //MCR-TTCM  关闭时间触发通信模式使能
     Can_Handle.Init.ABOM=ENABLE;                       //MCR-ABOM  自动离线管理
     Can_Handle.Init.AWUM=ENABLE;                       //MCR-AWUM  使用自动唤醒模式
     Can_Handle.Init.NART=DISABLE;                      //MCR-NART  禁止报文自动重传   DISABLE-自动重传
     Can_Handle.Init.RFLM=DISABLE;                      //MCR-RFLM  接收FIFO 锁定模式  DISABLE-溢出时新报文会覆盖原有报文
     Can_Handle.Init.TXFP=DISABLE;                      //MCR-TXFP  发送FIFO优先级 DISABLE-优先级取决于报文标示符
     Can_Handle.Init.Mode = CAN_MODE_NORMAL;    //正常工作模式
     Can_Handle.Init.SJW=CAN_SJW_1TQ;                   //BTR-SJW 重新同步跳跃宽度 1个时间单元

     /* ss=1 bs1=3 bs2=3 位时间宽度为(1+3+3) 波特率即为时钟周期tq*(1+3+3)  */
     Can_Handle.Init.BS1=CAN_BS1_3TQ;                   //BTR-TS1 时间段1 占用了3个时间单元
     Can_Handle.Init.BS2=CAN_BS2_3TQ;                   //BTR-TS2 时间段2 占用了3个时间单元

     /* CAN Baudrate = 1 MBps (1MBps已为stm32的CAN最高速率) (CAN 时钟频率为 APB 1 = 42 MHz) */
     Can_Handle.Init.Prescaler =6;              ////BTR-BRP 波特率分频器  定义了时间单元的时间长度 42/(1+3+3)/6=1 Mbps
     HAL_CAN_Init(&Can_Handle);
 }

CAN_Filter_Config是can的滤波器配置,筛选器组定义为第14个筛选器,设置工作在掩码模式,筛选器位宽为单个32位,要筛选的ID设置为0,所有位不需要匹配(实际应用中这里的配置需要自行修改)。 筛选器被关联到FIFO0,使能筛选器。

CAN_Filter_Config
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 static void CAN_Filter_Config(void)
 {
     CAN_FilterConfTypeDef  CAN_FilterInitStructure;

     /*CAN筛选器初始化*/
     CAN_FilterInitStructure.FilterNumber=14;                                                //筛选器组14
     CAN_FilterInitStructure.FilterMode=CAN_FILTERMODE_IDMASK;       //工作在掩码模式
     CAN_FilterInitStructure.FilterScale=CAN_FILTERSCALE_32BIT;      //筛选器位宽为单个32位。
     /* 使能筛选器,按照标志的内容进行比对筛选,扩展ID不是如下的就抛弃掉,是的话,会存入FIFO0。 */

     CAN_FilterInitStructure.FilterIdHigh= 0;                 //要筛选的ID高位
     CAN_FilterInitStructure.FilterIdLow= 0;                  //要筛选的ID低位
     CAN_FilterInitStructure.FilterMaskIdHigh= 0;             //筛选器高16位不须匹配
     CAN_FilterInitStructure.FilterMaskIdLow= 0;              //筛选器低16位不须匹配
     CAN_FilterInitStructure.FilterFIFOAssignment=CAN_FILTER_FIFO0 ; //筛选器被关联到FIFO0
     CAN_FilterInitStructure.FilterActivation=ENABLE;                        //使能筛选器
     HAL_CAN_ConfigFilter(&Can_Handle,&CAN_FilterInitStructure);
 }

再是can中断回调函数,提取报文ID,判断是数据帧还是远程帧,提取数据长度,将数据保存至RxMSG.data通过串口打印到上位机。

HAL_CAN_RxCpltCallback
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 void HAL_CAN_RxCpltCallback(CAN_HandleTypeDef* hcan)
 {
     unsigned int i = 0;
     Message RxMSG ;

     /* 提取ID */
     RxMSG.cob_id = (uint16_t)(Can_Handle.pRxMsg->StdId);

     /* 判断是数据帧还是远程帧 */
     if( Can_Handle.pRxMsg->RTR == CAN_RTR_REMOTE )
     {
         RxMSG.rtr = 1;  //远程帧
     }
     else
     {
         RxMSG.rtr = 0; //数据帧
     }

     /* 提取数据长度 */
     RxMSG.len = Can_Handle.pRxMsg->DLC;
     for(i=0;i<RxMSG.len;i++)
     {
         /* 提取数据 */
         RxMSG.data[i] = Can_Handle.pRxMsg->Data[i];

         /* 将数据can接收到的数据发送至串口 */
         printf("Slave RxMSG.data[%d]=%x\n",i,RxMSG.data[i]);
     }
     printf("can slaver receive data\n");
     canDispatch(&TestSlave_Data, &(RxMSG));

     /* 准备中断接收 */
     HAL_CAN_Receive_IT(&Can_Handle, CAN_FIFO0);
 }

canSend函数是发送函数,用于发送CAN报文。

canSend
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 unsigned char canSend(CAN_PORT notused, Message *msg)
 {
     uint32_t        i;
     Can_Handle.pTxMsg->StdId = msg->cob_id;       //COB - ID
     if(msg->rtr)
         Can_Handle.pTxMsg->RTR = CAN_RTR_REMOTE;  //远程帧
     else
         Can_Handle.pTxMsg->RTR = CAN_RTR_DATA;    //数据帧

     Can_Handle.pTxMsg->IDE = CAN_ID_STD;          //标准帧
     Can_Handle.pTxMsg->DLC = msg->len;            //数据长度
     printf("msg->cob_id=%x\r\n",msg->cob_id);     //打印COB - ID号
     for(i = 0; i < msg->len; i++)
         Can_Handle.pTxMsg->Data[i] = msg->data[i];//装载数据

     if( HAL_CAN_Transmit( &Can_Handle, 0xFFFF)==HAL_OK)//发送数据
     {
         printf("Send successfully!\r\n"); //发送成功
         return CAN_SEND_OK;
     }
     else
     {
         printf("Send error!\r\n"); //发送失败
         return CAN_SEND_OK;
     }
 }

最后看一下main函数里做了什么,配置系统时钟为168 MHz,初始化HAL库,初始化按键,初始化串口1(按键和串口的配置这里不做介绍),最后就是测试函数。

main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 int main(void)
 {
     /* 配置系统时钟为168 MHz */
     SystemClock_Config();

     /* HAL库初始化 */
     HAL_Init();

     /* 初始化LED */
     LED_GPIO_Config();

     /* 按键初始化 */
     Key_GPIO_Config();

     /* 初始化调试串口,一般为串口1 */
     DEBUG_USART_Config();


     printf("\r\n 欢迎使用野火  STM32 F407 开发板。\r\n");
     printf("\r\n 野火F407 CANOpen_PDO主机例程\r\n");

     printf("\r\n 实验步骤:\r\n");

     printf("\r\n 1.使用导线连接好两个CAN讯设备\r\n");
     printf("\r\n 2.使用跳线帽连接好:5v --- C/4-5V \r\n");
     printf("\r\n 3.按下开发板的KEY1键,改变需要发送的数据 \r\n");
     printf("\r\n 5.本例中的can波特率为1MBps,为stm32的can最高速率。 \r\n");

     /*初始化can,在中断接收CAN数据包*/
     test();
 }

上面的can驱动程序及main函数在PDO、SDO、框架程序中是相同的,所以单独列出来不免重复介绍。

这里只列出了关键配置,完整的代码还需要参考对应的例程。

2.4.1. PDO主机

PDO是过程数据对象,它会一直发送数据到总线,不管从机是否接收到。

测试函数中初始化CAN驱动,再初始化CANOpen主机,在循环中通过按键改变DO1和DO2的值,DO1和DO2在TestMaster中定义,程序会不断发送DO1和DO2D的值。

test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
 void test(void)
 {
 /* CAN驱动初始化 */
 CAN_Config();

 /* 主机初始化 */
 test_master();

 /* Infinite loop*/
 while(1)
 {
     /* 按下按键,改变被映射的变量 */
     if(Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON)
     {
         DO1++;
         DO2++;
     }
 }
 }

2.4.2. PDO从机

PDO从机会不断接收总线上的数据,接收到数据后不会有接收应答,但是可以通过按下按键1发送数据。

初始化CAN驱动,准备中断接收,初始化从机,在循环中扫描按键,当按键1被按下时,主动发送报文。

test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 void test(void)
 {
 Message msg;

 /* CAN驱动初始化 */
 CAN_Config();

 /* 准备中断接收 */
 HAL_CAN_Receive_IT(&Can_Handle, CAN_FIFO0);

 /* 从机初始化 */
 test_slave();

 while(1)
 {
     if(Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON)
     {
         printf("***********************************************************\r\n");
         msg.cob_id = (UNS16)0x602;          // COB - ID号
         msg.len = (UNS8)0x08;               // 数据长度
         msg.rtr = (UNS8)0;                  // 0:数据帧 1:远程帧
         msg.data[0] = (UNS8)0x0;
         msg.data[1] = (UNS8)0x1;
         msg.data[2] = (UNS8)0x2;
         msg.data[3] = (UNS8)0x3;
         msg.data[4] = (UNS8)0x4;
         msg.data[5] = (UNS8)0x5;
         msg.data[6] = (UNS8)0x6;
         msg.data[7] = (UNS8)0x7;
         MSG_WAR(0x20000, "Producing heartbeat: ", 0x0);
         canSend(&TestSlave_Data.canHandle,&msg );
         printf("***********************************************************\r\n");
     }
 }
 }

2.4.3. SDO主机

SDO是服务数据对象,主机发送完数据后需要从机回复后才能再次发送。

初始化can驱动,初始化主机,在循环中扫描按键,当按键被按下时发送报文。

test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 void test(void)
 {
 Message msg;

 /* CAN驱动初始化 */
 CAN_Config();

 /* 主机初始化 */
 test_master();

 /* Infinite loop*/
 while(1)
 {
     if(Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON)
     {
         printf("***********************************************************\r\n");
         msg.cob_id = (UNS16)0x602;          // COB - ID号
         msg.len = (UNS8)0x08;               // 数据长度
         msg.rtr = (UNS8)0;                  // 0:数据帧 1:远程帧
         msg.data[0] = (UNS8)0x1;
         msg.data[1] = (UNS8)0x2;
         msg.data[2] = (UNS8)0x3;
         msg.data[3] = (UNS8)0x4;
         msg.data[4] = (UNS8)0x5;
         msg.data[5] = (UNS8)0x6;
         msg.data[6] = (UNS8)0x7;
         msg.data[7] = (UNS8)0x8;
         MSG_WAR(0x20000, "Producing heartbeat: ", 0x0);
         canSend(&TestMaster_Data.canHandle,&msg );
         printf("***********************************************************\r\n");
     }
 }
 }

2.4.4. SDO从机

SDO从机接收到SDO主机发送的消息后会给到回复,在test中同样需要初始化can驱动,启动从机在循环中扫描案按键, 当按键1被按下时可以发送报文。

test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 void test(void)
 {
 Message msg;

 /* 初始化CAN驱动 */
 CAN_Config();

 /* 准备接收数据 */
 HAL_CAN_Receive_IT(&Can_Handle, CAN_FIFO0);

 /* 启动从机 */
 test_slave();

 while(1)
 {
     if(Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON)
     {
         printf("***********************************************************\r\n");
         msg.cob_id = (UNS16)0x602;           // COB - ID号
         msg.len = (UNS8)0x08;                // 数据长度
         msg.rtr = (UNS8)0;                   // 0:数据帧 1:远程帧
         msg.data[0] = (UNS8)0x0;
         msg.data[1] = (UNS8)0x1;
         msg.data[2] = (UNS8)0x2;
         msg.data[3] = (UNS8)0x3;
         msg.data[4] = (UNS8)0x4;
         msg.data[5] = (UNS8)0x5;
         msg.data[6] = (UNS8)0x6;
         msg.data[7] = (UNS8)0x7;
         MSG_WAR(0x20000, "Producing heartbeat: ", 0x0);
         canSend(&TestSlave_Data.canHandle,&msg );
         printf("***********************************************************\r\n");
     }
 }
 }