STM32之CAN通讯(十一)

news2025/1/8 17:38:08

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()函数调用。代码中有详细的介绍,代码如下(示例):

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寄存器状态。

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

#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文库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2273340.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

初学Linux电源管理

学习文档出处&#xff1a; 万字整理 | 深入理解Linux电源管理&#xff1a;万字整理 | 深入理解Linux电源管理-CSDN博客 电源管理 因为设备需要用电&#xff0c;而且设备中的各个硬件所需要的电压是不一样的。故计算机需要对硬件的电源状态管理。但是电能并不是免费的&#x…

若依中Feign调用的具体使用(若依微服务版自身已集成openfeign依赖,并在此基础上定义了自己的注解)

若依中Feign调用具体使用 注意&#xff1a;以下所有步骤实现的前提是需要在启动类上加入注解 EnableRyFeignClients 主要是为开启feign接口扫描 1.创建服务提供者(provider) 导入依赖(我在分析依赖时发现若依本身已经引入openfeign依赖,并在此基础上自定义了自己的EnableRyF…

CS·GO搬砖流程详细版

说简单点&#xff0c;就是Steam买了然后BUFF上卖&#xff0c;或许大家都知道这点&#xff0c;但就是一些操作和细节问题没那么明白。我相信&#xff0c;你看完这篇文章以后&#xff0c;至少会有新的认知。 好吧&#xff0c;废话少说&#xff0c;直接上实操&#xff01; 首先准…

每日一题:链表中环的入口结点

文章目录 判断链表环的入口节点描述数据范围&#xff1a;复杂度要求&#xff1a;输入输出 示例代码实现思路解析注意事项&#xff1a; 判断链表环的入口节点 描述 给定一个链表&#xff0c;判断该链表是否存在环。如果存在环&#xff0c;返回环的入口节点&#xff1b;如果不存…

以C++为基础快速了解C#

using System: - using 关键字用于在程序中包含 System 命名空间。 一个程序一般有多个 using 语句, 相当于C的 using namespace std; C# 是大小写敏感的。 所有的语句和表达式必须以分号&#xff08;;&#xff09;结尾。 程序的执行从 Main 方法开始。 与 Java 不同的是&#…

保险丝驱动电路·保险丝有什么用应该如何选型详解文章!!!

目录 保险丝基础知识 保险丝常见类型 保险丝功能讲解 保险丝驱动电路 ​​​​​​​ ​​​​​​​ 编写不易&#xff0c;仅供学习&#xff0c;请勿搬运&#xff0c;感谢理解 常见元器件驱动电路文章专栏连接 LM7805系列降压芯片驱动电路降压芯片驱动电…

如何在读博过程中缓解压力

博士生涯充满了挑战和压力&#xff0c;但通过一些实用的方法&#xff0c;我们可以有效地缓解这些压力。以下是我在博士期间采用的一些策略&#xff0c;希望能对正在读博或即将开始博士生涯的你有所帮助。 1. 周末彻底放松 在周末&#xff0c;我尽量避免进行论文写作。这两天…

ue5 替换角色的骨骼网格体和动画蓝图

一开始动画蓝图&#xff0c;骨骼网格体都是用的女性角色 现在把它换成男性 编译 保存 运行 把动画类换成ABP_Manny 进入ABP_Manny中 进入到idle 找到这个拖进来 编译 就变成站着端枪 运行一下&#xff0c;没有问题

西南大学计算机复试该怎么准备?有哪些注意事项?

西南大学计算机专业复试只有面试&#xff01;只要你表现的自信大方&#xff0c;专业知识问题回答的很好&#xff0c;一般都没问题 一、考试内容 复试的考核内容包含以下几个方面&#xff1a; 1.专业素质和能力&#xff08;占复试成绩的60%&#xff09; &#xff08;1&#x…

【UI自动化测试】selenium八种定位方式

&#x1f3e1;个人主页&#xff1a;謬熙&#xff0c;欢迎各位大佬到访❤️❤️❤️~ &#x1f472;个人简介&#xff1a;本人编程小白&#xff0c;正在学习互联网求职知识…… 如果您觉得本文对您有帮助的话&#xff0c;记得点赞&#x1f44d;、收藏⭐️、评论&#x1f4ac;&am…

RDD的相关算子

一&#xff0c;算子的分类 整个DRR算子分为两大类&#xff1a; Transformation&#xff08;转换算子&#xff09;&#xff1a; 返回值&#xff1a;是一个新的DRR 特点&#xff1a;转换算子只是定义数据的处理规则&#xff0c;并不会立刻执行&#xff0c;是lazy&#xff08;…

简单编程实现QT程序黑色主题显示

代码如下 int main(int argc, char *argv[]) {QApplication a(argc, argv);//QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());QPalette darkpalette;a.setStyle(QStyleFactory::create("Fusion"));darkpalette.setColor(QPalette::Wind…

【Redis】简介|优点|使用场景|为什么Redis快

目录 一、简介 二、特性&#xff08;优点&#xff09; 三、使用场景 一、简介 内存中存储数据的中间件&#xff0c;用于数据库&#xff0c;数据缓存&#xff0c;在分布式系统中能够大展拳脚 中间件&#xff1a;应用程序可以直接从 Redis 中获取数据&#xff0c;而不必频繁地…

封装深拷贝方法

前言 在今年的四月份我写了一篇有关深拷贝的博客文章 我与深拷贝_radash 深拷贝-CSDN博客。在该文章中有一个令我感到遗憾的点就是我没有实现一个自己手写的深拷贝。如今我想弥补当初的遗憾&#xff0c;在这篇文章中详细的讲述一下如何手写一个深拷贝方法。 lodash中是如何实…

maven多模块项目编译一直报Failure to find com.xxx.xxx:xxx-xxx-xxx:pom:1.0-SNAPSHOT in问题

工作中项目上因为多版本迭代&#xff0c;需要对不同迭代版本升级版本号&#xff0c;且因为项目工程本身是多模块结构&#xff0c;且依然多个其他模块工程。 在将工程中子模块的pom.xml中版本号使用变量引用父模块中定义的版本号时&#xff0c;一直报Failure to find com.xxx.x…

uniapp 微信小程序 自定义日历组件

效果图 功能&#xff1a;可以记录当天是否有某些任务或者某些记录 具体使用&#xff1a; 子组件代码 <template><view class"Accumulate"><view class"bx"><view class"bxx"><view class"plank"><…

Mysql--基础篇--函数(字符串函数,日期函数,数值函数,聚合函数,自定义函数及与存储过程的区别等)

MySQL提供了丰富的内置函数&#xff0c;涵盖了字符串处理、数值计算、日期和时间操作、聚合统计、控制流等多种功能。这些函数可以帮助你简化SQL查询&#xff0c;提升开发效率。 除了内置函数&#xff0c;MySQL还支持自定义函数&#xff08;User-Defined Functions&#xff09;…

关于Mac中的shell

1 MacOS中的shell 介绍&#xff1a; 在 macOS 系统中&#xff0c;Shell 是命令行与系统交互的工具&#xff0c;用于执行命令、运行脚本和管理系统。macOS 提供了多种 Shell&#xff0c;主要包括 bash 和 zsh。在 macOS Catalina&#xff08;10.15&#xff09;之前&#xff0c…

外卖院长帝恩以专业打法,开启外卖运营新格局

据中国饭店协会数据显示&#xff0c;2023年我国在线餐饮外卖市场规模已达到15254亿元&#xff0c;同比增长36.7%。预计到2027年&#xff0c;我国在线餐饮外卖市场规模有望达到19567亿元&#xff0c;行业渗透率有望达到30.4%。在这一蓬勃发展的行业背后&#xff0c;离不开无数从…

高清绘画素材3600多张动漫线稿线描上色练习参考插画原画

工作之余来欣赏一波线稿&#xff0c;不务正业版... 很多很多的线稿... 百度网盘 请输入提取码