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的时钟。
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上拉。
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。
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,使能筛选器。
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通过串口打印到上位机。
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报文。
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(按键和串口的配置这里不做介绍),最后就是测试函数。
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的值。
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被按下时,主动发送报文。
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驱动,初始化主机,在循环中扫描按键,当按键被按下时发送报文。
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被按下时可以发送报文。
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");
}
}
}
|