FreeRTOS — 临界段和开关中断
以下内容转载自安富莱电子:http://forum.armfly.com/forum.php
1、临界段
代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。
FreeRTOS 源码中就有多处临界段的处理,跟 FreeRTOS 一样,uCOS-II 和 uCOS-III 源码中都是有临界段的,而 RTX 的源码中不存在临界段。另外,除了 FreeRTOS 操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
2、任务代码临界段处理
FreeRTOS 任务代码中临界段的进入和退出主要是通过操作寄存器 basepri 实现的。进入临界段前操作寄存器 basepri 关闭了所有小于等于宏定义 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY所定义的中断优先级,这样临界段代码就不会被中断干扰到,而且实现任务切换功能的 PendSV 中断和滴答定时器中断是最低优先级中断,所以此任务在执行临界段代码期间是不会被其它高优先级任务打断的。退出临界段时重新操作 basepri 寄存器,即打开被关闭的中断(这里我们不考虑不受 FreeRTOS 管理的更高优先级中断)。FreeRTOS 进入和退出临界段的函数如下:
通过上面的两个函数 vPortEnterCritical 和 vPortExitCritical 可以看出,进入临界段和退出临界段是通过函数调用开关中断函数 portENABLE_INTERRUPTS 和 portDISABLE_INTERRUPTS 实现的。细心的读者还会发现上面的这两个函数都对变量 uxCriticalNesting 进行了操作。这个变量比较重要,用于临界段的嵌套计数。初学的同学也许会问这里直接的开关中断不就可以了吗,为什么还要做一个嵌套计数呢?主要是因为直接的开关中断方式不支持在开关中断之间的代码里再次执行开关中断的嵌套处理,假如当前们
的代码是关闭中断的,嵌套了一个含有开关中断的临界区代码后,退出时中断就成开的了,这样就出问题了。通过嵌套计数就有效地防止了用户嵌套调用函数 taskENTER_CRITICAL 和 taskEXIT_CRITICAL 时出错。
经过这么多次的宏定义后,终于来到了最终的原始函数。FreeRTOS 的这种层层调用宏定义的方法在带来便利操作的同时,却让用户在分析源码的时候非常不方便。通过上面的源码实现可以看出,FreeRTOS 的开关全局中断是通过操作寄存器 basepri 实现的,关于这个寄存器,已经在 “FreeRTOS— 中断优先级配置” 进行了详细的讲解,这里不再赘述。
使用举例:
使用的时候一定要保证成对使用
嵌套使用举例:
3 、中 断 服 务 程 序 临 界 段 处 理
与任务代码里临界段的处理方式类似,中断服务程序里面临界段的处理也有一对开关中断函数。
通过上面的源码可以看出,中断服务程序里面的临界段代码的开关中断也是通过寄存器 basepri 实现的。初学的同学也许会问,这里怎么没有中断嵌套计数了呢?是的,这里换了另外一种实现方法,通过保存和恢复寄存器 basepri 的数值就可以实现嵌套使用。如果大家研究过 uCOS-II 或者 III 的源码,跟这里的实现方式是一样的,具体看下面的使用举例。
使用举例:
使用的时候一定要保证成对使用
4 、开 关 中 断 的 实 现
FreeRTOS 也专门提供了一组开关中断函数,实现比较简单,其实就是前面 第2小节里面临界段进入和退出函数的精简版本,主要区别是不支持中断嵌套。具体实现如下:
从上面的源码可以看出,FreeRTOS 的全局中断开关是通过操作寄存器 basepri 实现的,关于这个寄存器,前面已经讲过,这里不再赘述。
使用举例:
使用的时候一定要保证成对使用
5 、BSP 板 级 支 持 包 中 开 关 中 断 的 特 别 处 理
前面为大家讲解了 FreeRTOS 临界段的处理方法和开关中断方法,加上了 FreeRTOS 操作系统后,我们实际编写的外设驱动又该怎么修改呢?因为外设驱动编写时,有些地方有用到开关中断操作,这里以教程配套的 STM32F103,F407 和 F429 开发板为例进行说明,这三种开发板的外设驱动的编写架构都是统一的,用户只需将 bsp.h 文件里面的宏定义:
临界段开关中断实验,实验现象:
K1键按下 挂起任务VTaskLED
K2键按下 启动单次定时器中断,50ms后在定时器中断将任务vTaskLED恢复
接口消息处理函数:
static void vTaskTaskUserIF(void *pvParameters) { while(1) { if(ucKeyCode != 0) { switch( ucKeyCode) { /* K1键按下 挂起任务VTaskLED */ case 1: taskENTER_CRITICAL(); /* 进入临界区 */ printf("K2键按下,挂起任务vTaskLED\r\n"); taskEXIT_CRITICAL(); /* 退出临界区 */ vTaskSuspend(xHandleTaskLED1); ucKeyCode = 0; break; /* K2键按下 启动单次定时器中断,50ms后在定时器中断将任务vTaskLED恢复 */ case 2: taskENTER_CRITICAL(); /* 进入临界区 */ printf("K3键按下,启动单次定时器中断,50ms后在定时器中断将任务vTaskLED恢复\r\n"); taskEXIT_CRITICAL(); /* 退出临界区 */ BASIC_TIMx_Mode_Config(); ucKeyCode = 0; break; /* 其他的键值不处理 */ default: break; } vTaskDelay(20); } } }
K1键按下,程序执行case1,把原本闪烁的LED灯挂起。
k2键按下,程序执行case2,然后调用定时器初始化结构体,BASIC_TIMx_Mode_Config();
void BASIC_TIMx_Mode_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; // 开启TIMx_CLK,x[6,7] RCC_APB1PeriphClockCmd(BASIC_TIMx_CLK, ENABLE); /* 累计 TIM_Period个后产生一个更新或者中断*/ //当定时器从0计数到49,即为50次,为一个定时周期 TIM_TimeBaseStructure.TIM_Period = 50-1; //定时器时钟源TIMxCLK = 2 * PCLK1 // PCLK1 = HCLK / 4 // => TIMxCLK=HCLK/2=SystemCoreClock/2=90MHz // 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=1MHz TIM_TimeBaseStructure.TIM_Prescaler = 90-1; /* T = 50/1000000 = 50us */ // 初始化定时器TIMx, x[2,3,4,5] TIM_TimeBaseInit(BASIC_TIMx, &TIM_TimeBaseStructure); // 清除定时器更新中断标志位 TIM_ClearFlag(BASIC_TIMx, TIM_FLAG_Update); // 开启定时器更新中断 TIM_ITConfig(BASIC_TIMx,TIM_IT_Update,ENABLE); // 使能定时器 TIM_Cmd(BASIC_TIMx, ENABLE); }
定时器开始定时,执行定时器中断函数,
void BASIC_TIMx_IRQHandler(void) { if ( TIM_GetITStatus( BASIC_TIMx, TIM_IT_Update) != RESET ) { ulHighFrequencyTimerTicks++; TimeOut++; TIM_ClearITPendingBit(BASIC_TIMx , TIM_IT_Update); } if(TimeOut == 1000) //50us*1000 = 50ms { TIM_Cmd(BASIC_TIMx, DISABLE); //不使能定时器,此时定时器不定时 TimeOut = 0; //让TimeOut重新置零 BaseType_t xYieldRequired; UBaseType_t uxSavedInterruptStatus; uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); /* 进入临界区 */ { /* 用户可以在这里添加临界段代码,我们这里暂时未用到 */ } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); /* 退出临界区 */ /* 恢复挂起任务 */ xYieldRequired = xTaskResumeFromISR(xHandleTaskLED1); /* 退出中断后是否需要执行任务切换 */ if( xYieldRequired == pdTRUE ) { portYIELD_FROM_ISR(xYieldRequired); } } }
定时器定时到50ms时,TIM_Cmd(BASIC_TIMx, DISABLE); //不使能定时器,此时定时器不定时。
等到下次按下K2的时候再调用用定时器初始化结构体,并使能定时器 TIM_Cmd(BASIC_TIMx, ENABLE);
notice
/*===========================================可屏蔽的中断优先级配置====================================================*/ /* * 用于配置STM32的特殊寄存器basepri寄存器的值,用于屏蔽中断,当大于basepri值的优先级的中断将被全部屏蔽。basepri只有4bit有效, * 默认只为0,即全部中断都没有被屏蔽。configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY配置为:5,意思就是中断优先级大于5的中断都被屏蔽。 * 当把配置好的优先级写到寄存器的时候,是按照8bit来写的,所以真正写的时候需要经过转换,公式为: * ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff),其中的priority就是我们配置的真正的优先级。经过这个公式之后得到的是下面的这个宏: * configMAX_SYSCALL_INTERRUPT_PRIORITY * * 在FreeRTOS中,关中断是通过配置basepri寄存器来实现的,关掉的中断由配置的basepri的值决定,小于basepri值的 * 中断FreeRTOS是关不掉的,这样做的好处是系统设计者可以人为的控制那些非常重要的中断不能被关闭,在紧要的关头必须被响应。 * 而在UCOS中,关中断是通过控制PRIMASK来实现的,PRIMASK是一个单1的二进制位,写1则除能除了NMI和硬 fault的所有中断。当UCOS关闭 * 中断之后,即使是你在系统中设计的非常紧急的中断来了都不能马上响应,这加大了中断延迟的时间,如果是性命攸关的场合,那后果估计挺严重。 * 相比UCOS的关中断的设计,FreeRTOS的设计则显得人性化很多。 * */ #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 2 #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
欢迎加入作者的小圈子
扫描下方左边二维码加入QQ交流群,扫描下方右边二维码关注个人微信公众号并,获取更多隐藏干货,QQ交流群:859800032 微信公众号:Crystal软件学堂
作者:Liu_Jing bilibili视频教程地址:https://space.bilibili.com/5782182 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在转载文章页面给出原文连接。 如果你觉得文章对你有所帮助,烦请点个推荐,你的支持是我更文的动力。 文中若有错误,请您务必指出,感谢给予我建议并让我提高的你。 |