FreeRTOS 正点原子教程学习笔记

  • Post author:
  • Post category:其他



正点原子视频教程



FreeRTOS(教程非常详细)




小知识

如果创建了任务却完全空着,没有while(1){延时}的话,整个程序会卡住,其他正常的任务无法运行。如果任务里单单有赋值之类的操作也会卡死在这个任务,一定要记得加延时vTaskDelay(10);。

任务调度周期是1ms,能改,但是别改。


FreeRTOS中的变量,函数命名规则(u.v.x.p什么意思)


前面的字母是返回值的缩写

u :代表unsigned。

s :代表short。

c :char。 所以类似uc,us类的变量就是unsigned char,unsigned

short,分别对应uint8_t,uint16_t。

x :为用户自定义的数据类型,比如结构体,队列等。

常看到ux开头的函数,就是unsigned且用户自定义的类型。需要注意的是size_t变量前缀也是ux。

e :枚举变量

p :指针变量 类似(uint16_t *)变量前缀为pus。

prv :static函数

v: void函数

1、c语言中%x的意思是16进制输出。

2、c语言中符合%#的意思是带格式输出。比如,%#x的意思是在输出前面加上0x,%#b的意思是在输出前面加上0b。

用来输出地址




各种函数

  • vTaskList()能一次性看所有任务的状态、优先级、堆栈剩余大小和编号
  • vTaskGetRunTimeStates()用来统计各任务运行时间及其占比【很有用的】如果哪个太耗时就可以拆分。不过要用的话得专门弄个定时器。正点原子官网视频第13.2讲会详细讲,我用到再学吧。
  • 调用taskYIELD(),主动让出cpu,让

    同优先级

    的其他task获得cpu
  • vTaskDelay(1000);延时一秒,可被调度


FreeRTOSConfig.h


使用“INCLUDE_”开头的宏用来表示使能或除能 FreeRTOS 中相应的 API 函数,作用就 是用来配置 FreeRTOS 中的可选 API 函数的

“config”开始的宏和“INCLUDE_”开始的宏一样,都是用来完成 FreeRTOS 的配置和裁剪的

有些宏定义有默认值,是用#ifndef实现的




中断

关闭中断函数只能关掉大于等于5【5这个值是宏定义的,下图第一行】的中断优先级的事件【优先级数值比较小的比较高】

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段。

#define taskENTER_CRITICAL() portENTER_CRITICAL()

#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()

#define taskEXIT_CRITICAL() portEXIT_CRITICAL()

#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

没有_FROM_ISR的是任务级的临界段代码保护,另外俩是中断级的临界段代码保护。

void start_task(void *pvParameters)
{
	taskENTER_CRITICAL(); //进入临界区
	xTaskCreate((TaskFunction_t)led0_task,
		(const char*)"led0_task",
		(uint16_t)LED0_STK_SIZE,
		(void*)NULL,
		(UBaseType_t)LED0_TASK_PRIO,
		(TaskHandle_t*)&LED0Task_Handler);
	vTaskDelete(StartTask_Handler);				//删除任务
	taskEXIT_CRITICAL();									//退出临界区

}

在这里插入图片描述

中断里的临界区出入函数有返回值和传入参数




任务堆栈

用来存被中断时的保存现场的指针以及函数内部定义的内部变量、数组等。 如果堆栈开得太小运行时会卡。当堆内存不够的时候 这个函数的返回值会返回错误信息

在这里插入图片描述

堆栈开的大小是都要定的,两种函数都要定,用宏定义自己定,上面的代码里是LED0_STK_SIZE 开辟的大小是LED0_STK_SIZE*4 字节

截图里说的是堆栈所在的首地址,它下面的那个传入参数是任务控制块的首地址。如果不是静态创建而是动态创建的话,就不用这俩参数,而是要一个句柄。


欸,任务删除的传入参数是句柄啊,那静态创建的任务的句柄在哪啊??

啊知道了,静态任务创建函数的返回值是任务句柄,句柄要先自己定义,类型是TaskHandle_t




删除任务

假设现在正在编写的子函数是一个任务,并且要在这个函数执行完成之后就删除任务,那就在最后一句写 vTaskDelete(StartTask_Handler); 删除掉任务。传入参数可以是这个函数创建时的句柄,也可以是NULL。

不过删除任务的时候要先判断那个句柄是否已经是NULL了,免得把已经删除的任务再删几次。【人是不能死两次的,任务也不能】


但是删除自身的时候.要是这个任务有返回值,又只运行这一次就删掉,return是最后一句,vTaskDelete(NULL); 是倒数第二句。这样没问题嘛??




任务优先级

0级最低。

这里说的是线程里面的函数调用的优先级,范围是 0~(configMAX_PRIORITIES-1) ,空闲任务的优先级为0,一般都是自己创建任务的时候自己写个宏定义然后传入对应的参数,比如上面的LED0_TASK_PRIO。

上面说的是中断优先级,反正不一样,哼。

乱来的,UCOS的任务优先级里0就是最高级,而且前面几个都是系统用的,我们自己创的任务直接10起步。

在这里插入图片描述


视频第20讲,文章第14.6章提到【优先级翻转】


优先级翻转是指同样要插队运行,较低优先级的插队到高优先级前面去了。

原因是

高优先级的任务

需要等一个信号量

,这个信号量交给优先级最低的任务处理了,中优先级的任务就跑来插最低优先级的任务 它做完再让最低优先级任务继续处理信号量,然后等最低优先级处理完、释放信号量之后 最高优先级才能继续任务。




创建静态任务

首先说明一下,静态任务并不比动态好什么东西,又麻烦。以后的教程也只会用动态不会用静态。


官网讲解优缺点



FreeRTOS的软件结构

//大佬


FreeRTOS动态内存管理

静态任务创建的函数的定义在tasks.c文件里,刚移植好的时候是不支持创建静态任务的

在这里插入图片描述

上面的宏定义要跳转过去修改【这个系统原本给它的默认值是0】

当我们把这个开关打开的时候,麻烦来了

我们之前关着的时候调用 vTaskStartScheduler(); 开启任务调度。它在不被支持静态创建的时候会动态创建空闲函数,但是支持静态的时候就会创建静态的空闲函数了,然而它要创建却少条件,还得自己写函数写堆栈,把空闲任务和定时器任务所需的内存定义上。正点原子有教,静态和动态所创建的堆栈大小是一样的。我嘛,直接把宏定义再加个条件,仍然让它创建动态的。

记录一下自己作死的地方,免得忘了

我在main函数上面定义了 #define STATIC_MAFAN 0 然后在下面两个地方 &&了这么个条件

在这里插入图片描述
在这里插入图片描述


话说为什么开启任务调度函数里创建函数的时候 那些传入参数都不用强制类型转换的??

静态和动态不一样的参数就最后两个,一个是堆栈,应该创建一个数组,另一个是控制块,创建一个结构体变量

StackType_t StackBuffer[START_STK_SIZE];

StaticTask_t TaskBuffer_TCB;




任务挂起和恢复

挂起就是暂停

在这里插入图片描述

  • 可以重复挂起任务很多次,恢复只要调用一次就能恢复 【任务调度器也能挂起和恢复,但是如果挂起它两次就得调用恢复两次】
  • 挂起自身任务的时候传入参数可以是NULL
  • 挂起时,函数内的局部变量的内存不会被释放。
  • 中断的恢复函数有返回值

在这里插入图片描述

在这里插入图片描述

(1)优先级设置成5也正常,但是4就不行了。定时器中断、串口中断的优先级也要小于等于5

(3)这条函数就是任务重新调度的

按正点原子的教程做,KEY0按键中断的时候程序会卡在xEventGroupSetBitsFromISR()这个函数。解决方法是HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);//这个中断优先级分组要改成NVIC_PRIORITYGROUP_4 总共的优先级分为16个等级 //如果分组用第二组的话就只有4个优先级,没办法把按键中断优先级设为6.

第0组:所有4位用于指定响应优先级

第1组:最高1位用于指定抢占式优先级,最低3位用于指定响应优先级

第2组:最高2位用于指定抢占式优先级,最低2位用于指定响应优先级

第3组:最高3位用于指定抢占式优先级,最低1位用于指定响应优先级

第4组:所有4位用于指定抢占式优先级


程序是正常运行了,但是我怎么证明这中断里的恢复有重新安排优先级更高的程序呢??


我先复述一下在这个

任务挂起和恢复

里的程序效果:

两个灯,4个按键,两个按键负责挂起,两个按键负责恢复,其中按键KEY0是外部中断 负责LED1的恢复。两个灯在正常情况下会不断闪烁。

我在任务函数里用delay_xms(500);给灯做延时,这个函数不能被任务调度打断,所以运行时只有一个任务优先级比较高的那个灯会闪【任务创建时给定的宏,数字大的优先级高】,中断时也只能帮优先级高的任务抢回闪灯权,否则就只能等高优先级的任务挂起了才能让低优先级的灯闪。




列表与列表项

1.先定义一个列表 List_t 和三个列表项 ListItem_t

2.在一个任务里进行初始化。调用vListInitialise()和vListInitialiseItem()函数,传入第一步定义的变量的地址就好

3.然后给列表项的.xItemValue值赋值,用来排序

在这里插入图片描述

现在就是一个空的TestList和三个独立的列表项

4.vListInsert(&列表,&列表项);往列表里自动按从小到大的顺序插入那个输入的列表项

在这里插入图片描述

在这里插入图片描述

5.删除列表项 uxListRemove(&列表项);

6.在末尾添加列表项 vListInsertEnd(&列表,&列表项);

在这里插入图片描述




队列

先进先出,值传递(数据量大的时候可以把指针当作值来传递啊!)【UCOS是引用传递,源数据在消息传到之前不能删改】

  1. 动态创建队列

    返回句柄 xQueueGenericCreate(队列可容纳的消息数量,单个消息的长度,队列类型)

    在这里插入图片描述

    if(Key_Queue!=NULL) //在用队列之前都先问问这个队列是不是创建成功

    //二值信号量和互斥信号量的队列长度都是1 ,单个消息的长度都是0.只用看是有信号还是没信号就行,不用存具体消息。


    在哪创建啊??删除任务之后队列会被删掉吗??


    句柄是全局变量 要跨任务传消息

    创建队列是和创建一大批任务的时候一起创的,返回值已经赋值给全局的句柄所以删掉任务没事。

  2. 向队列发送消息

    在这里插入图片描述

xQueueSend( 目标队列句柄, 消息内容的变量的地址, 阻塞时间 )

中断级入队函数的参数里有一个pxHigherPriorityTaskWoken 它要我们在这个子函数里新建一个变量,然后把地址给它当传入参数。整好之后要用portYIELD_FROM_ISR(变量名);重新按任务优先级选择要运行的任务。

  1. 从队列中读取消息

    在这里插入图片描述

    xQueueReceive(key_queue,&key,0);

    //从key_queue队列中取出消息存到新建u8变量里去,等0ms,要是没拿到消息就返回0 拿到就返回1




信号量

要#include “semphr.h”


  • 情景要求是电脑发数据,串口接收,任务获取数据并处理

    如果在串口中断把消息内容直接传给任务,就用

    队列

    传消息

    如果在串口中断做个标记,任务里查询标记然后从串口缓冲区拿消息,就用

    信号量

    做标记

信号量其实就是队列,创建信号量的函数调用的就是创建队列的函数。

多个任务同时要读取某一个信号量时,最高优先级的任务能读到,其他的进入阻塞态


这个文章适合复习操作

  1. 二值信号量

    只有一个队列项 而且只用判断是满的还是空的 不用看内容

    give函数是任务发出信号量给队列,take函数是任务从队列里取出信号量。初始化、发消息和收消息的流程和队列一样,函数也分任务级和中断级,就是函数名不一样,在不同的头文件里。

    //创建二值信号量

    BinarySemaphore=xSemaphorecreateBinary ();

    //二值信号量创建成功以后要先释放一下才能给别的任务使用

    if(BinarySemaphore!=NULL)xSemaphoreGive(BinarySemaphore) ;



    在什么情况下用?



    【串口收到消息时,要避免在串口中断里进行文本处理以及显示反馈】串口助手里发送消息之后会标记二值信号量,定时器中断里查到二值信号量被标记时会在LCD和串口助手里显示收到的字符串。【串口的优先级是7,定时器的优先级是8】

  2. 计数型信号量/数值信号量

    反正就是帮忙计数的,可以设最大值和初值。一般用来看资源使用量或者剩余量

    xSemaphoreCreateCounting()创建

    后面任务里每调用一次xSemaphoreGive(句柄); 就会有个变量加1.

    semavalue=uxSemaphoreGetCount(句柄);读那个变量

    xSemaphoreTake(句柄,portMAX_DELAY); 变量减1.

  3. 互斥信号量

互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。

因此二值信号量更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问

定时器中断和按键中断这些中断能迅速插入一段新任务运行,但是如果要在程序运行过程中某个变量置1时插入新任务的话,在裸机写代码的时候只能等程序运行到那去检查那个变量,现在有操作系统就能实时检查信号量并在拿到信号时运行新任务。

创建 xSemaphoreCreateMutex(),//创建之后不用特地释放

发信号和读信号的函数跟二值信号量一样

不同的地方是

1 当高优先级的任务向低优先级的任务请求互斥信号量的时候,如果高优先级任务要等着,就会把那个低优先级任务的优先级拉高,让它快点处理好所需要的信号量【这就叫优先级继承】

2 互斥信号量不能用在中断函数里。中断里也不能用长时间的阻塞态等信号量来着,所以没必要

//文档里讲互斥信号量和递归互斥信号量比视频里讲的要多很多

释放互斥信号量的时候和二值信号量、计数型信号量一样,都是用的函数 xSemaphoreGive()(实际上完成信号量释放的是函数

xQueueGenericSend())。不过由于互斥信号量 涉及到优先级继承的问题,所以具体处理过程会有点区别。使用函数

xSemaphoreGive()释放信 号 量 最 重 要 的 一 步 就 是 将 uxMessagesWaiting 加 一 , 而 这

一 步 就 是 通 过 函 数 prvCopyDataToQueue() 来 完 成 的 , 释 放 信 号 量 的 函 数

xQueueGenericSend() 会调用 prvCopyDataToQueue()。互斥信号量的优先级继承也是在函数

prvCopyDataToQueue()中完成的

  1. 递归互斥信号量

    已经获取了互斥信号量的任务就不能 再次获取这个互斥信号量,但是递归互斥信号量不同,已经获取了递归互斥信号量的任务可以 再次获取这个递归互斥信号量,而且次数不限!一个任务使用函数 xSemaphoreTakeRecursive()成功的获取了多少次递归互斥信号量就得使用函数 xSemaphoreGiveRecursive()释放多少次

    同互斥信号量一样,递归互斥信号量的获取和释放要在同一个任务中完成!如果当前正在运行的任务不是递归互斥信号量的拥有者就不能释放!

    创建 xSemaphoreCreateRecursiveMutex()




软件定时器

硬件定时器时间准 但是资源不多,拿软件定时器凑合用。回调函数里不能有延时或者阻塞

可以设置成 周期定时器 或者 单次定时器

有创建、开启、复位、停止 函数

可以用于LCD背光每按下一次按键就亮5秒的应用




事件标志组

要#include “event_groups.h”

使用信号量来同步的话任务只能与单个的事件或任务进行同步。要是某个任务可能会需要与多个事件或任务进行同步就要用事件标志组。【例程里就是:两个按键各自管一个标志位,另一个任务等待指定的事件位,达成的条件可以自己设置为 两个标志位都要为1/其中有一个置1就行/达成条件后要不要清除标志位/等多久】

实际使用时可以

● 事件标志组的 bit0 表示队列中的消息是否处理掉。

● 事件标志组的 bit1 表示是否有消息需要从网络中发送出去。

● 事件标志组的 bit2 表示现在是否需要向网络发送心跳信息。

//跟手册里的寄存器说明差不多。

在代码里用的时候是

#define EVENTBIT_0 (1<<0) //事件位

#define EVENTBIT_1 (1<<1)

#define EVENTBIT_2 (1<<2)

#define EVENTBIT_ALL (EVENTBIT_0|EVENTBIT_1|EVENTBIT_2)

句柄类型 EventGroupHandle_t

创建 xEventGroupCreate()

清零 xEventGroupClearBits(句柄,要清零的事件位)

xEventGroupClearBitsFromISR(同上)

置1 xEventGroupSetBits(同上) //说是有检查任务状况,清零没有。没听懂,文档里没写

xEventGroupSetBitsFromISR(同上)

获取标志 xEventGroupGetBits()

xEventGroupGetBitsFromISR()

要清零的事件位:比如要清除 bit3 的话就设置为 0X08。可以同时清除多个bit,如设置为 0X09 的话就是同时清除 bit3 和 bit0。//一个字节8个位,8421 8421

通过宏定义可以选择事件标志组是有8位还是24位

等待指定的事件位 //这个常用

在这里插入图片描述

xEventGroup: 指定要等待的事件标志组。

uxBitsToWaitFord: 指定要等待的事件位,比如要等待bit0和(或)bit2的时候此参数就是0X05

xClearOnExit: 此参数要是为pdTRUE的话,那么在退出此函数之前由参数uxBitsToWaitFor 所设置的这些事件位就会清零。如果设置位 pdFALSE 的话这些事件位就不会改变。

xWaitForAllBits: 此参数如果设置为

pdTRUE

的话,当 uxBitsToWaitFor 所设置的这些事件位

都置 1,或者指定的阻塞时间到

的时候函数 xEventGroupWaitBits()

才会返回

。当此函数为

pdFALSE

的话,只要 uxBitsToWaitFor 所设置的这些事 件位其中的

任意一个置 1,或者指定的阻塞时间到

的话函数 xEventGroupWaitBits()就会返回。

xTicksToWait: 设置阻塞时间,单位为节拍数。

返回值 返回当所等待的事件位置 1 以后的事件标志组的值,或者阻塞时间到。根据这个值我们就知道哪些事件位置 1 了。如果函数因为阻塞时间到而返回的话那么这个返回值就不代表任何的含义。




任务通知

可以使用任务通知来代替信号量、消息队列、事件标志组等这些东西。使用任务通知的话效率会更高。

以前是创建对应的结构体存信号量信息,现在是直接往任务的句柄里存那些信息。


但是任务通知只能单个任务对单个任务进行通信,要一对多的时候还是要用那些功能。

而且任务通知只有出队阻塞没有入队阻塞【接收任务时可能阻塞,但是如果发送失败就不会阻塞】

在这里插入图片描述

在这里插入图片描述

  1. 代替二值信号量

    删掉二值信号量的创建和信号量的NULL检测。要检测任务句柄是不是NULL。


    释放/发送信号量


    原先 xSemaphoreGiveFromISR(二值信号量句柄,任务切换变量)

    现在 vTaskNotifyGiveFromISR(目标任务句柄,任务切换变量)

    //我要把这个信号量给多个任务的话,我再写一句不就好了??



    等待/获取信号量


    原先 xSemaphoreTake(二值信号量句柄,阻塞时间)

    现在 ulTaskNotifyTake(要清零还是减一,阻塞时间) //参数为 pdFALSE 模拟计数型信号量;参数为 pdTRUE 模拟二值信号量。

  2. 代替数值信号量

    数值信号量原本有计数和资源管理两个功能。但是资源管理涉及多个任务,所以这个功能代替不了,只能代替它计数。

    获取信号量的函数的返回值是执行那个函数之前的信号量的数值。简单讲就是 把原本的值另外存起来,函数运行的时候会减一,返回值返回的是原本的值。

    函数的替换操作和二值一样

  3. 代替队列 实现消息内容的传递//内容也可以是指针

    1、只能发送 32 位的数据值。//强制类型转换

    2、消息被保存为任务的任务通知值,而且一次只能保存一个任务通知值,相当于队列长度为 1。


    释放/发送信号量


    原先 xQueueSend(队列句柄,消息内容变量的地址,阻塞时间)//任务通知就没这个入队阻塞

    现在 xTaskNotify(目标任务句柄,消息内容变量,要不要覆写)//第三个参数为 eSetValueWithOverwrite 的话不管接收任务的通知值是否已经被处理,这个通知值都会被更新;为 eSetValueWithoutOverwrite 的话如果上一个任务通知值话还没有被处理,那么新的任务通知值就不会更新。


    等待/获取信号量


    原先 xQueueReceive(队列句柄,保存消息内容变量的地址,阻塞时间)

    现在 xTaskNotifyWait(没有接收到任务通知时 是否清空消息,接收到了任务通知并处理好内容后 是否清空消息,保存消息内容变量的地址,阻塞时间) /前面两个参数 如果填0就不清空,填ULONG_MAX就清空。//这个宏要加一个头文件limits.h才能用

  4. 代替事件标志组


    释放/发送信号量


    原先 xEventGroupSetBits(事件句柄,要设置的事件位)

    现在 xTaskNotify(目标任务句柄,要设置的事件位,eSetBits)//第三个参数选择要修改指定的位//对,上面写的是要不要覆写,其实第三个参数总共有5个选项可以选。


    等待/获取信号量


    原先 xEventGroupWaitBits(事件组,事件位,退出是否清零,与/或,阻塞时间)

    现在 xTaskNotifyWait(没有接收到任务通知时 是否清空消息,接收到了任务通知并处理好内容后 是否清空消息,保存消息内容变量的地址,阻塞时间) //填ULONG_MAX就清空,要清除指定的位就对应位置1换成十六进制传入。


    在这个任务通知里,获取信号量的函数里没有

    与/或

    的功能选项,所以接到一个位的信号后如果选择清零,那就是跟二值信号量一样的用法;如果不清零,那这个与的事件真正达成之后怎么触发第二次??要不自己另外清零?




低功耗模式

//Tickless模式

任务处理函数的运行时间很少,大部分时间都是在运行空闲任务。所以空闲任务的耗电量一定要想办法降低。所以,要在空闲任务阶段进入低功耗模式。【进入低功耗模式前还可以关掉时钟,降低系统主频等等 出来的时候记得恢复】


如果关定时器是要自己操作,那低功耗模式究竟做了什么事能让功耗下降啊??


进入低功耗模式的要求

  • 空闲任务是唯一可运行的任务,其他所有的任务都处于阻塞态或者挂起态。
  • 系统处于低功耗模式的时间至少大于2

进入低功耗模式时

  • 要维持滴答定时器的运行【心跳时钟】
  • 要在下次任务需要运行时退出低功耗模式【中断任务本身就能打断低功耗模式】//居然有一个变量直接就存着’距离下一个任务需要开始运行的时间还有多久‘
  • 低功耗模式有最大运行时长,在这里是93ms。要维持更久的话就出去重新进一次低功耗模式

空闲任务的工作内容

1、释放内存

2、检查是否使用抢占内核,如果没使用,调用taskYIELD

3、如果使用抢占式内核,而且configIDLE_SHOULD_YIELD等于1,那么空闲任务就把CPU使用权让给同优先级的其他任务

4、是否使能钩子函数,使能的话就调用

5、是否使能Tickless模式,使能的话就做相应的处理工




钩子函数


Freertos的钩子函数

在任务里给指针赋内存不可以用malloc() 和free ()要用pvPortMalloc()和vPortFree()

而且使用的时候要判断NULL,释放之后要赋值NULL


任务调度器源码讲解

  1. 创建空闲任务
  2. 【可选】打开软件定时器
  3. 关中断
  4. 初始化一些静态全局变量【获取下一次任务要开始的时间,任务调度器开始,freertos整个系统的时钟节拍计数器清零】
  5. 时基定时器初始化

艾玛,一直在讲汇编。遭不住了。溜了溜了



版权声明:本文为qq_42635852原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。