FreeRTOS保姆级教程:STM32任务与协程详解及实战代码示例
目录
一、任务和协程概述:
(1)任务的特点:
(2)协程的特点:
(3)注意事项:
二、任务:
(1)任务状态:
a.任务状态说明:
b.有效任务状态转换:
c.作用说明:
三、任务优先级:
(1)任务优先级说明:
优先级范围:
优先级值:
调度器行为:
相同优先级的任务:
时间片轮询调度:
优先级设置:
优先级注意事项:
(2)API函数:
特点:
在FreeRTOS中的API函数:
(3)作用说明:
四、任务调度:
(1)默认RTOS调度策略(单核):
(2)避免任务饥饿:
(3)配置RTOS调度策略:
(4)FreeRTOS AMP调度策略:
(5)FreeRTOS SMP调度策略:
(6)配置SMPRTOS调度策略:
(7)作用总结:
五、任务实现:
在FreeRTOS中实现和使用任务:
(1)任务函数的结构:
(2)事件驱动型任务:
(3)任务创建和删除:
(4)任务创建宏:
(5)函数原型:
(6)总结:
六、协程:
(1)协程状态:
(2)注意事项:
七、协程实现:
(1)协程的结构:
(2)协程的创建:
(3)注意事项:
(4)协程的调度:
(5)协程的状态:
(6)协程的限制:
(7)总结:
八、协程优先级:
(1)协程优先级的范围和定义:
(2)协程优先级的特性:
(3)协程与任务的优先级比较:
(4)协程优先级的应用:
(5)协程优先级设置的注意事项:
(6)总结:
九、 协程调度:
(1)调度协程:
(2)混合任务和协程:
(3)实现示例:
(4)总结:
十、 局限和限制:
(1)共享堆栈:
(2)阻塞调用的位置:
(3)switch语句中的阻塞调用:
(4)协程的限制和复杂性:
(5)总结:
十一、协程示例:
(1)创建一个简单的协程来闪烁 LED:
(2)调度协程:
(3)创建协程并启动 RTOS 调度器:
(4)示例扩展:使用索引参数:
十二、空闲任务:
(1)空闲任务的作用:
(2)空闲任务的特点:
(3)空闲任务钩子:
(4)空闲任务钩子的使用场景:
(5)示例代码:
(6)注意事项:
(7)总结:
十三、线程本地存储指针:
(1)线程本地存储指针:
(2)线程本地整数:
(3)线程本地结构体:
(4)注意事项:
(5)总结
十四、以STM32F103C8T6为例的实际任务和协程操作:
(1)通过任务调度,闪烁一个LED:
(2)通过协程调度,闪烁一个LED:
十五、FreeRTOS移植所需文件:
一、任务和协程概述:
在FreeRTOS操作系统中,任务(Task)和协程(Coroutine)是两种不同的并发执行单位,它们各自有不同的特点和适用场景。
(1)任务的特点:
(2)协程的特点:
(3)注意事项:
在选择使用任务还是协程时,需要考虑应用程序的具体需求。如果应用程序对RAM资源有严格的限制,且任务之间的同步和通信需求较低,协程可能是一个合适的选择。然而,如果应用程序需要复杂的同步机制、优先级调度和抢占式行为,任务可能是更好的选择。
在实际应用中,任务是FreeRTOS中更常用的并发执行单位,而协程由于其限制和RTOS的发展,使用较少。在设计RTOS应用程序时,通常推荐使用任务作为主要的并发执行单元,并利用FreeRTOS提供的任务调度、同步和通信机制来实现应用程序逻辑。如果需要类似协程的行为,可以通过手动管理任务的执行和挂起状态来模拟,但这通常比使用真正的协程要复杂得多。
二、任务:
(1)任务状态:
在FreeRTOS操作系统中,任务可以处于以下几种状态,并且可以在这些状态之间转换。
a.任务状态说明:
运行态(Running):
taskYIELD()
或vTaskDelay()
等函数,或者由于更高优先级任务的启动而从运行态转换到就绪态或阻塞态。就绪态(Ready):
阻塞态(Blocked):
vTaskDelay()
后延时结束。挂起态(Suspended):
vTaskSuspend()
进入挂起态,通过调用xTaskResume()
或vTaskResumeFromISR()
从挂起态转换回就绪态。b.有效任务状态转换:
vTaskSuspend()
。xTaskResume()
或vTaskResumeFromISR()
。
c.作用说明:
这些状态和转换是FreeRTOS调度器管理任务执行的关键机制,确保任务可以根据优先级和事件触发进行合理的调度和执行。
三、任务优先级:
在FreeRTOS中,任务是执行的最小单位,每个任务都被分配了一个优先级,这个优先级决定了任务的调度顺序。
(1)任务优先级说明:
优先级范围:
configMAX_PRIORITIES - 1
,其中configMAX_PRIORITIES
是在FreeRTOSConfig.h
中定义的。configUSE_PORT_OPTIMISED_TASK_SELECTION
在FreeRTOSConfig.h
中设置为1,则configMAX_PRIORITIES
通常不超过32。优先级值:
调度器行为:
相同优先级的任务:
configUSE_TIME_SLICING
未定义或者设置为0,相同优先级的任务将按照它们成为就绪状态的顺序执行,直到它们被更高优先级的任务抢占。configUSE_TIME_SLICING
设置为1,相同优先级的任务将通过时间片轮询调度方案共享CPU时间。这意味着每个任务将获得一个时间片,在时间片用完后,如果任务仍然处于就绪状态,它将被放回就绪队列的末尾,让其他同优先级的任务获得执行机会。时间片轮询调度:
configTICK_RATE_HZ
(定义了tick的频率)和configMAX_PRIORITIES
来调整。优先级设置:
xTaskCreate()
或xTaskCreateStatic()
函数的uxPriority
参数设置。vTaskPrioritySet()
进行动态调整。优先级注意事项:
(2)API函数:
API函数(应用程序编程接口函数)是指一组预定义的函数,允许程序员与软件库、操作系统或其他服务进行交互。API函数提供了一种标准化的方式,使得开发者能够使用特定的功能,而无需了解其内部实现细节。
特点:
在FreeRTOS中的API函数:
在FreeRTOS中,API函数用于管理任务、队列、信号量等。
以下是一些常用的FreeRTOS API函数示例:
任务管理:
xTaskCreate()
: 创建一个新的任务。vTaskDelete()
: 删除指定的任务。vTaskDelay()
: 使当前任务延迟一段时间。任务优先级:
vTaskPrioritySet()
: 设置指定任务的优先级。uxTaskPriorityGet()
: 获取指定任务的优先级。队列管理:
xQueueCreate()
: 创建一个新的队列。xQueueSend()
: 向队列发送数据。xQueueReceive()
: 从队列接收数据。信号量管理:
xSemaphoreCreateBinary()
: 创建一个二进制信号量。xSemaphoreTake()
: 获取信号量。xSemaphoreGive()
: 释放信号量。(3)作用说明:
通过合理配置和使用任务优先级,可以确保FreeRTOS系统能够高效、公平地调度多个任务,满足实时性要求。
四、任务调度:
FreeRTOS是一个可预占的实时操作系统内核,它提供了多种调度策略,可以应用于单核、非对称多核(AMP)和对称多核(SMP)处理器架构。
(1)默认RTOS调度策略(单核):
FreeRTOS默认使用固定优先级的抢占式调度策略,并且对同等优先级的任务执行时间切片轮询调度:
(2)避免任务饥饿:
高优先级任务可能会永久性地剥夺低优先级任务的执行时间,这就是为什么通常最好创建事件驱动型任务的原因。事件驱动型任务在等待事件时会进入阻塞状态,从而允许低优先级任务运行。
(3)配置RTOS调度策略:
在FreeRTOSConfig.h
中,可以通过以下宏来配置调度策略:
configUSE_PREEMPTION
:如果设置为0,则关闭抢占,只有在任务主动放弃CPU时才会进行调度。configUSE_TIME_SLICING
:如果设置为0,则关闭时间切片,相同优先级的任务不会轮流执行。(4)FreeRTOS AMP调度策略:
在非对称多处理(AMP)配置中,每个处理器核心运行自己的FreeRTOS实例,核心之间可以通过共享内存和核间通信原语(如流缓冲区或消息缓冲区)进行通信。
(5)FreeRTOS SMP调度策略:
在对称多处理(SMP)配置中,一个FreeRTOS实例可以跨多个处理器核心调度任务。每个核心必须具有相同的处理器架构并共用相同的内存空间。SMP调度策略允许同时在多个核心上运行多个任务。
(6)配置SMPRTOS调度策略:
在SMP配置中,可以使用以下配置选项:
configRUN_MULTIPLE_PRIORITIES
:如果设置为0,则只有当多个任务具有相同优先级时,调度器才会同时运行多个任务。configUSE_CORE_AFFINITY
:如果设置为1,则可以使用vTaskCoreAffinitySet()
API函数定义任务可以在哪些核心上运行,从而避免同时执行假设了自身执行顺序的两个任务。(7)作用总结:
FreeRTOS提供了灵活的调度策略,可以适应不同的处理器架构和应用需求。通过合理配置和使用这些调度策略,可以确保系统资源得到有效利用,同时满足实时性要求。在设计RTOS应用程序时,需要考虑任务的优先级、调度策略以及多核处理器的特定特性,以实现高效和可靠的系统。
五、任务实现:
在FreeRTOS中,任务是执行的最小单位,每个任务都是一个无限循环的函数,它定义了任务的行为和功能。
在FreeRTOS中实现和使用任务:
(1)任务函数的结构:
void vATaskFunction( void *pvParameters )
{
for( ;; )
{
// 任务应用代码在这里。
}
// 任务不应该尝试从其实现函数返回或以其他方式退出。
// 在较新的FreeRTOS版本中,如果尝试这样做,将调用configASSERT()(如果定义)。
// 如果需要任务退出,则应让任务调用vTaskDelete(NULL)以确保其退出是干净的。
vTaskDelete( NULL );
}
任务函数必须永不返回,因此通常实现为一个无限循环。任务函数的参数pvParameters
可以用来传递任何类型的信息给任务。
(2)事件驱动型任务:
为了避免低优先级任务饥饿,最好创建事件驱动型任务。任务在等待事件时应该进入阻塞状态,而不是忙等待。这可以通过使用FreeRTOS提供的通信和同步原语来实现,例如:
void vATaskFunction( void *pvParameters )
{
for( ;; )
{
if( WaitForEvent( EventObject, TimeOut ) == pdPASS )
{
// 处理事件。
}
else
{
// 超时后的错误处理。
}
}
// 任务退出。
vTaskDelete( NULL );
}
在这里,WaitForEvent()
是一个伪代码,代表等待事件的调用,可以是xQueueReceive()
、ulTaskNotifyTake()
、xEventGroupWaitBits()
或其他FreeRTOS通信和同步原语。
(3)任务创建和删除:
xTaskCreate()
或xTaskCreateStatic()
函数创建新任务。vTaskDelete()
来删除任务。在FreeRTOS中,任务的创建和删除是通过特定的API函数进行的。
创建任务:
可以使用xTaskCreate()
或xTaskCreateStatic()
函数来创建任务。
使用xTaskCreate()
创建任务: xTaskCreate()
函数动态地创建任务,由FreeRTOS的内存管理器分配任务控制块和堆栈。
TaskHandle_t xTaskCreate(
TaskFunction_t pxTaskCode, // 任务函数
const char * const pcName, // 任务名称
uint32_t ulStackDepth, // 堆栈深度,单位为字(取决于处理器架构)
void * const pvParameters, // 传递给任务的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t * const pxCreatedTask // 任务句柄的指针,可选参数
);
示例代码:
// 任务函数
void vTaskFunction(void *pvParameters)
{
// 任务代码
}
// 创建任务
TaskHandle_t xHandle = NULL;
xTaskCreate(vTaskFunction, "Task1", 500, NULL, 1, &xHandle);
使用xTaskCreateStatic()
创建任务:
xTaskCreateStatic()
函数静态地创建任务,需要预先提供任务控制块和堆栈。
BaseType_t xTaskCreateStatic(
TaskFunction_t pxTaskCode, // 任务函数
const char * const pcName, // 任务名称
uint32_t ulStackDepth, // 堆栈深度,单位为字(取决于处理器架构)
void * const pvParameters, // 传递给任务的参数
UBaseType_t uxPriority, // 任务优先级
StackType_t *pxStackBuffer, // 提供的堆栈空间
TaskHandle_t *pxTaskBuffer // 提供的任务控制块
);
示例代码:
// 任务函数
void vTaskFunction(void *pvParameters)
{
// 任务代码
}
// 静态任务创建所需的堆栈和任务控制块
StackType_t xStack[STACK_SIZE];
TaskHandle_t xTaskBuffer;
// 创建任务
BaseType_t xStatus = xTaskCreateStatic(vTaskFunction, "Task1", STACK_SIZE, NULL, 1, xStack, &xTaskBuffer);
xTaskCreate()
创建任务,FreeRTOS会在内部释放任务的堆栈和任务控制块。如果使用xTaskCreateStatic()
,则需要手动管理这些资源。(4)任务创建宏:
FreeRTOS提供了portTASK_FUNCTION
和portTASK_FUNCTION_PROTO
宏,这些宏允许将编译器特定的语法添加到函数定义和原型中。这些宏的使用取决于特定的硬件和编译器。通常,如果端口相关文档中没有特别说明,就不需要使用这些宏。
(5)函数原型:
任务函数的原型可以简单地写为:
void vATaskFunction( void *pvParameters );
或者使用宏:
portTASK_FUNCTION_PROTO( vATaskFunction, pvParameters );
函数定义也可以使用宏:
portTASK_FUNCTION( vATaskFunction, pvParameters )
{
for( ;; )
{
// 任务应用代码在这里。
}
}
(6)总结:
在FreeRTOS中实现任务时,需要遵循特定的结构和模式,以确保任务能够正确地与RTOS内核交互。任务通常是事件驱动的,并且在不再需要时应该能够自我删除。通过使用FreeRTOS提供的API和宏,可以简化任务的创建和管理。
六、协程:
(1)协程状态:
在FreeRTOS中,协程是一种轻量级的任务,它们在内存受限的极小处理器上非常有用,但对于现代32位微控制器来说,它们通常不是首选。
协程可以存在于以下几种状态中:
运行(Running):当协程实际执行时,它处于运行状态,此时正在使用处理器。
就绪(Ready):就绪的协程是那些能够执行(未阻塞)但目前未执行的协程。协程可能处于就绪状态的原因包括:另一个具有相同或更高优先级的协程已处于运行状态,或者任务处于运行状态(仅当应用程序同时使用任务和协程时才会出现这种情况)。
阻塞(Blocked):如果协程当前正在等待时间事件或外部事件,则该协程处于阻塞状态。例如,如果协程调用crDELAY()
,它将阻塞,直到延迟期结束。阻塞的协程不可用于调度。
与任务不同,当前没有等同于任务挂起状态的协程。有效的协程状态转换包括从就绪状态到运行状态,以及从运行状态到阻塞状态。协程在阻塞时会自动让出CPU资源,这有助于提高系统的并发性和响应速度,避免了CPU资源的浪费。
在FreeRTOS中,协程的调度是通过重复调用vCoRoutineSchedule()
来实现的,这通常在空闲任务钩子中完成。如果应用程序只使用协程,那么在空闲任务中调用vCoRoutineSchedule()
是必要的,因为空闲任务在调度程序启动时会自动创建。
(2)注意事项:
随着FreeRTOS的发展,协程的支持可能已经在后续版本中被移除或不再推荐使用。在早期版本的FreeRTOS中,通过设置configUSE_CO_ROUTINES
宏来决定是否包含协程的相关代码。如果该宏被设置为1,则FreeRTOS会包含协程的相关代码,并允许在系统中使用协程;如果设置为0,则协程相关的代码不会被包含,从而节省内存空间。在最新版本的FreeRTOS中,由于协程在现代实时操作系统中并不常用,且其实现可能不如任务调度那样高效和灵活,因此协程的支持可能已经被完全移除。因此,在编写基于FreeRTOS的应用程序时,推荐使用任务作为主要的并发执行单元。
七、协程实现:
在FreeRTOS中,协程是一种比任务更轻量级的并发执行单位,它们通常用于内存资源非常有限的系统中。协程的实现和使用有其特定的模式和要求。
(1)协程的结构:
协程的实现通常遵循以下结构:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
crSTART( xHandle );
for( ;; )
{
// 协程应用代码在这里。
}
crEND();
}
在这个结构中:
crSTART()
宏在协程开始执行时调用,它负责设置协程的初始状态。crEND()
宏在协程结束时调用,它标志着协程的结束。(2)协程的创建:
协程是通过调用xCoRoutineCreate()
函数创建的。这个函数接受协程函数、协程的优先级和协程索引作为参数,并返回一个协程句柄,该句柄在后续的操作中用于控制协程。
(3)注意事项:
crSTART()
开始,并以crEND()
结束。这两个宏是FreeRTOS协程实现的一部分,用于管理协程的执行和退出。uxIndex
参数区分。这个索引参数在协程函数内部可以用来存储与特定协程实例相关的数据或状态。(4)协程的调度:
协程的调度是通过调用vCoRoutineSchedule()
函数手动进行的。这个函数通常在空闲任务中调用,或者在任何其他需要显式调度协程的地方调用。协程调度是协作式的,意味着协程需要显式地让出控制权,通常是通过调用如crDELAY()
这样的宏来实现。
(5)协程的状态:
协程主要有以下几种状态:
(6)协程的限制:
(7)总结:
FreeRTOS协程是一种轻量级的并发执行机制,适用于资源受限的环境。它们通过特定的宏和函数进行创建和管理,并且需要程序员显式地进行调度和状态管理。协程的使用可以减少系统的内存占用,但同时也带来了一些限制和复杂性。在现代的FreeRTOS版本中,协程的使用已经不如任务普遍,因此在选择使用协程之前,需要仔细考虑应用程序的具体需求和约束。
八、协程优先级:
在FreeRTOS中,协程的优先级管理是一个关键的概念,它决定了协程之间的调度顺序。
(1)协程优先级的范围和定义:
configMAX_CO_ROUTINE_PRIORITIES - 1
的优先级,其中configMAX_CO_ROUTINE_PRIORITIES
是在FreeRTOSConfig.h
中定义的。(2)协程优先级的特性:
(3)协程与任务的优先级比较:
(4)协程优先级的应用:
crDELAY()
)来允许优先级较低的协程运行。(5)协程优先级设置的注意事项:
(6)总结:
协程优先级是FreeRTOS中协程调度的一个重要方面。正确地设置和管理协程优先级对于确保系统的有效运行至关重要。在设计系统时,开发者需要考虑协程之间的相对优先级以及协程和任务之间的优先级关系,以确保系统的实时性和效率。由于FreeRTOS的版本更新,协程的使用已经不如以前普遍,因此在考虑使用协程时,应该评估是否有更合适的任务或线程机制可以满足系统的需求。
九、 协程调度:
在FreeRTOS中,协程的调度是通过协作式调度机制实现的,这意味着协程需要显式地让出CPU以便其他协程运行。
(1)调度协程:
vCoRoutineSchedule()
是FreeRTOS提供的用于调度协程的函数。它应该在系统的某个固定点被周期性调用,以确保所有就绪状态的协程都有机会被执行。vCoRoutineSchedule()
的最佳位置是在空闲任务钩子(vApplicationIdleHook()
)中。即使应用程序仅使用协程,空闲任务也会在调度器启动后自动创建,因此在这个钩子中调用vCoRoutineSchedule()
可以保证协程得到调度。crDELAY()
或其他阻塞调用,以允许其他协程运行。这种调度方式减少了上下文切换的开销,但需要程序员更仔细地管理协程的执行。(2)混合任务和协程:
(3)实现示例:
// 空闲任务钩子函数
void vApplicationIdleHook( void )
{
// 调用协程调度器
vCoRoutineSchedule();
}
// 创建协程的示例
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
crSTART( xHandle );
for( ;; )
{
// 协程应用代码
// ...
// 让出CPU,允许其他协程运行
crDELAY( xHandle, 100 );
}
crEND();
}
// 主函数中创建协程
int main( void )
{
// 创建协程
xCoRoutineCreate( vACoRoutineFunction, 0, 0 );
// 启动任务调度器
vTaskStartScheduler();
// 如果调度器启动成功,以下代码不会执行
for( ;; );
}
(4)总结:
在FreeRTOS中,协程的调度需要程序员的显式管理,通常通过在空闲任务钩子中调用vCoRoutineSchedule()
来实现。在混合任务和协程的应用程序中,协程通常在系统空闲时运行,这要求开发者在设计系统时仔细考虑任务和协程之间的优先级和资源分配。通过合理地调度协程,可以提高系统的整体效率和响应性,尤其是在资源受限的嵌入式系统中。
十、 局限和限制:
协程在FreeRTOS中提供了一种轻量级的并发执行方式,但它们也有一些局限性和使用上的限制。以下是协程在使用时需要注意的一些关键点:
(1)共享堆栈:
static
。这样,即使协程阻塞,它们的值也会在协程恢复时保持不变。共享堆栈的问题和解决方案:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
static char c = 'a'; // 静态变量,用于在阻塞后保持其值
crSTART( xHandle );
for( ;; )
{
c = 'b'; // 设置变量值
crDELAY( xHandle, 10 ); // 阻塞调用,协程让出控制权
// 此处变量 c 仍然为 'b',因为它是静态的
}
crEND();
}
在这个例子中,变量c
被声明为static
,这样即使协程在crDELAY()
调用中阻塞,它的值也会在协程恢复时保持不变。
(2)阻塞调用的位置:
crDELAY()
。阻塞调用的位置限制:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
crSTART( xHandle );
for( ;; )
{
crDELAY( xHandle, 10 ); // 正确的使用:直接在协程函数中调用阻塞函数
vACalledFunction(); // 错误的使用:不能在协程调用的函数中调用阻塞函数
}
crEND();
}
void vACalledFunction( void )
{
// 不能在此处调用阻塞函数,如 crDELAY()
}
在这个例子中,crDELAY()
可以直接在协程函数中调用,但不能在vACalledFunction()
中调用,因为这可能会导致协程的执行流程出现问题。
(3)switch语句中的阻塞调用:
switch
语句中进行阻塞调用。这是因为switch
语句中的代码路径可能不会立即返回到协程函数,从而破坏了协程的执行流程。switch语句中不允许阻塞调用:
void vACoRoutineFunction( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
crSTART( xHandle );
for( ;; )
{
crDELAY( xHandle, 10 ); // 正确的使用:直接在协程函数中调用阻塞函数
int aVariable = 1;
switch( aVariable )
{
case 1:
// 不能在此处调用阻塞函数,如 crDELAY()
break;
default:
// 也不行
break;
}
}
crEND();
}
在这个例子中,switch
语句内不能进行阻塞调用,因为这可能会破坏协程的调度机制。
(4)协程的限制和复杂性:
switch
语句中进行阻塞调用,也不能在协程函数调用的其他函数中进行阻塞调用。(5)总结:
尽管协程在内存使用上更为高效,但它们在FreeRTOS中使用时需要注意以下几点:
static
变量来维持状态。switch
语句中不允许进行阻塞调用。由于这些限制,协程通常只在特定的、对内存使用有严格要求的场景中使用。在大多数情况下,任务是更可取的选择,因为它们提供了更多的灵活性和更少的限制。在设计系统时,应该根据实际需求和资源限制来选择使用任务还是协程。
十一、协程示例:
(1)创建一个简单的协程来闪烁 LED:
void vFlashCoRoutine( CoRoutineHandle_t xHandle,
UBaseType_t uxIndex )
{
//协例程必须从调用crSTART()开始。
crSTART( xHandle );
for( ;; )
{
//指定时间延迟。
crDELAY( xHandle, 10 );
//闪烁LED
vParTestToggleLED( 0 );
}
//协例程必须以调用crEND()结束。
crEND();
}
(2)调度协程:
通过重复调用 vCoRoutineSchedule() 来调度协程。执行这一操作的最佳位置是 空闲任务内部,通过编写空闲任务钩子函数来完成。首先,请确保 configUSE_IDLE_HOOK 在 FreeRTOSConfig.h中设置为 1。然后编写空闲任务钩子函数,如下所示:
void vApplicationIdleHook( void )
{
vCoRoutineSchedule( void );
}
如果空闲任务没有执行任何其他函数,那按以下方式在循环中调用 vCoRoutineSchedule() 效率会更高:
void vApplicationIdleHook( void )
{
for( ;; )
{
vCoRoutineSchedule( void );
}
}
(3)创建协程并启动 RTOS 调度器:
#include "task.h"
#include "croutine.h"
#define PRIORITY_0 0
void main( void )
{
//在这种情况下,索引不被使用,并作为0传入。
xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, 0 );
//注意:任务也可以在这里创建!
//启动RTOS调度器
vTaskStartScheduler();
}
(4)示例扩展:使用索引参数:
现在假设我们要从同一函数中创建 8 个这样的协程。每个协程将 以不同速度闪烁不同的 LED。索引参数可用于在协程函数中 区分协程。
这一次,我们将创建 8 个协程,并向每个协程传递不同的索引。
#include "task.h"
#include "croutine.h"
#define PRIORITY_0 0
#define NUM_COROUTINES 8
void main( void )
{
int i;
for( i = 0; i < NUM_COROUTINES; i++ )
{
//这次i作为索引传入。
xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, i );
}
//注意:任务也可以在这里创建!
//启动RTOS调度器
vTaskStartScheduler();
}
协程函数也被扩展,因此每个协程使用的 LED 和闪烁速度都不同。
const int iFlashRates[ NUM_COROUTINES ] = { 10, 20, 30, 40, 50, 60, 70, 80 };
const int iLEDToFlash[ NUM_COROUTINES ] = { 0, 1, 2, 3, 4, 5, 6, 7 }
void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
//协同例程必须从调用crSTART()开始。
crSTART( xHandle );
for( ;; )
{
//指定时间延迟。uxIndex用于索引到iFlashRates。
//因为每个协同程序都是由不同的索引值,每个会延迟不同的时间。
crDELAY( xHandle, iFlashRate[ uxIndex ] );
//闪烁LED。uxIndex再次用作数组索引,
//这次定位应该被切换的LED。
vParTestToggleLED( iLEDToFlash[ uxIndex ] );
}
//协例程必须以调用crEND()结束。
crEND();
}
十二、空闲任务:
在FreeRTOS中,空闲任务(Idle Task)是系统启动时自动创建的特殊任务,它具有最低的优先级。
(1)空闲任务的作用:
(2)空闲任务的特点:
vApplicationIdleHook()
的钩子函数,允许开发者在空闲任务的每次循环中执行自定义代码。(3)空闲任务钩子:
FreeRTOSConfig.h
中将configUSE_IDLE_HOOK
设置为1,以启用空闲任务钩子。vApplicationIdleHook(void)
函数,实现需要在空闲任务中执行的代码。(4)空闲任务钩子的使用场景:
(5)示例代码:
以下是如何实现和使用空闲任务钩子的示例:
// 在 FreeRTOSConfig.h 中启用空闲钩子
#define configUSE_IDLE_HOOK 1
// 空闲任务钩子函数
void vApplicationIdleHook( void )
{
// 将CPU设置为低功耗模式
// 例如,使用特定的微控制器指令或库函数
EnterLowPowerMode();
}
在这个示例中,EnterLowPowerMode()
是一个假设的函数,用于将微控制器CPU设置为低功耗模式。实际的实现将取决于特定的硬件平台和其提供的低功耗管理功能。
(6)注意事项:
vTaskDelay()
。(7)总结:
FreeRTOS的空闲任务是系统不可或缺的一部分,它在没有其他任务运行时执行,并提供了一个钩子函数,允许开发者执行自定义的操作。通过合理利用空闲任务钩子,可以提高系统的效率和性能,特别是在需要节能的应用场景中。
十三、线程本地存储指针:
在FreeRTOS中,线程本地存储(Thread Local Storage,TLS)是一种机制,允许每个任务拥有自己的数据副本,这对于管理任务特定的数据非常有用。
(1)线程本地存储指针:
FreeRTOS为每个任务提供了一个线程本地存储指针数组,可以通过configNUM_THREAD_LOCAL_STORAGE_POINTERS
在FreeRTOSConfig.h
中配置数组的大小。这些指针可以用于存储任务特定的数据。
设置和获取线程本地存储指针:
vTaskSetThreadLocalStoragePointer()
:用于设置数组中特定索引的值。pvTaskGetThreadLocalStoragePointer()
:用于获取数组中特定索引的值。(2)线程本地整数:
如果需要存储小于或等于void*
大小的值,可以直接存储在线程本地存储指针数组中。例如,如果void*
是32位的,可以直接存储32位的整数。
示例代码:
uint32_t ulVariable;
// 将32位值存储在调用任务的线程本地存储数组的索引1中
vTaskSetThreadLocalStoragePointer(NULL, 1, (void *)0x12345678);
// 将32位变量ulVariable的值存储在调用任务的线程本地存储数组的索引0中
ulVariable = ERROR_CODE;
vTaskSetThreadLocalStoragePointer(NULL, 0, (void *)ulVariable);
// 从调用任务的线程本地存储数组的索引5中读取值到ulVariable
ulVariable = (uint32_t)pvTaskGetThreadLocalStoragePointer(NULL, 5);
(3)线程本地结构体:
线程本地存储指针数组也可以用来存储指向结构体的指针,这些结构体可以存储更复杂的数据。
示例代码:
typedef struct
{
uint32_t ulValue1;
uint32_t ulValue2;
} xExampleStruct;
xExampleStruct *pxStruct;
// 为任务创建一个结构体
pxStruct = pvPortMalloc(sizeof(xExampleStruct));
// 设置结构体成员
pxStruct->ulValue1 = 0;
pxStruct->ulValue2 = 1;
// 将结构体的指针存储在调用任务的线程本地存储数组的索引0中
vTaskSetThreadLocalStoragePointer(NULL, 0, (void *)pxStruct);
// 从调用任务的线程本地存储数组的索引0中读取结构体的位置
pxStruct = (xExampleStruct *)pvTaskGetThreadLocalStoragePointer(NULL, 0);
(4)注意事项:
NULL
作为任务句柄调用vTaskSetThreadLocalStoragePointer()
和pvTaskGetThreadLocalStoragePointer()
时,这些函数将操作调用它们的任务的线程本地存储。pvPortMalloc()
为结构体分配内存时,确保内存在任务间是独立的,以防止数据冲突。(5)总结:
FreeRTOS的线程本地存储提供了一种灵活的机制,允许任务存储和管理自己的数据。通过合理使用线程本地存储指针,可以避免任务间的相互干扰,确保数据的隔离性和安全性。
十四、以STM32F103C8T6为例的实际任务和协程操作:
(1)通过任务调度,闪烁一个LED:
#include "stm32f10x.h"
#include "Delay.h"
#include "FreeRTOS.h" // 包含FreeRTOS实时操作系统的头文件,用于多任务管理。
#include "task.h" // 包含任务相关函数的头文件,用于任务创建和管理。
TaskHandle_t myTaskHandler; // 定义一个任务句柄变量,用于跟踪任务。
// 定义任务函数,用于控制LED的闪烁。
void myTask1(void *arg)
{
while(1) // 无限循环,任务会一直运行。
{
GPIO_ResetBits(GPIOC,GPIO_Pin_13); // 将GPIOC的第13脚位设置为低,通常用于熄灭LED。
vTaskDelay(500); // 任务延迟500个时钟节拍,大约半秒(具体时间取决于时钟配置)。
GPIO_SetBits(GPIOC,GPIO_Pin_13); // 将GPIOC的第13脚位设置为高,通常用于点亮LED。
vTaskDelay(500); // 再次延迟500个时钟节拍。
}
}
// 主函数
int main(void)
{
// 启动GPIOC端口的时钟,以便能够使用该端口的引脚。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
// 定义GPIO初始化结构体变量。
GPIO_InitTypeDef GPIO_InitStructure;
// 设置GPIO模式为推挽输出。
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
// 设置要初始化的GPIO引脚为第13脚。
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;
// 设置GPIO速度为50MHz。
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
// 根据上面定义的参数初始化GPIOC的第13脚。
GPIO_Init(GPIOC,&GPIO_InitStructure);
// 创建名为"led1"的任务,栈大小为64字节,优先级为2,任务函数为myTask1,不传递参数给任务。
xTaskCreate(myTask1, "led1", 64, NULL, 2, &myTaskHandler);
// 启动任务调度器,这是FreeRTOS开始执行任务的地方。
vTaskStartScheduler();
while(1) // 通常,主循环中不需要任何代码,因为任务调度器接管了CPU。
{
}
}
通过任务调度,闪烁一个LED
(2)通过协程调度,闪烁一个LED:
#include "stm32f10x.h"
#include "FreeRTOS.h" // 包含FreeRTOS实时操作系统的头文件,用于多任务管理。
#include "task.h" // 包含任务相关函数的头文件,用于任务创建和管理。
#include "croutine.h" // 包含协程相关函数的头文件,用于协程创建和管理。
#define PRIORITY_0 0 // 定义优先级0,通常用于最低优先级的任务或协程。
// 协程函数原型
void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex );
// 空闲任务钩子函数,用于调度协程
void vApplicationIdleHook( void )
{
vCoRoutineSchedule(); // 调用协程调度函数,以确保协程得到执行。
}
// 协程函数,用于控制LED闪烁
void vFlashCoRoutine( CoRoutineHandle_t xHandle, UBaseType_t uxIndex )
{
// 协程必须从调用crSTART()开始。
crSTART( xHandle );
for( ;; ) // 无限循环,协程会一直运行,直到手动停止或复位。
{
// 点亮LED
GPIO_SetBits(GPIOC, GPIO_Pin_13); // 将GPIOC的第13脚位设置为高,通常用于点亮LED。
crDELAY( xHandle, 500 ); // 延迟500个时钟节拍,大约半秒(具体时间取决于时钟配置)。
// 熄灭LED
GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 将GPIOC的第13脚位设置为低,通常用于熄灭LED。
crDELAY( xHandle, 500 ); // 再次延迟500个时钟节拍。
}
// 协程必须以调用crEND()结束。
crEND();
}
int main(void)
{
// 启动GPIOC端口的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能GPIOC端口的时钟。
// 定义GPIO初始化结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置为推挽输出模式。
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; // 初始化第13脚。
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置GPIO速度为50MHz。
GPIO_Init(GPIOC, &GPIO_InitStructure); // 根据初始化结构体配置GPIOC的第13脚。
// 启动协程
xCoRoutineCreate( vFlashCoRoutine, PRIORITY_0, 0 ); // 创建协程,传递协程函数、优先级和索引。
// 启动任务调度器
vTaskStartScheduler(); // 启动FreeRTOS的任务调度器。
// 如果调度器启动成功,以下代码不会执行
for( ;; ); // 无限循环,防止main函数返回。
}
在实际应用协程调度时,闪烁一个LED时,报错:
.\Objects\Project.axf: Error: L6218E: Undefined symbol vCoRoutineAddToDelayedList (referred from main.o).
.\Objects\Project.axf: Error: L6218E: Undefined symbol vCoRoutineSchedule (referred from main.o).
.\Objects\Project.axf: Error: L6218E: Undefined symbol xCoRoutineCreate (referred from main.o).
Not enough information to list image symbols.
Not enough information to list load addresses in the image map.
Finished: 2 information, 0 warning and 3 error messages.
".\Objects\Project.axf" - 3 Error(s), 0 Warning(s).
Target not created.
解决办法:
FreeRTOS 协程相关的源文件没有包含在项目中:确保项目包含了实现这些函数的 FreeRTOS 协程相关的源文件。通常这些文件包括 croutine.c
。
FreeRTOS 配置问题:检查FreeRTOSConfig.h
文件,确保已经定义了 configUSE_CO_ROUTINES
为 1,以启用协程的支持。如果没有定义或者定义为0,协程相关的函数将不会被包含在编译中。
个人看法:
在较新版本的FreeRTOS中,协程(co-routines)的支持可能已经被移除或不再推荐使用。这是因为协程在现代实时操作系统中并不常用,且其实现可能不如任务(task)调度那样高效和灵活。如果正在使用的FreeRTOS版本中找不到关于协程的相关函数定义,这可能是因为协程的支持已经不再包含在该版本中。
在编写基于FreeRTOS的应用程序时,推荐使用任务作为主要的并发执行单元,并利用FreeRTOS提供的任务调度、同步和通信机制来实现应用程序逻辑。如果您需要类似协程的行为,可能需要通过手动管理任务的执行和挂起状态来模拟,但这通常比使用真正的协程要复杂得多。
如果项目确实需要使用协程,并且使用的FreeRTOS版本还支持协程,那么您需要确保在FreeRTOSConfig.h
中定义了configUSE_CO_ROUTINES
为1,以启用协程的相关代码。同时,确保项目包含了实现协程功能的FreeRTOS源文件,如croutine.c
。
实践发现,在FreeRTOSConfig.h中添加:
#define configUSE_IDLE_HOOK 1
#define configUSE_CO_ROUTINES 1
#define configMAX_CO_ROUTINE_PRIORITIES 10
即可解决该报错问题。
十五、FreeRTOS 协程补充说明:
FreeRTOS 协程是 FreeRTOS 实时操作系统中的一个轻量级任务机制,它们比传统的任务更简单,通常用于实现有限状态机或简单的通信协议。协程在 FreeRTOS 中通常用于实现简单的、协作式的、非抢占式的任务。
以下是关于 FreeRTOS 协程的一些关键点:
协程与任务的区别:
配置:
FreeRTOSConfig.h
文件中定义 configUSE_CO_ROUTINES
为 1。configUSE_IDLE_HOOK
为 1。示例文件:
crflash.c
是一个示例文件,它使用协程来控制 LED 的闪烁,而不是使用任务。crhook.c
演示了如何将数据从中断传递到协程。替换任务为协程:
croutine.c
文件,并在 main.c
中包含 croutine.h
头文件。vStartLEDFlashTasks()
)替换为创建协程的函数调用(如 vStartFlashCoRoutines(n)
)。调度协程:
vCoRoutineSchedule()
来调度协程。空闲钩子函数:
main.c
中添加或修改空闲钩子函数,以便在系统空闲时调度协程:
void vApplicationIdleHook( void )
{
vCoRoutineSchedule( void );
}
内存优化:
FreeRTOSConfig.h
中的 portTOTAL_HEAP_SPACE
定义来优化内存使用。注意事项:
十六、FreeRTOS移植所需文件:
通过网盘分享的文件:FreeRTOSv202212.01.zip
链接: https://pan.baidu.com/s/1I5QGQsoFgaGMDOVduHaaGA?pwd=38u9 提取码: 38u9
作者:The_xz