FreeRTOS学习笔记(5、定时器、中断管理、调试与优化)

前言

这是第五弹,由于CSDN长度的限制,所以把FreeRTOS学习分为几部分来发,这是第五部分


主要包括定时器、中断管理、调试和优化

往期学习笔记链接

第一弹FreeRTOS学习笔记(1、FreeRTOS初识、任务的创建以及任务状态理论、调度算法等)
第二弹: FreeRTOS学习笔记(2、同步与互斥通信、队列、队列集的使用)
第三弹: FreeRTOS学习笔记(3、信号量、互斥量的使用)
第四弹: FreeRTOS学习笔记(4、事件组、任务通知)
第五弹: FreeRTOS学习笔记(5、定时器、中断管理、调试与优化)

学习工程

所有学习工程
oufen / FreeRTOS学习
都在我的Gitee工程当中,大家可以参考学习

定时器

闹钟什么时候响,闹钟响了之后要做什么事情,这个闹钟是一次性的还是周期性的

定时器的三要素

  • 超时时间(定时器周期)
  • 回调函数
  • 单次触发还是周期性的触发

守护任务

  • 守护任务的作用
    • 处理命令,从命令队列中取出命令、处理
    • 执行定时器的回调函数

能否及时执行定时器中断回调函数,严重依赖于守护任务的优先级

image.png
image.png

创建定时器后,然后启动定时器

启动定时器,实质上是向队列中写入命令数据,第二个参数就是等待时间,如果队列满的话,就无法写入队列,阻塞等待

image.png

启动定时器时,会记录启动定时器的当前tick,此时的tick是初始时间,当定时器过了定时时间后,即当前时间tick>=初始tick+定时时间时,就会触发Timer

对于一次性的定时器,将会触发一次
对于周期性的定时器,将会周期性的触发

定时器回调函数应该尽快执行完,如果执行时间过长,将会阻碍其他定时器函数的执行

在定时器中断回调函数中不要使用delay,不能进入阻塞状态

可以调用在任务中使用的函数,但是等待事件要设置为0,即刻返回,不可以阻塞
否则将会影响其他任务的执行,一直堵塞在定时器中断回调函数中

触发指的是回调函数被调用,被谁调用呢?

  • tick中断中去调用回调函数 (在Linux中调用)
  • 在FreeRTOS中,只能在某个任务中执行

tick中断,每定时1次,就会判断一下,有没有超时的定时器,如果有的话将会唤醒这个任务,调用回调函数,这个任务被称为守护任务

守护任务的函数
image.png

image.png

当tick中断把守护任务唤醒之后,如果他的优先级比较高的话,最高的话就可以执行定时器的回调函数
如果优先级比较低,就会被更高优先级任务抢占,那么定时器中断回调函数就无法执行,从而阻塞

守护任务的三个宏定义,一旦使用定时器就需要定义这三个宏,分别表示守护任务的优先级、队列长度、任务分配栈的大小

image.png

守护任务执行Timer回调函数,如果其他任务想要设置Timer,只能通过队列发消息给守护任务(比如启动定时器)

image.png

用户使用定时器时,实质都是把某些命令都发送到定时器命令队列里
把数据存入队列,对方任务就会被唤醒,就会从队列里取出命令,从而修改定时器的值

写入队列,都会有等待时间

定时器的状态

定时器中有两个状态

  • Dormant状态 (休眠状态)
  • Running状态 (运行状态)

image.png

创建定时器时,这个定时器处于Dormant状态,定时器并未运行,我们可以修改定时器的,启动/复位/修改周期
然后从Dormant状态变为Running状态,这个状态在等待定时时间到,时间到了之后函数将会被调用

一次性定时器和周期性定时器不同的在于
周期性定时器被调用后,仍然处于运行状态,会等待下一个超时时间
对于一次性定时器,执行一次后,就不再等待,从Running状态变为Dormant状态

从Running状态进入Dormant状态还可以通过xTimerStop(),停止定时器

定时器的基本使用

1、创建定时器并书写中断回调函数

创建定时器时需要定义相关宏,这是关于守护任务的相关宏
image.png
image.png

#define configUSE_TIMERS 					1 					  /*定时器相关宏*/
#define configTIMER_TASK_PRIORITY 			configMAX_PRIORITIES /*守护任务优先级 设置成最大*/
#define configTIMER_QUEUE_LENGTH    		10					/*守护任务队列的长度*/
#define configTIMER_TASK_STACK_DEPTH        100				   /*守护任务队列的栈大小*/

image.png
image.png

2、开启定时器

image.png

定时100ms,打印一次数据
image.png
可以看到定时器中断回调函数打断了task1的执行
守护任务调用的定时器中断回调函数,优先级最高
image.png

如果修改守护任务的优先级,设置成比task1低
image.png
image.png
可以看到守护任务调用中断回调函数并没有抢占task1
image.png
image.png

任务的优先级最多只能达到最大值-1,不能达到最大值

image.png

定时器的函数,实际上是在另外一个task中运行的,这个task也要有优先级,如果优先级比其他task低,那么守护任务调用回调函数,就无法进入中断,打断其他task的执行

定时器消除抖动

按下按键之后,有可能会产生多次震动,再抖动过程中可能会产生中断,从而进入中断多次

我们需要消除这些抖动

普通的外部中断

image.png

定时器防抖

image.png

可以看出,按键被触发多次,但是回调函数获取里只获得了一次
外部中断处理按键思路,当按键被按下,会产生下降沿或者上升沿,这个时候就会触发中断,在中断服务函数里再次判断按键状态,这个时候才读取按键的值

定时器处理按键消抖,当按键被按下,进入外部中断服务处理函数,然后复位定时器,等待一段定时器周期,然后进入定时器中断回调函数,进行处理按键值,这个时候按键的键值将会是正确的
image.png

image.png

中断管理

当用户按下按键时,触发了按键中断,这个时候中断的处理流程是

  • CPU跳到固定地址去执行代码,这个固定地址称为中断向量,这个跳转是硬件实现的
  • 执行代码做啥子
    • 保存现场,task1被打断,需要先保存task1的运行环境,比如各类寄存器的值
    • 分辨中断,调用中断处理函数(ISR interrupt service routine)
    • 恢复现场,继续运行task1,或者运行其他优先级更高的任务

image.png

如果中断非常耗时,对于中断的处理就要分为两部分

  • ISR:尽快做些清理、记录的工作,然后触发某个任务
  • 任务:更复杂的事情放在任务中处理

image.png
ISR的优先级高于任务,如果不高于任务的话,就无法打断任务,进入中断

就拿队列来说明,使用普通的函数和使用中断
image.png

1、阻塞,当队列满后,ISR不能阻塞,会立马返回一个err
在任务中使用队列的话,写队列,当队列满后可以阻塞等待

但是在ISR中写队列,即使队列满了也不能写队列,因为一旦阻塞,中断就阻塞

中断一旦阻塞,其他任务就不能运行

2、写队列时,队列满,ISR立马返回,不会阻塞
队列满时
在任务中,可以决定是否立马返回(返回err),还是决定阻塞等待(把任务放到等待链表中并且阻塞等待),阻塞的话将会发起任务调度,让其他task运行

在ISR中,立马返回,不会阻塞

3、两者参数不一样

4、写队列时,唤醒等待任务
写队列时
在任务中写队列,会把等待任务的队列唤醒(把等待任务的等待链表的task移到就绪链表task中去,从阻塞态变为就绪态),如果被唤醒任务的优先级更高的话,将会发起调度

在ISR中,会唤醒,但是不会调度,但是会记录下来,是否需要发起调度

image.png

FreeRTOS中很多API函数都有两套:一套在任务中使用,另一套在ISR中使用。后者的函数名含有"FromISR"后缀。

image.png

image.png

两套API函数的区别

image.png
image.png

xHigherPriorityTaskWoken 参数

image.png

ISR和普通的在任务调用函数不一样,以队列为例,写队列时,队列满了会发生阻塞,当向队列中写入数据时,会唤醒等待任务,其他高优先级的任务将会抢占执行
但是在ISR中,并不会阻塞,但是会唤醒等待任务,是否执行决定于xHigherPriorityTaskWoken参数

xHigherPriorityTaskWoken初始值为pdFALSE,当ISR中调用函数,唤醒等待任务后,判断xHigherPriorityTaskWoken,如果为pdTRUE的话,就开启任务调度

image.png

多次调用ISR函数,但是我们只在最后决定是否任务切换
image.png

使用portEND_SWITCHING_ISR 宏来切换任务

image.png

定时器在ISR中的使用

在ISR中复位定时器,本质即是向队列发数据

ISR中使用ISR函数,并不会进入阻塞状态
image.png
image.png

可以看到,在中断中使用ISR函数实现了按键的消抖
image.png

资源管理

在多个任务去访问,队列、信号量、互斥量、事件组、任务通知时,每个任务在运行时都不能有其他任务的干扰
只能互斥的(独占的)运行访问

如何访问临界资源

临界资源,只能由一个任务互斥的使用的资源,就叫做临界资源

1、任务访问临界资源

比如有两个任务都去获取这个临界资源,那么访问临界资源前,首先禁止任务调度

  • taskA访问时,禁用任务调度,那么taskB就无法访问了,这时taskA就是互斥的使用临界资源
  • taskB同

2、任务和中断访问临界资源

任务和中断都可以访问临界资源,那么访问临界资源前,首先关闭中断

  • taskA先关闭中断,那么中断被屏蔽,taskA就可以互斥的访问临界资源
  • 中断运行过程中,taskA本来就无法执行,所以taskA无法抢占中断访问临界资源

中断也可以关闭其他中断,以确保临界资源始终是被当前中断所独占的

禁止任务调度

当当前task调用此函数时,任务调度器将会被禁止,此时访问临界资源时,当前任务就是独占使用(互斥)
image.png

恢复任务调度器
image.png

如何屏蔽中断

屏蔽中断是指屏蔽某些优先级较低的中断

Contrex M3 和 Contrex M4的中断优先级排列

复位 -3
NMI -2
硬件错误 -1
不使用SysCall的中断 0-190
使用SysCall的中断 191-255

使用SysCall的中断,是调用FreeRTOS的API的中断

屏蔽中断,只能屏蔽某一类的中断(使用到SysCall的中断)
而不能够屏蔽掉更高优先级的一类中断(没有使用到SysCall)

在使用更高优先级的中断的时候,不能够去使用到SysCall

屏蔽中断分为在中断中屏蔽和在任务中屏蔽

  • 在任务中屏蔽,使用taskENTER_CRITICA()/taskEXIT_CRITICAL(),屏蔽中断/开启中断
  • 在ISR中屏蔽中断,使用taskENTER_CRITICAL_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR(),屏蔽中断or开启中断

image.png
image.png

调试和优化

调试

FreeRTOS提供了很多种调试手段

  • 打印
  • 断言 configASSERT
  • Trace
  • Hook函数(回调函数)

1、打印(printf)

printf:FreeRTOS中使用到了Use MicroLIB库,来实现printf函数
我们只需要实现一下函数即可使用printf
image.png
image.png

实现printf函数即可

int fputc( int ch, FILE *f );

2、断言

在一般的c语言当中,断言就是一个函数

void  assert(scalar  expression);

它的作用是,确保expression为真,如果expression为假的话就中止程序

在FreeRTOS中,使用的是configASSERT()
image.png
比如

/*如果x为False 就让程序陷入死循环 从而阻塞运行*/
#define configASSERT(x)  if(!x) while(1);

还可以获得更多有效信息

/*如果x为False, 打印出此时的文件名,函数名,和发生err的行数*/
#define configASSERT(x)  \
	if (!x) \
	{
      
      
		printf("%s %s %d\r\n", __FILE__, __FUNCTION__, __LINE__); \
        while(1); \
 	}

configASSERT(x)中,如果x为假,表示发生了很严重的错误,必须停止系统的运行。

3、Trace

FreeRTOS中定义了很多trace开头的宏,这些宏被放在系统的关键位置

它们一般都是空的宏,这不会影响代码:不影响编程处理的程序大小、不影响运行时间。

我们要调试某些功能时,可以修改宏:修改某些标记变量、打印信息等待。

4、回调函数 Hook

回调函数Hook,一般分为两种回调函数

  • Malloc Hook函数
  • 栈溢出 Hook函数
Malloc Hook函数

当malloc失败时,可以提供一个Malloc 回调函数

内存越界经常发生在堆的使用过程

堆也就是使用Malloc得到的内存

并没有很好的方法监测内存越界,但是可以提供一些回调函数

Malloc失败时,如果在FreeRTOSConfig.h里配置configUSE_MALLOC_FAILED_HOOK为1,会调用回调函数

/*malloc失败时,可以调用此回调函数,可以在里面打印出相关err信息*/
void vApplicationMallocFailedHook( void );
栈溢出Hook函数

在任务的执行过程中,局部变量什么的都是存放在栈内,栈的大小可能会溢出

而栈又有一个栈顶指针和一个栈底指针,当每次入栈,SP指针就会+1

当栈顶指针的值 > 栈底指针的值时,就会溢出

如何判断栈是否溢出?

  • 方法1(比对pxTopOfStack的地址和pxStack的地址)
    • 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时很可能就是它对栈的使用到达了峰值
    • 这种方法很高效,但是并不准确
    • 比如,任务在运行过程中,调用了函数A,大量的使用了栈,调用完A之后才被调度

image.png

  • 方法2
    • 创建任务时,它的栈被填入固定的值,比如:0xA5 (所有的栈空间被填入0xA5)
    • 然后检测栈里最后16字节的数据,如果不是0xA5的话表示栈即将、或者已经被用完了
    • 没有方法1快速,但是也足够快
    • 能捕获几乎所有的栈溢出
    • 为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈设置为0xA5:几乎不可能

image.png

(从栈底从栈顶向上查找,对于连续16个字节的0xA5就表示是空闲的,没有被占用的,也称之为水位

一旦查找出某一个不是0xA5,就知道了从这个字节开始,上面的栈空间都是被用过的栈)

当水位接近于0的时候,就表示栈即将溢出

优化

在FreeRTOS中,我们也可以查看任务使用CPU的情况、使用栈的情况,然后针对性地进行优化。

这就是查看"任务的统计"信息

栈的使用情况

在创建任务时分配了栈,可以填入固定的数值比如0xa5,以后可以使用以下函数查看"栈的高水位",也就是还有多少空余的栈空间

UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );

从栈底往栈顶逐个字节地判断,它们的值持续是0xa5就表示它是空闲的

从栈底从栈顶向上查找,对于连续16个字节的0xA5就表示是空闲的,没有被占用的,也称之为水位

一旦查找出某一个不是0xA5,就知道了从这个字节开始,上面的栈空间都是被用过的栈)

当水位接近于0的时候,就表示栈即将溢出

一般来说,保证水位十几个字节即可

CPU的运行时间

对于同优先级的任务,它们按照时间片轮流运行:一个任务执行一个Tick,另一个任务也执行一个Tick。

不可以在tick中断中,统计当前任务的累积运行时间,因为如果有更高优先级的任务就绪后,当前任务还没运行一个完整的tick就被抢占了

使用一个定时器,让其定时周期比tick还短,比如0.1ms
image.png

image.png
image.png

获取任务统计信息、CPU的占用率等

不使用tick中断来获取任务的运行时间,tick中断1ms一次,太慢了
使用一个较快的定时器来获得任务的运行时间,这里采用0.1ms

1、开启定时器中断

这里的定时器中断采用0.1ms一次

有点奇怪啊,我使用定时器2不行
后面一查看,原来这个FreeRTOS的源码自带的MCU的型号是
image.png
image.png

2、配置宏

image.png

#define configGENERATE_RUN_TIME_STATS 1 /*初始化更快的定时器*/
/*使用更快定时器 这个宏定义在任务调度函数里使用*/
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS  Timer3_Init  
/*得到定时器的当前时间*/
#define portGET_RUN_TIME_COUNTER_VALUE TimerGetCount 

/*获得任务统计信息相关宏定义*/
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
#define configSUPPORT_DYNAMIC_ALLOCATION 1

image.png
image.png

3、获取任务统计信息 or CPU占用率

image.png

获取任务统计信息
image.png

获取CPU占用率
image.png

image.png

猜你喜欢

转载自blog.csdn.net/cyaya6/article/details/132539520
今日推荐