FreeRTOS队列基础知识

一、malloc函数

FreeRTOS队列基础知识_第1张图片

 FreeRTOS队列基础知识_第2张图片

 malloc函数使用形式
        关于malloc所开辟空间类型:malloc只开辟空间,不进行类型检查,只是在使用的时候进行类型的强转。
        举个例子:‘我’开辟你所需要大小的字节大小空间,至于怎么使用是你的事
        mallo函数返回的实际是一个无类型指针,必须在其前面加上指针类型强制转换才可以使用
指针自身 = (指针类型*)malloc(sizeof(指针类型)*数据数量)

c语言malloc函数的用法和意义-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_42565910/article/details/90346236

	int *p = NULL;
	int n = 10;
	p = (int *)malloc(sizeof(int)*n);

 头部信息中存储了len长度,那么就知道要free掉多大的空间大小。

FreeRTOS队列基础知识_第3张图片

堆栈存放什么_堆栈分别存放什么数据-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/Lin_XiJun/article/details/107973703栈:存放基本类型 的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放的常量池中),局部变量【注意:(方法中的局部变量使用final修饰后,放在堆中,而不是栈中)】 

FreeRTOS队列基础知识_第4张图片

(栈由高地址存放到低地址)那如果栈中存放了大量的局部变量以致于覆盖了前面的头部甚至TCB1。

二、FreeRTOS队列的特性

2.1常规操作

队列的简化操如入下图所示,从此图可知:
        队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
        每个数据大小固定
        创建队列时就要指定长度、数据大小
        数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
        也可以强制写队列头部:覆盖头部数据

FreeRTOS队列基础知识_第5张图片

2.2传输数据的两种方法

使用队列传输数据时有两种方法:
        拷贝:把数据、把变量的值复制进队列里
        引用:把数据、把变量的地址复制进队列里
FreeRTOS使用拷贝值的方法:

        局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
        无需分配buffer来保存数据,队列中有buffer
        局部变量可以马上再次使用
        发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
        如果数据实在太大,你还是可以使用队列传输它的地址
        队列的空间由FreeRTOS内核分配,无需任务操心

        对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。

        如果储存在队列中的数据过大,最好是使用数据的指针而不是数据本身。这不管是在处理时间上,还是创建队列所需要的内存上都更有效。但同时也需要注意:

        指针使用的内存的拥有者必须明确定义。当在 task 之间通过指针分享内存时,最基本的要确认多个 task 不会同时修改内存,或者做其它会使内存中数据无效的操作。理想情况下,发送 task 可以在内存入队之前有权访问。接收 task 只有在出队之后可以访问。

        指针指向的内存应保持有效。如果内存是动态地分配的,或者从一个预先分配好的 buffer 里获得的,必须有且只有一个 task 负责释放它。释放后不能再访问。指针不能指向 task 栈空间上的内存。

【壹拾玖】FreeRTOS 上使用队列传输大型或可变大小数据 - 知乎 (zhihu.com)icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/453888790        接下来示例如何在 task 之间使用队列传输指针。首先创建一个可以存储 5 个指针的队列,然后在发送 task 中申请一个内存并写入一个字符串,将该内存的指针压入队列。最后从接收 task 中接收这个字符串并打印。

FreeRTOS中的xQueueCreate,xQueueSend,xQueueReceive-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_38531460/article/details/117807671

xQueueCreate:

创建一个新的队列。为新的队列分配所需的存储内存,并返回一个队列处理。

原型:
xQueueHandle xQueueCreate( 
   unsigned portBASE_TYPE uxQueueLength, 
   unsigned portBASE_TYPE uxItemSize 
);

         uxQueueLength    队列中包含最大项目数量。
        uxItemSize    队列中每个项目所需的字节数。
项目通过复制而不是引用排队,因为,所需的字节数,将复制给每个项目。队列中每个项目必须分配同样大小。

struct tagMsgQueuePara
{
	xQueueHandle ParseQueueHandle;
}RamPara;
 
struct tagTaskQueueMessage
{	
    int Message;
};
 
RamPara = xQueueCreate(1, sizeof(struct tagTaskQueueMessage));
这样就创建了一个tagTaskQueueMessage结构体的空间
xQueuSend:
原型:
 portBASE_TYPE xQueueSend( 
   xQueueHandle xQueue, 
   const void * pvItemToQueue, 
   portTickType xTicksToWait 
);

这是一个函数原型的声明,用于向队列中发送数据。

参数说明:

  • xQueueHandle xQueue:要发送数据的队列句柄
  • const void * pvItemToQueue:要发送的数据指针,项目的大小,由队列创建时定义,因为许多字节可以从 pvItemToQueue复制到队列的储存区域 
  • portTickType xTicksToWait:发送数据时等待的时间,单位为时钟节拍数,因此如果需要,常量portTICK_RATE_MS应该用来转换实时时间,如果设置为0,调用将立即返回。

函数返回一个portBASE_TYPE类型的值,表示数据发送是否成功。

struct tagTaskQueueMessage
{	
    int Message;
};
struct tagTaskQueueMessage message;
 
message.Message = 1;
 
xQueueSend(RamPara,
		    (void *)&message,
			(portTickType)0);
沿用了上面的创建,这样就入队了。
 xQueueReceive:   

从队列接收一个项目。这个项目通过复制接收,因此缓冲器必须提供足够大的空间。复制进缓冲器的字节数,在队列创建时已经定义。

原型:
portBASE_TYPE xQueueReceive( 
  xQueueHandle xQueue, 
  void *pvBuffer, 
  portTickType xTicksToWait 
);

 pxQueue    将要接收项目的队列句柄
pvBuffer    指向将要复制接收项 目的缓冲器的指针。
xTicksToWait    任务中断并等待队列中可用空间的最大时间,应该是满的。如果设置为0,调用将立刻返回。时间在片区间中定义,如果需要,portTICK_RATE_MS常量用来转换为实际时间。
如果 INCLUDE_vTaskSuspend 定义为1 ,指定的中断时间( portMAX_DELAY) 将导致任务无限期中断(没有时间溢出)。

struct tagTaskQueueMessage
{	
    int Message;
};
struct tagTaskQueueMessage message1;
 
xQueueReceive(RamPara,
			(void *)&message1,
			0xffff);
沿用上面的,,这样就出队获取了数据。

        任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。

        某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

        既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?
        优先级最高的任务
        如果大家的优先级相同,那等待时间最久的任务会进入就绪态

2.3使用队列的流程:创建队列、写队列、读队列、删除队列

三、创建队列

FreeRTOS队列基础知识_第6张图片

    删除队列的函数为vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。原型如下:

void vQueueDelete( QueueHandle_t xQueue );

FreeRTOS队列基础知识_第7张图片

// 示例代码
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint32_t )
// xQueueBuffer用来保存队列结构体
StaticQueue_t xQueueBuffer;
// ucQueueStorage 用来保存队列的数据
// 大小为:队列长度 * 数据大小
uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];
void vATask( void *pvParameters )
{
QueueHandle_t xQueue1;
// 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小是ITEM_SIZE
xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
ITEM_SIZE,
ucQueueStorage,
&xQueueBuffer );
}

        队列刚被创建时,里面没有数据;使用过程中可以调用xQueueReset() 把队列恢复为初始状态,此函数原型为:

/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);

四、写队列

        可以把数据写到队列头部,也可以写到尾部。这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
这些函数用到的参数是类似的,统一说明如下:
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
所谓中断是指当CPU正在处理某件事情的时候,外部发生的某一事件
(如一个电平的变化,一个脉冲沿的发生或 定时器计数溢出等)请求CPU迅速去处理,
于是CPU暂时中止当前的工作,转去处理所发生的事件。
中断服务处理完该事件以后,再回到原来被中止的地方继续原来的工作。
中断是一种硬件机制,用于通知CPU有个异步事件发生了。
中断一旦被系统识别,CPU则保存部分(或全部)现场(context),即部分(或全部) 寄存器的值,跳转到专门的 子程序,称为中断服务程序(ISR)。 
中断服务程序做事件处理,处理完成后执行任务调度,程序回到就绪态优先级最高的任务开始运行(对于可剥夺型内核)。

FreeRTOS队列基础知识_第8张图片

        返回值是BaseType_t,经典返回值,要不就是pdPass,要不就是队列满了,失败,这是FreeRTOS中最高效的返回类型,eg:BaseType_t xReturn,其中xReturn的第一个x代表的就是类型是BaseType_t,而Return表示变量的含义。同理其他参数也可以这样解释,如pvItemToQueue,p代表他是个指针,v代表返回类型是void,所以他就是个void*类型的指针。

        此外,prvSetupHardware()这类型的名字,其中prv代表的是private,意思是私有,就是说这个函数往往是被static修饰的。

FreeRTOS队列基础知识_第9张图片

FreeRTOS源码的编程规范:

FreeRTOS队列基础知识_第10张图片

        第一个参数是指哪个队列进行操作,第二个参数是传入的数据,或者说是写入队列的数据,但注意传入的是数据的地址,不是数据本身,第三个参数是等待时间,一般写0就行,表示无法写入数据时函数会立即返回。

/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
            QueueHandle_t xQueue,
            const void *pvItemToQueue,
            TickType_t xTicksToWait);

五、读队列

         使用xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);

FreeRTOS队列基础知识_第11张图片

读队列,返回值同样是经典的pdPASS,第一个参数是确定读取的队列是哪一个,第二个参数是要读取的数据的地址,第三个参数是等待时间。读数据往往会设置一个最大阻塞时间,通过下语句进行设置:

const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );

可以查询队列中有多少个数据、有多少空余空间。函数原型如下:

/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

六、队列的基本使用

        本程序会创建一个队列,然后创建2个发送任务、1个接收任务:
                发送任务优先级为1,分别往队列中写入100、200
                接收任务优先级为2,读队列、打印数值

下代码是动态开辟的队列大小,所以要进行一个判断,动态空间开辟成功或者开辟失败。

队列创建成功返回一个队列句柄,不成功就返回NULL.

/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;
int main( void )
{
prvSetupHardware();
/* 创建队列: 长度为5,数据大小为4字节(存放一个整数) */
xQueue = xQueueCreate( 5, sizeof( int32_t ) );
if( xQueue != NULL )
{
/* 创建2个任务用于写队列, 传入的参数分别是100、200
* 任务函数会连续执行,向队列发送数值100、200
* 优先级为1
*/
xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
/* 创建1个任务用于读队列
* 优先级为2, 高于上面的两个任务
* 这意味着队列一有数据就会被读走
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建队列 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}

 往队列中写入数值:

0就是不等待,如果队列满了就直接退出,他的返回值是BaseType_t,而第二个参数传入的Item的地址,这样改变Item不会改变队列中的数值。

static void vSenderTask( void *pvParameters )
{
int32_t lValueToSend;
接收任务的函数中,读取队列、判断返回值、打印,代码如下:
BaseType_t xStatus;
/* 我们会使用这个函数创建2个任务
* 这些任务的pvParameters不一样
*/
lValueToSend = ( int32_t ) pvParameters;
/* 无限循环 */
for( ;; )
{
/* 写队列
* xQueue: 写哪个队列
* &lValueToSend: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
* 0: 不阻塞, 如果队列满的话, 写入失败, 立刻返回
*/
xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
if( xStatus != pdPASS )
{
printf( "Could not send to the queue.\r\n" );
}
}
}

接收任务的函数中,读取队列、判断返回值、打印,代码如下:

static void vReceiverTask( void *pvParameters )
{
/* 读取队列时, 用这个变量来存放数据 */
int32_t lReceivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
/* 无限循环 */
for( ;; )
{
/* 读队列
* xQueue: 读哪个队列
* &lReceivedValue: 读到的数据复制到这个地址
* xTicksToWait: 如果队列为空, 阻塞一会
*/
xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
if( xStatus == pdPASS )
{
/* 读到了数据 */
printf( "Received = %d\r\n", lReceivedValue );
}
else
{
/* 没读到数据 */
printf( "Could not receive from the queue.\r\n" );
}
}
}

   某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

运行结果:

FreeRTOS队列基础知识_第12张图片

任务调度情况分析:

FreeRTOS队列基础知识_第13张图片

你可能感兴趣的:(51单片机,stm32)