裸机中断和FreeRTOS中断

FreeRTOS可以与STM32原生的中断机制结合使用,但它提供了自己的中断管理机制,主要是为了提供更强大和灵活的任务调度和管理功能,但是FreeRTOS并不是接管硬件中断本身,而是管理中断和任务调度之间安全协作

在STM32中,中断优先级是通过中断优先级配置寄存器的高4位[7:4]来配置的,因此STM32支持最多16级中断优先级,其中数值越小表示优先级越高,即更紧急的中断(Free RTOS任务调度的任务优先级相反,是数值大的越优先)

裸机中断

在裸机中的中断,当外设满足条件的时候,比如串口接收到数据、按键触发、定时器溢出,然后CPU会暂停当前的执行流,然后进入中断服务函数ISR,等ISR执行完后再返回原来的地方继续跑

FreeRTOS中断

在FreeRTOS的中断中前面与裸机中断的流程差不多,但是在ISR执行完后不一定回到原来那个任务中,也可能切换去另一个被ISR唤醒的更高优先级的任务(FromISR的API主要任务是更新内核对象和标记是否需要切换或唤醒任务)

  • 在FreeRTOS中,将PendSV和SysTick设置最低中断优先级(数值最小为15,因为SysTick被用作FreeRTOS的时钟源所以也得设置为较低优先级),保证系统任务切换不会阻塞系统其他中断的响应
  • FreeRTOS利用STM32中的BASEPRI寄存器(在Cortex-M架构中的用来设置最小可响应中断优先级的寄存器)实现中断管理,屏蔽优先级低于某一个阈值的中断,比如: BASEPRI设置为0x50(只看高四位,也就是5),代表中断优先级在5~15内的均被屏蔽,0~4的中断优先级正常执行,只能在被屏蔽的中断优先级的ISR函数内部调用xxxFromISR函数
    • 在ESP32中是用Xtensa架构的,中断优先级、屏蔽方式、上下文切换实现细节都不是同一套硬件逻辑
  • FreeRTOS中普通的API不能在ISR中调用,只能调用有FromISR后缀的API,因为普通任务的上下文和中断的上下文是区分来的,而且普通API允许阻塞等待,在ISR中的栈、运行方式和任务不同,而且在ISR中如果要触发调度时不能像任务里那样直接做完整切换,而是要用更安全的“申请切换”方式

以下就是在FreeRTOS配置文件中配置FreeRTOS可管理范围的中断,0~4优先级不能调用FreeRTOS的API

/*设置RTOS内核自身使用的中断优先级,一般设置为最低优先级,不至于屏蔽其他优先级程序*/
#define configKERNEL_INTERRUPT_PRIORITY (15 << 4)
/*设置了 调用中断安全的 FreeRTOS API 函数的最高中断优先级,FreeRTOS中断安全API的最高中断优先级阈值*/        
#define configMAX_SYSCALL_INTERRUPT_PRIORITY  (5 << 4)
/*同上,仅用于新版移植,这两者是等效的*/  
#define configMAX_API_CALL_INTERRUPT_PRIORITY   configMAX_SYSCALL_INTERRUPT_PRIORITY

FreeRTOS的开关中断

FreeRTOS开关中断函数其实是宏定义,在portmacro.h中有定义,如下:

#define portDISABLE_INTERRUPTS()                  vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS()                   vPortSetBASEPRI( 0 )

  • 调用portDISABLE_INTERRUPTS() , FreeRTOS会打开管理的所有中断,即开启可屏蔽中断,把BASEPRI提高到FreeRTOS设定的阈值,在被屏蔽的优先级可以调用FreeRTOS中断
  • 调用portENABLE_INTERRUPTS() , FreeRTOS会关闭管理的所有中断,0表示不屏蔽,把BASEPRI清零,恢复所有中断响应

FreeRTOS中断流程代码

  • 这里涉及到了任务通知的API,这里主要是展示了在FreeRTOS中定时器5ms中断唤醒任务
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */
  if(htim->Instance==TIM1)
  {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if(chassic_ctrl_task_handle != NULL)
    {
      vTaskNotifyGiveFromISR(task_handle,&xHigherPriorityTaskWoken);
      portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
  }
  /* USER CODE END Callback 0 */
}

void task(void * pvParameters)
{
    for (;;)
    {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        ...
    }
}

FreeRTOS临界区

临界区的作用是为了保护共享资源或者内核关键数据结构,避免执行到一般时被别的执行流打断,导致数据错乱

  • 比如:在单片机中可能同时存在几种执行流,当前正在运行的任务、另一个更高优先级的任务、中断服务函数ISR,这几个如果同时访问同一个变量、同一个缓冲区、同一个链表就可能出问题,就比如count++,CPU将count从内存读到寄存器,寄存器+1,写回内存,如果+1的过程如果被打断了在中断服务函数里面也改变了count然后回去再写数据就被覆盖掉了

所以FreeRTOS需要一个临界区来保护变量、缓冲区、状态机等,防止可管理范围的中断打断当前代码或者是调度器在不合适的时机切换任务,也是借助BASEPRI屏蔽一部分中断

taskENTER_CRITICAL() :
//进入临界段,就会开启可屏蔽中断,任务切换和系统滴答定时器中断都是最低优先级为15,但是只要开启可屏蔽中断,要小于设置的可屏蔽优先级才可以触发中断即便为15也还是会被屏蔽,所以就保护了任务不会被切换
//但是注意临界段不是一定保险,只保证不发生任务切换,和不会被屏蔽的中断给打断,但是没有被屏蔽的中断还是会有可能打断任务

taskEXIT_CRITICAL() ://退出临界段,同样道理就是关闭可屏蔽中断

taskENTER_CRITICAL_FROM_ISR() ://进入临界段(中断级)

taskEXIT_CRITICAL_FROM_ISR()://退出临界段(中断级)

与portDISABLE_INTERRUPTS()的区别:

  • portDISABLE_INTERRUPTS() / portENABLE_INTERRUPTS()更底层,直接操作端口层的中断屏蔽机制

  • taskENTER_CRITICAL() / taskEXIT_CRITICAL()更适合应用代码使用,而且支持嵌套

进入和退出临界段是成对使用的,每进入一次临界段,全局变量uxCriticalNesting都会加一,每调用一次退出临界段,uxCriticalNesting减一,只有当 uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS()使能可屏蔽中断,这确保了在存在多个临界段代码的情况下,不会因为某个临界段代码的退出而破坏其他临界段的保护,只有当所有的临界段代码都退出时,中断才会被重新使能

但是注意的是:临界区期间会屏蔽中断或者阻止调度的一部分能力,所以尽量保持短的临界区,不适合在临界区进行串口打印、大量循环处理、协议解析、访问慢速外设、延时、等待某个条件成立等等

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐