tips:学习韦东山老师课程中的一些顺手散记,主要看纸质笔记
FreeRtos的本质是任务的轮流运转
一、栈
任务包含:一段代码、运行位置、运行环境 》 也就是运行起来的函数(函数和它的栈)
现场:被打断的瞬间,CPU里所有寄存器的值
保护现场:把寄存器的值保存在内存里,也就是栈里;三个场景:函数调用、中断处理、任务切换
栈:保存寄存器的一些内存空间?
二、创建任务的2个核心
1.栈
创建任务时分配了栈,在栈里写入了函数地址、参数
目的:保存局部变量,保护现场
大小:局部变量数量、调用深度
2.任务结构体
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, //函数句柄
const char* const pcName, //函数名
const configSTACK_DEPTH_TYPE usStackDepth, //栈大小,malloc分配
void* const pvParameters, //参数
UBaseType_t uxPriority, //优先级
TaskHandle_t* const pxCreatedTask) //TCB(Task Control Block)结构体,任务控制块,传出的参数
三、任务调度机制(重点)
3.1 优先级和状态
1.优先级不同
高优先级的任务优先执行,可以抢占低优先级的任务
高优先级的任务不停止,低优先级的任务永远无法执行
同等优先级的任务轮流执行(时间片轮转)
空闲任务礼让:同是优先级0的其他就绪任务,空闲任务主动放弃一次运行机会(可配置项)
2.状态不同
运行running、就绪ready、阻塞blocked、暂停/挂起suspend
阻塞:想干点活,但得等待某个时间/事件来触发
挂起:摆烂,老板来催活才干
3.2 如何管理
1. 怎么取出要运行的任务?
(1)找到最高优先级的运行态、就绪态任务,运行
(2)如果大家平级,轮流执行;TCB指向的任务首先执行,然后链表前面的先运行,一定时间后去链表尾部排队
每一次都从优先级高到低的顺序挨个找;
那么,谁取出?谁放置到链表尾部?
2. 谁进行调度?
TICK中断(定时器中断)1ms。到时间后,进入TICK中断函数。首先取出下一个Task,然后切换任务。切换时,首先保存当前Task,然后恢复新Task。
3.3 状态切换
1.启动调度器的时候会创建空闲任务
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); //创建任务1,当前TCB指向任务1
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL); //创建任务2,当前TCB指向任务2
vTaskStartScheduler(); //启动调度器,同时创建空闲任务,优先级为0
2.使用不同的链表来维护不同状态的任务
链表容易管理不同状态的任务:一种状态一条链表
改变状态时链被移动到相应的状态链表下,这样和原来的状态不产生冲突
3.状态切换
关于优先级导致的任务和空闲任务运行顺序问题,可以看P9任务调度深入探讨20min左右
//case1:同优先级
xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL); //创建任务1,当前TCB指向任务1
xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL); //创建任务2,当前TCB指向任务2
xTaskCreate(vTask3, "Task 3", 1000, NULL, 0, NULL); //创建任务3,当前TCB指向任务3
vTaskStartScheduler(); //启动调度器,同时创建空闲任务,和上面三个任务同优先级0,当前TCB指向空闲任务
//TCB指向空闲任务,因此第一个被执行。然后按照链表顺序,先运行任务1,再运行任务2,最后是任务3
//case2:任务优先级高于空闲
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL); //创建任务1,当前TCB指向任务1
xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL); //创建任务2,当前TCB指向任务2
xTaskCreate(vTask3, "Task 3", 1000, NULL, 1, NULL); //创建任务3,当前TCB指向任务3
vTaskStartScheduler(); //启动调度器,同时创建空闲任务,优先级0
//TCB指向任务3,因此第一个被执行,然后按照链表顺序,先运行任务1,再运行任务2。最后检索下一个优先级0,开始执行该优先级里的空闲任务
四、队列(重点)
4.1 队列的互斥访问(读/写)
如何实现互斥?
在QueueSend()函数里先关中断,再写数据,最后再开中断。在中断关闭的时间里没有别的任务会来打断。这里关的中断包括Tick。在FreeRtos内部已经实现了,只要调用函数,写数据就行
为什么关中断?
能写入一个队列的有:多个Task和中断IRQ。如果只是为了防止其他任务写入队列,关闭调度器就行,如果要防止中断写入,那么就要彻底关中断,此时调度器也没办法运行,不需要再单独关调度器了。
4.2 队列的休眠唤醒
Tick1的时候首先轮到Task1运行,但Task1等某种条件到来才能写队列。此时Tick2的时候轮到Task2运行,Task2需要读队列才能执行。若此时队列里有Data,则返回,若此时队列里没Data,则休眠。
等到某个Tickn的时候,Task1终于写Data进队列了,会查看哪个Task处于读队列的阻塞(休眠)状态,有的话就把那个任务(Task2)放回就绪态。
好处:休眠Task2,Task1能全速运行,Task2不占用CPU资源,程序效率更高
Task1承担的功能:1.把Data写到队列里;2.唤醒(Task2)
那么如何唤醒?唤醒谁?休眠都干什么事?
Task1要写的队列是个结构体,有个链表Queue.list1。
Task2休眠:1. 把自己从Ready链表移动到某个Delay链表;2. 把自己记录在Queue.list1链表里
五、事件组和任务通知
1. 事件组只关调度器,不关中断。
不需要关中断意味着,不会在中断中使用事件组
2. 任务通知
六、链表
1.数组在内存中是连续的单元,但链表的内存可以不连续,因此可以更好地利用内存(直接截图老师的了)
初始化链表
删除某成员
加入头节点的目的是为了统一操作
实时和非实时?