Bootstrap

FreeRTOS的资源管理

在多任务系统中,如果一个任务开始访问资源,但在从运行状态转换出来之前没有完成访问,则可能会出现错误。如果任务使资源处于不一致的状态,那么任何其他任务或中断对同一资源的访问都可能导致数据损坏或其他类似问题。
以下是一些示例:
1.访问外围设备
考虑以下场景,其中两个任务试图写入液晶显示器(LCD)。
1.任务A执行并开始将字符串“Hello world”写入LCD。
2.任务A在只输出字符串的开头“Hello w”后被任务B抢先执行。
3.任务B在进入阻止状态之前向LCD写入“中止、重试、失败?”。
4.任务A从它被抢占的点开始继续,并完成输出其字符串的剩余字符——“world”。
LCD现在显示损坏的字符串“Hello wAbort,Retry,Fail?world”。

2.读取、修改、写入操作
清单111显示了一行C代码,以及C代码通常如何转换为汇编代码的示例。可以看出,PORTA的值首先从内存读取到寄存器中,在寄存器内修改,然后写回内存。这被称为读取、修改、写入操作。

在这里插入图片描述

这是一个“非原子”操作,因为它需要多条指令才能完成,并且可以被中断。考虑以下场景,其中两个任务尝试更新名为PORTA的内存映射寄存器。
1.任务A将PORTA的值加载到寄存器中——操作的读取部分。
2.任务A在完成同一操作的修改和写入部分之前被任务B抢占。
3.任务B更新PORTA的值,然后进入阻塞状态。
4.任务A从被抢先执行的点开始继续执行。在将更新后的值写回PORTA之前,它会修改已保存在寄存器中的PORTA值的副本。

在这种情况下,任务A更新并写回PORTA的过期值。在任务A获取PORTA值的副本之后,在任务A将其修改后的值写回PORTA寄存器之前,任务B修改了PORTA。当任务A写入PORTA时,它会覆盖任务B已经执行的修改,从而有效地损坏PORTA寄存器值。

此示例使用外围寄存器,但在对变量执行读取、修改、写入操作时,同样的原理也适用。

3.对变量的非原子访问
更新结构的多个成员,或更新大于架构自然字长的变量(例如,在16位机器上更新32位变量),都是非原子操作的示例。如果它们被中断,可能会导致数据丢失或损坏。

4 函数可重入
每个任务都维护自己的堆栈和自己的一组处理器(硬件)寄存器值。
如果一个函数不访问堆栈上存储或寄存器中保存的数据以外的任何数据,则该函数是可重入的,并且是线程安全的。清单112是一个可重入函数的示例。清单113是一个不可重入的函数示例。

在这里插入图片描述
在这里插入图片描述

互斥

为了确保始终保持数据一致性,必须使用“互斥”技术来管理对任务之间或任务和中断之间共享的资源的访问。目标是确保,一旦任务开始访问不可重入且不线程安全的共享资源,同一任务就可以独占访问该资源,直到资源恢复到一致状态。

FreeRTOS提供了几个可用于实现互斥的功能,但最好的互斥方法是(尽可能,因为这通常是不切实际的)以不共享资源的方式设计应用程序,并且每个资源只能从单个任务访问。

关键部分和暂停调度

基本关键部分
基本临界区是分别被对宏taskENTER_critical()和taskEXIT_CRITINAL()的调用包围的代码区域。临界部分也称为临界区。
taskENTER_CRITICAL()和taskEXIT_CRITICAL)不接受任何参数,也不返回值。清单114展示了它们的用法。

在这里插入图片描述示例项目使用一个名为vPrintString()的函数将字符串写入标准输出,即使用FreeRTOS Windows端口时的终端窗口。vPrintString()是从许多不同的任务中调用的;因此,从理论上讲,它的实现可以使用关键部分来保护对标准输出的访问,如清单115所示。
在这里插入图片描述

以这种方式实现的关键部分是一种非常粗糙的互斥方法。它们的工作原理是完全禁用中断,或者根据所使用的FreeRTOS端口,禁用由configMAX_SYSCALLINTERRUPT_priority设置的中断优先级。抢占的上下文切换只能在中断内发生,因此,只要中断保持禁用状态,调用taskENTER_CRITICAL()的任务就保证保持在运行状态,直到退出关键部分。

基本关键部分必须保持很短,否则会对中断响应时间产生不利影响。每次调用taskENTER_CRITICAL()都必须与调用taskEXIT_CRITINAL()紧密配对。因此,标准输出(stdout,或计算机写入其输出数据的流)不应使用关键部分进行保护(如清单115所示),因为写入终端可能是一个相对较长的操作。本章中的示例探讨了替代解决方案。

关键部分嵌套是安全的,因为内核会记录嵌套深度。只有当嵌套深度返回零时,临界部分才会退出——也就是说,对于之前对taskENTER_critical()的每次调用,都执行了一次对taskEXIT_CRITINAL()的调用。

调用taskENTER_CRITICAL()和taskEXIT_CRITICAL是任务更改运行FreeRTOS的处理器的中断启用状态的唯一合法方法。
通过任何其他方式更改中断启用状态将使宏的嵌套计数无效。

taskENTER_CRITICAL()和taskEXIT_CRITICAL不以“FromISR”结尾,因此不能从中断服务例程调用。taskENTER_CRITICAL_FROM_ISR()是taskENTER_CRITICAL()的中断安全版本,而taskEXIT_CRITICAL.FROM_ISR()是task EXIT_CRITICAL(。中断安全版本仅提供给允许嵌套中断的FreeRTOS端口——在不允许嵌套中断端口中,它们将被淘汰。

taskENTER_CRITICAL_FROM_ISR()返回一个值,该值必须传递给对taskEXIT_CRITICAL.FROM_ISR()的匹配调用。如清单116所示。

在这里插入图片描述与执行实际受关键部分保护的代码相比,使用更多的处理时间来执行进入并随后退出关键部分的代码是浪费的。基本关键部分进入非常快,退出非常快,并且总是确定性的,这使得它们在受保护的代码区域非常短的情况下非常理想。

暂停(或锁定)调度程序
也可以通过暂停调度程序来创建关键部分。暂停调度程序有时也称为“锁定”调度程序。
基本关键部分保护代码区域免受其他任务和中断的访问。通过暂停调度程序实现的关键部分仅保护一个代码区域免受其他任务的访问,因为中断仍然处于启用状态。

一个太长而无法通过简单地禁用中断来实现的关键部分,可以通过挂起调度器来实现。然而,在调度器挂起时的中断活动可能会使恢复(或“取消挂起”)调度器成为一项相对较长的操作,因此必须考虑在每种情况下使用哪种最佳方法。

The vTaskSuspendAll() API 函数
void vTaskSuspendAll( void );

通过调用vTaskSuspendAll()暂停调度程序。挂起调度程序可以防止发生上下文切换,但会启用中断。如果中断在调度器挂起时请求上下文切换,则该请求将保持挂起状态,并且仅在调度器恢复(未挂起)时执行。

在计划程序挂起时,不得调用FreeRTOS API函数。

The xTaskResumeAll() API 函数
BaseType_t xTaskResumeAll( void );

通过调用xTaskResumeAll()来恢复(取消挂起)调度程序。

返回值
在调度器挂起时请求的上下文切换处于挂起状态,仅在调度器恢复时执行。如果在xTaskResumeAll()返回之前执行了挂起的上下文切换,则返回pdTRUE。否则返回pdFALSE。

对vTaskSuspendAll()和xTaskResumeAll()的调用嵌套是安全的,因为内核会记录嵌套深度。只有当嵌套深度返回零时,调度程序才会恢复,也就是说,每次调用vTaskSuspendAll()都会执行一次xTaskResumeAll()调用。

以下显示了vPrintString()的实际实现,它挂起调度器以保护对终端输出的访问。

void vPrintString( const char *pcString )
{
/* Write the string to stdout, suspending the scheduler as a method of mutual
exclusion. */
vTaskSuspendScheduler();
{
printf( "%s", pcString );
fflush( stdout );
}
xTaskResumeScheduler();
}
;