STM32之CAN通信

单片机 专栏收录该内容
24 篇文章 12 订阅

23.1关于 CAN

23.1.1 CAN 电气特性与协议

控制器局域网(Controller Area Network,CAN),是由德国BOSCH(博世)公司开发,是目前国际上应用最为广泛的现场总线之一。其特点是可拓展性好,可承受大量数据的高速通信,高度稳定可靠,因此常应用于汽车电子领域、工业自动化、医疗设备等高要求环境。

CAN总线有两个ISO国际标准:ISO11519 和ISO11898。

  • ISO11519定义了通信速率为10~125Kbps的低速CAN通信标准,属于开环总线,传输速率为40Kbps时,总线长度可达1000米;
  • ISO11898定义了通信速率为125Kbps~1 Mbps的高速CAN通信标准,属于闭环总线,传输速率可达1Mbps,总线长度≤40米;

高速CAN主要应用在发动机、变速箱等对实时性、传输速度要求高的场景。低速CAN主要应用在车身控制系统等可靠性要求高的场景,低速CAN在断掉其任一导线后,仍可以继续接收数据,因此在汽车发生交通事故时,使用低速CAN能更大提高设备正常接收数据工作的可能性,提高安全性。

如图 23.1.1 所示,是低速CAN的拓扑结构图,如图 23.1.2 是高速CAN的拓扑结构图。低速CAN总线为开环,高速CAN总线为闭环,总线由CAN_H和CAN_L两根线组成,总线上可以挂多个节点设备。每个节点设备由CAN控制器和CAN收发器组成,CAN控制器通常作为外设集成在MPU/MCU上,而CAN收发器则需要外围添加芯片电路。

从两个网络拓扑结构可以看出,基于ISO11519标准的低速CAN,是一个“开环网络”,每根总线上个串联一个2.2KΩ的电阻;基于ISO11898标准的高速CAN,是一个“闭环网络”,总线的两端各需串联一个120Ω的电阻用于阻抗匹配,以减少回波反射。
在这里插入图片描述
在这里插入图片描述
类似RS485,CAN也使用差分信号传输数据。CAN总线使用CAN_H和CAN_L的电位差来表示数据电平。电位差分为显性电平和隐性电平,分别表示逻辑0和1。如图 23.1.3 所示,是低速CAN(ISO11519标准)的电平定义,如图 23.1.4 是高速CAN(ISO11898标准)的电平定义,两者物理层电气特性不一样,因此不能将它们连接在一起。可以看到当CAN_H和CAN_L电压相近,则表示隐性电平,对应逻辑1,当两个电压相差较大,表示显性电平,对应逻辑0。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
CAN是一种基于消息广播模式的串行通信总线,即在同一时刻网络上所有节点监测到的数据是一致的,各节点根据报文ID来甄别是否是发给自己的报文。

CAN总线以“帧”(Frame)的形式进行通信。CAN 总线协议规定了5种帧,分别是数据帧、远程帧、错误帧、超载帧以及帧间隔,其中数据帧最常用,表 23.1.2 是各个帧的用途。
在这里插入图片描述
数据帧由七段组成,如图 23.1.5 所示。数据帧又分为标准帧(CAN2.0A)和扩展帧(CAN2.0B),主要体现在在仲裁段和控制段上。
在这里插入图片描述

  • 帧起始(Start Of Frame-SOF):1bit,显性信号,表示数据帧(或远程帧)的开始;
  • 仲裁段(Arbitration Field):包括标识符位(Identifier field-ID)和远程发送请求位(Remote Transfer Request,RTR);
  • 标准帧的ID位是11位,即范围是0x000~0x7FF,而扩展帧的ID是11+18=29位;在CAN协议中,ID决定报文的优先级高低,也决定这拓扑结构的节点是否接收此ID的帧数据;
  • 远程发送请求位,用于区分该帧是数据帧还是远程帧,显性信号(0)代表数据帧(Data Frame),隐性信号(1)代表远程帧(Remote Frame);
  • 控制段(Control Field):标准帧中由扩展标识符位(Identifier Extension bit-IDE,1 bit)、保留位0(Reseved bit0-r0,1 bit)、数据长度编码位(Data Length Code-DLC,4 bits)组成;扩展帧用由两个保留位(Reseved bit,2 bit)、数据长度编码位(Data Length Code-DLC,4 bits)组成;
  • 数据段(Data Field):发送数据的内容,最多8个字节(64bit),它的实际长度会写到前面的数据长度编码位DLC里。
  • 循环校验段(CRC Field):包括循环校验序列(CRC Sequence)和界定符(Delimiter,DEL);循环校验序列用于校验传输是否正确;界定符用于表示循环校验序列是否结束;
  • 确认段(ACK Field):包括确认位(ACK SLOT)和界定符(Delimiter,DEL);确认位在节点收到正确的CRC序列时,发送端的ACK位被置位;界定符表示确认是否正常接收;
  • 帧结束(End of Frame-EOF):7位长度,隐性信号,表示帧的结束;

当CAN总线网络中有多个CAN节点设备时,某一CAN设备发出数据帧,总线上所有设备(无过滤时)都获取该数据帧中仲裁段中的ID,如果是自己关注ID的数据,则获取数据段的内容,完成数据的传输。

23.1.2 CAN 控制器

STM32F103系列的CAN控制器(Basic Extended CAN,bxCAN),支持CAN 2.0A和CAN 2.0B Active版本协议。CAN 2.0A只能处理标准数据帧,扩展帧的内容会识别为错误;CAN 2.0B Active可以处理标准数据帧和扩展数据帧;CAN 2.0B Passive只能处理标准数据帧,扩展帧的内容会忽略。

STM32F103系列只有一个CAN控制器,STM32F105/STM32F107互联型有两个CAN控制器,互联型内部CAN控制器结构如图 23.1.6 所示,⑤是CAN2,STM32F103系列没有,先忽略。
在这里插入图片描述
①CAN1内核:包含各种控制、状态、配置寄存器。其中比较重要的是主控制寄存器(CAN_MCR)和位时序寄存器(CAN_BTR)。主控制寄存器主要控制CAN的工作模式,在后面设置CAN协议初始化时,实现对该寄存器的修改。位时序寄存器主要涉及CAN的工作速率,由于CAN是异步信号,同串口类似,需要收发双方提前统一通信速率。除此之外,为保证通信稳定,CAN采用“位同步”机制,实现对电平的正确采样。传输中的每位数据由四段组成:同步段(Synchronization Segment,SS)、传播时间段(Propagation TimeSegment,PTS)、相位缓冲段1(Phase Buffer Segment 1,PBS1)和相位缓冲段2(Phase Buffer Segment 2, PBS2)。每段又由多个位时序(Time Quantum,Tq)组成,如图 23.1.6 所示,为各段组成示意图。
在这里插入图片描述
假设CAN对应逻辑电平持续的时间为9Tq,即一位数据持续的时间为9Tq。其中SS段长度为1Tq(只能为1Tq),PTS段长度为2Tq(范围为18Tq),PSB1段长度为3Tq(范围为18Tq),PSB2段长度为3Tq(范围为2~8Tq)。采样点在PSB1和PSB2之间,调整各段的长度,即可对采样点位置进行调整,实现补偿准确采样。

如图 23.1.8 所示,为STM32F103系列的CAN控制器位时序,和标准CAN协议的位时序略有不同。STM32只有三段,同步段长度为1Tq(只能为1Tq),标准CAN协议中的PTS段和PSB1合并为位段1(范围为1-16Tq),标准CAN协议中的PSB2段对应位段2(范围为1-8Tq)。
在这里插入图片描述
当知道CAN控制器的工作时钟频率、tBS1和tBS2的长度时,即可计算出CAN传输的波特率,关系如下:
在这里插入图片描述

  • Tq=(BRP[9:0]+1) x tPCLK,BRP[9:0]为Tq长度,tPCLK为APB时钟周期;
  • tBS1=Tq x (TS1[3:0] + 1),TS1[3:0]位于CAN_BTR寄存器中;
  • tBS2=Tq x (TS2[2:0] + 1),TS2[2:0]位于CAN_BTR寄存器中;由图 6.1.2 可知,bxCAN挂在APB1总线上,当系统时钟为最高72MHz时,APB1时钟最高为36MHz。此时若BRP[9:0]+1为8,TS1[3:0] + 1为6,TS2[2:0] + 1为2,则波特率为:
    在这里插入图片描述

② 发送邮箱:STM32F103的CAN控制器有三个发送邮箱,可最多缓存三个待发送的报文。由传输调度负责决定邮箱报文的发送顺序。

③接收FIFO:STM32F103的CAN控制器有两个个接收FIFO来存储传入的数据,每个FIFO由三个邮箱存储三个接收到报文。

④:验收筛选器:STM32F105/STM32F107互联型有28个筛选器,STM32F103系列只有14个筛选器(编号0~13)。前面介绍CAN协议介绍到,在CAN总线网络中,总线上的所有设备都获取总线数据帧中ID,如果是自己关注的ID,则继续获取数据段的内容。当总线上报文过多时,每个CAN设备将频繁获取报文,消耗比较大。因此,提供筛选器实现选择性的获取报文,降低系统负担。

每个筛选器组由两个32位寄存器CAN_FxR1和CAN_FxR2组成。根据不同的实际需求,筛选器支持设置筛选范围和筛选模式。

筛选范围可设置为32位和16位,两种方式筛选的范围有所差异:

  • 32位方式:筛选报文的STDID[10:0]、EXTID[17:0]、IDE、RTR;
  • 16位方式:筛选报文的STDID[10:0]、EXTID[17:15]、IDE、RTR;筛选模式可设置为列表模式和掩码模式,前者常用于筛选单个标识符,后者常用于筛选单组标识符:
  • 列表模式:此时两个寄存器都作为标识符寄存器,这两个标识符寄存器组成一个表,只有在此列表中的ID,才能通过筛选器,存入FIFO;
  • 掩码模式:此时两个寄存器作为标识符寄存器和掩码寄存器,根据掩码寄存器指定的哪些位与标识符寄存器匹配的ID,才能通过筛选器,存入FIFO;

举个例子,如表 23.1.3 所示,ID为0xF,掩码为0x7FC。掩码位为1表示必须与ID一致,掩码位为0表示可不与ID一致,因此结果bit[1:0]为任意值,其它都需要与ID一致,则最后结果为11XX,即0xC~0xF之间的ID都可经过筛选器存入FIFO,其它则无法通过;
在这里插入图片描述
如图 23.1.9 所示,通过设置筛选范围和筛选模式进行组合,每个筛选器有四种情况。

①FSCx=1,FBMx=0:处于32位掩码模式,此时两个32位寄存器CAN_FxR1和CAN_FxR2,一个存放ID,一个存放掩码;

②FSCx=1,FBMx=1:处于32位列表模式,此时两个32位寄存器CAN_FxR1和CAN_FxR2,两个都存放ID,组成列表;

③FSCx=0,FBMx=0:处于16位掩码模式,此时两个32位寄存器CAN_FxR1和CAN_FxR2,它们各自低16位存放ID,高16位存放掩码;

②FSCx=0,FBMx=1:处于16位列表模式,此时两个32位寄存器CAN_FxR1和CAN_FxR2,它们各自低16位和高16位都存放ID,组成列表;

举个例子,假设CAN总线上有ID为0至99的100个报文,现在只需要ID为0-5的报文,筛选器该如何设置?首先设置筛选器组0处于32位掩码模式,ID为0x0,掩码为0x7FC,结果将筛选出0x0-0x3。接着设置筛选器组1处于32位列表模式,列表两个ID分别设为0x04和0x05。最后ID为0x0~0x05的报文将通过筛
在这里插入图片描述
关于CAN的基础介绍就到这里,读者重点理解CAN电气特性、数据帧结构、STM32的CAN控制器波特率和筛选器。

23.2 硬件设计

如图 23.2.1 为开发板CAN部分的原理图。U17为CAN收发器TJA1042芯片,该芯片将MCU的CAN控制器的逻辑电平转换成差分信号,发送到CAN总线中。

U17的1脚接MCU的CAN发送引脚(PB9),2脚接MCU的CAN接收引脚(PB8), 7脚、8脚为CAN输出引脚,上面挂了一个120欧的终端电阻,工作在高速CAN模式。
在这里插入图片描述
另外,CAN和RS485都是半双工的差分信号,需要两个设备连接测试。百问网制作了一个CAN/RS485互转模块,可以直接连接到100ASK系列开发板上,实现RS485的CAN的透传,同时验证、学习两个接口,该模块外形如图 23.2.2 所示。
在这里插入图片描述
该模块左边是RS485接口(波特率9600bps,无校验,8位数据位,1位停止位),右边是CAN接口(波特率500Kbps,屏蔽位模式,允许所有帧接收),中间是转换芯片,实现协议转换。由于CAN协议中包含有ID, 而RS485不存在ID。因此,当RS485转CAN时,模块会自动加上0x00的ID,当CAN转RS485时,RS485只会收到数据部分,扔掉ID部分。

23.3 软件设计

23.3.1 软件设计思路

实验目的:本实验通过RS485_CAN互转模块,实现RS485和CAN互传数据,让读者熟悉如何使用CAN。开发板的RS485首先发送数据,经过RS485_CAN互转模块变为CAN信号,传入开发板CAN接口;开发板CAN接口接到数据后,CAN再发送数据,经过RS485_CAN互转模块变为RS485信号,传入开发板R485接口。

  1. 初始化CAN协议相关参数和筛选器:设置预分频、位段长度等实现需要的500Kbps波特率;
  2. 初始化CAN硬件相关参数:CAN时钟使能、GPIO端口时钟使能、引脚重映射、中断优先级等;
  3. 使用HAL提供的CAN收发函数,收发数据;
  4. 主函数编写控制逻辑:按下按键KEY1(KEY_U),RS485发送数据,经过RS485_CAN互转模块传入CAN接口并打印,随后CAN接口发送数据,经过RS485_CAN互转模块传入RS485接口并打印;本实验配套代码位于“5_程序源码\15_通信—CAN\”。

23.3.2 软件设计讲解

  1. 初始化CAN
    CAN的初始化,包含两部分:协议部分和硬件部分。
    协议部分初始化如代码段 23.3.1 所示。

代码段 23.3.1 CAN 协议初始化(driver_can.c)

/*
* 函数名:void CAN_Init(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:初始化 CAN1
*/
void CAN_Init(void) {
CAN_FilterTypeDef sFilterConfig;
hcan.Instance = CAN1;
/* 配置 CAN 的基本参数 */
hcan.Init.Prescaler = 8; // 预分频,范围(1~1024)
hcan.Init.Mode = CAN_MODE_NORMAL; // 正常工作模式
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; // 再次同步跳跃宽度为 1Tq
hcan.Init.TimeSeg1 = CAN_BS1_6TQ; // 位段 1(BS1)的长度为 6Tq
hcan.Init.TimeSeg2 = CAN_BS2_2TQ; // 位段 2(BS2)的长度为 2Tq
hcan.Init.TimeTriggeredMode = DISABLE; // 禁止时间触发通信模式
hcan.Init.AutoBusOff = DISABLE; // 禁止总线自动关闭
hcan.Init.AutoWakeUp = DISABLE; // 禁止自动唤醒
hcan.Init.AutoRetransmission = ENABLE; // 使能自动重传
hcan.Init.ReceiveFifoLocked = DISABLE; // 禁止接收 FIFO 锁定
hcan.Init.TransmitFifoPriority = DISABLE; // 禁止传输 FIFO 优先级
if (HAL_CAN_Init(&hcan) != HAL_OK)
{
Error_Handler(); }
/* 配置 CAN 的筛选器,此处全部接收,不做过滤 */
sFilterConfig.FilterBank = 0; // 选择筛选器组 0
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 设置为掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 32 位长度
sFilterConfig.FilterIdHigh = 0x0000; // ID 高字节
sFilterConfig.FilterIdLow = 0x0000; // ID 低字节
sFilterConfig.FilterMaskIdHigh = 0x0000; // 掩码高字节
sFilterConfig.FilterMaskIdLow = 0x0000; // 掩码低字节
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; // 过滤器关联 FIFO
sFilterConfig.FilterActivation = ENABLE; // 使能筛选器
if(HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
{
Error_Handler(); }
// 启用 CAN
if(HAL_CAN_Start(&hcan) != HAL_OK)
{
Error_Handler(); }
// 使能 CAN 接收 FIFO0 中断
if(HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
Error_Handler(); } }

12~25行:设置CAN协议参数;

  • 12行:设置哪一个CAN控制器,STM32F103ZET6只有一个CAN控制器;
  • 15行:设置时钟的预分频,与后面的位段时间长度组合,实现需要的500Kbps,这里先设置为8分频;
  • 16行:设置CAN工作模式,CAN控制有四种模式:正常模式、静默模式、回环模式及静默回环模式,通常设置为正常模式即可;
  • 17行:设置再次同步补偿宽度,因时钟频率偏差、传送延迟等,各单元有同步误差,这里设置补偿此误差的最大值,范围为1~4Tq;
  • 18行:设置位段1(BS1)的长度为6Tq;
  • 19行:设置位段2(BS2)的长度为2Tq,由前面的波特率计算公式可得:36M/(1+6+2)/8 = 500Kbps;
  • 20行:设置间触发通信模式,禁止时间触发通信模式;
  • 21行:禁止总线自动关闭,控制CAN在退出总线关闭状态时的行为;
  • 22行:禁止自动唤醒,控制CAN在眠模式下接收到消息时的行为;
  • 23行:开启自动重传,CAN将自动重发消息,直到CAN消息发送成功;关闭后,无论成功、错误或仲裁丢失,都只发送一次;
  • 24行:禁止接收FIFO锁定,当接收FIFO装满后,下一条传入消息将覆盖前一条消息;使能后,接收FIFO装满后,下一条传入消息将被丢弃;
  • 25行:禁止传输FIFO优先级,则优先级由消息标识符决定;使能后,由请求顺序(时间顺序)决定;

27~30行:初始化前面设置的CAN参数,同时会调用CAN硬件相关初始化函数“HAL_CAN_MspInit()”;

32~41行:设置CAN筛选器;

  • 33行:设置哪一个CAN筛选器组,设置设置筛选器组0,STM32F103ZET6共有14个筛选器组;
  • 34行:“CAN_FILTERMODE_IDMASK”设置为掩码模式,“CAN_FILTERMODE_IDLIST”设置为列表模式;
  • 35行:设置筛选范围可设置为32位;
  • 36~37行:设置ID的高低字节,这里设置ID为0;
  • 38~39行:设置掩码的高低字节,这里设置掩码为0,则没有做任何过滤;
  • 40行:设置本筛选器的消息存储在哪个FIFO(接收FIFO共有两个);
  • 41行:使能本筛选器;

43~46行:配置前面设置的筛选器;
48~52行:启动CAN;
54~58行:使能CAN接收FIFO0的中断;注意这里使能的是FIFO0,需要和前面设置筛选器的FIFO保持一致;这里可以设置三种类型FIFO中断,分别为“CAN_IT_RX_FIFO0_MSG_PENDING”FIFO接收到数据就
产生中断、“CAN_IT_RX_FIFO0_FULL”FIFO接收满后产生中断“CAN_IT_RX_FIFO0_OVERRUN”FIFO溢出产生中断;

前面“HAL_CAN_Init()”会调用CAN硬件相关初始化函数“HAL_CAN_MspInit()”,该函数需要我们自己完善,如代码段 23.3.2 所示。

代码段 23.3.2 CAN 硬件初始化(driver_can.c)

/*
* 函数名:void HAL_CAN_MspInit(CAN_HandleTypeDef *hcanhandle)
* 输入参数:hcanhandle-CAN 句柄
* 输出参数:无
* 返回值:无
* 函数作用:初始化 CAN 硬件相关
*/
void HAL_CAN_MspInit(CAN_HandleTypeDef *hcanhandle)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_AFIO_CLK_ENABLE(); // 重映射时钟使能
__HAL_AFIO_REMAP_CAN1_2(); // 重映射
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); //PB9 CAN TX
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); //PB8 CAN RX
// HAL_Init()里默认优先级分组 4
HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 1, 0); // 设置 CAN 接收中断的优先级和使能
HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);
HAL_NVIC_SetPriority(CAN1_TX_IRQn, 1, 0); // 设置 CAN 发送中断的优先级和使能
HAL_NVIC_EnableIRQ(CAN1_TX_IRQn); }

需要注意,PB8、PB9如果用于CAN引脚,不是引脚典型复用,而是需要引脚复用重映射,如图 23.3.1所示为《数据手册》部分引脚描述截图。因此代码段 23.3.2 中14~15行需要先使能重映射时钟,再重映射CAN功能。
在这里插入图片描述

  1. 封装CAN发送函数和接收回调函数
    初始化完CAN后,便可以调用HAL库提供的“HAL_CAN_AddTxMessage()”将报文放入发送信箱,供CAN控制器发送。在这之前,需要准备好报文,如代码段 23.3.3 所示。

代码段 23.3.3 CAN 发送函数(driver_can.c)

/*
* 函数名:void CAN_Transmit(uint16_t ID, uint8_t *pdata, uint8_t length)
* 输入参数:ID -> CAN 发送报文的 ID
* pdata -> 发送报文的首地址
* length-> 发送报文的个数,最大 8 个字节
* 输出参数:无
* 返回值:无
* 函数作用:CAN1 发送函数
*/
void CAN_Transmit(uint16_t ID, uint8_t *pdata, uint8_t length)
{
uint32_t tx_mailbox = 0;
if(length>8) {
return ; }
TxHeader.StdId = ID; // 标准标识符
TxHeader.ExtId = 0; // 扩展标识符
TxHeader.IDE = CAN_ID_STD; // 帧模式(标准帧或扩展帧)
TxHeader.RTR = CAN_RTR_DATA; // 帧类型(数据帧或远程帧)
TxHeader.DLC = length; // 数据长度
TxHeader.TransmitGlobalTime = DISABLE; // 不发送时间标记
if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, pdata, &tx_mailbox) != HAL_OK)
{
Error_Handler(); } }

12~23行:准备发送报文;

  1. 初始化CAN
    CAN的初始化,包含两部分:协议部分和硬件部分。
    协议部分初始化如代码段 23.3.1 所示。

代码段 23.3.1 CAN 协议初始化(driver_can.c)

/*
* 函数名:void CAN_Init(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:初始化 CAN1
*/
void CAN_Init(void) {
CAN_FilterTypeDef sFilterConfig;
hcan.Instance = CAN1;
/* 配置 CAN 的基本参数 */
hcan.Init.Prescaler = 8; // 预分频,范围(1~1024)
hcan.Init.Mode = CAN_MODE_NORMAL; // 正常工作模式
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; // 再次同步跳跃宽度为 1Tq
hcan.Init.TimeSeg1 = CAN_BS1_6TQ; // 位段 1(BS1)的长度为 6Tq
hcan.Init.TimeSeg2 = CAN_BS2_2TQ; // 位段 2(BS2)的长度为 2Tq
hcan.Init.TimeTriggeredMode = DISABLE; // 禁止时间触发通信模式
hcan.Init.AutoBusOff = DISABLE; // 禁止总线自动关闭
hcan.Init.AutoWakeUp = DISABLE; // 禁止自动唤醒
hcan.Init.AutoRetransmission = ENABLE; // 使能自动重传
hcan.Init.ReceiveFifoLocked = DISABLE; // 禁止接收 FIFO 锁定
hcan.Init.TransmitFifoPriority = DISABLE; // 禁止传输 FIFO 优先级
if (HAL_CAN_Init(&hcan) != HAL_OK)
{
Error_Handler(); }
/* 配置 CAN 的筛选器,此处全部接收,不做过滤 */
sFilterConfig.FilterBank = 0; // 选择筛选器组 0
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 设置为掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 32 位长度
sFilterConfig.FilterIdHigh = 0x0000; // ID 高字节
sFilterConfig.FilterIdLow = 0x0000; // ID 低字节
sFilterConfig.FilterMaskIdHigh = 0x0000; // 掩码高字节
sFilterConfig.FilterMaskIdLow = 0x0000; // 掩码低字节
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; // 过滤器关联 FIFO
sFilterConfig.FilterActivation = ENABLE; // 使能筛选器
if(HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
{
Error_Handler(); }
// 启用 CAN
if(HAL_CAN_Start(&hcan) != HAL_OK)
{
Error_Handler(); }
// 使能 CAN 接收 FIFO0 中断
if(HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
Error_Handler(); } }

12~25行:设置CAN协议参数;

  • 12行:设置哪一个CAN控制器,STM32F103ZET6只有一个CAN控制器;
  • 15行:设置时钟的预分频,与后面的位段时间长度组合,实现需要的500Kbps,这里先设置为8分频;
  • 16行:设置CAN工作模式,CAN控制有四种模式:正常模式、静默模式、回环模式及静默回环模式,通常设置为正常模式即可;
  • 17行:设置再次同步补偿宽度,因时钟频率偏差、传送延迟等,各单元有同步误差,这里设置补偿此误差的最大值,范围为1~4Tq;
  • 18行:设置位段1(BS1)的长度为6Tq;
  • 19行:设置位段2(BS2)的长度为2Tq,由前面的波特率计算公式可得:36M/(1+6+2)/8 = 500Kbps;
  • 20行:设置间触发通信模式,禁止时间触发通信模式;
  • 21行:禁止总线自动关闭,控制CAN在退出总线关闭状态时的行为;
  • 22行:禁止自动唤醒,控制CAN在眠模式下接收到消息时的行为;
  • 23行:开启自动重传,CAN将自动重发消息,直到CAN消息发送成功;关闭后,无论成功、错误或仲裁丢失,都只发送一次;
  • 24行:禁止接收FIFO锁定,当接收FIFO装满后,下一条传入消息将覆盖前一条消息;使能后,接收FIFO装满后,下一条传入消息将被丢弃;
  • 25行:禁止传输FIFO优先级,则优先级由消息标识符决定;使能后,由请求顺序(时间顺序)决定;

27~30行:初始化前面设置的CAN参数,同时会调用CAN硬件相关初始化函数“HAL_CAN_MspInit()”;

32~41行:设置CAN筛选器;

  • 33行:设置哪一个CAN筛选器组,设置设置筛选器组0,STM32F103ZET6共有14个筛选器组;
  • 34行:“CAN_FILTERMODE_IDMASK”设置为掩码模式,“CAN_FILTERMODE_IDLIST”设置为列表模式;
  • 35行:设置筛选范围可设置为32位;
  • 36~37行:设置ID的高低字节,这里设置ID为0;
  • 38~39行:设置掩码的高低字节,这里设置掩码为0,则没有做任何过滤;
  • 40行:设置本筛选器的消息存储在哪个FIFO(接收FIFO共有两个);
  • 41行:使能本筛选器;

43~46行:配置前面设置的筛选器;
48~52行:启动CAN;
54~58行:使能CAN接收FIFO0的中断;注意这里使能的是FIFO0,需要和前面设置筛选器的FIFO保持一致;这里可以设置三种类型FIFO中断,分别为:“CAN_IT_RX_FIFO0_MSG_PENDING”FIFO接收到数据就产生中断、“CAN_IT_RX_FIFO0_FULL”FIFO接收满后产生中断、“CAN_IT_RX_FIFO0_OVERRUN”FIFO溢出产生中断;

前面“HAL_CAN_Init()”会调用CAN硬件相关初始化函数“HAL_CAN_MspInit()”,该函数需要我们自己完善,如代码段 23.3.2 所示。

代码段 23.3.2 CAN 硬件初始化(driver_can.c)

/*
* 函数名:void HAL_CAN_MspInit(CAN_HandleTypeDef *hcanhandle)
* 输入参数:hcanhandle-CAN 句柄
* 输出参数:无
* 返回值:无
* 函数作用:初始化 CAN 硬件相关
*/
void HAL_CAN_MspInit(CAN_HandleTypeDef *hcanhandle)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_AFIO_CLK_ENABLE(); // 重映射时钟使能
__HAL_AFIO_REMAP_CAN1_2(); // 重映射
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); //PB9 CAN TX
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); //PB8 CAN RX
// HAL_Init()里默认优先级分组 4
HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 1, 0); // 设置 CAN 接收中断的优先级和使能
HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);
HAL_NVIC_SetPriority(CAN1_TX_IRQn, 1, 0); // 设置 CAN 发送中断的优先级和使能
HAL_NVIC_EnableIRQ(CAN1_TX_IRQn); }

需要注意,PB8、PB9如果用于CAN引脚,不是引脚典型复用,而是需要引脚复用重映射,如图 23.3.1所示为《数据手册》部分引脚描述截图。因此代码段 23.3.2 中14~15行需要先使能重映射时钟,再重映射CAN功能。
在这里插入图片描述

  1. 封装CAN发送函数和接收回调函数
    初始化完CAN后,便可以调用HAL库提供的“HAL_CAN_AddTxMessage()”将报文放入发送信箱,供CAN控制器发送。在这之前,需要准备好报文,如代码段 23.3.3 所示。

代码段 23.3.3 CAN 发送函数(driver_can.c)

/*
* 函数名:void CAN_Transmit(uint16_t ID, uint8_t *pdata, uint8_t length)
* 输入参数:ID -> CAN 发送报文的 ID
* pdata -> 发送报文的首地址
* length-> 发送报文的个数,最大 8 个字节
* 输出参数:无
* 返回值:无
* 函数作用:CAN1 发送函数
*/
void CAN_Transmit(uint16_t ID, uint8_t *pdata, uint8_t length)
{
uint32_t tx_mailbox = 0;
if(length>8) {
return ; }
TxHeader.StdId = ID; // 标准标识符
TxHeader.ExtId = 0; // 扩展标识符
TxHeader.IDE = CAN_ID_STD; // 帧模式(标准帧或扩展帧)
TxHeader.RTR = CAN_RTR_DATA; // 帧类型(数据帧或远程帧)
TxHeader.DLC = length; // 数据长度
TxHeader.TransmitGlobalTime = DISABLE; // 不发送时间标记
if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, pdata, &tx_mailbox) != HAL_OK)
{
Error_Handler(); } }

12~23行:准备发送报文;

  • 12行:定义变量“tx_mailbox”,保存返回的邮箱编号;
  • 13~16行:限制每次报文,数据最多8字节;
  • 18行:设置报文的标准标识符;
  • 19行:设置报文的扩展标识符;
  • 20行:设置帧模式,这里设置为标准帧;
  • 21行:设置帧类型,这里设置为数据帧;
  • 22行:设置数据长度;
  • 23行:设置帧传输时是否获取时间标记,通常关闭;

25~28行:调用“HAL_CAN_AddTxMessage()”发送报文;

前面设置了CAN中断和中断优先级,当CAN控制器,发送完成或接收完成时,都将跳转到中断向量表的对应位置,执行中断处理函数,如代码段 23.3.4 所示。

代码段 23.3.4 CAN 中断处理函数(driver_can.c)

//CAN1 发送完成后,进入该中断
void CAN1_TX0_IRQHandler(void) {
HAL_CAN_IRQHandler(&hcan); }
//CAN1 接收完成后,进入该中断
void CAN1_RX0_IRQHandler(void) {
HAL_CAN_IRQHandler(&hcan); // 回调 HAL_CAN_RxFifo0MsgPendingCallback()
}
/*
* 函数名:void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *CanHandle)
* 输入参数:CanHandle -> CAN 句柄
* 输出参数:无
* 返回值:无
* 函数作用:CAN1 接收回调函数
*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *CanHandle)
{
/* Get RX message */
if (HAL_CAN_GetRxMessage(CanHandle, CAN_RX_FIFO0, &RxHeader, can_msg.rx_msg) != HAL_OK)
{
Error_Handler(); }
can_msg.ID = RxHeader.StdId; // 获取标准标识符
can_msg.length = RxHeader.DLC; // 获取数据长度
CAN_SetRxFlag(1); // 设置接收标志位 }
  • 1~5行:CAN1发送完成时,将进入中断向量“CAN1_TX0_IRQHandler”。这里发送完成后,不需要任何操作,因此没有编写发送完成的中断函数;
  • 7~11行:CAN1接收完成时,将进入中断向量“CAN1_RX0_IRQHandler”。这里接收完成后,会调用一系列中断函数,其中“HAL_CAN_RxFifo0MsgPendingCallback()”为接收FIFO0消息等待回调函数;
  • 13~30行:编写接收FIFO0消息等待回调函数,使用“HAL_CAN_GetRxMessage()”获取报文,随后修改接收标志位,以便其它函数查询是否接收到数据;

本实验中涉及的RS485、串口打印、按键等实现,参考之前相关章节的讲解。

  1. 主函数控制逻辑
    在主函数里,每按一下按键,先构造RS485要发送的数据,然后调用“RS485_Tx()”发送数据。随后查询CAN是否收到数据,如果收到数据,打印CAN收到的数据。接着,构造CAN要发送的ID和数据,调用“CAN_Transmit()”发送报文,然后使用“RS485_Rx()”接收数据,并打印,如代码段 23.3.5 所示。

代码段 23.3.5 主函数控制逻辑(main.c)

// 初始化 CAN
CAN_Init();
// 初始化 USART1,设置波特率为 115200 bps
DEBUG_USART_Init(115200);
// 初始化 USART2,设置波特率为 9600 bps
RS485_Init(9600);
// 初始化按键
KeyInit();
// 在 windows 下字符串\n\r 表示回车
// 如果工程在编译下面这句中文的时候报错,请在“Option for target”->"C/C++"->"Misc Controls"添加“ --locale=english”
printf("**********************************************\n\r");
printf("-->百问科技 www.100ask.net\n\r");
printf("-->CAN 收发实验\n\r");
printf("**********************************************\n\r");
while(1) {
// RS485 开始发送数据给 CAN
if(step==0) {
// 初始化 RS485 发送信息
RS485_Msg.length = 8;
for(i=0; i<8; i++)
RS485_Msg.tx_data[i] = i;
RS485_Tx((uint8_t*)&RS485_Msg.tx_data[0], 8);
//如果 CAN 收到了帧报文,即 RS485 的数据帧
if(CAN_GetRxFlag()) {
CAN_SetRxFlag(0);
step = 1;
printf("\n\r");
printf("* RS485 发送数据: \n\r");
printf("Tx Data: ");
for(i=0; i<can_msg.length; i++) {
printf("0x%x ", RS485_Msg.tx_data[i]); }
printf("\n\r");
printf("* CAN 接收报文内容: \n\r");
printf("ID: 0x%x \n\r", can_msg.ID);
printf("Rx Data: ");
for(i=0; i<can_msg.length; i++) {
printf("0x%x ", can_msg.rx_msg[i]); }
printf("\n\r"); } }
// CAN 发送帧 ID+帧数据给 RS485
if(step==1) {
// 初始化 CAN 发送信息
can_msg.ID = 0x306;
can_msg.length = 8;
for(i=0; i<8; i++)
can_msg.tx_msg[i] = i|0xF0;
CAN_Transmit(can_msg.ID, can_msg.tx_msg, can_msg.length);
if(RS485_Rx(RS485_Msg.rx_data, RS485_Msg.length)) {
step = 0xFF;
printf("\n\r");
printf("* CAN 发送报文内容:\n\r");
printf("ID: 0x%x \n\r",can_msg.ID);
printf("Tx Data: ");
for(i=0; i<can_msg.length; i++) {
printf("0x%x ", can_msg.tx_msg[i]); }
printf("\n\r");
printf("* RS485 接收数据:\n\r");
printf("Rx Data: ");
for(i=0; i<RS485_Msg.length; i++) {
printf("0x%x ", RS485_Msg.rx_data[i]); }
printf("\n\r");
} } }

23.4 实验效果

本实验对应配套资料的“5_程序源码\15_通信—CAN\”。打开工程后,编译,下载。按图 23.4.1 所示,将开发板、RS485_CAN互转模块连接,开发板的CAN接模块的CAN,开发板的RS485接模块的RS485,且线都不交叉,注意此时的J11(蓝色拨码开关)的1脚拨为ON。
在这里插入图片描述
按下按键KEY1(KEY_U),即可看到串口如图 23.4.2 所示。首先RS485发送数据,CAN接收到相同的数据,RS485_CAN互换模块为CAN添加了0x0的ID。随便CAN发送数据,RS485接收到相同的数据,CAN报文的ID被RS485_CAN互换模块省略。
在这里插入图片描述


百问网技术论坛:
http://bbs.100ask.net/

百问网嵌入式视频官网:
https://www.100ask.net/index

百问网开发板:
淘宝:https://100ask.taobao.com/
天猫:https://weidongshan.tmall.com/

技术交流群2(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
QQ群:752871361

单片机-嵌入式Linux交流群:
QQ群:536785813

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值