STM32 ——CAN协议
物理层
CAN 收发器根据两根总线(CAN_High 和 CAN_Low)的电位差来判断总线电平。
总线电平分为显性电平和隐性电平两种。总线必须处于两种电平之一。总线上执行逻辑上的线“与”时,显性电平为“0”,隐性电平为“1”。
显性电平对应逻辑 0,CAN_H 和 CAN_L 之差为 2.5V 左右。而隐性电平对应逻辑 1,CAN_H 和 CAN_L 之差为 0V
图为CAN协议的物理层,CAN有ISO11898和ISO11519-2( 两个标准,我使用的的ISO11898,最高的波特率是1Mbps
CAN——帧种类
CAN 协议是通过以下 5 种类型的帧进行的:
数据帧
要控帧
错误帧
过载帧
帧间隔
另外,数据帧和遥控帧有标准格式和扩展格式两种格式。标准格式有 11 个位的标识符(ID),扩展格式有 29 个位的 ID。
各种帧的用途:
数据帧 :用于发送单元向接收单元传送数据的帧。
遥控帧 :用于接收单元向具有相同 ID 的发送单元请求数据的帧。
错误帧 :用于当检测出错误时向其它单元通知错误的帧。
过载帧 :用于接收单元通知其尚未做好接收准备的帧。
间隔帧 :用于将数据帧及遥控帧与前面的帧分离开来的帧。
其中数据帧由以下7个段构成:
(1) 帧起始。表示数据帧开始的段。
(2) 仲裁段。表示该帧优先级的段。
(3) 控制段。表示数据的字节数及保留位的段。
(4) 数据段。数据的内容,一帧可发送 0~8 个字节的数据。
(5) CRC 段。检查帧的传输错误的段。
(6) ACK 段。表示确认正常接收的段。
(7) 帧结束。表示数据帧结束的段。
接下来,我们再来看看 CAN 的位时序。
由发送单元在非同步的情况下发送的每秒钟的位数称为位速率。一个位可分为 4 段。
同步段(SS)
传播时间段(PTS)
相位缓冲段 1(PBS1)
相位缓冲段 2(PBS2)
这些段又由可称为 Time Quantum(以下称为 Tq)的最小时间单位构成。
1 位分为 4 个段,每个段又由若干个 Tq 构成,这称为位时序。
1 位由多少个 Tq 构成、每个段又由多少个 Tq 构成等,可以任意设定位时序。通过设定
位时序,多个单元可同时采样,也可任意设定采样点。
所谓采样点是读取总线电平,并将读到的电平作为位值的点。位置在 PBS1 结束处。
除了SS段固定是1Tq以外,SJW/PBS1/PBS2需要我们进行给值。
当检测到电平的跳变不在帧起始时,就会进行BS1段合BS2段的延长和缩短,其中延长和缩短的最大值就是SJW的值。
其中:
波特率=APB1Clock/(1+CAN_BS1+CAN_BS2)/CAN_Prescaler
例如我使用的是 SJW:1Tq BS1:5Tq BS2:3Tq 预分频系数:4.
所以波特率是36000000/(1+5+3)/4=1Mbps
但波特率的计算并不重要,在网上有大佬将要用的波特率做成表格的形式,只要找到表,将数值一一填进就行了。
过滤器
STM32的标识符过滤是一个比较复杂的东东,它的存在减少了CPU处理CAN通信的开销。
STM32 的过滤器组最多有 28 个(互联型),但是 STM32F103ZET6 只有 14 个(增强型),每个
滤波器组 x 由 2 个 32 为寄存器,CAN_FxR1 和 CAN_FxR2 组成。
STM32 每个过滤器组的位宽都可以独立配置,以满足应用程序的不同需求。根据位宽的不
同,每个过滤器组可提供:
● 1 个 32 位过滤器,包括:STDID[10:0]、EXTID[17:0]、IDE 和 RTR 位
● 2 个 16 位过滤器,包括:STDID[10:0]、IDE、RTR 和 EXTID[17:15]位
此外过滤器可配置为,屏蔽位模式和标识符列表模式。
为了过滤出一组标识符,应该设置过滤器组工作在屏蔽位模式。
为了过滤出一个标识符,应该设置过滤器组工作在标识符列表模式。
应用程序不用的过滤器组,应该保持在禁用状态。
在屏蔽位模式下,标识符寄存器和屏蔽寄存器一起,指定报文标识符的任何一位,应该按
照“必须匹配”或“不用关心”处理。
例如我使用32位的过滤器标识符屏蔽模式,把要接受的报文ID写成 CAN_F0R1=0xFFFF0000(STID+EXID+IDE+RTR+0),我想要我收到我报文是0xFFF****( * 号代表随意十六进制数额),也就是说 * 号是我不关心的内容,那么我就需要将屏蔽位设为CAN_F0R2=0XF0F0F0000,就代表着我收到的报文要是0xFFF****的形式,不然就过滤掉。
工作模式
can有4种工作模式——静默模式、回环(环回)模式、静默回环模式和正常模式。
其中我用的是回环模式来进行调试,当回环模式中我能收到我想要的报文和数据时,再改为正常模式就能正常的使用了。
回环模式简单来说就是自己发送,自己接受。不需要两块板子,自己就能进行CAN的发送和接受。
代码实现
在进行初步的学习过后,我们就开始尝试写写代码。
第一步先使能CAN和引脚的时钟和配置CAN_High和CAN_LOW引脚,并配置成上拉输入和复用输出。
GPIO_InitTypeDef gpio_init;
CAN_InitTypeDef can_init;
CAN_FilterInitTypeDef can_filterinit; // 过滤器的结构体
RCC_APB2PeriphClockCmd (RCC_APB2Periph_GPIOA,ENABLE );
RCC_APB1PeriphClockCmd (RCC_APB1Periph_CAN1 ,ENABLE );//使能 CAN1 时钟
gpio_init.GPIO_Pin=GPIO_Pin_11;
gpio_init.GPIO_Mode=GPIO_Mode_IPU ;
GPIO_Init(GPIOA,&gpio_init);
gpio_init.GPIO_Pin=GPIO_Pin_12;
gpio_init.GPIO_Mode=GPIO_Mode_AF_PP;
gpio_init.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA ,&gpio_init);
第二步配置工作模式和波特率
//can单元设置 配置工作模式 CAN_Mode_LoopBack CAN_Mode_Normal
can_init.CAN_Mode=CAN_Mode_LoopBack;//回环模式,测试完后改为普通模式
can_init.CAN_TTCM=DISABLE; //非时间触发通信模式
can_init.CAN_ABOM=ENABLE; //软件自动离线管理
can_init.CAN_AWUM=ENABLE; //睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位)
can_init.CAN_NART=DISABLE; //报文错误自动传送
can_init.CAN_RFLM=DISABLE; //报文不锁定,新的覆盖旧的
can_init.CAN_TXFP=DISABLE; //优先级由报文标识符决定
//设置波特率 1Mbps SYNC_SE段固定是1Tq
can_init.CAN_SJW =CAN_SJW_1tq;
can_init.CAN_BS1= CAN_BS1_5tq;
can_init.CAN_BS2= CAN_BS2_3tq;//bs1+bs2+se段=9Tq
can_init.CAN_Prescaler=4;//内部会自减1
CAN_Init(CAN1 ,&can_init);
需要注意的是 你F12进SJW的宏定义你会发现,他内部已经帮你自减好了。Tq的限制也能在里面看到。
第三步是过滤器
/* can过滤器 */
can_filterinit.CAN_FilterActivation=ENABLE; //是否使能过滤器
can_filterinit.CAN_FilterFIFOAssignment=CAN_FilterFIFO0;//数据存入FIFO0
can_filterinit.CAN_FilterNumber=0;//0到13 过滤器0
can_filterinit.CAN_FilterScale=CAN_FilterScale_32bit;//16位 32位
can_filterinit.CAN_FilterMode=CAN_FilterMode_IdMask;
/* 过滤的是扩展帧、扩展数据帧 */
can_filterinit.CAN_FilterIdHigh= ((PASS_ID<<3|CAN_ID_EXT|CAN_RTR_Data)&0xFFFF0000)>>16 ;//过滤的ID0x0000;//((PASS_ID<<3|CAN_ID_EXT|CAN_RTR_Data)&0xFFFF0000)>>16
can_filterinit.CAN_FilterIdLow= ((PASS_ID<<3|CAN_ID_EXT|CAN_RTR_Data)&0xFFFF) ;//0x0000;// ((PASS_ID<<3|CAN_ID_EXT|CAN_RTR_Data)&0xFFFF)
/*扩展数据帧的低16位,不需要位移 */
can_filterinit.CAN_FilterMaskIdHigh=0x0000;//32位MASK0x01E0;//
can_filterinit.CAN_FilterMaskIdLow=0x0000;//不正常的配置0x0000;//
CAN_FilterInit(&can_filterinit); //滤波器初始化
nvic_cfg (USB_LP_CAN1_RX0_IRQn,0,0);//接收邮箱0
/*中断函数错的话 会卡在启动文件*/
CAN_ITConfig (CAN1 ,CAN_IT_FMP0,ENABLE );//CAN_IT_FMP0接受邮箱0产生的中断,使能中断
需要的报文和上边的配置大同小异,就是先将想要的报文先左移3位,给IDE和RTR腾出位置,最后一位补0。我这里用的是32位的屏蔽位模式,下面的我把屏蔽位都为0,也就是说不管发送的报文是什么,我都接受,但这样是不符合标准的,不用过滤器要把他给屏蔽了。
最后就是报文的发送和接受了
CanRxMsg CAN_Rece_Data;//接受的数据存储在这里
CanTxMsg CAN_Tran_Data;//发送的数据
这里用到的是这两个结构体。
CAN_Tran_Data.StdId=PASS_ID ;
CAN_Tran_Data.ExtId=0;//发送的是扩展帧
CAN_Tran_Data.RTR=CAN_RTR_Data;//数据帧
CAN_Tran_Data.IDE=CAN_Id_Standard;
CAN_Tran_Data.DLC=2;//一个字节
CAN_Tran_Data.Data[0]=1+m;
CAN_Tran_Data.Data[1]=100+m;
/*CAN_Transmit()返回的是他的发送邮箱号 */
box=CAN_Transmit (CAN1 ,&CAN_Tran_Data);
while(CAN_TransmitStatus(CAN1 ,box)==CAN_TxStatus_Failed);
/* 等待报文的发送完成*/
Stdid表示我要发送的是标准帧的报文,其实扩展帧不用为0也行,用RTR位表示要发送的是标准帧(0)还是扩展帧(1),IDE位表示我要发送的是数据帧还是遥控帧。如果是遥控帧,那么只发送报文。DLC就是要发送的字节了,最高有8个字节。而Data【】就是要发送的内容了。
void USB_LP_CAN1_RX0_IRQHandler(void)
{
CAN_Receive (CAN1 ,CAN_FIFO0,&CAN_Rece_Data);//接受的数据位存储在这里
if(CAN_GetITStatus(CAN1,CAN_IT_FMP0 )==RESET)
{
for(j=0;j<8;j++)
{
printf("第%d个接受的数据是:%d/r/n ",j,CAN_Rece_Data.Data[j]);
}
}
}
实现的效果
但要注意的是,回环模式不会对IO口和波特率进行检测。
所以但回环能使用而正常模式不能时,可以检查一下自己的IO是否配置正确。CAN-H是否接CAN-H,引脚是否已经被使用,波特率是否正确等等进行一步步的排除,直到最后还是没找到问题的所在的话,可以怀疑下自己的板子是否有问题,找找例程烧烧看。
还有一个问题是,我用的是正点原子的精英版,默认是p6的跳帽接的是USB的,需要把跳帽接到CAN上。
附波特率参考图:
http://www.51hei.com/bbs/dpj-145867-1.html