目录
一、协程介绍:
(1)协程的特点:
(2)协程的优势:
二、协程状态:
(1)协程状态说明:
(2)协程状态图示:
三、协程实现:
(1)协程结构体:
(2)协程优先级:
(3)调度协程:
(4)混合任务和协程
四、协程的局限和限制:
(1)共享堆栈:
(2)switch 语句的使用:
五、协程API:
(1)xCoRoutineCreate:
(2)crDELAY:
(3)crQUEUE_SEND:
(4)crQUEUE_RECEIVE:
(5)crQUEUE_SEND_FROM_ISR:
(6)crQUEUE_RECEIVE_FROM_ISR:
(7)vCoRoutineSchedule:
六、协程使用示例:
(1)创建协程示例:
(2) 调度协程:
(3) 创建协程并启动 RTOS 调度器:
(4)使用索引参数:
七、空闲任务:
(1)空闲任务说明:
(2)空闲任务钩子:
八、线程本地存储指针:
(1)线程本地存储指针:
(2)线程本地整数:
(3)线程本地结构体:
九、协程常见问题报错:
(1)CoRoutineCreate函数未定义:
(2)协程最高优先级未定义:
(3)内存空间不足报错:
(4)空闲任务未定义:
十、协程程序编写:
(1)基础协程创建:
(2)协程中索引的使用:
十一、FreeRTOS教程示例代码下载:
一、协程介绍:
协程是一种计算机程序组件,它允许不同的执行线程进行协作式的多任务处理。与传统的线程或任务相比,协程具有一些独特的特点和优势,尤其是在资源受限的环境中,如小型嵌入式系统或物联网设备。
(1)协程的特点:
- 堆栈使用:协程之间共享同一个堆栈,这大大减少了程序运行时所需的RAM资源。这种共享堆栈的方式使得协程在内存使用上比传统的多线程或多任务模型更为高效。
- 调度和优先级:协程通常采用优先级协同调度,这意味着协程的执行顺序是由其优先级决定的,但它们也可以在支持抢占式调度的系统中使用。这种调度方式允许协程在需要时主动放弃CPU控制权,从而实现更高效的任务切换。
- 宏实现:协程通常通过一组宏来实现,这意味着它们的创建和管理是通过预处理器指令来完成的,而不是通过函数调用。这种方式简化了协程的实现,但也可能限制了它们的灵活性。
- 使用限制:由于协程间共享堆栈,它们在构造时需要遵守一些严格的限制,以避免堆栈溢出或数据损坏。此外,协程的使用也受到API调用位置的限制,因为不是所有的API调用都适合在协程环境中使用。
(2)协程的优势:
- 减少RAM使用:由于协程间共享堆栈,它们在运行时所需的内存资源大大减少,这对于内存受限的设备来说是一个显著的优势。
- 减少重入问题:协程的协作式操作减少了函数重入的问题,因为协程在执行过程中可以主动放弃CPU控制权,从而避免了多线程环境中常见的同步和互斥问题。
- 跨平台移植:协程可以在不同的架构之间移植,这意味着开发者可以更容易地将协程程序从一个平台迁移到另一个平台。
- 优先级完全:协程相对于其他协程具有完全的优先级,但如果与任务混用,协程总是会被任务抢占。
- 协作式操作:协程之间只能进行协作式操作,这意味着它们需要主动放弃CPU控制权,以便其他协程可以执行。
二、协程状态:
协程仅用于 RAM 严重受限的极小处理器, 通常不会用于 32 位微控制器。
(1)协程状态说明:
协程可以存在于以下状态中:
- 运行:当协程实际执行时,它被称为处于运行状态。协程当前正在使用处理器。
- 就绪:就绪的协程是那些能够执行(未阻塞)但目前未执行的协程。
- 协程处于就绪状态的可能情况包括:
- 另一个具有相同或更高优先级的协程已处于运行状态
- 任务处于运行状态——只有在应用程序同时使用任务和协程时才会出现这种情况。
- 阻塞:如果协程当前正在等待时间事件或外部事件,则该协程被称为处于阻塞状态 。
例如,如果协程调用 crDELAY(),它将阻塞(被置于阻塞状态), 直到延迟期结束(即时间事件)。阻塞的协程不可用于 调度。
(2)协程状态图示:
当前没有等同于任务挂起状态的协程。
三、协程实现:
(1)协程结构体:
协程应具有以下结构体:
类型 crCOROUTINE_CODE 定义为返回 void 并以 CoRoutineHandle_t 和索引作为其参数的函数 。
void vACoRoutineFunction( CoRoutineHandle_t xHandle,
UBaseType_t uxIndex )
{
crSTART( xHandle );
for( ;; )
{
//应用程序代码.
}
crEND();
}
实现协程的函数都应属于这种类型(如上代码所示) 。
调用 xCoRoutineCreate() 即可创建协程。
注意事项:
- 所有协程函数都必须以调用 crSTART() 开始。
- 所有协程函数都必须以调用 crEND() 结束。
- 协程函数不应返回任何值,因此通常实现为连续循环。
- 可通过单个协程函数创建多个协程。提供的 uxIndex 参数 作为区分此类协程的方法。
(2)协程优先级:
每个协程均会被分配优先级,值从 0 到 (configMAX_CO_ROUTINE_PRIORITIES-1) 不等。 configMAX_CO_ROUTINE_PRIORITIES 在 FreeRTOSConfig.h 中定义, 可以根据应用程序进行设置。
- 优先级数字较低,表示协程优先级也较低。
- 协程优先级只与其他协程相关。如果在同一应用程序内混用任务和协程, 则任务的优先级将始终高于协程。
(3)调度协程:
通过重复调用 vCoRoutineSchedule()来调度协程。调用 vCoRoutineSchedule() 的最佳位置为空闲任务钩子。即使应用程序 仅使用协程也是如此,因为一旦启动调度器,将仍然会自动 创建空闲任务。
(4)混合任务和协程
从空闲任务中调度协程,可在同一应用程序中轻松混合任务和 协程。这种情况下,只有在没有优先级高于 空闲任务的任务可以执行时,协程才会执行。
四、协程的局限和限制:
与同等任务相比,协程的优势是使用的 RAM 较少,但代价是使用协程时存在一些限制条件。与任务相比,协程的限制性更强,使用起来也更复杂 。
(1)共享堆栈:
当协程阻塞时,协程的堆栈无法维持。这意味着在堆栈上分配的变量很可能丢失其值。要克服这一问题,需将在阻塞调用中维持其值的变量声明为静态。例如:
void vACoRoutineFunction( CoRoutineHandle_t xHandle,
UBaseType_t uxIndex )
{
static char c = 'a'; // 定义一个静态字符变量c,并初始化为'a'。
// 由于是静态的,它的值在协程调用之间会保持不变。
// 协程必须以调用crSTART()开始。
crSTART( xHandle ); // 标记协程的开始。
for( ;; ) // 无限循环,协程的主要执行部分。
{
// 如果我们在这里将c设置为'b'...
c = 'b'; // 将变量c的值设置为'b'。
// ...然后进行一个阻塞调用...
crDELAY( xHandle, 10 ); // 协程延迟一段时间(以tick为单位)。这里设置为10个tick。
// ...只有在c被声明为静态变量(如这里所示)的情况下,
// 才能保证在这里c仍然等于'b'。
// 因为如果c不是静态的,它的值可能会在协程被挂起和恢复时被其他协程改变。
}
// 协程必须以调用crEND()结束。
crEND(); // 标记协程的结束。
}
共享堆栈的另一个结果是,能从协程函数本身调用可能导致协程阻塞的 API 函数, 而不能从协程调用的函数中调用 。例如:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 协程必须以调用crSTART()开始。
crSTART( xHandle ); // 初始化协程,设置协程的状态为运行中。
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 在这里进行阻塞调用是可以的,
crDELAY( xHandle, 10 ); // 协程执行阻塞调用,延迟10个tick时间。
// 但是不能在vACalledFunction()函数内进行阻塞调用。
vACalledFunction(); // 调用另一个函数,该函数中不能有阻塞调用。
}
// 协程必须以调用crEND()结束。
crEND(); // 标记协程结束,设置协程的状态为结束。
}
void vACalledFunction( void )
{
// 在这里不能进行阻塞调用!
// 因为vACalledFunction()可能在协程或任务中被调用,
// 如果在这里进行阻塞调用,可能会导致死锁或其他不可预测的行为。
}
(2)switch 语句的使用:
FreeRTOS 下载进中的默认协程实现不允许在 switch 语句中进行阻塞调用。例如:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 协程必须以调用crSTART()开始。
crSTART( xHandle ); // 初始化协程,设置协程的状态为运行中。
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 在这里进行阻塞调用是可以的,
crDELAY( xHandle, 10 ); // 协程执行阻塞调用,延迟10个tick时间。
// 根据变量aVariable的值执行不同的操作
switch( aVariable )
{
case 1 : // 当aVariable等于1时
// 在这里不能进行阻塞调用!
break; // 结束当前case分支
default: // 当aVariable不等于1时
// 或者在这里也不能进行阻塞调用!
}
// 注意:在switch语句中的任何一个case分支中都不能进行阻塞调用。
}
// 协程必须以调用crEND()结束。
crEND(); // 标记协程结束,设置协程的状态为结束。
}
五、协程API:
(1)xCoRoutineCreate:
函数原型:
BaseType_t xCoRoutineCreate
(
crCOROUTINE_CODE pxCoRoutineCode,
UBaseType_t uxPriority,
UBaseType_t uxIndex
);
- 作用:创建新协程,并将其添加到做好运行准备的协程的列表中。
参数/返回值名 | 类型 | 描述 |
---|---|---|
pxCoRoutineCode | CrCOROUTINE_CODE | 指向协程函数的指针。协程函数需要特殊的语法,以适应FreeRTOS协程的要求。 |
uxPriority | UBaseType_t | 协程运行时相对于其他协程的优先级。 |
uxIndex | UBaseType_t | 用于区分执行相同函数的不同协程,有助于在系统中标识特定的协程实例。 |
返回值(成功) | BaseType_t | 如果协程成功创建并添加到做好运行准备的协程的列表中,则返回pdPASS 。 |
返回值(失败) | BaseType_t | 否则返回ProjDefs.h 定义的错误代码,指示创建协程过程中遇到的问题。 |
用法示例:
// 要创建的协程函数。
void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 如果协程中的变量需要在阻塞调用之间保持值,则必须声明为静态变量。
// 对于const变量,这可能不是必要的。
static const char cLedToFlash[2] = { 5, 6 };
static const TickType_t uxFlashRates[2] = { 200, 400 };
// 每个协程都必须以调用crSTART()开始。
crSTART( xHandle );
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 这个协程只是延迟一个固定的时间,然后切换一个LED。
// 使用这个函数创建了两个协程,因此使用uxIndex参数来告诉协程
// 应该闪烁哪个LED以及延迟多长时间。这假设xQueue已经创建。
vParTestToggleLED( cLedToFlash[uxIndex] );
crDELAY( xHandle, uxFlashRates[uxIndex] );
}
// 每个协程都必须以调用crEND()结束。
crEND();
}
// 创建两个协程的函数。
void vOtherFunction( void )
{
unsigned char ucParameterToPass;
TaskHandle_t xHandle;
// 在优先级0创建两个协程。第一个协程被赋予索引0
// (从上面的代码来看)每200个tick闪烁LED 5。第二个
// 协程被赋予索引1,因此每400个tick闪烁LED 6。
for( uxIndex = 0; uxIndex < 2; uxIndex++ )
{
xCoRoutineCreate( vFlashCoRoutine, 0, uxIndex );
}
}
- CoRoutineHandle_t:引用协程的类型。
- 协程句柄会自动传递给所有协程函数。
(2)crDELAY:
函数原型:
void crDELAY( CoRoutineHandle_t xHandle,
TickType_t xTicksToDelay )
- crDELAY是一个宏。原型中的数据类型仅供参考。该函数可以将协程延迟一段固定时间。crDELAY只能从协程函数本身调用,不能从协程函数调用的函数调用。这是因为协程无法维持自己的堆栈。
参数名 | 类型 | 描述 |
---|---|---|
xHandle | CoRoutineHandle_t | 要推迟的协程的句柄。这是协程函数的参数之一。 |
xTicksToDelay | TickType_t | 协程应推迟的滴答数。此滴答数可以转换为实际时间,实际时间由configTICK_RATE_HZ (在FreeRTOSConfig.h中设置)定义。 |
用法示例:
// 要创建的协程函数。
void vACoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 如果协程中的变量需要在阻塞调用之间保持值,则必须声明为静态变量。
// 对于const变量,这可能不是必要的。我们在这里要延迟200毫秒。
static const xTickType xDelayTime = 200 / portTICK_PERIOD_MS;
// 每个协程都必须以调用crSTART()开始。
crSTART( xHandle );
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 延迟200毫秒。
crDELAY( xHandle, xDelayTime );
// 在这里执行一些操作。
// 例如,可以切换LED状态、发送消息、处理数据等。
}
// 每个协程都必须以调用crEND()结束。
crEND();
}
(3)crQUEUE_SEND:
函数原型:
crQUEUE_SEND( CoRoutineHandle_t xHandle,
QueueHandle_t xQueue,
void *pvItemToQueue,
TickType_t xTicksToWait,
BaseType_t *pxResult )
- crQUEUE_SEND是一个宏。原型中的数据类型仅供参考。
- crQUEUE_SEND()和 crQUEUE_RECEIVE() 宏是协程版的函数, 对应于任务中使用的xQueueSend()和 xQueueReceive()函数。
- crQUEUE_SEND和 crQUEUE_RECEIVE只能在协程中使用,而 xQueueSend()和xQueueReceive()只能在任务中使用。
- 请注意,协程只能向其他协程发送数据。协程不能通过队列向任务发送数据, 任务也不能通过队列向协程发送数据。
- crQUEUE_SEND只能通过协程函数本身调用, 不能通过协程函数调用的其他函数调用。这是因为 协程无法维持自己的堆栈。
参数名 | 类型 | 描述 |
---|---|---|
xHandle | CoRoutineHandle_t | 调用协程的句柄。这是协程函数的参数之一。 |
xQueue | QueueHandle_t | 队列的句柄,数据将发布到此队列。使用xQueueCreate() API函数创建队列时,该句柄作为返回值获得。 |
pvItemToQueue | void * | 指向发布到队列中的数据的指针。每个队列项的字节数会在创建队列时指定。此字节数从pvItemToQueue 复制到队列本身。 |
xTicksToDelay | TickType_t | 如果队列中无立即可用空间,协程会阻塞以等待队列可用空间的滴答数。此滴答数可以转换为实际时间,实际时间由configTICK_RATE_Hz (在FreeRTOSConfig.h中设置)定义。常量portTICK_PERIOD_MS 可用于将滴答数转换为毫秒。 |
pxResult | BaseType_t * | 如果从队列中成功检索到数据,则pxResult 指向的变量将设置为pdPASS ,否则设置为ProjDefs.h 中定义的错误代码。 |
用法示例:
// 协程函数,它阻塞一段固定时间,然后将一个数字发布到队列上。
static void prvCoRoutineFlashTask( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 如果协程中的变量需要在阻塞调用之间保持值,则必须声明为静态变量。
static BaseType_t xNumberToPost = 0; // 要发布到队列的数字。
static BaseType_t xResult; // 用于存储队列操作的结果。
// 协程必须以调用crSTART()开始。
crSTART( xHandle );
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 假设队列已经创建好了。
crQUEUE_SEND( xHandle, // 协程句柄。
xCoRoutineQueue, // 队列句柄。
&xNumberToPost, // 指向要发送数据的指针。
NO_DELAY, // 不等待,如果队列满则立即返回。
&xResult ); // 存储操作结果。
if( xResult != pdPASS )
{
// 消息未能发布到队列!
// 可以在这里添加错误处理代码。
}
// 增加要发布到队列的数字。
xNumberToPost++;
// 延迟100个tick。
crDELAY( xHandle, 100 );
}
// 协程必须以调用crEND()结束。
crEND();
}
(4)crQUEUE_RECEIVE:
函数原型:
void crQUEUE_RECEIVE(
CoRoutineHandle_t xHandle,
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait,
BaseType_t *pxResult
)
- crQUEUE_RECEIVE是一个宏。原型中的数据类型仅供参考。
- crQUEUE_SEND()和 crQUEUE_RECEIVE()宏是协程版的函数, 对应于任务中使用的 xQueueSend()和 xQueueReceive()函数。crQUEUE_SEND和 crQUEUE_RECEIVE只能在协程中使用,而 xQueueSend()和 xQueueReceive() 只能在任务中使用。
- 请注意,协程只能向其他协程发送数据。协程不能通过队列向任务发送数据, 任务也不能通过队列向协程发送数据。
- crQUEUE_RECEIVE只能通过协程函数本身调用, 不能通过协程函数调用的其他函数调用。这是因为 协程无法维持自己的堆栈。
参数名 | 描述 |
---|---|
xHandle | 调用协程的句柄。这是协程函数的参数之一。 |
xQueue | 要接收数据的队列的句柄。使用xQueueCreate() API函数创建队列时,该句柄作为返回值获得。 |
pvBuffer | 将接收到的项目复制到其中的缓冲区。每个排队项的字节数是在创建队列时指定的。此字节数已复制到pvBuffer 。 |
xTickToDelay | 在无立即可用数据的情况下,协程处于阻塞状态以等待队列中可用数据时的滴答数。此滴答数可以转换为实际时间,实际时间由configTICK_RATE_HZ (在FreeRTOSConfig.h中设置)定义。常量portTICK_PERIOD_MS 可用于将滴答数转换为毫秒。 |
pxResult | 用于存储队列操作结果的指针。如果从队列中成功检索到数据,则pxResult 指向的变量将设置为pdPASS ,否则设置为ProjDefs.h 中定义的错误代码。 |
用法示例:
// 协程函数,从队列中接收要闪烁的LED编号,并控制LED闪烁。
static void prvCoRoutineFlashWorkTask( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 如果协程中的变量需要在阻塞调用之间保持值,则必须声明为静态变量。
static BaseType_t xResult; // 用于存储从队列接收操作的结果。
static UBaseType_t uxLEDToFlash; // 存储从队列中接收到的LED编号。
// 所有协程都必须以调用crSTART()开始。
crSTART( xHandle );
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 等待队列中有数据可用。
crQUEUE_RECEIVE( xHandle, // 协程句柄。
xCoRoutineQueue, // 队列句柄。
&uxLEDToFlash, // 指向存储接收数据的变量的指针。
portMAX_DELAY, // 无限期等待,直到队列中有数据。
&xResult ); // 存储操作结果。
if( xResult == pdPASS )
{
// 成功接收到LED编号 - 控制对应的LED闪烁。
vParTestToggleLED( uxLEDToFlash );
}
}
// 所有协程都必须以调用crEND()结束。
crEND();
}
(5)crQUEUE_SEND_FROM_ISR:
函数原型:
BaseType_t crQUEUE_SEND_FROM_ISR
(
QueueHandle_t xQueue,
void *pvItemToQueue,
BaseType_t xCoRoutinePreviouslyWoken
)
- crQUEUE_SEND_FROM_ISR()是一个宏。原型中的数据类型仅供参考。
- crQUEUE_SEND_FROM_ISR()和 crQUEUE_RECEIVE_FROM_ISR()宏是协程版的函数,对应于任务中使用的 xQueueSendFromISR()和 xQueueReceiveFromISR()函数。
- crQUEUE_SEND_FROM_ISR()和 crQUEUE_RECEIVE_FROM_ISR()只能用于 在协程和 ISR 之间传递数据,而 xQueueSendFromISR()和 xQueueReceiveFromISR()只能用于在任务和 ISR 之间 传递数据。crQUEUE_SEND_FROM_ISR只能从 ISR 调用,以将数据发送到 协程内正在使用的队列。
参数/返回值名 | 类型 | 描述 |
---|---|---|
xQueue | QueueHandle_t | 队列的句柄,数据项将发布到此队列。 |
pvItemToQueue | void * | 指向待入队列的数据项的指针。队列能够存储的项目的大小在创建队列时即已定义,因此pvItemToQueue 中的这些字节将复制到队列存储区。 |
xCoRoutinePreviouslyWoken | BaseType_t | 包含此参数,ISR就可以从单个中断多次发送到同一个队列。第一个调用应始终传入pdFALSE 。后续调用应传入从上一个调用返回的值。 |
返回值(pdTRUE) | BaseType_t | 如果发布到队列将协程唤醒,则返回pdTRUE 。ISR使用此信息来确定ISR之后是否需要上下文切换。 |
返回值(pdFALSE) | BaseType_t | 否则返回pdFALSE 。 |
用法示例:
// 一个协程,它在队列上阻塞等待接收字符。
static void vReceivingCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
char cRxedChar; // 用于存储接收到的字符。
BaseType_t xResult; // 用于存储队列操作的结果。
// 所有协程都必须以调用crSTART()开始。
crSTART( xHandle );
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 等待队列中有数据可用。这假设队列xCommsRxQueue已经创建好了!
crQUEUE_RECEIVE( xHandle,
xCommsRxQueue,
&cRxedChar,
portMAX_DELAY,
&xResult );
// 是否接收到了字符?
if( xResult == pdPASS )
{
// 在这里处理接收到的字符。
}
}
// 所有协程都必须以调用crEND()结束。
crEND();
}
// 一个ISR,它使用队列将从串口接收的字符发送到协程。
void vUART_ISR( void )
{
char cRxedChar; // 用于存储从UART接收到的字符。
BaseType_t xCRWokenByPost = pdFALSE; // 用于记录是否有协程被唤醒。
// 我们循环读取字符,直到UART中没有剩余字符。
while( UART_RX_REG_NOT_EMPTY() )
{
// 从UART获取字符。
cRxedChar = UART_RX_REG;
// 将字符发布到队列。xCRWokenByPost将在循环的第一次迭代中被设置为pdFALSE。
// 如果发布导致协程被唤醒(取消阻塞),那么xCRWokenByPost将被设置为pdTRUE。
// 通过这种方式,我们可以确保如果多个协程被阻塞在队列上,无论有多少字符被发布到队列,只有一个协程会被这个ISR唤醒。
xCRWokenByPost = crQUEUE_SEND_FROM_ISR( xCommsRxQueue,
&cRxedChar,
xCRWokenByPost );
}
}
(6)crQUEUE_RECEIVE_FROM_ISR:
函数原型:
BaseType_t crQUEUE_SEND_FROM_ISR
(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t * pxCoRoutineWoken
)
- crQUEUE_SEND_FROM_ISR() 和 crQUEUE_RECEIVE_FROM_ISR()宏是 协程版的函数,对应于任务中使用的 xQueueSendFromISR()和 xQueueReceiveFromISR()函数。
- crQUEUE_SEND_FROM_ISR()和 crQUEUE_RECEIVE_FROM_ISR()只能用于在协程和 ISR 之间传递数据,而 xQueueSendFromISR()和 xQueueReceiveFromISR()只能用于在任务和 ISR 之间 传递数据。
- crQUEUE_RECEIVE_FROM_ISR()只能从 ISR 中调用,以从协程中(发布到队列的协程)正在使用的队列接收数据 。
参数/返回值名 | 类型 | 描述 |
---|---|---|
xQueue | QueueHandle_t | 队列的句柄,数据项将发布到此队列。 |
pvBuffer | void * | 指向缓冲区的指针,接收到的项目将被放入此缓冲区中。队列能够存储的项目的大小在创建队列时即已定义,因此队列中的这些字节将复制到pvBuffer 。 |
pxCoRoutineWoken | BaseType_t * | 协程在等待队列空间可用时可能会被阻塞。如果函数导致协程解除阻塞,*pxCoRoutineWoken 将被设置为pdTRUE ,否则*pxCoRoutineWoken 将保持不变。 |
返回值(pdTRUE) | BaseType_t | 如果从队列中成功接收项目,则返回pdTRUE 。 |
返回值(pdFALSE) | BaseType_t | 否则返回pdFALSE 。 |
用法示例:
// 协程函数,向队列发送一个字符,然后阻塞一段固定时间。每次发送的字符都会递增。
static void vSendingCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// cCharToTx在协程阻塞时需要保持其值,因此必须声明为静态变量。
static char cCharToTx = 'a';
BaseType_t xResult;
// 所有协程都必须以调用crSTART()开始。
crSTART( xHandle );
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 将下一个字符发送到队列。
crQUEUE_SEND( xHandle,
xCoRoutineQueue,
&cCharToTx,
NO_DELAY,
&xResult );
if( xResult == pdPASS )
{
// 字符成功发布到队列。
}
else
{
// 无法将字符发布到队列。
}
// 启用UART发送中断,以在这个假设的UART中引发中断。中断将从队列中获取字符并发送它。
ENABLE_RX_INTERRUPT();
// 递增到下一个字符,然后阻塞一段固定时间。
// cCharToTx将在延迟期间保持其值,因为它被声明为静态变量。
cCharToTx++;
if( cCharToTx > 'x' )
{
cCharToTx = 'a';
}
crDELAY( 100 ); // 延迟100个tick。
}
// 所有协程都必须以调用crEND()结束。
crEND();
}
// 使用队列从UART接收要发送的字符的ISR。
void vUART_ISR( void )
{
char cCharToTx;
BaseType_t xCRWokenByPost = pdFALSE;
while( UART_TX_REG_EMPTY() ) // 检查UART发送寄存器是否为空。
{
// 队列中有等待发送的字符吗?
// 如果发布操作唤醒了协程,xCRWokenByPost将自动设置为pdTRUE - 确保无论我们循环多少次,只有一个协程被唤醒。
if( crQUEUE_RECEIVE_FROM_ISR( xQueue, &cCharToTx, &xCRWokenByPost ) )
{
SEND_CHARACTER( cCharToTx ); // 发送字符。
}
}
}
(7)vCoRoutineSchedule:
函数原型:
void vCoRoutineSchedule( void );
- 该函数用于运行协程。
- vCoRoutineSchedule()执行优先级最高的可运行协程 。协程保持执行,直到它阻塞、挂起或 被任务抢占。协同间互相协作执行,因此一个协程无法被另一个协程抢占,但可以被一个任务抢占。
- 如果应用程序同时包含任务和协程,那么应从空闲任务(在空闲任务钩子中)调用 vCoRoutineSchedule 。
用法示例:
void vApplicationIdleHook( void )
{
vCoRoutineSchedule( void );
}
如果空闲任务没有执行任何其他函数,那按以下方式在循环中调用 vCoRoutineSchedule():
void vApplicationIdleHook( void )
{
for( ;; )
{
vCoRoutineSchedule( void );
}
}
六、协程使用示例:
(1)创建协程示例:
创建一个简单的协程来闪烁 LED。
// 协程函数,用于周期性地闪烁一个LED。
void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 协程必须以调用crSTART()开始。
// crSTART()用于初始化协程的状态。
crSTART( xHandle );
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 延迟一段固定时间。这里设置为10个tick。
// crDELAY()是一个宏,用于挂起协程的执行,直到指定的tick数过去。
crDELAY( xHandle, 10 );
// 闪烁一个LED。这里假设vParTestToggleLED()是一个函数,
// 用于切换指定编号的LED的状态。
// 参数0表示第一个LED(编号从0开始)。
vParTestToggleLED( 0 );
}
// 协程必须以调用crEND()结束。
// crEND()用于标记协程的结束,设置协程的状态为结束。
crEND();
}
(2) 调度协程:
通过重复调用 vCoRoutineSchedule() 来调度协程。执行这一操作的最佳位置是 空闲任务内部,通过编写空闲任务钩子函数来完成。首先,请确保 configUSE_IDLE_HOOK 在 FreeRTOSConfig.h中设置为 1。然后编写空闲任务钩子函数,如下所示:
void vApplicationIdleHook( void )
{
vCoRoutineSchedule( void );
}
如果空闲任务没有执行任何其他函数,那按以下方式在循环中调用 vCoRoutineSchedule() 效率会更高:
void vApplicationIdleHook( void )
{
for( ;; )
{
vCoRoutineSchedule( void );
}
}
(3) 创建协程并启动 RTOS 调度器:
协程可在 main() 中创建。
#include "task.h" // 包含任务管理相关的头文件,提供任务创建和管理的函数。
#include "croutine.h" // 包含协程管理相关的头文件,提供协程创建和管理的函数。
#define PRIORITY_0 0 // 定义优先级为0,这是最低的优先级。
void main( void ) // 主函数入口
{
// 创建一个协程,调用vFlashCoRoutine函数作为协程的任务。
// 这里使用的优先级是PRIORITY_0,即0,表示最低优先级。
// 第三个参数0在这里没有被使用,仅作为参数传递。
xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, 0 );
// 注意:这里也可以创建任务(Tasks)!
// 启动RTOS调度器。
// 一旦调用vTaskStartScheduler(),调度器将开始运行,并且之前创建的协程和任务将开始执行。
vTaskStartScheduler();
}
(4)使用索引参数:
现在假设我们要从同一函数中创建 8 个这样的协程。每个协程将 以不同速度闪烁不同的 LED。索引参数可用于在协程函数中 区分协程。
这一次,我们将创建 8 个协程,并向每个协程传递不同的索引。
#include "task.h" // 包含FreeRTOS任务管理相关的头文件。
#include "croutine.h" // 包含FreeRTOS协程管理相关的头文件。
#define PRIORITY_0 0 // 定义优先级0,表示最低优先级。
#define NUM_COROUTINES 8 // 定义要创建的协程数量为8。
void main( void )
{
int i; // 用于循环计数的变量。
for( i = 0; i < NUM_COROUTINES; i++ ) // 循环创建NUM_COROUTINES个协程。
{
// 创建一个协程,调用vFlashCoRoutine函数作为协程的任务。
// 使用PRIORITY_0作为协程的优先级,即0,表示最低优先级。
// 将循环变量i作为索引传递给协程,这样每个协程可以有不同的索引值。
xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, i );
}
// 注意:这里也可以创建任务(Tasks)!
// 启动RTOS调度器。
// 一旦调用vTaskStartScheduler(),调度器将开始运行,并且之前创建的协程和任务将开始执行。
vTaskStartScheduler();
}
协程函数也被扩展,因此每个协程使用的 LED 和闪烁速度都不同。
// 定义一个常量数组,包含每个协程控制的LED闪烁频率(以tick为单位)。
const int iFlashRates[ NUM_COROUTINES ] = { 10, 20, 30, 40, 50, 60, 70, 80 };
// 定义一个常量数组,包含每个协程控制的LED编号。
const int iLEDToFlash[ NUM_COROUTINES ] = { 0, 1, 2, 3, 4, 5, 6, 7 };
// 协程函数,用于控制LED以不同的频率闪烁。
void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 协程必须以调用crSTART()开始。
crSTART( xHandle );
for( ;; ) // 无限循环,协程的主要执行逻辑在这里。
{
// 延迟一段固定时间。这里使用uxIndex作为数组索引,
// 从iFlashRates数组中获取对应的闪烁频率。由于每个协程都使用不同的索引值创建,
// 因此每个协程将延迟不同的时间。
crDELAY( xHandle, iFlashRates[ uxIndex ] );
// 闪烁一个LED。再次使用uxIndex作为数组索引,
// 从iLEDToFlash数组中获取要切换的LED编号。
vParTestToggleLED( iLEDToFlash[ uxIndex ] );
}
// 协程必须以调用crEND()结束。
crEND();
}
七、空闲任务:
(1)空闲任务说明:
- RTOS 调度器启动时,自动创建空闲任务,以确保始终存在一个能够运行的任务。它以最低优先级创建, 以确保如果有更高的优先级应用程序任务处于准备就绪状态,则不使用任何 CPU 时间。
- 空闲任务负责释放 RTOS 分配给 已删除任务的内存。因此,在使用 vTaskDelete() 函数来确保闲置任务不会匮乏处理时间的应用程序中, 这很重要。 空闲任务没有其他激活函数,因此可以在所有其他条件下合理地缺乏微控制器时间 。
- 应用程序任务可以共享空闲任务优先级 (tskIDLE_PRIORITY)。 由configIDLE_SHOULD_YIELD 配置参数。
(2)空闲任务钩子:
- 空闲任务钩子是在空闲任务的每个周期中调用的函数。
- 如果希望应用程序函数以空闲优先级运行,则有两个选择:
- 在空闲任务钩子中实现此函数:
- 必须始终有至少一个任务已准备好运行。因此,必须确保钩子 函数不调用任何可能导致空闲任务阻塞的 API 函数(例如,vTaskDelay(),或 具有阻塞时间的队列或信号量函数)。协程 可以在钩子函数内阻塞。
- 创建空闲优先级任务以实现该函数:
- 这是一种更灵活的解决方案,但具有更高的 RAM 使用开销。
- 要创建一个空闲钩子:
- 在 FreeRTOSConfig.h中将 configUSE_IDLE_HOOK 设置为 1。
- 定义具有以下名称和原型的函数:
- void vApplicationIdleHook(void);
- 通常使用空闲钩子函数将微控制器 CPU 设为节能模式。
八、线程本地存储指针:
- 线程本地存储 (TLS) 允许应用程序编写者将值存储在任务的控制块中, 控制块内,使值特定于(本地)任务本身, 并允许每个任务具有自己的唯一值。
- 线程本地存储最常用于 存储单线程程序会存储在全局变量中的值。例如,许多库包含一个 名为 errno 的全局变量。如果库函数向调用函数返回错误条件, 则调用函数可以检查 errno 值 以确定是什么错误。在单线程应用程序中,只需 将 errno 声明为全局变量,但在多线程应用程序中, 每个线程(任务)必须有其唯一的 errno 值, 否则一个任务可能会读取另一个任务的 errno 值。
(1)线程本地存储指针:
- 通过使用线程本地存储指针,FreeRTOS 为应用程序编写者提供了灵活的线程本地存储机制。
- configNUM_THREAD_LOCAL_STORAGE_POINTERS 编译时配置常量为每个任务的 void 指针数组 (void*) 确定维度。 vTaskSetThreadLocalStoragePointer() API 函数用于 在 void 指针数组中设置值, pvTaskGetThreadLocalStoragePointer() API 函数用于从 void 指针数组读取值。
(2)线程本地整数:
- 小于或等于 void 指针大小的值 可以直接存储在线程本地存储指针数组中。例如, 如果 sizeof( void * ) 为 4,则可以使用简单的强制转换将 32 位值存储在 void 指针变量中, 以避免编译器发出警告。但是, 如果 sizeof( void * ) 为 2,则只能直接存储 16 位值。
示例: 直接从线程本地存储数组中的索引存储和检索 32 位值
uint32_t ulVariable; // 定义一个32位无符号整数变量。
/* 将32位的0x12345678值直接写入线程本地存储数组的索引1。
传递NULL作为任务句柄的效果是写入调用任务的线程本地存储数组。 */
vTaskSetThreadLocalStoragePointer( NULL, /* 任务句柄。 */
1, /* 数组索引。 */
( void * ) 0x12345678 ); /* 要写入的值。 */
/* 将32位变量ulVariable的值存储到调用任务的线程本地存储数组的索引0。 */
ulVariable = ERROR_CODE; // 假设ERROR_CODE是一个已定义的错误代码。
vTaskSetThreadLocalStoragePointer( NULL, /* 任务句柄。 */
0, /* 数组索引。 */
( void * ) ulVariable ); /* 要写入的值。 */
/* 从调用任务的线程本地存储数组的索引5读取值到ulVariable。 */
ulVariable = ( uint32_t ) pvTaskGetThreadLocalStoragePointer( NULL, 5 ); // 读取的值转换为uint32_t类型。
(3)线程本地结构体:
使用数组中的值作为指向内存中其他位置的结构体的指针。
typedef struct // 定义一个结构体类型。
{
uint32_t ulValue1; // 结构体的第一个成员,32位无符号整数。
uint32_t ulValue2; // 结构体的第二个成员,32位无符号整数。
} xExampleStruct; // 结构体类型的名称。
xExampleStruct *pxStruct; // 定义一个指向该结构体类型的指针。
/* 创建一个结构体实例供此任务使用。 */
pxStruct = pvPortMalloc( sizeof( xExampleStruct ) ); // 使用pvPortMalloc分配内存。
/* 设置结构体成员的值。 */
pxStruct->ulValue1 = 0; // 设置结构体的第一个成员为0。
pxStruct->ulValue2 = 1; // 设置结构体的第二个成员为1。
/* 将结构体指针存储在调用任务的线程本地存储数组的索引0。 */
vTaskSetThreadLocalStoragePointer( NULL, /* 任务句柄,NULL表示当前任务。 */
0, /* 数组索引。 */
( void * ) pxStruct ); /* 要存储的结构体指针。 */
/* 从调用任务的线程本地存储数组的索引0读取结构体指针。 */
pxStruct = ( xExampleStruct * ) pvTaskGetThreadLocalStoragePointer( NULL, 0 ); // 读取并转换为结构体指针类型。
存储指向调用任务线程本地存储数组中结构体的指针。
九、协程常见问题报错:
(1)CoRoutineCreate函数未定义:
显示xCoRoutineCreate函数未定义:
xCoRoutineCreate()的定义在#include "croutine.h"文件中。我们添加该头文件并打开,在其中查找xCoRoutineCreate的定义位置。
我们可以看到该函数在croutine.h中定义了,我们再到croutine.c文件中查看可以看到:该函数并没有启用。
向上翻动可以看到有:
即,由于configUSE_CO_ROUTINES没有定义默认为0,此时协程并不会启用。
我们可以在FreeRTOSconfig.h头文件中添加启用协程的宏定义。
(2)协程最高优先级未定义:
在添加完启用协程宏定义之后,还需要添加优先级宏定义,定义协程的最高优先级。不然会报错如下:
(3)内存空间不足报错:
注意:使用协程时需要将静态创建任务宏定义注释或置0:
//#define configSUPPORT_STATIC_ALLOCATION 1
否则,会报错如下:
(4)空闲任务未定义:
同上我们在,Tasks.c和Tasks.h文件中查找vApplicationIdleHook()函数的定义。发现vApplicationIdleHook的定义只存在与Tasks.c文件中:
向上滑动到该函数开头,添加空闲任务函数即可。
十、协程程序编写:
(1)基础协程创建:
创建协程实现串口打印,实现每隔1s打印字符串。
/***********************************
* @method 创建协程实现串口打印,
* 实现每隔1s打印字符串
* @Platform CSDN
* @author The_xzs
* @date 2025.1.24
************************************/
// 协程函数
void Croutine( CoRoutineHandle_t xHandle,
UBaseType_t uxIndex )
{
crSTART( xHandle );
for( ;; )
{
printf("Croutine1\r\n");
crDELAY( xHandle, 1000); // 再次延迟1000个时钟节拍。
}
crEND();
}
// 创建协程
void Croutine_Init( void )
{
/***********************************************
* @pxCoRoutineCode 指向协程函数的指针。
* @uxPriority 协程运行时相对于其他协程的优先级。
* @uxIndex 用于区分执行相同函数的不同协程(索引)。
* BaseType_t xCoRoutineCreate
* (
* crCOROUTINE_CODE pxCoRoutineCode,
* UBaseType_t uxPriority,
* UBaseType_t uxIndex
* );
*************************************************/
//协程函数为Croutine,优先级为0,索引为0
xCoRoutineCreate(Croutine, 1, 0);
}
// 主函数
int main(void)
{
Uart_Init(115200);
DMA1_Init();
Croutine_Init();
//启动RTOS调度器
vTaskStartScheduler();
return 0;
}
运行效果:
(2)协程中索引的使用:
根据索引值打印不同的字符串
#include "stm32f10x.h" // 包含STM32F10x系列微控制器的头文件
#include "FreeRTOS.h"
#include "task.h" // 包含任务相关函数的头文件,用于任务创建和管理。
#include "croutine.h" // 包含协程相关函数的头文件,用于协程创建和管理。
#include "stdio.h"
#include "uart.h"
/***********************************
* @method 根据索引值打印不同的字符串
*
* @Platform CSDN
* @author The_xzs
* @date 2025.1.24
************************************/
// 协程函数
void Croutine( CoRoutineHandle_t xHandle,
UBaseType_t uxIndex )
{
crSTART( xHandle );
for( ;; )
{
// 根据索引值打印不同的字符串
if( uxIndex == 0 )
{
printf("Croutine1\r\n");
}
else if( uxIndex == 1 )
{
printf("Croutine2\r\n");
}
else if( uxIndex == 2 )
{
printf("Croutine3\r\n");
}
else
{
printf("CroutineX\r\n");
}
crDELAY( xHandle, 1000); // 再次延迟1000个时钟节拍。
}
crEND();
}
// 创建协程
void Croutine_Init( void )
{
// 协程函数为Croutine,优先级为0,索引分别为0、1、2
xCoRoutineCreate(Croutine, 1, 0);
xCoRoutineCreate(Croutine, 1, 1);
xCoRoutineCreate(Croutine, 1, 2);
}
// 主函数
int main(void)
{
Uart_Init(115200);
DMA1_Init();
Croutine_Init();
//启动RTOS调度器
vTaskStartScheduler();
return 0;
}
运行效果:
十一、FreeRTOS教程示例代码下载:
FreeRTOS教程示例代码将会持续更新...
通过网盘分享的文件:FreeRTOS教程示例代码
链接: https://pan.baidu.com/s/1363h7hHmf8u2pjauwKyhtw?pwd=mi98 提取码: mi98