STM32之CAN通讯(十一)

STM32F407 系列文章 - CAN通讯(十一)


目录

前言

一、CAN

二、CAN驱动电路

三、CAN软件设计

1.CAN状态初始化

2.头文件相关定义

3.接收中断服务函数

4.用户层使用

1.用户层相关定义

2.发送数据

3.接收数据

1.查询方式处理

2.中断方式处理

3.总结

4.其它功能函数

总结


前言

一般STM32F407芯片都会自带2路CAN接口,分别为CAN1和CAN2,其通讯速度高达1Mb/s,每个CAN总线发送端具备三个发送邮箱,用来区别发送优先级,接收端具备两个具有三级深度的接收 FIFO,用来存储数据。一般从407芯片端口输出的CAN信号抗干扰性比较差,不足以保证通讯的稳定性和可靠性,这时我们就需要通过添加驱动电路,可以增强信号的驱动能力,确保信号在传输过程中不受干扰或衰减,从而提高通讯的稳定性和可靠性。一般市场上所卖的板子都带这一功能的,因此要实现CAN总线通讯功能,需准备STM32F407开发板一块和CAN-Tool分析仪工具一个。


一、CAN

控制器局域网总线(CAN,Controller Area Network)是ISO国际标准化的串行通信协议总线,使用双绞线来传输信号,具有高性能、高可靠性和易于扩展的特点,广泛应用于汽车、工业控制、机器人控制等领域,是世界上应用最广泛的现场总线之一。‌CAN总线协议_百度百科 (baidu.com)

CAN通讯是一种多主机串行异步通信总线,允许网络中的各个节点(设备)进行无中心控制的通信。每个节点都可以在总线上发送报文,当多个节点同时发送报文时,CAN总线会使用仲裁机制决定哪个报文优先发送,优先级高的报文会先发送,低优先级的报文则会停止发送。CAN总线的通信过程分为发送报文、仲裁机制、数据传输和错误检测与处理四个阶段。

要了解更为详细的CAN总线协议及其报文构成,可参考CAN总线通信协议-CSDN博客,讲的挺全面的。

二、CAN驱动电路

STM32单片机在进行CAN通讯时加驱动电路是为了增强信号传输能力、提供总线保护以及满足CAN总线物理层规范。这些措施有助于提高通讯的稳定性和可靠性,确保单片机与CAN总线上的其他设备之间的正常通讯。下面提供一款国产驱动芯片SIT1050,详细的芯片参数及引脚特性参考其datasheet。

SIT1050是一款应用于CAN协议控制器和物理总线之间的接口芯片,可应用于卡车、公交、小汽车、工业控制等领域,速率可达到1Mbps,具有在总线与CAN协议控制器之间进行差分信号传输的能力,设计电路原理如下。

三、CAN软件设计

CAN软件包括底层代码和用户层代码,底层代码主要完成CAN状态的初始化工作,主要涉及到对底层硬件引脚、时钟、中断的定义及调用;用户层代码主要完成对CAN总线上数据消息的解析和处理。

关于底层代码的实现可以通过调用HAL官方库文件,或者在可视化工具STM32CubeMX上配置,然后一键画生成底层代码。不管那种方式其实都是在以CAN_TypeDef *结构完成CAN寄存器的配置,在stm32f407xx.h文件上可查看。

1.CAN状态初始化

通过函数can1_init()完成,主要为CAN1的设置,包括波特率、工作模式设置、底层驱动配置( 包括引脚配置、时钟配置、中断配置)、过滤器设置、CAN总线外围设备、CAN中断使能设置等等,该函数被main()函数调用。代码中有详细的介绍,代码如下(示例):

c 复制代码
CAN_HandleTypeDef hcan1;                /* CAN1句柄 */
CAN_HandleTypeDef hcan2;                /* CAN2句柄 */     

/**
 * @brief       CAN初始化
 *  @note       Prescaler    : 重新同步跳跃时间单元.范围: 1~3;
 *              TimeSeg2    : 时间段2的时间单元.范围: 1~8;
 *              TimeSeg1    : 时间段1的时间单元.范围: 1~16;
 *              Prescaler     : 波特率分频器.范围: 1~1024;
 *              以上4个参数, 在函数内部会减1, 所以, 任何一个参数都不能等于0
 *              CAN挂在APB1上面, 其输入时钟频率为 Fpclk1 = PCLK1 = 42Mhz
 *              tq     = Prescaler * tpclk1;
 *              波特率 = Fpclk1 / ((TimeSeg1 + TimeSeg2 + 1) * Prescaler);
 *              已知42M时钟和500Kbps要求, 根据波特率公式
 *              配置TimeSeg1 = 7, TimeSeg2 = 6 , 为Prescaler = 6
 *              得出CAN波特率为: 42M / ((6 + 7 + 1) * 6) = 500Kbps
 *
 * @param       mode    : CAN_MODE_NORMAL,  普通模式;
                          CAN_MODE_LOOPBACK,回环模式;
 * @retval      0,  初始化成功; 其他, 初始化失败;
 */
uint8_t can1_init(uint32_t mode)
{
	/*** 1.完成CAN1的波特率和模式设置
	*    这里也可以通过外部选择波特率配置can1_init(uint32_t bps, uint32_t mode)
	*    case 125:
	*    case 500:
  *    case 1000:
	***/
	hcan1.Instance = CAN1;
  hcan1.Init.Prescaler = 6;                 /* 分频系数(Fdiv)为Prescaler+1 */
  hcan1.Init.Mode = mode;                   /* 模式设置 */
  hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;   /* 重新同步跳跃宽度为SyncJumpWidth+1个时间单位 CAN_SJW_1TQ~CAN_SJW_4TQ */
  hcan1.Init.TimeSeg1 = CAN_BS1_7TQ;        /* 范围CAN_BS1_1TQ~CAN_BS1_16TQ */
  hcan1.Init.TimeSeg2 = CAN_BS2_6TQ;        /* 范围CAN_BS2_1TQ~CAN_BS2_8TQ */
  hcan1.Init.TimeTriggeredMode = DISABLE;   /* 非时间触发通信模式 */
  hcan1.Init.AutoBusOff = ENABLE;           /* 软件自动离线管理 */
  hcan1.Init.AutoWakeUp = DISABLE;          /* 睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位) */
  hcan1.Init.AutoRetransmission = ENABLE;   /* 禁止报文自动传送 */
  hcan1.Init.ReceiveFifoLocked = DISABLE;   /* 报文不锁定,新的覆盖旧的 */
  hcan1.Init.TransmitFifoPriority = ENABLE; /* 优先级由报文标识符决定 */
	// 2.完成CAN1的底层驱动配置 包括引脚配置、时钟配置、中断配置
  if (HAL_CAN_Init(&hcan1) != HAL_OK)
  {
    Error_Handler();
		return 1;
  }
	// 3.完成ID号为1#设备的过滤器设置
	CAN_Filter_Config(&hcan1, 1);
	// 4.启动CAN1总线外围设备
#if CAN1_iSOpen
	if(HAL_CAN_Start(&hcan1)!=HAL_OK) {
		Error_Handler();
		return 1;
	}
	// 5.使能CAN1中断
	else
		Enable_CAN1_Interrupts();
#endif
    return 0;
}

上面can1_init()函数包含了HAL_CAN_Init()、CAN_Filter_Config()、HAL_CAN_Start()、Enable_CAN1_Interrupts(),这4个函数分别完成如下功能:

  • HAL_CAN_Init(),此函数为HAL库函数,主要是调用HAL_CAN_MspInit()函数,以完成CAN1的底层驱动配置 包括引脚配置、时钟配置、中断配置。函数HAL_CAN_MspInit()同样为HAL库函数,但是其被定义为若函数,可以用来被重写的,代码如下(示例)。

    /**

    • @brief CAN底层驱动,引脚配置,时钟配置,中断配置
      此函数会被HAL_CAN_Init()调用
    • @param hcan:CAN句柄
    • @retval 无
      */
      void HAL_CAN_MspInit(CAN_HandleTypeDef hcan)
      {
      GPIO_InitTypeDef gpio_init_struct = {0};
      if (CAN1 == hcan->Instance)
      {
      /
      CAN1 clock enable /
      __HAL_RCC_CAN1_CLK_ENABLE(); /
      使能CAN1时钟 */
      /**CAN1 GPIO Configuration
      PA11 ------> CAN1_RX
      PA12 ------> CAN1_TX
      /
      gpio_init_struct.Pin = GPIO_PIN_11|GPIO_PIN_12;
      gpio_init_struct.Mode = GPIO_MODE_AF_PP;
      gpio_init_struct.Pull = GPIO_PULLUP;
      gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
      gpio_init_struct.Alternate = GPIO_AF9_CAN1;
      HAL_GPIO_Init(GPIOA, &gpio_init_struct); /
      CAN1_RX和CAN1_TX脚 模式设置 /
      /
      CAN1 interrupt Init /
      HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 1, 1);
      HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);
      HAL_NVIC_SetPriority(CAN1_RX1_IRQn, 1, 1);
      HAL_NVIC_EnableIRQ(CAN1_RX1_IRQn);
      }
      else if(CAN2 == hcan->Instance)
      {
      /
      CAN2 clock enable /
      __HAL_RCC_CAN2_CLK_ENABLE(); /
      使能CAN2时钟 */
      /**CAN2 GPIO Configuration
      PB12 ------> CAN2_RX
      PB13 ------> CAN2_TX
      /
      gpio_init_struct.Pin = GPIO_PIN_12|GPIO_PIN_13;
      gpio_init_struct.Mode = GPIO_MODE_AF_PP;
      gpio_init_struct.Pull = GPIO_NOPULL;
      gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
      gpio_init_struct.Alternate = GPIO_AF9_CAN2;
      HAL_GPIO_Init(GPIOB, &gpio_init_struct); /
      CAN2_RX和CAN2_TX脚 模式设置 /
      /
      CAN2 interrupt Init */
      HAL_NVIC_SetPriority(CAN2_RX0_IRQn, 1, 1);
      HAL_NVIC_EnableIRQ(CAN2_RX0_IRQn);
      HAL_NVIC_SetPriority(CAN2_RX1_IRQn, 1, 1);
      HAL_NVIC_EnableIRQ(CAN2_RX1_IRQn);
      }
      }
  • CAN_Filter_Config(),此函数完成CAN设备的过滤器设置,具体含义见代码中说明,代码如下(示例)。

    /*
    过滤的基本说明:
    1 寄存器配置
    CAN_ID STID[10:3] STID[2:0]EXID[17:13] EXID[12:5] EXID[4:0]|IDE|RTR|0
    ID [31:24] [23:16] [15:8] [7:0]
    MASK [31:24] [23:16] [15:8] [7:0]
    STID 基本位11位 EXID扩展位18位 IDE扩展帧标识1位 RTR远程帧标识1位
    2 掩码方式过滤的原则
    在掩码方式下,掩码寄存器某位为1表示接收到的帧ID对应的位必须与标识符寄存器对应的位相同
    IDE为扩展帧(CAN_ID_EXT,4),RTR为数据帧(CAN_RTR_DATA,0),掩码为1<<1|1<<2|0 = 6
    3 应用实现
    仅处理扩展帧和数据帧,对标准帧、远程帧均不处理;
    使用FIFO0接收发送至本控制器的CAN消息;
    使用FIFO1接收其他控制器和单机的CAN消息;
    ID1单机需要接收全部CAN消息并进行转发,中间各单机仅接收并处理本机消息。
    4 ID识别
    控制器接收消息的ID:xx xxxxx xxxxxx YYYYYY xx xxxxxxxx
    掩码:(0xFC00<<3)|0x6
    */
    void CAN_Filter_Config(CAN_HandleTypeDef *hcan, uint8_t deviceID)
    {
    CAN_FilterTypeDef filter;
    uint32_t filterID = (((uint32_t)deviceID<<10)<<3)|CAN_ID_EXT|CAN_RTR_DATA;
    uint32_t maskID = (0xFC00<<3)|(CAN_ID_EXT|CAN_RTR_DATA);//0x6;
    // 配置CAN过滤器
    filter.FilterMode = CAN_FILTERMODE_IDMASK; //选择标识符掩码模式(指示标识符的哪些位"必须匹配")
    filter.FilterScale = CAN_FILTERSCALE_32BIT; //选择32位模式
    filter.FilterActivation = CAN_FILTER_ENABLE; //激活筛选器
    filter.SlaveStartFilterBank = 14;
    // FIFO0 筛选本机CAN消息
    filter.FilterBank = 0; //FilterBank=0 筛选器组编号(共28个可配置且可调整的筛选器组)
    filter.FilterIdHigh = filterID>>16; //32位标识符
    filter.FilterIdLow = filterID&0xFFFF;
    filter.FilterMaskIdHigh = maskID>>16; //32位掩码
    filter.FilterMaskIdLow = maskID&0xFFFF;
    filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;//FilterBank=0 关联到FIFO0
    if (CAN1 == hcan->Instance) //选择CAN1过滤器设置
    {
    if(HAL_CAN_ConfigFilter(&hcan1, &filter) != HAL_OK)
    Error_Handler();
    }
    else if (CAN2 == hcan->Instance) //选择CAN2过滤器设置
    {
    filter.FilterBank = 14;
    if(HAL_CAN_ConfigFilter(&hcan2, &filter) != HAL_OK)
    Error_Handler();
    }

      /*** 特殊设置 
      *    FIFO1过滤器设置 仅ID1单机收其他单机的CAN消息
      ***/
      if(DEVICEID == deviceID )
      {
      	filter.FilterBank = 1;						            //过滤器编号
      	filter.FilterIdHigh = 0x0000;				          //32位ID
      	filter.FilterIdLow = CAN_ID_EXT|CAN_RTR_DATA;
      	filter.FilterMaskIdHigh = 0x0000;			        //32位MASK
      	filter.FilterMaskIdLow = CAN_ID_EXT|CAN_RTR_DATA;//0x6;
      	filter.FilterFIFOAssignment=CAN_FILTER_FIFO1; //关联到FIFO1
      	if (CAN1 == hcan->Instance)                   //选择CAN1过滤器设置
      	{
      		if(HAL_CAN_ConfigFilter(&hcan1, &filter) != HAL_OK)
      			Error_Handler();
      	}
      	else if (CAN2 == hcan->Instance)              //选择CAN2过滤器设置
      	{
      		filter.FilterBank = 15;
      		if(HAL_CAN_ConfigFilter(&hcan2, &filter) != HAL_OK)
      			Error_Handler();
      	}
      }
      
      /*** 特殊设置
      *    为适应标准帧,增加过滤器
      ***/
      filterID = CAN_ID_STD|CAN_RTR_DATA;   //		?(0x0580UL<<21)|
      filter.FilterBank = 2;						    //过滤器编号
      filter.FilterIdHigh = 0x0000;				  //32位ID
      filter.FilterIdLow = CAN_ID_STD|CAN_RTR_DATA;
      filter.FilterMaskIdHigh = 0x0000;			//32位MASK
      filter.FilterMaskIdLow = CAN_ID_STD|CAN_RTR_DATA;
      filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;//关联到FIFO0
      if (CAN1 == hcan->Instance)                   //选择CAN1过滤器设置
      {
      	if(HAL_CAN_ConfigFilter(&hcan1, &filter) != HAL_OK)
      		Error_Handler();
      }
    

    }

  • HAL_CAN_Start(),此函数主要完成开启CAN总线外围设备状态,该函数为HAL库函数,具体说明见官方说明,此处不提供代码。

  • Enable_CAN1_Interrupts(),完成CAN接收中断使能,以及FIFO和中断模式选择,具体含义见代码中说明,代码如下(示例)。

    /**

    • @brief 使能CAN1接收中断 以及FIFO和中断模式选择
    • @note 根据STM官方手册F407每个CAN接收端具备两个具有三级深度的接收FIFO
    •          分别为FIFO0和FIFO1,每个接收中断又可以选择不同的中断模式,具体为
      
    •          CAN_IT_RX_FIFO0_MSG_PENDING模式:有消息就触发中断
      
    •          CAN_IT_RX_FIFO0_FULL模式:三级接收FIFO全满时触发中断
      
    •          CAN_IT_RX_FIFO0_OVERRUN:超出时就触发中断
      
    • @retval 一般推荐pending模式中断
      */
      void Enable_CAN1_Interrupts()
      {
      #if RXFifo0_iSOpen
      HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
      #endif

    #if RXFifo1_iSOpen
    HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO1_MSG_PENDING);
    #endif
    }
    void Enable_CAN2_Interrupts()
    {
    #if RXFifo0_iSOpen
    HAL_CAN_ActivateNotification(&hcan2, CAN_IT_RX_FIFO0_MSG_PENDING);
    #endif

    #if RXFifo1_iSOpen
    HAL_CAN_ActivateNotification(&hcan2, CAN_IT_RX_FIFO1_MSG_PENDING);
    #endif
    }
    /**

    • @brief 关闭CAN1接收中断
    • @retval
      */
      void Disable_CAN1_Interrupts()
      {
      #if RXFifo0_iSOpen
      HAL_CAN_DeactivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
      #endif

    #if RXFifo1_iSOpen
    HAL_CAN_DeactivateNotification(&hcan1, CAN_IT_RX_FIFO1_MSG_PENDING);
    #endif
    }
    void Disable_CAN2_Interrupts()
    {
    #if RXFifo0_iSOpen
    HAL_CAN_DeactivateNotification(&hcan2, CAN_IT_RX_FIFO0_MSG_PENDING);
    #endif

    #if RXFifo1_iSOpen
    HAL_CAN_DeactivateNotification(&hcan2, CAN_IT_RX_FIFO1_MSG_PENDING);
    #endif
    }

该CAN状态初始化函数can1_init()一般在main()函数开始被调用,,代码如下(示例)。

int main(void)
{
    /* 略.....初始化设置代码 */
    can1_init(CAN_MODE_LOOPBACK);  /* CAN1初始化, 环回模式, 波特率500Kbps */
    while (1) {
        /* CAN消息解析处理 */   
    }
}

2.头文件相关定义

上述函数相关头文件定义,代码如下(示例):

#ifndef __CAN_H
#define __CAN_H

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"

extern CAN_HandleTypeDef hcan1;
extern CAN_HandleTypeDef hcan2;
#define DEVICEID 1
#define CAN1_iSOpen 1
#define CAN2_iSOpen 0
#define RXFifo0_iSOpen 1
#define RXFifo1_iSOpen 1
#define isUserDefined 0
/******************************************************************************************/
/* CAN接收中断使能 */
#if isUserDefined
#define PRO_CAN1_RX0_IRQHandler  CAN1_RX0_IRQHandler
#define PRO_CAN1_RX1_IRQHandler  CAN1_RX1_IRQHandler
#define PRO_CAN2_RX0_IRQHandler  CAN2_RX0_IRQHandler
#define PRO_CAN2_RX1_IRQHandler  CAN2_RX1_IRQHandler
#else
void CAN1_RX0_IRQHandler(void); //ISR函数
void CAN1_RX1_IRQHandler(void); //ISR函数
void CAN2_RX0_IRQHandler(void); //ISR函数
void CAN2_RX1_IRQHandler(void); //ISR函数
#endif

/* 函数声明 */
uint8_t can_receive_msg(uint32_t id, uint8_t *buf);                                     /* CAN接收数据, 查询 */
uint8_t can_send_msg(uint32_t id, uint8_t *msg, uint8_t len);                           /* CAN发送数据 */
uint8_t can1_init(uint32_t mode);                                                        /* CAN初始化 */
void Error_Handler(void);
void Recv_CAN_Msgs(CAN_HandleTypeDef*, uint32_t);
void CAN_Filter_Config(CAN_HandleTypeDef*, uint8_t);
void Enable_CAN1_Interrupts(void);
void Enable_CAN2_Interrupts(void);
void Disable_CAN1_Interrupts(void);
void Disable_CAN2_Interrupts(void);
#endif

3.接收中断服务函数

处理中断服务例程(Interrupt Service Routine, ISR)函数,它们由硬件中断触发自动执行。 具体实现流程为:当处理器的CAN1接口的RX0缓冲区接收到数据时,硬件会触发一个中断;CPU响应这个中断,跳转到CAN1_RX0_IRQHandler这个中断服务例程的地址开始执行代码;这个中断服务例程通常负责读取接收到的数据,处理这些数据(比如,更新状态变量,发送数据到其他模块等),然后返回。

在startup_stm32f407xx.s文件(该文件每个项目工程中都有,主要作用为设置初始SP、设置初始处理器、设置带有异常ISR地址的向量表条目、调用main函数。)上可以看到代码如下(示例):

; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                ; External Interrupts                
                DCD     CAN1_RX0_IRQHandler               ; CAN1 RX0                                               
                DCD     CAN1_RX1_IRQHandler               ; CAN1 RX1 
                DCD     CAN2_RX0_IRQHandler               ; CAN2 RX0                                               
                DCD     CAN2_RX1_IRQHandler               ; CAN2 RX1 
__Vectors_End 

DCD CAN1_RX0_IRQHandler 这行代码是在汇编语言中使用的,用于定义一个数据常量。在这个上下文中,它定义了一个标签(或者说是一个内存地址),该标签指向 CAN1_RX0_IRQHandler 这个中断服务例程的地址。

这里介绍下ISR,通常有以下特点:

  • 它们必须尽可能快地执行,以减少中断延迟并避免阻塞其他中断的处理。
  • 它们通常访问特定的硬件寄存器来读取中断状态、清除中断标志,并处理中断引起的事件。
  • 在多任务环境或实时操作系统(RTOS)中,ISR可能负责设置标志或发送消息给任务,以便在ISR之外处理更耗时的任务。
  • 在编写ISR时,开发者需要确保它们遵循特定的硬件和编译器要求,比如使用特定的中断向量表入口点名称(在这个例子中是CAN1_RX0_IRQHandler),以及可能需要在函数开始和结束时添加特定的汇编指令或内联代码来保存和恢复CPU寄存器状态。

在上面第二小节-头文件相关定义中,可以看到对中断服务例程函数的定义或宏替换,代码如下(重写一下):

c 复制代码
#define isUserDefined 0

#if isUserDefined
#define PRO_CAN1_RX0_IRQHandler  CAN1_RX0_IRQHandler
#define PRO_CAN1_RX1_IRQHandler  CAN1_RX1_IRQHandler
#define PRO_CAN2_RX0_IRQHandler  CAN2_RX0_IRQHandler
#define PRO_CAN2_RX1_IRQHandler  CAN2_RX1_IRQHandler
#else
void CAN1_RX0_IRQHandler(void); //ISR函数
void CAN1_RX1_IRQHandler(void); //ISR函数
void CAN2_RX0_IRQHandler(void); //ISR函数
void CAN2_RX1_IRQHandler(void); //ISR函数
#endif

相关函数的实现如下。这里需要说明一下,如果你是通过STM32CubeMX生成的代码,相应的ISR函数会在stm32f4xx_it.c文件(此文件为所有异常处理程序和外围设备中断服务程序)上实现。

/**
 * @brief  弱函数 可被重写
 */
__weak void Recv_CAN_Msgs(CAN_HandleTypeDef *hcan, uint32_t RxFifo)
{
	  UNUSED(hcan);
  /* NOTE : This function Should not be modified, when the callback is needed,
            the Recv_CAN_Msgs could be implemented in the user file
   */
}
/**
 * @brief       CAN_RX中断服务函数 引用中断处理函数
 *   @note      处理CAN FIFO0的接收中断
 * @param       无
 * @retval      无
 */
void PRO_CAN1_RX0_IRQHandler()
{
#if isUserDefined
		Recv_CAN_Msgs(&hcan1, CAN_RX_FIFO0);
#else
		HAL_CAN_IRQHandler(&hcan1);
#endif
}
void PRO_CAN1_RX1_IRQHandler()
{
#if isUserDefined
		Recv_CAN_Msgs(&hcan1, CAN_RX_FIFO1);
#else
		HAL_CAN_IRQHandler(&hcan1);
#endif
}
void PRO_CAN2_RX0_IRQHandler()
{
#if isUserDefined
		Recv_CAN_Msgs(&hcan2, CAN_RX_FIFO0);
#else
		HAL_CAN_IRQHandler(&hcan2);
#endif
}
void PRO_CAN2_RX1_IRQHandler()
{
#if isUserDefined
		Recv_CAN_Msgs(&hcan2, CAN_RX_FIFO1);
#else
		HAL_CAN_IRQHandler(&hcan2);
#endif
}

#if !isUserDefined
/**
  * @brief  处理中断服务例程(ISR)函数 它们由硬件中断触发自动执行
  *  @note  This function handles CAN1 CAN1 RX interrupts.  
  */
void CAN1_RX0_IRQHandler(void)
{
	PRO_CAN1_RX0_IRQHandler();
}
void CAN1_RX1_IRQHandler(void)
{
  PRO_CAN1_RX1_IRQHandler();
}
void CAN2_RX0_IRQHandler(void)
{
  PRO_CAN2_RX0_IRQHandler();
}
void CAN2_RX1_IRQHandler(void)
{
  PRO_CAN2_RX1_IRQHandler();
}
#endif

void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */

  /* USER CODE END Error_Handler_Debug */
}

4.用户层使用

用户的使用主要为发送数据和接收数据两个动作,这两个动作中均包含CAN数据的处理与解析过程。在讲解这两个动作前,先完成其相关变量、功能函数、引用头文件的定义,在头文件上完成。

1.用户层相关定义

发送数据和接收数据的相关函数头文件定义,代码如下(示例):

#ifndef __CAN_USER_H
#define __CAN_USER_H

#ifdef __cplusplus
extern "C"
{
#endif

#include "string.h"
#include "./BSP/CAN/can.h"
#include "./BSP/CAN/lib_array.h"
extern Block_Circle_Array ProcCANArray;
extern Block_Circle_Array CANSendArray;
extern Block_Circle_Array g_PushCANArray;
typedef union _Union_Bit64 {
	uint64_t u64;
	int64_t i64;
	double f64;
	uint32_t u32[2];
	int32_t i32[2];
	float f32[2];
	uint16_t u16[4];
	int16_t i16[4];
	uint8_t b[8];
}UNION_BIT64;

typedef struct
{
	CAN_EXT_ID ID;	  // 消息ID
	uint8_t Len;	  // 消息数据长度(字节)
	UNION_BIT64 Data; // 消息数据
}__attribute__((packed)) CAN_Message;
typedef enum
{
	STD_CAN_MSG = 4,
	EXT_CAN_MSG = 0
}CAN_Msg_Type;

#define CAN_MSG_TYPE_FILTER 0x4
typedef enum
{
	CAN1_TIME = 1,
	CAN2_TIME,
	UART1_TIME,
	UART2_TIME,
	UART3_TIME,
	IIC_TIME,
	NET_TIME
}DeviceComTimeIndex; // 设备通讯时间索引

typedef enum
{
	CMD1 = 1,
	CMD2,
	CMD3,
	CMD4
}Info_Cmd;
typedef enum
{
	SRC1 = 1,
	SRC2,
	SRC3,
	SRC4
}Info_Src;
typedef enum
{
	DES1 = 1,
	DES2,
	DES3,
	DES4
}Info_Des;

typedef enum
{
	ID1 = 0x1,
	ID2 = 0x2,
	ID3 = 0x3,
	ID4 = 0x4,
	UNKNOWN_ID = 0x0 // 未知节点
}Device_ID;

#define CombCMD(cmd, src, des) (((cmd) << 12) | ((src) << 6) | des) // 根据消息命令,源节点和目标节点的ID组合成实际可辨别的命令
typedef enum
{
	ID1_BUS_CMD = CombCMD(CMD1, SRC1, DES1),
	ID2_BUS_CMD = CombCMD(CMD2, SRC2, DES2),
	ID3_BUS_CMD = CombCMD(CMD3, SRC3, DES3),
	ID4_BUS_CMD = CombCMD(CMD4, SRC4, DES4),
}CAN_Msg_Cmds;
void User_CAN_Init(void);
void Reset_CAN_Recv_Array(void);
uint32_t Create_Ext_CAN_Msg_ID(Bus_Pri_ID pri, uint8_t mid, Device_ID src, Device_ID des, CAN_Bus_ID bus, uint8_t index);
void Push_CAN_Send_Array(CAN_Bus_ID CANChannel, CAN_Message *msg);
void CAN_Data_Process(void);
void CAN_Msg_process(CAN_Message *msg);
#ifdef __cplusplus
}
#endif

#endif

2.发送数据

发送数据主要在Send_CAN_Msgs()函数上完成,一般我们使用的环境比较复杂,使用单机设备较多、或者数据量较大时,为保证数据的及时处理和数据的完整性,这时我们需要建立一个缓存块,用来存放数据,实现代码如下(示例):

//发送CAN消息
void Send_CAN_Msgs(void)
{
	CAN_TxHeaderTypeDef msgHead;  /* 发送参数句柄 */
	CAN_Message msg;
	uint32_t txMailbox;
#if CAN1_iSOpen
	uint8_t CAN1FreeBoxs = 0;
	uint8_t CAN1Msgs = BlockCircleArray_GetBlockCount(&CANSendArray); //缓存块
	if(CAN1Msgs > 0) //有缓存的数据
	{
		CAN1FreeBoxs = HAL_CAN_GetTxMailboxesFreeLevel(&hcan1);
		if(0 < CAN1FreeBoxs)  // 有空邮箱
		{
			BlockCircleArray_Get(&CANSendArray, (uint8_t*)&msg);
			Set_CAN_TxHeader(&msgHead, msg);
			if(HAL_OK == HAL_CAN_AddTxMessage(&hcan1, &msgHead, msg.Data.b, &txMailbox))
			{
                //发送成功在缓存块上删除该消息,不成功时保留
				BlockCircleArray_Slide(&CANSendArray); 
				//Delay_us(10);
			}
		}
	}
#endif
//是否开放CAN2
#if CAN2_iSOpen
	uint8_t CAN2FreeBoxs = 0;
	uint8_t CAN2Msgs = BlockCircleArray_GetBlockCount(&CANSendArray);
	if(CAN2Msgs > 0)
	{
		CAN2FreeBoxs = HAL_CAN_GetTxMailboxesFreeLevel(&hcan2);
		if(0 < CAN2FreeBoxs)
		{
			BlockCircleArray_Get(&CANSendArray, (uint8_t*)&msg);
			Set_CAN_TxHeader(&msgHead, msg);
			if(HAL_OK == HAL_CAN_AddTxMessage(&hcan2, &msgHead,msg.Data.b, &txMailbox))
			{
				BlockCircleArray_Slide(&CANSendArray);
				//Delay_us(5);
			}
		}
	}
#endif
//测试发送一组数据
#if 0
/**
 * @brief       测试CAN 发送一组数据
 *   @note      发送格式固定为: 标准ID, 数据帧
 * @param       id      : 标准ID(11位)
 * @param       msg     : 数据指针
 * @param       len     : 数据长度
 */
	uint32_t id = 0x11;
	uint8_t len = 8;
	uint8_t msgs[len];
    uint16_t t = 0;
    txMailbox = CAN_TX_MAILBOX0;
    
    msgHead.StdId = id;         /* 标准标识符 */
    msgHead.ExtId = id;         /* 扩展标识符(29位) */
    msgHead.IDE = CAN_ID_STD;   /* 使用标准帧 */
    msgHead.RTR = CAN_RTR_DATA; /* 数据帧 */
    msgHead.DLC = len;
    if (HAL_CAN_AddTxMessage(&hcan1, &msgHead, msgs, &txMailbox) != HAL_OK) /* 发送消息 */
    {
        return;
    }
    while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) != 3)   /* 等待发送完成,所有邮箱为空 */
    {
        t++;
        
        if (t > 0xFFF) {
            HAL_CAN_AbortTxRequest(&hcan1, txMailbox);     /* 超时,直接中止邮箱的发送请求 */
            return;
        }
    }
#endif
}

3.接收数据

CAN数据的接收,一般有两种实现方式:一种时通过中断接收并处理消息,另一种是通过查询的方式实现。这里推荐采用中断方式实现,这种方式高效、便捷,工业上采取这种方式实现;查询的方式,适用于功能简单、且数据量少的情况使用。

1.查询方式处理

采用查询方式实现,一般是在主函数的主循环中,通过while(1)中,不断地查询是否接收导数据,因此此方式缺点就在这里,需保证主循环中无其他重大耗时的功能,实现代码如下(示例)。

/**
 * @brief       CAN 接收数据查询
 *   @note      接收数据格式固定为: 标准ID, 数据帧
 * @param       id      : 要查询的 标准ID(11位)
 * @param       buf     : 数据缓存区
 * @retval      接收结果
 *   @arg       0   , 无数据被接收到;
 *   @arg       其他, 接收的数据长度
 */
uint8_t can_receive_msg(uint32_t id, uint8_t *buf)
{
    if (HAL_CAN_GetRxFifoFillLevel(&hcan1, CAN_RX_FIFO0) == 0)     /* 没有接收到数据 */
    {
        return 0;
    }

    CAN_RxHeaderTypeDef g_canx_rxheader;    /* 接收参数句柄 */
    if (HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &g_canx_rxheader, buf) != HAL_OK)  /* 读取数据 */
    {
        return 0;
    }

    if (g_canx_rxheader.StdId!= id || g_canx_rxheader.IDE != CAN_ID_STD || g_canx_rxheader.RTR != CAN_RTR_DATA)       /* 接收到的ID不对 / 不是标准帧 / 不是数据帧 */
    {
        return 0;    
    }

    return g_canx_rxheader.DLC;
}

在main()函数实现如下。

int main(void)
{
    uint32_t id = 0x12;
    uint8_t canbuf[8];
    uint8_t rxlen = 0;
    /* 略.....初始化设置代码 */
    while (1) {
        rxlen = can_receive_msg(id, canbuf);  /* CANID=0x12, 接收数据查询 */
        if (rxlen) /* 是否接收到有数据 */ {
             /* 解析处理 */   
        }
    }
}
2.中断方式处理

前面第三小节已经完成对CAN接收的中断服务函数定义,这里继续在上面实现解析处理功能。在前面的HAL库函数HAL_CAN_IRQHandler()里面,会调用HAL_CAN_RxFifo0MsgPendingCallback()函数,该函数为若函数,已被重写,调用Recv_CAN_Msgs()和Pre_Process_CAN_Msgs()完成数据解析功能,相关函数解释如下:

  • HAL_CAN_RxFifo0MsgPendingCallback(),此函数为HAL库函数,但是其被定义为若函数,可以用来被重写的。重写调用Recv_CAN_Msgs()函数,以完成数据解析;
  • Recv_CAN_Msgs(),关键数据接收处理函数,完成CAN消息的帧头以及数据内容接收,并判断来帧类型、帧来源等信息,该函数也可以不通过被HAL_CAN_RxFifo0MsgPendingCallback()函数调用,自行处理,直接被中断服务例程ISR函数调用,具体代码实现方式见第三小节-接收中断服务函数,将宏define isUserDefined定义为1,即可。
  • Pre_Process_CAN_Msgs(),消息预处理,放入缓存队列。

代码如下(示例)。

// 消息预处理,放入缓存队列
void Pre_Process_CAN_Msgs(CAN_Message* prcMsg)
{
	if(prcMsg != NULL)
	{
		if(Check_New_CAN_Msg(&RecvIDList, g_1msTick, prcMsg->ID))	// 检查是否是新消息,防止CAN1、CAN2上的重复消息
		{
			// 来自于SRC1的针对本机的控制命令,待进一步处理信息
			if(prcMsg->ID.s.src == SRC1)
				BlockCircleArray_Push(&ProcCANArray, (uint8_t*)prcMsg);
		}		
	}
}

void Recv_CAN_Msgs(CAN_HandleTypeDef *hcan, uint32_t RxFifo)
{
	CAN_RxHeaderTypeDef rxHeader;
	CAN_Message rcvMsg = {0};
	CAN_Msg_Type msgType;
	while(HAL_CAN_GetRxFifoFillLevel(hcan, RxFifo))
	{
	  // 获得接收到的数据头和数据
		if (HAL_CAN_GetRxMessage(hcan, RxFifo, &rxHeader, rcvMsg.Data.b) == HAL_OK)
		{
			if(rxHeader.IDE == CAN_ID_EXT) // 扩展帧
			{
				rcvMsg.ID.id = rxHeader.ExtId;
				msgType = EXT_CAN_MSG;
			}
			else // 标准帧
			{
				rcvMsg.ID.id = rxHeader.StdId;
				msgType = STD_CAN_MSG;
			}
			
			rcvMsg.Len = rxHeader.DLC;
			if(hcan == &hcan1) // 通过地址判断是CAN1地址区接受的数据还是CAN2
			{// 使用预留的数据区保存当前消息的接收总线通道号
				rcvMsg.ID.s.res = CAN1_BUS | msgType;
				g_DeviceComTime[CAN1_TIME] = g_1msTick;
			}
			else
			{
				rcvMsg.ID.s.res = CAN2_BUS | msgType;
				g_DeviceComTime[CAN2_TIME] = g_1msTick;
			}
			Pre_Process_CAN_Msgs(&rcvMsg);			
		}
		else
		{
			break;
		}
	}
}

/**
 * @brief  弱函数 已被重写
 *  @note  pending callback 接收中断模式 
 *         处理CAN句柄上接收FIFO0的消息 该函数被HAL_CAN_IRQHandler()调用
 */
// FIFO0收到的是本机信息
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
	Recv_CAN_Msgs(hcan, CAN_RX_FIFO0);
}
// FIFO1收到的是其他单机信息
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
	Recv_CAN_Msgs(hcan, CAN_RX_FIFO1);
}

在main()函数实现如下。

int main(void)
{
    User_CAN_Init();
    /* 略.....初始化设置代码 */
    while (1) {
        /*** 其它功能 ***/
        CAN_Data_Process();
    }
}
3.总结

从上面对CAN数据接收的这两种实现方式,可以看出不管是哪一种方式,其底层都是查询调用can的FIFO缓存是否有数据,不同的是,一种在中断里面查看,一种是在主循环里面查看。

4.其它功能函数

其它功能主要是完成一些参数变量的初始化设置,和对CAN消息缓存块的解析处理,代码如下(示例)。

#include "./BSP/CAN/can_user.h"

/*** 完成参数定义 ***/
void CAN_Arrays_Init(void)
{
	BlockCircleArray_Init(&ProcCANArray, (uint8_t*)ProcCANMsgs, CAN_MSG_LEN, MAX_CAN_PROC_MSGS);
	BlockCircleArray_Init(&CANSendArray, (uint8_t*)CANSendMsgs, CAN_MSG_LEN, MAX_CAN_SEND_MSGS);
	BlockCircleArray_Init(&g_PushCANArray, (uint8_t*)PushCANMsgs, CAN_MSG_LEN, MAX_CAN_PUSH_MSGS);
}
void User_CAN_Init(void)
{
	CAN_Arrays_Init();
}
// 将待发送的测试信息压入CAN待发送缓存队列
void Push_CAN_Send_Array(CAN_Bus_ID CANChannel, CAN_Message* msg)
{
#if CAN1_iSOpen
	if(CANChannel == CAN1_BUS && msg != NULL)
		BlockCircleArray_Push(&CANSendArray, (uint8_t*)msg);
#endif	
#if CAN2_iSOpen
	else if(CANChannel == CAN2_BUS && msg != NULL)
		BlockCircleArray_Push(&CANSendArray, (uint8_t*)msg);
#endif
}
/* CAN总线网络异常处理,尚未完成并验证
*  在系统初始化时设置了自动离线管理
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan)
{
    uint32_t err = hcan->ErrorCode;
	// uint8_t status=0;
	// uint32_t canTSR = hcan->Instance->TSR;
    // uint32_t canABRQ = CAN_TSR_ABRQ0;    // 终止发送
    // uint32_t canTERR = CAN_TSR_TERR0;    // 发送失败
    // uint32_t canALST = CAN_TSR_ALST0;    // 仲裁失败
    switch(err){
      case HAL_CAN_ERROR_EWG:    // EWG error   
        break;
      case HAL_CAN_ERROR_EPV:    // EPV error 
        break;
      case HAL_CAN_ERROR_BOF:    // BOF error
        break;
      case HAL_CAN_ERROR_STF:    // Stuff error
        break;
      case HAL_CAN_ERROR_FOR:    // Form error
        break;
      case HAL_CAN_ERROR_ACK:    // Acknowledgment error
        break;
      case HAL_CAN_ERROR_BR:    // Bit recessive 
        break;
      case HAL_CAN_ERROR_BD:    // LEC dominant 
        break;
      case HAL_CAN_ERROR_CRC:    // LEC transfer error
        break;
      case HAL_CAN_ERROR_NONE:    // No error
        break;
      default:
        break;
    }     
    hcan->ErrorCode = HAL_CAN_ERROR_NONE;
}
 */
void Set_CAN_TxHeader(CAN_TxHeaderTypeDef* header, CAN_Message msg)
{
	// if(msg.ID.s.res == EXT_CAN_MSG)// 扩展帧
	// {
		header->ExtId = msg.ID.id;
		header->StdId = 0;
		header->IDE = CAN_ID_EXT;
	// }
	// else // 标准帧
	// {
	// 	header->StdId = msg.ID.id & 0xFFFF;
	// 	header->ExtId = 0;
	// 	header->IDE = CAN_ID_STD;
	// }
	header->RTR = CAN_RTR_DATA;
	header->DLC = msg.Len;	
	header->TransmitGlobalTime = DISABLE;// 只能设置为disable 	
}
uint32_t Create_Ext_CAN_Msg_ID(Bus_Pri_ID pri, uint8_t mid, Device_ID src, Device_ID des, CAN_Bus_ID bus, uint8_t index)
{
	CAN_EXT_ID id;
	id.s.res = EXT_CAN_MSG;
	id.s.pri = pri;
	id.s.mid = mid;
	id.s.src = src;
	id.s.des = des;
	id.s.bus = bus;
	id.s.index = index;
	return id.id;
}
// 单消息解析处理
void CAN_Msg_process(CAN_Message* msg)
{
    /*** 功能函数 ***/
}
// 多消息的处理
void CAN_Msgs_Process(void)
{
    /*** 设计思路在main函数调用CAN_Msg_process() ***/
}

5.主函数使用

上述完成对CAN接口底层代码和用户层代码的编写,这里实现其功能被主函数main()使用,代码如下(示例)。

/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  */
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/CAN/can_user.h"

int main(void)
{
    uint8_t key;
    uint8_t mode = 1; /* CAN工作模式: 0,普通模式; 1,环回模式 */
    HAL_Init();                             /* 初始化HAL库 */
    sys_stm32_clock_init(336, 8, 2, 7);     /* 设置时钟,168Mhz */
    delay_init(168);                        /* 延时初始化 */
    usart_init(115200);                     /* 串口初始化为115200 */
    led_init();                             /* 初始化LED */
    lcd_init();                             /* 初始化LCD */
    key_init();                             /* 初始化按键 */
    can1_init(CAN_MODE_LOOPBACK);           /* CAN初始化, 环回模式, 波特率500Kbps */
		User_CAN_Init();
    //lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
    //lcd_show_string(30, 70, 200, 16, 16, "CAN TEST", RED);
    while (1)
    {
				/* 中断方式 包含发送数据和接收数据 */
				CAN_Data_Process();
				/* 查询方式 CAN ID = 0x12, 接收数据查询
        uint8_t rxlen = can_receive_msg(0x12, canbuf);  
        if (rxlen) { // 接收到有数据
            for (uint8_t i = 0; i < rxlen; i++) {
								// 处理数据 显示数据
            }
        }
				*/
			  key = key_scan(0);	
        if (key == KEY1_PRES) {  /* KEY1_PRES按下, 改变CAN的工作模式 */
            mode = !mode;
            /* CAN初始化, 普通(0)/回环(1)模式, 波特率500Kbps */
            can1_init(mode ? CAN_MODE_LOOPBACK : CAN_MODE_NORMAL);
            if (mode == 0)  /* 普通模式, 需要2个开发板 */
                printf("Normal Mode");
            else           /* 回环模式,一个开发板就可以测试了. */
                printf("LoopBack Mode");
						Reset_CAN_Recv_Array();
        }
        delay_ms(1);
    }
}

总结

下面提供的代码,基于STM32F407ZGT芯片编写,可直接在原子开发板上运行,也可运行在各工程项目上,但需要注意各接口以及相应的引脚应和原子开发板上保持一致。

相应的代码链接:单片机STM32F407-Case程序代码例程-CSDN文库

相关推荐
我想学LINUX21 分钟前
【STM32+QT项目】基于STM32与QT的智慧粮仓环境监测与管理系统设计(完整工程资料源码)
stm32·嵌入式硬件·qt·毕业设计·课程设计·项目开发
吾与春风皆过客26 分钟前
STM32和国民技术(N32)单片机串口中断接收数据及数据解析
stm32·单片机·嵌入式硬件
JaneZJW28 分钟前
江科大STM32入门——IIC通信笔记总结
c语言·笔记·stm32·单片机·嵌入式硬件·嵌入式·iic
JaneZJW31 分钟前
江科大STM32入门——SPI通信笔记总结
笔记·stm32·单片机·嵌入式硬件·嵌入式·spi
小禾苗_1 小时前
51单片机——定时器中断(重点)
单片机·嵌入式硬件·51单片机
网易独家音乐人Mike Zhou4 小时前
【TI毫米波雷达】DCA1000不使用mmWave Studio的数据采集方法,以及自动化实时数据采集
c语言·单片机·mcu·物联网·嵌入式·iot·毫米波雷达
qq_459730034 小时前
STM32-DMA数据转运
stm32·单片机·嵌入式硬件
不能只会打代码5 小时前
32单片机从入门到精通之数据处理——数学运算(十三)
单片机·嵌入式硬件·32单片机
佳心饼干-6 小时前
单片机-外部中断
单片机·嵌入式硬件
【0931】6 小时前
TIM的中断
stm32·单片机·学习