仅为个人理解,个人笔记,有些地方因为还没看源码纯属是自己的猜测,出错的地方以后如果弄明白了会改正
FreeRTOS内核版本为V10.0.1
感谢朱工大大的文章,FreeRTOS高级篇5—FreeRTOS队列分析
int8_t *pcHead
队列结构体的第一项绝对编译成员:
(1)队列结构体用于队列时,该处存放队列存储区域的首地址
(2)队列结构体用于二进制信号量和计数信号量时,该处存放队列结构体的首地址(目的是为了与互斥量区别开来,在内核中会根据这里存放的地址是否为空来判断该队列是否用于互斥量)
(3)队列结构体用于互斥量时,该处存放空地址,在内核中判断该地址为空时,在获取互斥量的时候队列结构体的第二项成员pcTail就会存放获取该互斥量的TCB的首地址,在释放互斥量的时候,在内核中判断该地址为空时,会将队列结构体的第二项成员置空,表示互斥量当前没被任何任务获取
int8_t *pcTail
队列结构体的第二项绝对编译成员:
(1)队列结构体用于队列时,该处存放队列存储区域的末地址
(2)队列结构体用于二进制信号量和计数信号量时,该处存放队列结构体的首地址(目的是为了和互斥量区别开来)
(3)队列结构体用于互斥量时,如果有任务获取到了这个互斥量,那么这里存放该任务的TCB的首地址,如果没有任务获取到这个互斥量,则被置空
int8_t *pcWriteTo
队列结构体的第三项绝对编译成员:
(1)队列结构体用于队列时,仅仅从内核源码使用该项成员来看,假设队列有三个成员空间ABC,该处在队列结构体初始化时存放的是A的首地址,目的是在从队尾入队时找到要入队的地址,在A处首地址开始拷贝队列成员大小的入队数据后,改处存放的地址改为B空间的首地址,当最后一个成员空间C被拷贝入队数据后,该处存放的地址又变回最初的A的首地址
(2)队列结构体用于二进制信号量和计数信号量时,该处自始至终存放的都是该队列结构体的首地址,表示内核源码并不会对此变量进行使用和修改
(3)队列结构体用于互斥量时,该处自始至终存放的都是该队列结构体的首地址,表示内核源码并不会对此变量进行使用和修改
union u联合体
队列结构体的第四项绝对编译成员:
int8_t *pcReadFrom :
(1)队列结构体用于队列时,仅仅从内核源码使用该项成员来看,假设队列有三个成员空间ABC,该处在队列结构体初始化时存放的是C的首地址,目的是在队首出队时要找到出队数据的首地址,这里将出队时的部分源码附上并做注释
// 当该队列结构体的uxItemSize成员不等于0,即该队列结构体只用于队列时,才能出队有效数据
if (pxQueue->uxItemSize != (UBaseType_t)0)
{
// 为了能够第一次出队时就找到队首(A),所以出队时不是先出队再移动地址,而是先移动地址再出队
pxQueue->u.pcReadFrom += pxQueue->uxItemSize;
// 如果出队地址到了队列存储区域的末尾(C):第一次出队时是这种情况,因为入队的首地址在A处,所以出队时要将出队地址也拨回到A处.不是第一次出队时,因为入队时的地址在C处入队成功后回拨回到A,为了保证先入先出,所以出队时也应该采用相同的方式
if (pxQueue->u.pcReadFrom >= pxQueue->pcTail) /*lint !e946 MISRA exception justified as use of the relational operator is the cleanest solutions. */
{
pxQueue->u.pcReadFrom = pxQueue->pcHead;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
// 拷贝数据
(void)memcpy((void *)pvBuffer, (void *)pxQueue->u.pcReadFrom, (size_t)pxQueue->uxItemSize);
}
(2)队列结构体用于二进制信号量和计数信号量时,该处自始至终存放的都是该队列结构体的首地址,表示内核源码并不会对此变量进行使用和修改
(3)队列结构体用于互斥量时,转而使用联合体内的另一变量
UBaseType_t uxRecursiveCallCount:
(1)队列结构体用于队列时,转而使用联合体内的另一变量
(2)队列结构体用于二进制信号量和计数信号量时,转而使用联合体内的另一变量
(3)队列结构体用于互斥量时:当用于非递归互斥量时,该值始终为0,内核并不会访问和修改,当用于递归互斥量时,该值初始值为0,当第一次调用递归互斥量获取函数获取互斥量时,会间接调用普通互斥量获取函数(任务TCB的统计获取到的互斥量的变量加1,队列结构体的pcTail指向当前任务),因此该值不会变化,当再调用递归互斥量获取函数获取互斥量时,每获取一次会加1,即该值=获取互斥量的次数-1,当调用递归互斥量释放函数释放互斥量时,先判断该值是否为0,若为0则直接调用普通互斥量释放函数释放互斥量,释放时和互斥量一样,会判断当前任务是否有继承优先级以及是否该恢复优先级,若调用递归互斥量释放函数时该值不为0,那么该值将会减1
List_t xTasksWaitingToSend
绝对编译成员
(1)队列结构体用于队列时,如果执行入队函数且设置了等待时间不为0,当队列满时任务TCB的事件列表项就有可能挂到此列表中,同时任务TCB的状态列表项在记录阻塞时间后也会被挂到延时列表上
注意TCB的事件列表项挂到阻塞列表上的时候是执行的有序插入,TCB的状态列表项在记录阻塞时间后挂到延时列表上时也是有序插入,而TCB的状态列表项插入到就绪列表时是直接插入到末尾
之所以说"如果执行入队函数且设置了等待时间不为0,当队列满时任务TCB的事件列表项就有可能挂到此列表中"一句中的"可能",是因为如果延时时间很少,这里假如就只有3个系统节拍,在任务运行中进入到入队函数->进入临界区->访问临界资源(这里指队列)->判断队列满->判断有设置延迟时间->初始化延迟结构体>退出临界区,就在这时,产生系统节拍中断,切换到另外一个任务,然后另外这个任务刚好出队,然后再次产生系统节拍中断,回到本任务->挂起调度器和上锁队列(临界资源仅可以有限制的被ISR访问,即不能操作队列结构体的列表,防止此时ISR出队造成低优先级的任务TCB的事件列表项从等待入队的列表上卸载以及状态列表项挂到就绪列表中)->依据延时结构体判断是否超时->没有超时(延迟时间为3个系统节拍,刚才的任务切换已经产生了两个系统节拍)->再次判断队列,发现没有满->恢复调度器和解锁队列->进入临界区重新入队,在这样的情况下即使设置了延迟时间也不会因为队列满而该任务TCB被阻塞,简单讲就是退出临界区是在延迟结构体被设置之后,如果刚退出临界区立即调度到其他的任务,且其他的任务出队造成队列没有满,那么在再一次调度到此任务时就会因为延迟时间不为0而不直接退出入队函数,会再一次判断队列,没满,所以直接重新入队
因此,在设置阻塞时间时,设置为比较小的数字(比如3)也比设置为0要好
(2)队列结构体用于二进制信号量和计数信号量时,因为信号量的释放是没有等待时间的,在一个任务中,必须先获取信号量才能释放信号量,任务只会因为没有获取到信号量才被阻塞,而不会因为没有释放信号量而被阻塞,所以本列表不会挂载任何的任务TCB的列表项,始终是一个空列表
(3)队列结构体用于互斥量时,和队列结构体用于二进制信号量和计数信号量时一样,本列表不会挂载任何的列表项
List_t xTasksWaitingToReceive
绝对编译成员:
(1)队列结构体用于队列时:同上一个列表同理,因为队列空并且有设置等待时间不为0个节拍的任务TCB的事件列表项可能会被有序插入到此列表中
(2)队列结构体用于二进制信号量和计数信号量时:当信号量已被获取且设置了等待时间不为0个节拍的任务TCB的事件列表项可能会被有序插入到此列表中
(3)队列结构体用于互斥量时:当信号量已被获取且设置了等待时间不为0个节拍的任务TCB的事件列表项可能会被有序插入到此列表中
volatile UBaseType_t uxMessagesWaiting;
绝对编译成员:
volatile关键字:内核会不断的访问并修改该成员,加上volatile来保护
(1)队列结构体用于队列时:该项记录队列中已有的成员数目,初始化时为0
(2)队列结构体用于二进制信号量和计数信号量时:对于二进制信号量,初始化时为0,当为1时表示已被获取,为0时表示没有被获取.对于计数信号量,初始化时由用户指定,获取的次数最大为uxLength,
(3)队列结构体用于互斥量时:初始化时为1,为0时表示已被获取,为1时表示没有被获取
UBaseType_t uxLength
绝对编译成员:
(1)队列结构体用于队列时:该项表示队列的长度
(2)队列结构体用于二进制信号量和计数信号量时:对于二进制信号量,初始化时为1,表示二进制信号量最多获取一次.对于计数信号量,初始化时由用户指定,表示用户可以获取信号量的最大次数
(3)队列结构体用于互斥量时:该项始终为1,表示互斥量最多可以获取一次,保护资源
UBaseType_t uxItemSize
绝对编译成员:
(1)队列结构体用于队列时:该项表示队列成员的大小,单位是byte
(2)队列结构体用于二进制信号量和计数信号量时:该项始终为0,没有任何作用
(3)队列结构体用于互斥量时:该项始终为0,没有任何作用
volatile int8_t cRxLock
绝对编译成员:
(1)队列结构体用于队列时:该项初始化时为-1,表示队列没有被上锁,调用队列上锁函数后该值变为0,表示队列刚上锁
在队列上锁后(调度器也会被挂起,所以不会切换到其他任务),ISR中出队或入队成功后,会先判断该队列是否上锁
如果没有上锁,则判断队列结构体的相应列表(等待入队或等待出队列表)是否为空,为空pass,不为空的话卸载列表上的第一个任务TCB的事件列表项(同时任务TCB的状态列表项插入到就绪列表的末尾),再判断卸载的任务是否高于当前任务的优先级,没有pass,有的话记录(ISR执行入队或出队函数结束后会根据这个记录值来判断是否执行任务切换函数)
如果队列上锁,且执行的是出队函数,并且出队成功,那么该项会加1,表示在队列上锁期间ISR出队成功一次,在队列解锁时会根据此项来判断应该执行几次<判断等待入队的列表是否为空并做出相应处理>函数
综上所述:该项为-1时表示队列没有上锁,不为-1时的数目表示队列在解锁后应该执行<判断等待入队的列表是否为空并做出相应处理>函数的次数
(2)队列结构体用于二进制信号量和计数信号量时:比较特殊,即使在内核中进行了相关的操作但是没有用处.
ISR在获取信号量成功后,若信号量上锁(发生在内核API的获取信号量的函数内,释放信号量的函数因为不具有延迟时间所以不会将信号量上锁,同时任务调度器也会挂起),那么此值也会加1,在ISR结束后内核API解锁信号量(同样也只会发生在获取信号量的API函数内),此时会执行函数<判断等待释放信号量的列表是否为空并做出相应处理>,但是信号量的释放是没有延迟时间的(为0个系统节拍周期),所以即使信号量释放不成功,任务TCB的事件列表项也不会挂在等待获取信号量的列表上,所以在上锁信号量后,ISR成功获取信号量,后面的操作–此值加1,解锁信号量,执行<判断等待释放信号量的列表是否为空并做出相应处理>函数–实际上是没有任何用处的
(3)队列结构体用于互斥量时:因为互斥量不能用于ISR(ISR不具有任务优先级,当然也无法继承任务优先级),但是在内核API函数xQueueSemaphoreTake中依旧在信号量已被获取,设置了等待时间不为0个系统节拍后挂起了调度器并且上锁了信号量,但是ISR中不会使用信号量,所以互斥信号量的上锁和解锁是没有意义的,因此在互斥量中该值只会是-1(代表信号量没有上锁)或者0(代表刚上锁)
volatile int8_t cTxLock
绝对编译成员:
(1)队列结构体用于队列时,同理队列结构体成员cRxLock
(2)队列结构体用于二进制信号量和计数信号量时:
在执行获取信号量的API函数后,若信号量已被别的任务获取,且设置了等待时间,那么在将本任务TCB的事件列表项挂到等待获取信号量的列表上之前,会先挂起调度器和上锁信号量(此值变为0),此时若产生中断,ISR中释放了信号量,但是信号量已上锁,此值加1变为1,然后ISR退出,返回到任务,任务解锁信号量,同时会执行<判断等待获取信号量的列表是否为空并做出相应处理>,若较低优先级的任务被解除阻塞,那么该任务稍后会获取到该信号量,若是相同优先级或更高优先级的任务(假设B任务)被解除阻塞,那么B任务会获得该信号量,相反该任务会进入阻塞(若B是更高优先级的任务,那么该任务会处于非常尴尬的状态,此时该任务没进入阻塞列表,仅仅卡在了要再次进入临界区获取信号量的时候,不过因为已经初始化了延时时间,所以如果B任务再次进入阻塞,回到该任务后,如果信号量还是已经被获取,那么该任务可能因为阻塞时间已到而直接返回errQUEUE_EMPTY)