深度解剖~ FreeRtos阅读笔记3 freertos调度器启动、中断优先级管理、中断优先级分组

3. freertos调度器启动、中断优先级管理、中断优先级分组

永远不要小看不起眼的东西,哪怕是短短的一行代码!

(某些图片分辨率过大显示不清楚,保存到本地或拖动可放大)。

很多例程将vTaskStartScheduler函数作为main执行的最后一行代码,因为执行了vTaskStartScheduler后主权便交给了freertos调度器,程序永远不会在这个函数中得到返回。

vTaskStartScheduler主要动作有:创建IDLE任务,初始化systick等重要寄存器,手动触发中断重新配置MSP并返回到handle模式执行第一个任务。如图展示源码主要流程:

(调度器启动流程)

流程分析:

3.1 创建IDLE任务

vTaskStartScheduler函数中创建了名为IDLE的任务,这个任务的功能先不管它,先饶它不死等对源码逐个击破后它就会不攻自破。

 

3.2 关中断

某些不可重入代码段或定义的一些全局变量,在使用它们之前往往先进入临界区保护下,以免突发事件产生时破坏当前正要处理的数据。比如一些全局变量,如果在中断里或多个任务中都被使用到,当任务正在处理某个全局数组的数据时,此时中断发生并更新了数组中的数据,这样中断返回后就会产生一些不可预料的结果。

记得在实习那会写过不少这样的错误代码,没有考虑代码能不能重入,结果只能害人害己啊~。

在task.c中有一些全局变量,freertos为了保护这些变量数据在处理时不被异常破坏而使用了中断屏蔽,不过这种操作并没有将所有可屏蔽中断一股脑全部杀死:freertos提供了“可管理的中断范围”。以某个中断优先级为阈值,优先级低于(或等于)此分界线的中断将全部屏蔽,较高的中断则不受影响。当然不会白白让“高等”优先级执行的,代价便是freertos不会让高优先级的中断参与任何api的调用。所以“高等”优先级是否触发都不会影响freertos的正常运行。

事实上能够有如此方便的屏蔽操作还要归功于CM3/CM4中NVIC的强大。写到这里,终于引出今天的主角了~。NVIC :嵌套向量中断控制器是CM3/CM4内核水平上搭载了一个异常响应系统,支持为数众多的系统异常和外部中断。不仅如此,NVIC拥有者更多精巧的设计,BASEPRI寄存器便是其中之一,屏蔽中断所使用的阈值就存储在其中,屏蔽时将需要屏蔽中断的最高优先级值填入寄存器便能达到效果。如果向BASEPRI写0则意味着停止掩蔽任何中断。

在配置头文件中可以看到阈值的身影:configMAX_SYSCALL_INTERRUPT_PRIORITY

freertos执行中断屏蔽的源码:

mov r1, #configMAX_SYSCALL_INTERRUPT_PRIORITY                    

msr basepri, r1   

3.3 优先级定义

CM3中使用了8位寄存器来表达中断优先级,所以原则上,CM3支持256级可编程优先级和3个固定优先级(复位,NMI ,硬 fault)。

但是,绝大多数 CM3 芯片都会精简设计,对优先级有效位数进行裁剪以达到减少优先级数的目的,不过无论怎样裁剪,有效位都是在高位开始算起。

注:CM3 允许的最少使用位数为 3 个位,亦即至少要支持 8 级优先级。增加位数将增加更多成本和功耗。

优先级的数值越小,则优先级越高

举个例子,使用了3位有效位表达优先级的寄存器:

(使用了3位有效位表达优先级)

对有效位写1能读回1,无效位读回0,上图寄存器中,若对它写入0xFF则读回0xE0,可以使用读回的方法判断芯片支持多少级中断。

对比下3位和4位优先级状况:

(3-4优先级对比)

上图还是来自宝典,有图截取真爽。很明显4位要比3位表示的优先级范围更细致些,当然这是废话,不过为什么一定要将有效位放在高位,而不放在低位更直观。这也完全是为了开发者考虑,高位可以简化程序的跨器件移植:

假设一个程序要从4位向3位上移植(3位向4位上移植肯定是没问题的,因为4位范围更广),在没有刻意更改优先级的情况下如图:

(MSB减少有效位状况)

移植后优先级有效位减少,原来相邻的两个优先级被置为一个,消失的中断优先级被拉高,不过这些改变将不会带来致命错误。

如果使用低位来代表有效位的话将不会这样幸运了:

(LSB减少有效位状况)

红色区域出现移植后超过7的优先级反而升高,这对于之前的程序将会带来不可预测的错误。

 

3.4优先级分组

最后一个函数xPortStartScheduler中有一段断言宏打开时执行的代码,调试阶段肯定要将断言配置打开,这样便于程序在调试阶段及时发现异常。程序段如下:

乍一看完全不知道执行这段程想要达到的目的,想要弄清楚之前首先要搞懂优先级分组的原则。

关于优先级分组的信息,引用宝典的原话:“为了使抢占机能变得更可控,CM3 还把 256 级优先级按位分成高低两段,分别是抢占优先级和亚优先级。 NVIC 中有一个寄存器是“应用程序中断及复位控制寄存器”,它里面有一个位段名为“优先级组”。该位段的值对每一个优先级可配置的异常都有影响——把其优先级分为个位段:MSB 所在的位段(左边的)对应抢占优先级,而 LSB 所在的位段(右边的)对应亚优先级”。

(分组位与分组优先级对应表)

抢占优先级决定了抢占行为:当系统正在响应某异常 L 时,如果来了抢占优先级更高的异常 H,则 H 可以抢占 L。亚优先级则处理“内务”:当抢占优先级相同的异常有不止一个悬起时,就优先响应亚优先级最高的异常。这种优先级分组规定:亚优先级至少是 1 个位。因此抢占优先级最多是 7 个位,造成了最多只有 128 级抢占的现象。但是 CM3 允许从比特 7 处分组,此时所有的位都表达亚优先级,没有任何位表达抢占优先级,因而所有优先级可编程的异常之间就不会发生抢占——相当于在它们之中除能了CM3 的中断嵌套机制。

注:分组位置可以在无效位,在任意无效位的效果都一样,它们都舍去了亚优先级。

那么了解优先级分组之后,再去分析上面断言宏的主要动作,先向某个外部中断优先级配置寄存器写0xFF再读回,主要判断芯片有多少优先级有效位,然后根据有效位左移依次将最大分组数7减1,这样便得到了能够划分抢占优先级为最大数的分组最大位置,例如开发板优先级有效位为3(优先级寄存器7,6,5位),优先级最多能被分为8组,但是能够将其分为8组的分组位置为4,3,2,1,0。这段程序只取最大值所以ulMaxPRIGROUPValue值得到为4。

ulMaxPRIGROUPValue值将会在freertos提供的中断api中被使用到,这些api中都会首先执行vPortValidateInterruptPriority()函数。这个函数中有两个断言宏,第一个检查了当前的中断优先级是否低于可屏蔽的中断(上文提到高于可屏蔽的优先级不允许调用freertos的中断api);第二个检查NVIC中设置的分组值是否大于ulMaxPRIGROUPValue,如果大于则意味着存在着主优先级和亚优先级,freertos不允许存在亚优先级,否则断言宏伺候,贴下注释原话:Priority grouping:  The interrupt controller (NVIC) allows the bits that define each interrupt's priority to be split between bits that define the interrupt's pre-emption priority bits and bits that define the interrupt's sub-priority.  For simplicity all bits must be defined to be pre-emption priority bits.  The following assertion will fail if this is not the case (if some bits represent a sub-priority).

按注释的描述是为了简化而不去设置亚优先级。

3.5 调度器最后的配置

在调度器启动之前还进行了:

1.      配置了两个中断PendSV 和SysTick的优先级,配置头文件中的宏

configKERNEL_INTERRUPT_PRIORITY表示它们的中断级别,configKERNEL_INTERRUPT_PRIORITY数值一定要大于或等于宏configMAX_SYSCALL_INTERRUPT_PRIORITY的值,或者说PendSV 和SysTick中断优先级一定要在可屏蔽中断范围内,若相反的话可就乱成一锅粥了。

2.      开启freertos的心脏systick,填入计数值来决定systick中断频率,也是任务调度频率或说是时间片长度。

3.      跳入prvPortStartFirstTask函数:

ldr r0, NVIC_VTABLE_R  // NVIC_VTABLE_R: 0xE000ED08 ,向量表偏移量寄存器的地址,需要现将向量表重定向

ldr r0, [r0]        

ldr r0, [r0]      //取出MSP新地址  

msr msp, r0    //重新配置MSP地址                               

cpsie i       //开中断                     

dsb

isb

svc #0   //触发SVC中断

至此,在进入SVC中断之前,freertos开启调度器代码流程已执行完毕,等待SVC中断处理完成后freertos便进入正轨。奔跑吧!fucking source code

猜你喜欢

转载自my.oschina.net/u/3699634/blog/1546484