美文网首页
FreeRTOS学习笔记-2-任务管理

FreeRTOS学习笔记-2-任务管理

作者: Mr_Michael | 来源:发表于2019-04-23 17:18 被阅读0次

    基于 FreeRTOS 的应用程序由一组独立的任务构成——每个任务都是具有独立权
    限的小程序。这些独立的任务之间很可能会通过相互通信以提供有用的系统功能。
    FreeRTOS 中所有的通信与同步机制都是基于==队列==实现的。

    本章目的

    • 如何创建一个队列
    • 队列如何管理其数据
    • 如何向队列发送数据
    • 如何从队列接收数据
    • 队列阻塞是什么意思
    • 往队列发送和从队列接收时,任务优先级会有什么样的影响

    1.队列的特性

    • 数据存储

      队列可以保存有限个具有确定长度的数据单元。队列被作为 FIFO(先进先出)使用。

    • 可被多任务存取

      队列是具有自己独立权限的内核对象,并不属于或赋予任何任务。所有任务都可以向同一队列写入和读出。

    • 读队列时阻塞

      当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如
      果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务例程
      往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超
      过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

      由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞
      状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除
      阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级
      相同,那么被解除阻塞的任务将是等待最久的任务。

    • 写队列时阻塞

      同读队列一样,任务也可以在写队列时指定一个阻塞超时时间。这个时间是当被写
      队列已满时,任务进入阻塞态以等待队列空间有效的最长时间。

      由于队列可以被多个任务写入,所以对单个队列而言,也可能有多个任务处于阻塞
      状态以等待队列空间有效。这种情况下,一旦队列空间有效,只会有一个任务会被解除
      阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级
      相同,那么被解除阻塞的任务将是等待最久的任务。

    • 队列读写过程

      image

    2.使用队列

    xQueueCreate() API 函数

    xQueueCreate()用于创建一个队列,并返回一个 xQueueHandle 句柄以便于对其创建的队列进行引用。

    当创建队列时, FreeRTOS 从堆空间中分配内存空间。分配的空间用于存储队列数
    据结构本身以及队列中包含的数据单元。如果内存堆中没有足够的空间来创建队列,
    xQueueCreate()将返回 NULL。

    xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength,  //队列能够存储的最大单元数目,即队列深度。
                            unsigned portBASE_TYPE uxItemSize );  //队列中数据单元的长度,以字节为单位。
                            
                            //NULL 表示没有足够的堆空间分配给队列而导致创建失败。
                           //非NULL值表示队列创建成功。此返回值应当保存下来,以作为操作此队列的句柄。
    

    xQueueSendToBack() 与 xQueueSendToFront() API 函数

    xQueueSendToBack()用于将数据发送到队列尾;而 xQueueSendToFront()用于将数据发送到队列首。

    portBASE_TYPE xQueueSendToFront( xQueueHandle xQueue,   //目标队列的句柄
                        const void * pvItemToQueue,         //发送数据的指针
                        portTickType xTicksToWait );    //阻塞超时时间,以系统心跳周期为单位
    //有两个可能的返回值:
    1. pdPASS, 数据被成功发送到队列
    2. errQUEUE_FULL,   队列已满而无法将数据写入
    
    portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue,    //目标队列的句柄
                        const void * pvItemToQueue,         //发送数据的指针
                        portTickType xTicksToWait );    //阻塞超时时间,以系统心跳周期为单位
    //有两个可能的返回值:
    1. pdPASS, 数据被成功发送到队列
    2. errQUEUE_FULL,   队列已满而无法将数据写入
    
    

    xQueueReceive()与 xQueuePeek() API 函数

    xQueueReceive()用于从队列中接收(读取)数据单元。接收到的单元同时会从队列中删除.

    xQueuePeek()也是从从队列中接收数据单元,不同的是并不从队列中删出接收到的单元。

    portBASE_TYPE xQueueReceive( xQueueHandle xQueue,  //被读队列的句柄
                            const void * pvBuffer,      //接收缓存指针
                            portTickType xTicksToWait );//阻塞超时时间。
    
    portBASE_TYPE xQueuePeek( xQueueHandle xQueue,      //被读队列的句柄
                            const void * pvBuffer,      //接收缓存指针
                            portTickType xTicksToWait );//阻塞超时时间。
                            
    

    uxQueueMessagesWaiting() API 函数

    uxQueueMessagesWaiting()用于查询队列中当前有效数据单元个数。

    切记不要在中断服务例程中调用 uxQueueMessagesWaiting()。应当在中断服务中
    使用其中断安全版本 uxQueueMessagesWaitingFromISR()。

    unsigned portBASE_TYPE uxQueueMessagesWaiting( xQueueHandle xQueue ); //被查询队列的句柄。
    //返回值 当前队列中保存的数据单元个数。返回 0 表明队列为空。
    

    读队列时阻塞示例

    本例示范创建一个队列,由多个任务往队列中写数据,以及从队列中把数据读出。
    这个队列创建出来保存 long 型数据单元。往队列中写数据的任务没有设定阻塞超时时
    间,而读队列的任务设定了超时时间。

    往队列中写数据的任务的优先级低于读队列任务的优先级。这意味着队列中永远不
    会保持超过一个的数据单元。因为一旦有数据被写入队列,读队列任务立即解除阻塞,
    抢占写队列任务,并从队列中接收数据,同时数据从队列中删除—队列再一次变为空队列。

    static void vSenderTask( void *pvParameters )
    {
        long lValueToSend;
        portBASE_TYPE xStatus;
        /* 该任务会被创建两个实例,所以写入队列的值通过任务入口参数传递 – 这种方式使得每个实例使用不同的
        值。队列创建时指定其数据单元为long型,所以把入口参数强制转换为数据单元要求的类型 */
        lValueToSend = ( long ) pvParameters;
        /* 和大多数任务一样,本任务也处于一个死循环中 */
        for( ;; )
        {
            /* 往队列发送数据
            第一个参数是要写入的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
            第二个参数是被发送数据的地址,本例中即变量lValueToSend的地址。
            第三个参数是阻塞超时时间 – 当队列满时,任务转入阻塞状态以等待队列空间有效。本例中没有设定超
            时时间,因为此队列决不会保持有超过一个数据单元的机会,所以也决不会满。
            */
            xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
            if( xStatus != pdPASS )
            {
                /* 发送操作由于队列满而无法完成 – 这必然存在错误,因为本例中的队列不可能满。*/
                vPrintString( "Could not send to the queue.\r\n" );
            }
            /* 允许其它发送任务执行。 taskYIELD()通知调度器现在就切换到其它任务,而不必等到本任务的时间片耗尽 */
            taskYIELD();
        }
    }
    
    static void vReceiverTask( void *pvParameters )
    {
        /* 声明变量,用于保存从队列中接收到的数据。 */
        long lReceivedValue;
        portBASE_TYPE xStatus;
        const portTickType xTicksToWait = 100 / portTICK_RATE_MS;
        /* 本任务依然处于死循环中。 */
        for( ;; )
        {
            /* 此调用会发现队列一直为空,因为本任务将立即删除刚写入队列的数据单元。 */
            if( uxQueueMessagesWaiting( xQueue ) != 0 )
            {
                vPrintString( "Queue should have been empty!\r\n" );
            }
            /* 从队列中接收数据
            第一个参数是被读取的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
            第二个参数是保存接收到的数据的缓冲区地址,本例中即变量lReceivedValue的地址。此变量类型与
            队列数据单元类型相同,所以有足够的大小来存储接收到的数据。
            第三个参数是阻塞超时时间 – 当队列空时,任务转入阻塞状态以等待队列数据有效。本例中常量
            portTICK_RATE_MS用来将100毫秒绝对时间转换为以系统心跳为单位的时间值。
            */
            xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
            if( xStatus == pdPASS )
            {
                /* 成功读出数据,打印出来。 */
                vPrintStringAndNumber( "Received = ", lReceivedValue );
            }
            else
            {
                /* 等待100ms也没有收到任何数据。
                必然存在错误,因为发送任务在不停地往队列中写入数据 */
                vPrintString( "Could not receive from the queue.\r\n" );
            }
        }
    }
    
    /* 声明一个类型为 xQueueHandle 的变量. 其用于保存队列句柄,以便三个任务都可以引用此队列 */
    xQueueHandle xQueue;
    int main( void )
    {
        /* 创建的队列用于保存最多5个值,每个数据单元都有足够的空间来存储一个long型变量 */
        xQueue = xQueueCreate( 5, sizeof( long ) );
        if( xQueue != NULL )
        {
            /* 创建两个写队列任务实例,任务入口参数用于传递发送到队列的值。所以一个实例不停地往队列发送
            100,而另一个任务实例不停地往队列发送200。两个任务的优先级都设为1。 */
            xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
            xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
            /* 创建一个读队列任务实例。其优先级设为2,高于写任务优先级 */
            xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
            /* 启动调度器,任务开始执行 */
            vTaskStartScheduler();
        }
        else
        {
            /* 队列创建失败*/
        }
        /* 如果一切正常, main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲
        任务无法创建。第五章有讲述更多关于内存管理方面的信息 */
        for( ;; );
    }
    
    

    写入队列时阻塞示例

    只是写队列任务与读队列任务的优先级交换了,即读队列任
    务的优先级低于写队列任务的优先级。并且本例中的队列用于在任务间传递结构体数
    据,而非简单的长整型数据。

    写队列任务具有最高优先级,所以队列正常情况下一直
    是处于满状态。这是因为一旦读队列任务从队列中读走一个数据单元,某个写队列任务
    就会立即抢占读队列任务,把刚刚读走的位置重新写入,之后便又转入阻塞态以等待队
    列空间有效。

    /* 定义队列传递的结构类型。 */
    typedef struct
    {
        unsigned char ucValue;
        unsigned char ucSource;
    } xData;
    
    /* 声明两个xData类型的变量,通过队列进行传递。 */
    static const xData xStructsToSend[ 2 ] =
    {
        { 100, mainSENDER_1 }, /* Used by Sender1. */
        { 200, mainSENDER_2 } /* Used by Sender2. */
    };
    
    
    static void vSenderTask( void *pvParameters )
    {
        portBASE_TYPE xStatus;
        const portTickType xTicksToWait = 100 / portTICK_RATE_MS;
        /* As per most tasks, this task is implemented within an infinite loop. */
        for( ;; )
        {
            /* Send to the queue.
            第二个参数是将要发送的数据结构地址。这个地址是从任务入口参数中传入,所以直接使用pvParameters.
            第三个参数是阻塞超时时间 – 当队列满时,任务转入阻塞态等待队列空间有效的最长时间。指定超时时
            间是因为写队列任务的优先级高于读任务的优先级。所以队列如预期一样很快写满,写队列任务就会转入
            阻塞态,此时读队列任务才会得以执行,才能从队列中把数据读走。 */
            xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );
            if( xStatus != pdPASS )
            {
                /* 写队列任务无法将数据写入队列,直至100毫秒超时。
                这必然存在错误,因为只要写队列任务进入阻塞态,读队列任务就会得到执行,从而读走数据,腾
                出空间 */
                vPrintString( "Could not send to the queue.\r\n" );
            }
            /* 让其他写队列任务得到执行。 */
            taskYIELD();
        }
    }
    
    static void vReceiverTask( void *pvParameters )
    {
        /* 声明结构体变量以保存从队列中读出的数据单元 */
        xData xReceivedStructure;
        portBASE_TYPE xStatus;
        /* This task is also defined within an infinite loop. */
        for( ;; )
        {
            /* 读队列任务的优先级最低,所以其只可能在写队列任务阻塞时得到执行。而写队列任务只会在队列写
            满时才会进入阻塞态,所以读队列任务执行时队列肯定已满。所以队列中数据单元的个数应当等于队列的
            深度 – 本例中队列深度为3 */
            if( uxQueueMessagesWaiting( xQueue ) != 3 )
            {
                vPrintString( "Queue should have been full!\r\n" );
            }
            /* Receive from the queue.
            第二个参数是存放接收数据的缓存空间。本例简单地采用一个具有足够空间大小的变量的地址。
            第三个参数是阻塞超时时间 – 本例不需要指定超时时间,因为读队列任会只会在队列满时才会得到执行,
            故而不会因队列空而阻塞 */
            xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );
            if( xStatus == pdPASS )
            {
                /* 数据成功读出,打印输出数值及数据来源。 */
                if( xReceivedStructure.ucSource == mainSENDER_1 )
                {
                    vPrintStringAndNumber( "From Sender 1 = ", xReceivedStructure.ucValue );
                }
                else
                {
                    vPrintStringAndNumber( "From Sender 2 = ", xReceivedStructure.ucValue );
                }
            }
            else
            {
                /* 没有读到任何数据。这一定是发生了错误,因为此任务只支在队列满时才会得到执行 */
                vPrintString( "Could not receive from the queue.\r\n" );
            }
        }
    }
    
    int main( void )
    {
        /* 创建队列用于保存最多3个xData类型的数据单元。 */
        xQueue = xQueueCreate( 3, sizeof( xData ) );
        if( xQueue != NULL )
        {
            /* 为写队列任务创建2个实例。 The
            任务入口参数用于传递发送到队列中的数据。因此其中一个任务往队列中一直写入
            xStructsToSend[0],而另一个则往队列中一直写入xStructsToSend[1]。这两个任务的优先级都
            设为2,高于读队列任务的优先级 */
            xTaskCreate( vSenderTask, "Sender1", 1000, &( xStructsToSend[ 0 ] ), 2, NULL );
            xTaskCreate( vSenderTask, "Sender2", 1000, &( xStructsToSend[ 1 ] ), 2, NULL );
            /* 创建读队列任务。
            读队列任务优先级设为1,低于写队列任务的优先级。 */
            xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
            /* 启动调度器,创建的任务得到执行。 */
            vTaskStartScheduler();
        }
        else
        {
            /* 创建队列失败。 */
        }
        /* 如果一切正常, main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲
        任务无法创建。第五章将提供更多关于内存管理方面的信息 */
        for( ;; );
    }
    
    

    当使用队列通过指针传递大量数据时,需注意

    1. 指针指向的内存空间的所有权必须明确

      当任务间通过指针共享内存时,应该从根本上保证所不会有任意两个任务同时
      修改共享内存中的数据,或是以其它行为方式使得共享内存数据无效或产生一致性
      问题。

    2. 指针指向的内存空间必须有效

      如果指针指向的内存空间是动态分配的,只应该有一个任务负责对其进行内存
      释放。当这段内存空间被释放之后,就不应该有任何一个任务再访问这段空间。

    相关文章

      网友评论

          本文标题:FreeRTOS学习笔记-2-任务管理

          本文链接:https://www.haomeiwen.com/subject/kvkbgqtx.html