侧边栏壁纸
博主头像
静水流深

静水流深,沧笙踏歌

  • 累计撰写 29 篇文章
  • 累计创建 0 个标签
  • 累计收到 6 条评论

目 录CONTENT

文章目录

HAL库回调机制

唐韵
2024-11-23 / 0 评论 / 0 点赞 / 15 阅读 / 0 字

在学习STM32中断的时候接触到了回调函数,虽然也在用,但对具体原理一直不明就里,一方面Callback这个名字很有迷惑性,这也是很多计算机术语的通病,另一方面个人对一些编程思想缺乏了解,造成理解上的困难。拖了很久终于下定决心来研究一下,回调机制在事件驱动编程中有很广泛的应用,搞清楚这个机制是很有必要的。

找到一篇文章感觉已经讲得非常清楚了,行文旁征博引,举例浅显易懂。
为什么HAL库有那么多的回调函数?如何理解? - 知乎

下面也根据自己的经验和从别处看到的资料记录一些想法


HAL库中有很多以Callback结尾的回调函数,根据了解到的回调函数的意义,在一个事件完成后自动执行某一个操作,执行这个操作的函数就叫做回调函数。回调函数本质上也体现了一种异步处理的思想:在触发一个事件后自动执行既定的操作,与此同时不影响主程序的运行,从而避免对主程序造成阻塞。

比如串口接收中断是一个事件,这个事件完成后要执行一些操作,这些操作写在一个函数里,这个函数就叫做HAL_UART_RxCpltCallback(UART_HandleTypeDef*huart),即串口接收回调函数。

在HAL库中跟事件有关特别是中断有关的事件基本都有相应的回调函数。如:

串口回调:stm32f4xx_hal_uart.c

voidHAL_UART_IRQHandler(UART_HandleTypeDef*huart);

voidHAL_UART_TxCpltCallback(UART_HandleTypeDef*huart);    //发送回调

voidHAL_UART_TxHalfCpltCallback(UART_HandleTypeDef*huart);

voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart);    //接收回调

voidHAL_UART_RxHalfCpltCallback(UART_HandleTypeDef*huart);

voidHAL_UART_ErrorCallback(UART_HandleTypeDef*huart);

voidHAL_UART_AbortCpltCallback(UART_HandleTypeDef*huart);

voidHAL_UART_AbortTransmitCpltCallback(UART_HandleTypeDef*huart);

voidHAL_UART_AbortReceiveCpltCallback(UART_HandleTypeDef*huart);

定时器回调:stm32f4xx_hal_tim.c

voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim);  //周期运行回调,配置定时进入中断

voidHAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef*htim);//输出比较回调

voidHAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef*htim);  

voidHAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef*htim);

voidHAL_TIM_TriggerCallback(TIM_HandleTypeDef*htim);

voidHAL_TIM_ErrorCallback(TIM_HandleTypeDef*htim);

还有GPIO中断回调等,如

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin == GPIO_PIN_5)
		key_state = 1;
}

在实际应用方面,还是以常用的串口接收中断回调函数为例。

在搞清楚中断回调函数的使用之前还要搞清楚一个重要的概念——中断服务函数(有的也叫中断处理函数) HAL_UART_IRQHandler(UART_HandleTypeDef *huart)

中断服务函数和中断回调函数都是用于处理中断的函数,在某些简单的中断业务中,把业务代码写在服务函数或者回调函数中都可以实现预期功能,但实际上在调用方式方面这两个函数是有一些不同的。

中断服务函数是由操作系统或硬件自动调用的,用于响应中断事件。当中断事件发生时,操作系统或硬件会自动跳转到对应的中断服务函数,并执行其中的代码。中断服务函数通常需要完成对中断事件的处理,包括保存寄存器状态、清除中断标志、响应中断等。

中断回调函数则是由应用程序注册并提供给操作系统或驱动程序的。当中断事件发生时,操作系统或驱动程序会调用应用程序注册的中断回调函数,并将中断事件的相关信息作为参数传递给回调函数。中断回调函数的作用是让应用程序能够处理中断事件,例如更新界面、处理数据等。

这两段话其实也挺抽象的,在这篇文章中分别用两个函数展示了中断服务函数和中断回调函数的使用,方便直观的理解。
STM32串口接收中断——基于HAL库 - 山无言 - 博客园
文章的最后一句话:方法2(即中断处理函数)使用了标准库中断处理数据的思想。这句话提供了一个重要的信息,即:在中断处理函数中编写中断业务代码是使用标准库时的习惯,而在HAL库中我们更习惯使用回调函数来编写业务代码。

以下信息来自AI:
在 STM32 标准库中,回调函数并不是一个直接的概念,但回调函数的实现是可以通过中断服务函 数(ISR)或其他事件驱动方式来实现的。
STM32 标准库没有内建的回调函数机制(像某些高级操作系统或框架那样),你仍然可以通过函数指针来模拟回调机制。通过设置函数指针,你可以在特定事件发生时调用不同的函数。
HAL 库已经内置了回调机制。例如,定时器、外部中断、DMA 等模块在初始化时支持指定回调函数,当中断发生或事件触发时,自动调用这些回调函数来响应各种事件。

HAL库:中断处理函数 及 weak弱声明中断回调函数 详解_hal库中断回调函数-CSDN博客

这篇文章中也提到了标准库和HAL库中中断处理方式的区别。

总结:

  • 中断服务函数 是由硬件触发的,是处理硬件中断的入口,主要用于执行最低级别的中断响应(如清除标志、保存上下文等)。
  • 中断回调函数 是一种事件驱动的机制,用于在中断发生后执行特定的业务逻辑。通常,它由中断服务函数触发,允许开发者在中断事件发生时灵活地指定要执行的操作。

由此可见,在HAL库中我们不必再过于关注中断服务函数,只要把中断业务代码写到相应的回调函数里面即可。

但从学习的角度来说,还是有必要探究以下中断回调函数是如何被调用的,其实它还是与中断服务函数有关的。

我们先来看看中断服务函数和中断回调函数都是在哪里定义的。

属于uart2的中断服务函数(这个函数定义于stm32fxx.it.c文件,显然这不属于HAL库而属于标准库)

void USART2_IRQHandler(void)
{
  /* USER CODE BEGIN USART2_IRQn 0 */
	__HAL_UART_ENABLE_IT(&huart3, UART_IT_RXNE);
  /* USER CODE END USART2_IRQn 0 */
  
    HAL_UART_IRQHandler(&huart2);

  /* USER CODE BEGIN USART2_IRQn 1 */
    //按照标准库的习惯,可以直接在这里编写业务代码
  /* USER CODE END USART2_IRQn 1 */

}

属于所有uart的中断回调函数(定义于stm32f4xx_hal_uart.c文件中,明显这是独属于HAL库的函数)

__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);
  /* NOTE: This function should not be modified, when the callback is needed,
           the HAL_UART_RxCpltCallback could be implemented in the user file
   */
}

另外我们也发现一个有趣的点:每个串口都有一个单独的中断服务函数,这个函数没有参数,对应串口中断触发后就调用服务函数,然后执行服务函数里的业务代码;而中断回调函数面向所有的串口,参数是串口句柄指针,所以还要在回调函数里先写if(huart==&huart2)来判断中断具体来自哪个串口。

if(huart==&huart2)
    {
		//业务代码    
        HAL_UART_Receive_IT(&huart2, (uint8_t *)&aRxBuffer, 1);  
    }

同时也要注意HAL_UART_Receive_IT(&huart2, (uint8_t *)&aRxBuffer, 1);这一句,如果不写这一句,串口中断只会执行这一次(在main的初始化中曾经打开过一次接收中断) ,然后就关闭了中断。所以在每次回调之后在执行一遍打开接收中断,以等待下一次的中断发生。

那么回调函数具体是怎么被调用的呢,在USART2_IRQHandler(void)中有一句 HAL_UART_IRQHandler(&huart2);,具体去查看这个函数的定义,就会发现有callback相关的代码。

再举一个关于定时器中断回调的例子,之前在使用一个电机驱动器时需要上位机以500ms为周期向驱动器发送心跳包,心跳包是一串固定的十六进制数,这时候就要把发送函数(当时使用的是CAN通信)放到定时器中断回调函数里面。在配置好定时器周期后,定时器每计数500ms后就触发定时器中断,中断发生后就自动调用定时器中断回调函数,从而执行里面的发送函数。这样就实现了定时发送功能。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	
  if (htim->Instance == TIM6) 
   {
     HAL_IncTick();
   }
***********************************************************************
   if(htim->Instance == TIM2)  
    {
      //定时器TIM2周期500ms,向驱动器发送心跳指令
	  HAL_GPIO_TogglePin(GPIOG, GPIO_PIN_3);//LED闪烁指示心跳
            //CAN发送心跳指令  
		    CAN2_0x201_Tx_Data[0] =0x00;			
			CAN_Send_Data(&hcan1, 0x05, CAN2_0x201_Tx_Data, 1);
			CAN_Send_Data(&hcan1, 0x06, CAN2_0x201_Tx_Data, 1);
			CAN_Send_Data(&hcan1, 0x07, CAN2_0x201_Tx_Data, 1);
	  }
************************************************************************	  
	if (htim->Instance == TIM3)
	 {
		//业务代码未列出
     }

}

同样需要注意的是,在STM32中所有定时器的中断回调函数都是同一个,即HAL_TIM_PeriodElapsedCallback(),具体是哪一个定时器中断完成后完成后调用的回调函数,还需要用 if 语句具体的判断,因为这个回调函数传入的参数就是定时器的句柄。


关于回调函数应该写在哪个文件中

上文提到,HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) 以弱定义的方式定义在 stm32f4xx_hal_uart.c 文件中,我们可以在所有包含stm32f4xx_hal_uart.h的文件中重新强定义HAL_UART_RxCpltCallback()函数并在其中编写业务代码。

__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);
  /* NOTE: This function should not be modified, when the callback is needed,
           the HAL_UART_RxCpltCallback could be implemented in the user file
   */
}

一般来说,在裸机开发中,直接将回调函数写在main.c文件中即可。如果有更复杂的业务代码,或许需要为回调函数单独写一个文件,目前我个人还没接触到那么复杂的业务,日后若有新的了解再来补充。

关于__weak:函数名称前面加上__weak 修饰符,我们一般称这个函数为“弱函数”。加上了__weak 修饰符的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak 声明的函数,并且编译器不会报错。


从本质上说,回调机制的使用是把底层和应用层尽量的隔离出来 ,从而不去过多修改底层库中的各种服务函数,让一个写应用层的人不用理解背后的逻辑,直接调用回调函数就可以了,这极大的增强了程序的可读性和可移植性。

参考
为什么HAL库有那么多的回调函数?如何理解? - 知乎
浅谈HAL设计(3)- 回调函数 - 知乎
STM32 HAL库学习系列第8篇—回调函数总结 - CodeAllen - 博客园
__weak与函数指针的使用与见解_弱函数代替函数指针-CSDN博客

0

评论区