博客很久没更新了,忙着写简历、刷题、找实习去了,实习offer倒是拿到几个,但是导师不让我出去实习,哎…….. 苦逼牛马研究生的无奈

FreeRTOS内部机制剖析

FreeRTOS是一个嵌入式实时系统,内核设计十分精简,核心代码也比较少,是一个运行在单核cpu的系统 ,支持多种架构比如ARMRISCV

1.FreeRTOS编译运行

移植此系统之前我们先来看一下如何编译运行,我们还是在qemu上运行。首先去下载源码:

我下载的是GIthubFreeRTOSv202107.00版本,FreeRTOS源码文件夹主要组成如下:

image-20240414144234864

  • sourceFreeRTOS源代码,包含不同平台移植相关的源码和FreeRTOS内核本身的代码
  • Demo为不同平台下运行的示例

我们直接来到FreeRTOS/Demo/RISC-V-Qemu-virt_GCC文件夹下,这个demo工程就是官方为Qemu-virt平台适配的,在此目录下直接make编译,大概率是会直接报错的,错误大概有如下几项:

  • target emulation elf32-littleriscv' does not match elf64-littleriscv

    这个错误是编译工具链的选择问题,在Makefile第一行中,官方将默认编译器设置成了

    CROSS   = riscv64-unknown-elf-

    修改成32位的编译器即可

    CROSS   = riscv32-unknown-elf-
  • Error: unrecognized opcode csrc mstatus,8, extension zicsr required

    这个错误是在编译时需要去读取控制寄存器但是却没有启用控制寄存器相关的拓展,在编译选项后加入_zicsr的拓展即可

    image-20240414151147506

  • can't link double-float modules with soft-float modules

    ”这个错误编译环境中存在浮点数支持配置的不一致性。具体来说,项目中有些模块是用硬浮点(double-float)编译的,而有些则是用软浮点(soft-float)编译的。通过统一浮点支持配置来解决,加上d拓展就可以了

    image-20240414151415134

解决上述问题后再编译应该就不会报错了,接下来是使用qemu运行,在demo工程下官方有一个readme来说明了如何运行:

image-20240414151546548

qemu-system-riscv32可以直接apt下载

运行试试看,出现下面的打印就说明运行成功了

image-20240414152043708

2.FreeRTOS常用API

任务创建

任务控制

RTOS内核控制

队列

信号量/互斥锁

软件定时器

事件组

3. 任务管理与调度

3.1 FreeRTOS的TCB

typedef struct tskTaskControlBlock             
{
// 这里栈顶指针必须位于TCB第一项是为了便于上下文切换操作,详见xPortPendSVHandler中任务切换的操作。
volatile StackType_t *pxTopOfStack;

// MPU相关暂时不讨论
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings;
#endif

// 表示任务状态,不同的状态会挂接在不同的状态链表下,这是链表的节点,可以用于挂载到链表上
ListItem_t xStateListItem;
// 事件链表项,会挂接到不同事件链表下
ListItem_t xEventListItem;
// 任务优先级,数值越大优先级越高
UBaseType_t uxPriority;
// 指向堆栈起始位置,这只是单纯的一个分配空间的地址,可以用来检测堆栈是否溢出
StackType_t *pxStack;
// 任务名
char pcTaskName[ configMAX_TASK_NAME_LEN ];

// 指向栈尾,可以用来检测堆栈是否溢出
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t *pxEndOfStack;
#endif

// 记录临界段的嵌套层数
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif

// 跟踪调试用的变量
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif

// 任务优先级被临时提高时,保存任务原本的优先级
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority;
UBaseType_t uxMutexesHeld;
#endif

// 任务的一个标签值,可以由用户自定义它的意义,例如可以传入一个函数指针可以用来做Hook 函数调用
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif

// 任务的线程本地存储指针,可以理解为这个任务私有的存储空间
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif

// 运行时间变量
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter;
#endif

// 支持NEWLIB的一个变量
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent;
#endif

// 任务通知功能需要用到的变量
#if( configUSE_TASK_NOTIFICATIONS == 1 )
// 任务通知的值
volatile uint32_t ulNotifiedValue;
// 任务通知的状态
volatile uint8_t ucNotifyState;
#endif

// 用来标记这个任务的栈是不是静态分配的
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif

// 延时是否被打断
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif

// 错误标识
#if( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno;
#endif

} tskTCB;
typedef tskTCB TCB_t;

3.2 任务创建

xTaskCreate

     
TCB_t * pxNewTCB;
BaseType_t xReturn;
StackType_t * pxStack;
pxStack = pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); //分配任务栈内存
if( pxStack != NULL )
{
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); //分配TCB内存
if( pxNewTCB != NULL )
{
pxNewTCB->pxStack = pxStack; //栈指针赋值
}
else
{
vPortFree( pxStack );
}
}
else
{
pxNewTCB = NULL;
}
if( pxNewTCB != NULL )
{
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
{
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif
//初始化任务
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
//将此任务挂载到ready链表中
prvAddNewTaskToReadyList( pxNewTCB );
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}

return xReturn;
  • 首先是创建一个TCB,分配一片内存
  • 然后是为创建的任务分配栈空间
  • 调用prvInitialiseNewTask初始化任务,会做如下操作:
    • 此函数内部会去调用pxPortInitialiseStack来初始化任务的栈,任务栈中存储的是任务的执行状态的寄存器的值,pxPortInitialiseStack会构建一份初始化的值将这个任务切换上下文对应寄存器的值全部设置为0
    • 填充任务的名字
    • 初始化任务优先级
    • 设置栈溢出地址
  • 将此任务挂载到对应优先级的ready链表中

3.3 多优先级链式TCB连接

FreeRTOS中任务有多种状态和多种优先级

/* Task states returned by eTaskGetState. */
typedef enum
{
eRunning = 0, // 运行态
eReady, // 初始化准备运行态
eBlocked, // 阻塞态
eSuspended, // 挂起态
eDeleted, // 任务被删除
eInvalid // 任务为空
} eTaskState;

任务切换的逻辑如下:

20181211190823136

  • 就绪态:任务在创建完成时是ready状态,就绪态的任务等待调度器调度
  • 运行态:任务独占cpu正在运行
  • 阻塞态:等待某个事件的到来,定时或者同步
  • 挂起态:退出调度系统,调度器不可见,只能使用vTaskSuspend()挂起和vTaskResume()唤醒后进入就绪态
PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;  //指向当前运行任务的全局指针
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; //就绪任务链表数组
PRIVILEGED_DATA static List_t xDelayedTaskList1; //存储被延时挂起的任务
PRIVILEGED_DATA static List_t xDelayedTaskList2; //存储被延时挂起的任务
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
PRIVILEGED_DATA static List_t xPendingReadyList; //挂起任务链表
  • pxCurrentTCB是一个TCB_t类型的全局指针,用于指向当前运行任务的地址

  • pxReadyTasksLists[ configMAX_PRIORITIES ];是一个数组,数组下标表示优先级,其每一个数组元素都是一个链表根节点数据结构。如下图所示。在FreeRTOS中,优先级数值越大,优先级越高。

    //链表相关数据结构
    typedef struct xLIST
    {
    volatile UBaseType_t uxNumberOfItems;
    ListItem_t * configLIST_VOLATILE pxIndex;
    MiniListItem_t xListEnd;
    listSECOND_LIST_INTEGRITY_CHECK_VALUE
    } List_t;
    //MiniListItem_t 是一个双向循环链表
    struct xMINI_LIST_ITEM
    {
    configLIST_VOLATILE TickType_t xItemValue;
    struct xLIST_ITEM * configLIST_VOLATILE pxNext;
    struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
    };
    typedef struct xMINI_LIST_ITEM MiniListItem_t;
    //
    struct xLIST_ITEM
    {
    configLIST_VOLATILE TickType_t xItemValue;
    struct xLIST_ITEM * configLIST_VOLATILE pxNext;
    struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
    void * pvOwner;
    struct xLIST * configLIST_VOLATILE pxContainer;
    };
    typedef struct xLIST_ITEM ListItem_t;

    e05eeeaea6e1992dbaf23dcdcbdd56cb

  • 对于数组中的每个优先级的任务链表,构成如下:

    27f0786016fb060fa35ec45fbc630c28

    TCB的定义中:定义了两个ListItem_t 类型的变量,因此任务挂载到任务链表上实际上就是通过第一个xStateListItem来实现的

    // 表示任务状态,不同的状态会挂接在不同的状态链表下,这是链表的节点,可以用于挂载到链表上
    ListItem_t xStateListItem;
    // 事件链表项,会挂接到不同事件链表下
    ListItem_t xEventListItem;

    任务链表:xLIST即根节点,可以是就绪链表、阻塞链表、挂起链表等。

    链表中的节点:每个红框为一个节点,通过pvOwner与任务控制块联系到一起,通过pvContainer挂载到就绪、阻塞、挂起链表上。

  • 就绪列表(数组)任务控制块(TCB) 的拓扑结构如下:

    watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATW9uc3RlciB4bg==,size_20,color_FFFFFF,t_70,g_se,x_16

  • 就绪列表是含有多个链表的数组集合,数组的索引代表优先级。
    任务可以通过TCB中的xLIST_ITEM挂在任意一个优先级上的链表中。如上图就表示一个有256个优先级的就绪列表。其中优先级为254的链表上有两个节点,这两个节点分别属于两个任务的TCB。因此这两个TCB的任务优先级就是254,而且目前处于就绪态。假设此时有一个优先级为0的任务从其他状态转换到就绪态。则该任务插入就绪列表的过程如下:该 任务的TCB中的xLIST_ITEM 会链接(link)到 就绪列表数组的0号元素根节点xList中的MiniListItem_t 上。·

3.4 任务调度方式

抢占式调度

  • 在可抢占式调度中,任务可以被更高优先级的任务抢占。当一个高优先级任务变得可用时,它可以打断当前正在执行的低优先级任务,从而使系统立即切换到高优先级任务执行。

  • FreeRTOS中,任务调度是基于任务优先级的。当一个任务抢占另一个任务时,它会立即执行,无论被抢占的任务是否已经执行完其时间片。这种方式确保了高优先级任务能够及时响应,并在需要时立即执行,不受低优先级任务的阻碍。

  • FreeRTOS中通过配置configUSE_PREEMPTION来决定是否启动抢占。

  • 如果高优先级不让出cpu,那么低优先级的任务将一直得不到执行

时间片轮转调度

  • 时间片轮转是指操作系统为每个任务分配一个时间片,即预定义的时间量。在时间片轮转调度方式下,每个任务可以执行一个时间片,然后系统将控制权移交给下一个就绪的任务。如果一个任务在其时间片结束前没有完成,系统会暂停该任务,将控制权交给下一个就绪的任务。

  • FreeRTOS允许你在配置系统时启用或禁用时间片轮转。时间片的大小可以根据应用程序的需要进行调整。这种调度方式有助于确保任务之间的公平性,避免某些任务长时间占用处理器,同时允许多个任务分享处理时间。

  • FreeRTOS中通过配置configUSE_TIME_SLICING来决定是否启动时间片轮转

  • 对于同优先级的任务才会启用时间片轮转

如何让出CPU

  • 任务主动让出:任务可以调用vTaskDelay()函数或者vTaskDelayUntil()函数,将自己挂起一段时间,以便其他任务能够运行。这种方式是任务主动放弃CPU的一种方式
  • 阻塞等待事件:任务可以调用FreeRTOS提供的阻塞函数,如xQueueReceive()、xSemaphoreTake()等,来等待特定事件的发生。当任务在等待某个事件时,它会被置于阻塞状态,从而释放CPU,直到事件发生后才会被唤醒。
  • 任务进入阻塞状态:任务在执行过程中,如果发生某些阻塞事件,如等待一个队列满足条件、等待互斥信号量等,会自动进入阻塞状态,这时会释放CPU。一旦阻塞条件得到满足,任务将被重新置于就绪状态。

4.消息队列和信号量

消息队列是用于进程间通讯的,且可以实现互斥的访问,避免多进程间由于任务切换导致的竞态问题

  • xQueueCreate创建一个队列
  • xQueueSend写队列
  • xQueueReceive读队列

4.1 队列创建和初始化

首先我们来看一下队列在FreeRTOS中的定义:


typedef struct QueuePointers
{
int8_t * pcTail;
int8_t * pcReadFrom;
} QueuePointers_t;
typedef struct QueueDefinition
{
int8_t * pcHead; /*< 指向队列存储的起始位置。*/
int8_t * pcWriteTo; /*< 数据存储区下一个可以写入数据的位置。 */

union
{
QueuePointers_t xQueue; /*< 当结构体作为一个队列时使用,包含两个指针 */
SemaphoreData_t xSemaphore; /*< 当队列作为信号量时使用. */
} u;

List_t xTasksWaitingToSend; /*< 阻塞等待向队列中放入元素的任务链表。按照优先级顺序存储。 */
List_t xTasksWaitingToReceive; /*< 阻塞等待从队列中读取元素的任务链表,按照优先级顺序存储。 */
volatile UBaseType_t uxMessagesWaiting; /*<当前队列中的元素个数. */
UBaseType_t uxLength; /*< 队列能够存储的元素个数。 */
UBaseType_t uxItemSize; /*< 每一个元素的大小,如果这个值为0表示Mutex*/

volatile int8_t cRxLock; //队列上锁后,储存从队列收到的列表项数目
volatile int8_t cTxLock; //队列上锁后,储存发送到队列的列表项数目

#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated;
#endif

#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition * pxQueueSetContainer; //队列集
#endif

#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType; //队列类型
#endif
} xQUEUE;

队列是通过xQueueCreate这个宏来创建的,这个宏内部会去调用xQueueGenericCreate函数来创建一个队列,例如:

QueueHandle_t xMyQueueHandle;
xMyQueueHandle = xQueueCreate(20,sizeof(int32_t));

创建了一个长度为20个元素、每个元素为int32_t类型的队列:

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
{
Queue_t * pxNewQueue; /* 消息队列控制块,一个结构体指针*/
size_t xQueueSizeInBytes; /* 需要分配的内存大小,Bytes为单位 */
uint8_t * pucQueueStorage; /* 实际存放消息的地址,即消息队列控制块的后面 */

configASSERT( uxQueueLength > ( UBaseType_t ) 0 ); /*判断要创建的消息队列的长度是否大于0*/
//计算需要分配的内存大小/字节 元素个数*单个元素大小
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );

configASSERT( ( uxItemSize == 0 ) || ( uxQueueLength == ( xQueueSizeInBytes / uxItemSize ) ) );


configASSERT( ( sizeof( Queue_t ) + xQueueSizeInBytes ) > xQueueSizeInBytes );

//分配内存,队列头+实际的元素占用的大小
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );

if( pxNewQueue != NULL )
{
//计算存放元素的内存开始地址
pucQueueStorage = ( uint8_t * ) pxNewQueue;
pucQueueStorage += sizeof( Queue_t );

#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{

pxNewQueue->ucStaticallyAllocated = pdFALSE;
}
#endif
/*初始化一个队列*/
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
}
else
{
traceQUEUE_CREATE_FAILED( ucQueueType );
mtCOVERAGE_TEST_MARKER();
}

return pxNewQueue;
}

对列在创建完毕分配内存后会进行初始化,调用prvInitialiseNewQueue函数,此函数内部会调用xQueueGenericReset去做一些初始化操作

static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
uint8_t * pucQueueStorage,
const uint8_t ucQueueType,
Queue_t * pxNewQueue )
{
( void ) ucQueueType;

if( uxItemSize == ( UBaseType_t ) 0 )
{
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
}
else
{
pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;//初始化头
}

pxNewQueue->uxLength = uxQueueLength;
pxNewQueue->uxItemSize = uxItemSize;
( void ) xQueueGenericReset( pxNewQueue, pdTRUE );

#if ( configUSE_TRACE_FACILITY == 1 )
{
pxNewQueue->ucQueueType = ucQueueType;
}
#endif /* configUSE_TRACE_FACILITY */

#if ( configUSE_QUEUE_SETS == 1 )
{
pxNewQueue->pxQueueSetContainer = NULL;
}
#endif /* configUSE_QUEUE_SETS */

traceQUEUE_CREATE( pxNewQueue );
}
BaseType_t xQueueGenericReset( QueueHandle_t xQueue,
BaseType_t xNewQueue )
{
Queue_t * const pxQueue = xQueue;

configASSERT( pxQueue );

taskENTER_CRITICAL(); //关中断
{
pxQueue->u.xQueue.pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize );
pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;
pxQueue->pcWriteTo = pxQueue->pcHead;
pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - 1U ) * pxQueue->uxItemSize );
pxQueue->cRxLock = queueUNLOCKED;
pxQueue->cTxLock = queueUNLOCKED;

if( xNewQueue == pdFALSE )
{
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
//初始化阻塞任务链表
vListInitialise( &( pxQueue->xTasksWaitingToSend ) );
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
}
}
taskEXIT_CRITICAL(); //开中断
return pdPASS;
}

队列在初始化完成后内存分布如下:

image-20240417161447063

pcHeadpcTail这两个指针用来记录队列的起始地址和结束地址,这两个值始终不变。数据的写入和读取分别使用pvWriteTopcReadFrom两个指针,pcReadFrom始终指向pvWriteTo指向元素的前一个元素的地址。

4.2 写队列

xQueueSend会去调用xQueueGenericSend写队列

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 = ( Queue_t * ) xQueue; /* 消息队列控制块*/

configASSERT( pxQueue );
configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );
#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
{
configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
}
#endif

/* 这个函数放松了编码标准,允许在函数本身中使用返回语句。这样做是为了提高执行时间效率 */
for( ;; )
{
taskENTER_CRITICAL();
{
/*当前消息队列的个数未满或设置了可以覆盖写入 */
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
{
traceQUEUE_SEND( pxQueue );
/* 复制数据到队列中 */
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

#if ( configUSE_QUEUE_SETS == 1 )
{
/* 队列所属队列集不为空 */
if( pxQueue->pxQueueSetContainer != NULL )
{
/* 向队列集发送通知消息 */
if( prvNotifyQueueSetContainer( pxQueue, xCopyPosition ) != pdFALSE )
{
/* 该队列是队列集的成员,向队列集发送消息会导致一个优先级更高的任务解除阻塞,需要上下文切换 */
queueYIELD_IF_USING_PREEMPTION();
}
}
/* 队列所属队列集为空 */
else
{
/* 如果有任务等待数据到达队列,那么现在解除阻塞 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
/* 未阻塞的任务的优先级比我们自己的要高,所以要立即执行。
是的,在临界区内这样做是没问题的——内核会负责这一点。 */
queueYIELD_IF_USING_PREEMPTION();
}
}
else if( xYieldRequired != pdFALSE )
{
/* 此路径是一种特殊情况,只有在任务持有多个互斥锁并且这些互斥锁返回的顺序与获取它们的顺序不同时才会执行 */
queueYIELD_IF_USING_PREEMPTION();
}
}
}
#else /* configUSE_QUEUE_SETS */
//...省略部分
#endif /* configUSE_QUEUE_SETS */

taskEXIT_CRITICAL();
return pdPASS;
}
else /* 队列已满,无法写入时 */
{
if( xTicksToWait == ( TickType_t ) 0 ) /* 若设置的阻塞等待时间为0 */
{ /* 那就不写了,直接退出临界区 */
taskEXIT_CRITICAL();
/* 退出函数之前返回原始权限级别 */
traceQUEUE_SEND_FAILED( pxQueue );
/* 返回“队列满”的提示 */
return errQUEUE_FULL;
}
else if( xEntryTimeSet == pdFALSE ) /* 若设置的阻塞等待时间不为0,且进入时间还未设置 */
{ /* 队列已满,并且指定了一个块时间,因此配置超时结构体 */
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
}
}
taskEXIT_CRITICAL();

/* 现在临界区已经退出,中断和其他任务可以向队列发送和从队列接收 */
vTaskSuspendAll();
prvLockQueue( pxQueue );
/* 更新超时状态,看看它是否已经过期 */
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) /*未超时*/
{
if( prvIsQueueFull( pxQueue ) != pdFALSE )
{
traceBLOCKING_ON_QUEUE_SEND( pxQueue );
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait );

/* 解除队列锁定意味着队列事件可以影响事件列表。现在发生的中断可能会再次将该任务从事件列表中删除
——但由于调度器被挂起,该任务将进入挂起的最后一个准备列表,而不是实际的准备列表。 */
prvUnlockQueue( pxQueue );

/*恢复任务调度器将从等待就绪列表进入就绪列表,所以它是可行的,这个任务已经就绪列表之前产量
——在这种情况下,不会导致上下文切换,除非也在等待一个更高优先级的任务就绪列表 */
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API();
}
}
else
{ /* 再试一次 */
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}
else /* 已经超时 */
{
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();

traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL;
}
}
}

img

  • 当前进程去写队列时,如果发现队列满了,则会将自己挂起,然后将自己挂到队列的等待写任务链表中,然后执行任务切换
  • 如果队列未满,即当前队列可写,则会调用prvCopyDataToQueue函数去写队列,写完数据后会从队列的delay任务链表中取出一个想要读数据的任务,将此任务添加到就绪链表,然后执行任务切换
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,
const void * pvItemToQueue,
const BaseType_t xPosition )
{
BaseType_t xReturn = pdFALSE;
UBaseType_t uxMessagesWaiting;

uxMessagesWaiting = pxQueue->uxMessagesWaiting;

if( pxQueue->uxItemSize == ( UBaseType_t ) 0 )
{
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{

xReturn = xTaskPriorityDisinherit( pxQueue->u.xSemaphore.xMutexHolder );
pxQueue->u.xSemaphore.xMutexHolder = NULL;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_MUTEXES */
}
else if( xPosition == queueSEND_TO_BACK )
{
( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
pxQueue->pcWriteTo += pxQueue->uxItemSize;

if( pxQueue->pcWriteTo >= pxQueue->u.xQueue.pcTail )
{
pxQueue->pcWriteTo = pxQueue->pcHead;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
( void ) memcpy( ( void * ) pxQueue->u.xQueue.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
pxQueue->u.xQueue.pcReadFrom -= pxQueue->uxItemSize;

if( pxQueue->u.xQueue.pcReadFrom < pxQueue->pcHead )
{
pxQueue->u.xQueue.pcReadFrom = ( pxQueue->u.xQueue.pcTail - pxQueue->uxItemSize );
}
else
{
mtCOVERAGE_TEST_MARKER();
}

if( xPosition == queueOVERWRITE )
{
if( uxMessagesWaiting > ( UBaseType_t ) 0 )
{
--uxMessagesWaiting;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

pxQueue->uxMessagesWaiting = uxMessagesWaiting + ( UBaseType_t ) 1;

return xReturn;
}
  • 可以看见prvCopyDataToQueue内部根据xPosition来判断如何写入,总共有三种方式:队尾、队头、覆盖

    #define queueSEND_TO_BACK       ( ( BaseType_t ) 0 )
    #define queueSEND_TO_FRONT ( ( BaseType_t ) 1 )
    #define queueOVERWRITE ( ( BaseType_t ) 2 )
  • xQueueSend默认是发送到队尾:

    #define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )

4.3 读队列

xQueueReceive()函数实际是使用xQueueGenericReceive()这个函数:

#define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdFALSE )

BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, const BaseType_t xJustPeeking )
{
BaseType_t xEntryTimeSet = pdFALSE;
TimeOut_t xTimeOut;
int8_t *pcOriginalReadPosition;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;

configASSERT( pxQueue );
configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
{
configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
}
#endif

/* 这个函数放松了编码标准,允许在函数本身中使用返回语句。这样做是为了提高执行时间效率 */
for( ;; )
{
taskENTER_CRITICAL();
{
const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;
/* 现在队列中是否有数据?要运行调用任务,必须是希望访问队列的优先级最高的任务 */
if( uxMessagesWaiting > ( UBaseType_t ) 0 ) /*消息队列中有数据*/
{
pcOriginalReadPosition = pxQueue->u.pcReadFrom;/* 记住读取位置,以防队列被读取 */
prvCopyDataFromQueue( pxQueue, pvBuffer );/* 从队列中复制数据 */
if( xJustPeeking == pdFALSE ) /*不是Peeking模式读取消息*/
{
traceQUEUE_RECEIVE( pxQueue );
pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;/* 实际上删除数据,而不仅仅是读取 */
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{ /* 如果有必要,记录实现优先级继承所需的信息 */
pxQueue->pxMutexHolder = ( int8_t * ) pvTaskIncrementMutexHeldCount();
}
}
#endif /* configUSE_MUTEXES */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{ queueYIELD_IF_USING_PREEMPTION(); }
}
}
else /*是Peeking模式读取消息(只读取,不删除)*/
{
traceQUEUE_PEEK( pxQueue ); /*执行PEEK*/
pxQueue->u.pcReadFrom = pcOriginalReadPosition;/* 数据没有被移除,所以重置读指针 */
/* 数据被留在队列中,因此请查看是否有其他任务在等待该数据 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{ /* 等待的任务具有比该任务更高的优先级 */
queueYIELD_IF_USING_PREEMPTION(); /*进行任务切换*/
}
}
}
taskEXIT_CRITICAL();
return pdPASS;
}
else /*消息队列中没有数据*/
{
if( xTicksToWait == ( TickType_t ) 0 )
{ /* 队列是空的,没有指定块时间(或者块时间已经过期),所以现在离开 */
taskEXIT_CRITICAL();
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
else if( xEntryTimeSet == pdFALSE )
{ /* 对列是空的,并且指定了块时间,因此配置超时结构 */
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
}
}
taskEXIT_CRITICAL();

/* 临界区已经退出,中断和其他任务可以向队列发送或从队列接收消息 */
vTaskSuspendAll();
prvLockQueue( pxQueue );
/* 更新超时状态,查看它是否已经过期 */
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )//阻塞等待未超时
{
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )//消息队列为空
{
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{
taskENTER_CRITICAL();
{ vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder ); }
taskEXIT_CRITICAL();
}
}
#endif
vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );/*添加任务到阻塞列表*/
prvUnlockQueue( pxQueue );
if( xTaskResumeAll() == pdFALSE )
{ portYIELD_WITHIN_API(); }
}
else //消息队列为空
{ /* 再试一次 */
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}
else//阻塞等待已超时
{
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )//消息队列为空
{
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
}
}
}

img

  • 当前任务去读队列时,如果队列中没有数据,则会将自己挂起,添加到队列的等待读的任务链表中,其他任务向队列中写入数据后会去队列的等待读链表中唤醒任务
  • 如果队列中有数据,则会去调用prvCopyDataFromQueue函数从队列中取出一个元素
static void prvCopyDataFromQueue( Queue_t * const pxQueue,
void * const pvBuffer )
{
if( pxQueue->uxItemSize != ( UBaseType_t ) 0 )
{
pxQueue->u.xQueue.pcReadFrom += pxQueue->uxItemSize;

if( pxQueue->u.xQueue.pcReadFrom >= pxQueue->u.xQueue.pcTail )
{
pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.xQueue.pcReadFrom, ( size_t ) pxQueue->uxItemSize );
}
}

4.4 信号量和互斥量

信号量和互斥量都是特殊的队列,都是通过队列来实现的,相当于队列中只放了一个元素供所有进程共享,信号量用于计数,互斥量只有0 , 1值,用于进程互斥

01_mutex_example

01_semaphore_usage

参考链接