在前面讲解互斥量时,引入过临界资源的概念,实现了对临界资源的互斥访问。
要独占式地访问临界资源,有 2 种方法:
如上图所示,在M3/M4
系列的芯片中,有一个8bit的寄存器来存放各类中断的优先级,其中复位,NMI,硬件错误三种中断优先级最高,不受该寄存器管辖。
该寄存器管理的中断优先级从0到255,数字越低,优先级越高,这256个中断又分为两类:
SYSCALL
的中断:优先级在0~190
范围内的中断。SYSCALL
的中断:优先级在191~255
范围内的中断,由两个宏来确定这个范围。
- SYSCALL:也就是系统调用,是由FreeRTOS提供的函数接口,比如向队列中写数据
xQueueSend
等等函数。
屏蔽中断有两套宏:任务中使用、ISR 中使用:
任务中使用:
taskENTER_CRITICA()
:入口处屏蔽中断。
如上图代码所示,在函数的入口处调用taskENTER_CRITICAL
来屏蔽中断,防止其他中断和当前任务抢夺临界资源。
底层会先调用portDISABLE_INTERRUPTS
来实现中断屏蔽,并且会让一个全局变量uxCriticalNesting
进行加加。
- 每屏蔽一次中断,变量
uxCriticalNesting
就会加加一次。
屏蔽中断的底层函数vPortRaiseBASEPRI
中,会将宏configMAX_SYSCALL_INTERRUPT_PRIORITY
代表的191赋值给ulNewBASEPRI
,然后再通过汇编代码将优先级低于191的所有中断屏蔽掉。
- 屏蔽中断并不是屏蔽所有中断,而是屏蔽低于191的低优先级中断。
- 高于191的高优先级中断仍然可以产生。
之所以设计为只屏蔽低优先级中断,是因为只有低优先级中断才会调用RTOS的系统调用,这些中断才会涉及到互斥。
taskEXIT_CRITICAL()
:出口处使能中断。
如上图所示代码,在函数的出口处调用taskEXIT_CRITICAL
来重新使能被屏蔽掉的中断,从而解除屏蔽。
底层会先让全局变量uxCriticalNesting
减减,当该全局变量被减为0的时候,再调用portENABLE_INTERRUPTS
实现解除屏蔽。
- 每重新使能一次中断,变量
uxCriticalNesting
就会减减一次。- 当
uxCriticalNesting
为0时,才真正解除屏蔽。
uxCriticalNesting
是为了实现递归调用,它的内部会记录嵌套的深度,只有嵌套深度变为 0 时,调用 taskEXIT_CRITICAL()
才会重新使能中断。在任务中屏蔽中断的示例如下:
如上图向队列中写入数据的函数,在 taskENTER_CRITICA()/taskEXIT_CRITICAL()
之间:进行临界资源的访问。
使用 taskENTER_CRITICA()
和taskEXIT_CRITICAL()
来访问临界资源是很粗鲁的方法:
ISR中使用:
portSET_INTERRUPT_MASK_FROM_ISR()
:入口处屏蔽中断。
如上图代码,在带有后缀FromISR
的系统调用函数入口处,调用port_INTERRUPT_MASK_FROM_ISR
来屏蔽中断。
可以看到,此时屏蔽中断直接调用ulPortRaiseBASEPRI
,中间没有其他封装,也没有记录次数等操作,屏蔽的小于191的低优先级中断,同时将中断状态保存到uxSavedInterruptStatus
。
portCLEAR_INTERRUPT_MASK_FROM_ISR
:出口处解除屏蔽。
如上图代码,在带有后缀FromISR
的函数出口处,调用portCLEAR_INTERRUPT_MASK_FROM_ISR
来解除屏蔽。
可以看到,解除屏蔽时也是直接调用vPortSetBASEPRI
,并且将前面屏蔽中断时保存的低优先级中断状态uxSavedInterruptStatus
传入,在函数内部将屏蔽的中断解除。
在任ISR中屏蔽中断的示例如下:
如上图所示带后缀FromISR
的向队列中写入数据的函数,在portSET_INTERRUPT_MASK_FROM_ISR()/portCLEAR_INTERRUPT_MASK_FROM_ISR()
之间:访问临界资源。
和在任务中屏蔽中断一样,在访问临界资源的过程中,只有高于191的高优先级中断能产生。
如果有别的任务来跟你竞争临界资源,你可以把中断关掉,无法产生Tick中断,也就无法调度,这当然可以禁止别的任务运行,但是这代价太大了,它会影响到其他中断的处理。
- 这种情况是竞争临界资源的既有普通任务,也有中断服务函数,或者只有中断服务函数。
如果只是禁止别的普通任务来跟你竞争,不需要关中断,暂停调度器就可以了:在这期间,中断还是可以发生、处理。
暂停调度器函数
vTaskSuspendAll()
:
如上图代码所示,在vTaskSuspendAll
函数中,有一个全局变量uxSchedulerSuspended
,每暂停一次调度器,该变量就会加加。
恢复调度器
xTaskResumeAll()
:
如上图代码所示,在xTaskResumeAll
中,会将全局变量uxSchedulerSuspended
进行减减,当该变量为0时,恢复调度。
uxSchedulerSuspended
变量同样也是为了禁止和恢复调度函数的递归调用。
使用示例如下:
如上图代码所示,在调用vTaskDelay
时,会访问到阻塞链表,这是一个临界资源,竞争该临界资源的只有普通任务。
所以在进入该函数后,先禁止调度,不让其他任务和当前任务竞争,当将该任务的TCB放入到阻塞链表后,再恢复调度。
FreeRTOS提供了很多调试手段:
configASSERT
打印:
printf:FreeRTOS工程里使用了microlib,里面实现了printf函数。我们只需实现一下fputc函数即可使用printf:
int fputc( int ch, FILE *f );
这就是本喵一直在使用重定向,将打印信息通过UART1打印出来,当然可以重定向到LCD,OLED等设备上。
断言:
一般的C库里面,断言就是一个函数:
void assert(scalar expression);
它的作用是:确认expression必须为真,如果expression为假的话就中止程序。
在FreeRTOS里,使用的是configASSERT()
,比如:
#define configASSERT(x) if (!x) while(1);
其实就是定义了一个宏函数,如果x为真则能顺利运行,如果为假则会陷入死循环。
我们可以让它提供更多信息,比如:
#define configASSERT(x) \
if (!x) \
{
printf("%s %s %d\r\n",\
__FILE__, __FUNCTION__, __LINE__); \
while(1); \
}
这在很多场合都有使用,比如:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = xQueue;
configASSERT( pxQueue );
configASSERT(!((pvItemToQueue == NULL) && (pxQueue->uxItemSize != (UBaseType_t)0U)));
configASSERT( !((xCopyPosition == queueOVERWRITE) && (pxQueue->uxLength != 1 )));
队列不能为空,写入的数据也不能是空指针,并且不能超出队列长度,一旦不符合要求就会发生断言错误。
void vPortValidateInterruptPriority( void )
{
uint32_t ulCurrentInterrupt;
uint8_t ucCurrentPriority;
/* Obtain the number of the currently executing interrupt. */
ulCurrentInterrupt = vPortGetIPSR();
/* Is the interrupt number a user defined interrupt? */
if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
{
/* Look up the interrupt's priority. */
ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];
configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
}
Trace:
FreeRTOS中定义了很多trace开头的宏,这些宏被放在系统个关键位置。
它们一般都是空的宏,这不会影响代码:不影响编程处理的程序大小、不影响运行时间。
我们要调试某些功能时,可以修改宏:修改某些标记变量、打印信息等待。
如上图,在向队列中写数据的时候会调用traceQUEUE_SEND
宏,它原本是一个空的宏,现在本喵让他打印当前任务的名字。
trace宏 | 描述 |
---|---|
traceTASK_INCREMENT_TICK(xTickCount) | 当tick计数自增之前此宏函数被调用。参数xTickCount当前的Tick值还没有增加。 |
traceTASK_SWITCHED_OUT() | vTaskSwitchContext中,把当前任务切换出去之前调用此宏函数。 |
traceTASK_SWITCHED_IN() | vTaskSwitchContext中,新的任务已经被切换进来了,就调用此函数。 |
traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue) | 当正在执行的当前任务因为试图去读取一个空的队列、信号或者互斥量而进入阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图读取的目标队列、信号或者互斥量的句柄,传递给此宏函数。 |
traceBLOCKING_ON_QUEUE_SEND(pxQueue) | 当正在执行的当前任务因为试图往一个已经写满的队列或者信号或者互斥量而进入了阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图写入的目标队列、信号或者互斥量的句柄,传递给此宏函数。 |
traceQUEUE_SEND(pxQueue) | 当一个队列或者信号发送成功时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。 |
traceQUEUE_SEND_FAILED(pxQueue) | 当一个队列或者信号发送失败时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。 |
traceQUEUE_RECEIVE(pxQueue) | 当读取一个队列或者接收信号成功时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。 |
traceQUEUE_RECEIVE_FAILED(pxQueue) | 当读取一个队列或者接收信号失败时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。 |
traceQUEUE_SEND_FROM_ISR(pxQueue) | 当在中断中发送一个队列成功时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue) | 当在中断中发送一个队列失败时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
traceQUEUE_RECEIVE_FROM_ISR(pxQueue) | 当在中断中读取一个队列成功时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue) | 当在中断中读取一个队列失败时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
traceTASK_DELAY_UNTIL() | 当一个任务因为调用了vTaskDelayUntil()进入了阻塞状态的前一刻此宏函数会在vTaskDelayUntil()中被立即调用。 |
traceTASK_DELAY() | 当一个任务因为调用了vTaskDelay()进入了阻塞状态的前一刻此宏函数会在vTaskDelay中被立即调用。 |
Malloc Hook函数:
编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。内存越界经常发生在堆的使用过程总:堆,就是使用malloc得到的内存。
并没有很好的方法检测内存越界,但是FreeRTOS提供了一些回调函数:
pvPortMalloc
失败时,如果在FreeRTOSConfig.h
里配置configUSE_MALLOC_FAILED_HOOK
为1,会调用:void vApplicationMallocFailedHook( void );
该函数和空闲任务的钩子函数一样,也需要我们自己定义,可以在该函数内输出一些错误信息,提示程序员动态开辟内存失败了。
栈溢出Hook函数:
在切换任务(vTaskSwitchContext
)时调用taskCHECK_FOR_STACK_OVERFLOW
来检测栈是否溢出,如果溢出会调用:
void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );
该函数需要程序员自己定义实现,可以在里面添加一些大于信息等等。
FreeRTOS是怎么判断栈溢出?有两种方法:
此时就会比较任务TCB中的当前栈顶pxTopOfStack
和创建任务时设置的最高栈顶pxStack
作比较,如果pxTopOfStack <= pxStack
则说明栈溢出。
- 栈是向下生长的。
如上图,在任务A中不断调用FunctionA
函数,该函数会创建一个1024字节大小的数组,但是创建任务A时指定的栈大小就是4*100字节,所以在调用FunctionA
函数的过程中,会产生栈溢出。
但是在红色线条位置,FunctionA
函数函数已经执行完一次了,它创建的1024字节所用的空间也被回收了,也就是pxTopOfStack + 1024
,栈顶向上移动回去了。
这个时候任务A被切换走了,而pxTopOfStack > pxStack
,此时就会判断没有栈溢出,实际上已经发生过溢出了。
创建任务时,它的整个栈被填入固定的值,比如:0xa5
检测栈里最后16字节的数据,如果不是0xa5的话表示栈即将、或者已经被用完了
没有方法1快速,但是也足够快,能捕获几乎所有的栈溢出
- 为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈最后16个字节设置为0xa5:几乎不可能。
configCHECK_FOR_STACK_OVERFLOW = 1
时使用第一种方法检测栈溢出,configCHECK_FOR_STACK_OVERFLOW > 1
时使用第二种方法检测栈溢出。在Windows中,当系统卡顿时我们可以查看任务管理器找到最消耗CPU资源的程序。在FreeRTOS中,我们也可以查看任务使用CPU的情况、使用栈的情况,然后针对性地进行优化。
栈使用情况:
在创建任务时分配了栈,可以填入固定的数值比如0xa5,以后可以使用以下函数查看栈的高水位,也就是还有多少空余的栈空间:
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
参数/返回值 | 说明 |
---|---|
xTask | 哪个任务 |
返回值 | 任务运行过程中空闲内存容量的最小值。 |
- 假设从栈尾开始连续为0xa5的栈空间是N字节,返回值是N/4。
- 因为创建任务时,指定栈大小时的单位是字(Word)。
任务运行时间统计:
对于同优先级的任务,它们按照时间片轮流运行:你执行一个Tick,我执行一个Tick。是否可以在Tick中断函数中,统计当前任务的累计运行时间?
不行!很不精确,因为有更高优先级的任务就绪时,当前任务还没运行一个完整的Tick就被抢占了。
- 我们需要比Tick更快的时钟,Tick周期时1ms,我们可以使用另一个定时器,让它发生中断的周期时0.1ms甚至更短。
任务1,2,3一共占用CPU的时间是13T,task1两次调度占用的时间是(4 + 2 = 6T),所以:
- 关键点:在
vTaskSwitchContext
函数中,切换任务的时候,会使用更快的定时器统计运行时间。- 定时器超时时间越短,得到的CPU占用率精度越高。
函数:
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
uint32_t * const pulTotalRunTime );
参数 | 描述 |
---|---|
pxTaskStatusArray | 指向一个TaskStatus_t结构体数组,用来保存任务的统计信息。 有多少个任务?可以用 uxTaskGetNumberOfTasks() 来获得。 |
uxArraySize | 数组大小、数组项个数,必须大于或等于uxTaskGetNumberOfTasks() |
pulTotalRunTime | 用来保存当前总的运行时间(更快的定时器),可以传入NULL |
返回值 | 传入的pxTaskStatusArray数组,被设置了几个数组项。 注意:如果传入的uxArraySize小于 uxTaskGetNumberOfTasks() ,返回值就是0 |
void vTaskList( signed char *pcWriteBuffer );
- pcWriteBuffer:系统输出信息到pcWriteBuffer,必须足够大。
如上图,使用该函数可以得到上面的信息格式,包括任务的状态,优先级,空闲栈(水位),任务号等等信息。
void vTaskGetRunTimeStats( signed char *pcWriteBuffer );
- pcWriteBuffer:系统输出信息到pcWriteBuffer,必须足够大。
如上图,可以获得上面格式的运行信息,包括任务名,运行时间,CPU占用率等等。
配置:
首先要配置一些宏开关,配置:
#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
还需要实现宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
,它用来初始化更快的定时器,具体的定时器初始化函数需要我们自己实现。
实现这两个宏之一,它们用来返回当前时钟值(更快的定时器)
portGET_RUN_TIME_COUNTER_VALUE()
:直接返回时钟值portALT_GET_RUN_TIME_COUNTER_VALUE(&Time)
:设置Time变量等于时钟值初始化定时器:
配置一个片内的定时器作为更快的定时器,详细过程本喵就不讲解了,直接贴代码:
如上图所示定时器初始化代码,初始化函数是TimerInit()
。
如上图,在定时器中断函数中,反转运行标志位test_cnt
,然后将计数值g_timer_cnt
加加,每产生一次超时加加一次。
还实现了一个TimerGetCount
函数来获取计数值g_timer_cnt
,该值可以反应时长。
如上图,原本是需要在main
函数中初始化定时器的,但是此时是让系统自动初始化,所以需要将初始化定义到宏中:
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS TimerInit
如上图,在启动调度器的时候,会调用portCONFIGURE_TIMER_FOR_RUN_TIME_STATS
,从而就实现了更快定时器的自动初始化。
然后就是将获取计数值值的函数定义到宏中:
#define portGET_RUN_TIME_COUNTER_VALUE TimerGetCount
如上图代码,在切换任务的时候,先调用portGET_RUN_TIME_COUNTER_VALUE
拿到当前时间,然后计算出运行时间,再累加运行时间,最后更新当前时间。
- 此时就得到了该任务此次执行所花费的时间。
如上图红色框中所示,此时就完成了使用更快定时器来统计系统信息的所有配置。
演示:
如上图代码所示,创建两个任务,任务1用来获取统计信息,任务2此时什么都不做,只是和任务1来竞争CPU资源。
如上图,此时的统计信息中,任务有四个,任务1和任务2,空闲任务,以及更快的定时器中断任务,以及它们的优先级水位线等等信息。
如上图,让任务2获取并打印运行信息,任务1什么都不做,只是和任务2竞争CPU资源。
如上图所示结果,任务仍然是这四个,还有每个任务的运行时间,以及CPU占用率,任务1几乎在一直运行,所以它的CPU占用率接近百分之百。
对于互斥的管理,虽然是简单粗暴的禁止其他任务,但是禁止又有两类:
禁止中断时,普通任务禁止会考虑到递归调用,有一个计数值,ISR禁止不考虑递归调用,直接禁止,没有计数值。
调试和信息统计虽然看着有点鸡肋,但是有时候还是还是非常有用的,要知道有这样的功能。