不知道大家有没有关注过Cortex-M内核的一些内容,在STM32大部分型号中都有MPU。
MPU是Cortex-M的选配件,拿STM32F1来说,STM32F10X_XL系列的芯片才具有这个MPU存储保护单元,而其他STM32F1芯片没有。
LPC17xx包括存储器保护单元(MPU)。这允许整个内存映射(包括Flash、RAM和外围设备)细分为若干区域,以及要分别分配给每个区域的访问权限。
区域是地址范围由起始地址和大小组成。
FreeRTOS MPU是一个FreeRTOS Cortex-M3端口,包括集成MPU支持。它允许额外的功能,并包括一个稍微扩展的API,但在其他方面与标准Cortex-M3端口兼容。
使用FreeRTOS MPU将始终:
保护内核免受任务的无效执行。
保护内核使用的数据不受任务的无效访问。
保护Cortex-M3核心资源的配置,如SysTick定时器。
确保所有任务堆栈溢出一发生就被检测到。
另外,在应用程序级别,可以确保任务独立于自己的任务并使内存空间和外围设备受到保护,不受意外修改。
FreeRTOS MPU通过隐藏MPU底层计算层面而提供一个简单的针对MPU的接口,然而给一个不允许数据随意访问的环境进行应用编程是一个很大的挑战
本章旨在让读者更好地理解:
微处理器硬件对如何定义内存区域的限制。
分配给每个内存区域的访问权限。
用户模式任务和特权模式任务之间的区别。
FreeRTOS MPU特有的API。
用户模式和特权模式
Cortex-M3可以在特权模式或用户(非特权)模式下执行代码。这个标准的FreeRTOS Cortex-M3端口以特权模式执行所有任务。FreeRTOS MPU可以在特权模式或用户模式下执行任务。处理器在执行中断服务程序之前自动进入特权模式。内核每当调用FreeRTOS MPU API函数时,总是切换到特权模式,当API函数完成时返回到以前的模式。
以特权模式执行的任务不会被阻止访问Cortex-M3核心或执行Cortex-M3指令的任何部分。MPU区域访问权限可用于阻止特权模式任务生成特定内存,例如,访问配置为只读的区域。
以用户模式执行的任务无法访问某些Cortex-M3资源执行某些Cortex-M3指令。例如,用户模式任务不能访问中断控制器或执行CPS(更改处理器状态)指令,MPU可以配置区域以防止用户模式访问,同时仍然允许特权模式。
访问权限属性
下表列出了FreeRTOS MPU中可用的访问权限相关定义。本章后面将有其使用案例:
重叠区域
区域是可以被应用申请访问权限访问的地址范围。最大一次可以定义八个区域。区域编号从0到7。
如果多个区域定义了重叠的内存范围,则将应用重叠区域编号中的最高值。
例如,如果区域2配置为读取权限,区域3仅仅配置为读权限,则内存区域将为配置为只读访问。
预定义区域和任务定义区域
内核使用0到4个区域预先配置可用的运行时环境
a.运行状态任务可以访问自己的堆栈,仅当LPC17xx以特权模式运行时,可以访问其他的RAM
b.内核所在的闪存区域和系统外围设备,仅当LPC17xx以特权模式运行时才可访问。
c.除内核所在的闪存外,所有非系统闪存外设(例如,uart和模拟输入)可以通过特权和用户模式任务两种方式访问。
内核在进行切换时,重新配置MPU,因此剩余的3个区域可以被每个任务进行不同的定义,使用最高的区域号码进行任务区域的定义。
MPU硬件规定了两个规则,区域起始地址和大小定义必须符合:
a.区域大小必须是介于32字节和64 GB之间的二进制幂,例如,32字节、64字节、128字节、256字节等等有效区域大小。
2。起始地址必须是区域大小的倍数。例如,一个区域被配置为65536字节长的地址必须可以被完整的整除。
大多数交叉编译器都包含语言扩展,可以用来强制变量放置在指定的地址对齐方式上。下面清单显示了用于此目的的语法
GCC、IAR和Keil编译器。
/* GCC语法定义并且对齐一个区域. */
char cAnArray[ 1024 ] __attribute__((aligned(1024)));
/* IAR语法定义并且对齐一个区域 */
#pragma data_alignment=1024
char cAnArray[ 1024 ];
/* 使用Keil语法定义并对齐数组。注意,这只适用于全局
变量。Keil还有一个GCC兼容模式,其中可以使用属性。
*/
__align( 1024 ) char cAnArray[ 1024 ];
/* 定义两个数组, 每个数组将由单独的MPU控制区域(GCC语法)*/
char cFirstArray[ 1024 ] __attribute__((aligned(1024)));
char cSecondArray[ 256 ] __attribute__((aligned(256)))
还需要考虑变量之间的相互关系。例如,cFirstArray以1024字节开始和结束边界。
cSecondArray以256字节边界开始和结束。因为1024可以被256整除,链接器很可能将cSecondArray直接放在cFirstArray之后并与其相邻。
如果任务已将一个MPU区域配置为提供对cFirstArray的写访问,则MPU区域提供对第二个阵列的写访问,则MPU不会阻止一个cFirstArray的结束。
标准FreeRTOS Cortex-M3端口中提供的所有API功能也可在FreeRTOS MPU。本节重点介绍xTaskCreate()的一些细微差别使用并介绍了特定于启用MPU的内核的API扩展。
xTaskCreateRestricted()API函数
xTaskCreateRestricted()是xTaskCreate()的扩展版本,用于创建任务具有受限的执行权限和受限的内存访问权限。
xTaskCreateRestricted()需要xTaskCreate()使用的所有参数,以及定义三个特定于任务的MPU区域和堆栈缓冲区的四个附加参数。
尝试在正常函数参数列表中使用此数量的参数将变得有些数量化,并且可能会大量使用堆栈空间。相反,freertompu定义了一个名为xTaskParameters的结构,该结构包含每个必需参数的成员,xTaskParameters结构可以声明为const,因此保持在Flash中。
xTaskCreateRestricted()将指向xTaskParameters结构的指针作为其两个参数之一。第二个参数用于传递正在创建的任务的句柄-
与同名的xTaskCreate()参数完全相同。如果不需要任务的句柄,则pxCreatedTask可以设置为空。
portBASE_TYPE xTaskCreateRestricted( xTaskParameters *pxTaskDefinition,xTaskHandle *pxCreatedTask );
清单包含xTaskParameters结构定义,以及xTaskParameters包含的xMemoryRegion结构定义。结构构件见下表。并显示了如何使用这些结构。
/*
定义一个单独的MPU区域
*/
typedef struct xMEMORY_REGION
{
void *pvBaseAddress;
unsigned long ulLengthInBytes;
unsigned long ulParameters;
} xMemoryRegion;
/*
*包含创建受限任务所需的每个参数的成员。
*/
typedef struct xTASK_PARAMTERS
{
pdTASK_CODE pvTaskCode;
const signed char * const pcName;
unsigned short usStackDepth;
void *pvParameters;
unsigned portBASE_TYPE uxPriority;
portSTACK_TYPE *puxStackBuffer;
xMemoryRegion xRegions[ portNUM_CONFIGURABLE_REGIONS ];
} xTaskParameters;
下表显示了配置为定义用户的xTaskParameters结构的示例模式任务。将uxPriority值从1更改为(1 | portPRIVILEGE_位)将导致
定义特权模式任务的结构。
/* 将创建一个需要对数组进行只读访问的用户任务。首先定义数组以符合大小和对齐规则。本例使用GCC语法。 */
char cArray[ 128 ] __attribute__((aligned(128)));
/*接下来定义xTaskParameters结构,该结构包括一个MPU定义,为任务提供所需的数组访问。三个可能的MPU区域中只有一个正在使用,但这三个区域都必须被定义。 */
static const xTaskParameters xCheckTaskParameters =
{
vDemoTask, /* pvTaskCode-实现任务的函数。 */
"Demo", /* pcName */
400, /* usStackDepth-以word而不是字节定义。 */
NULL, /* pvParameters-在本例中不使用。 */
1, /* uxPriority-用户模式优先级1。*/
cTaskStack, /* uxStackBuffer-用作任务堆栈的数组。*/
/*xRegions—在本例中,xRegions数组用于创建单个MPU
区域,只提供对一个数组的只读访问。的参数
这两个未使用的区域仅设置为0,以防止它们产生任何效果。 */
{
/* 基址长度参数 */
{ cArray, 128, portMPU_REGION_READ_ONLY },
{ 0, 0, 0 },
{ 0, 0, 0 }
}
};
上表显示了一个简单的例子,其中MPU被用来控制对单个变量(在本例中是数组)的访问,但是同样的技术可以通过将变量分组到单个结构中来控制对一组变量的访问。如果这不可行,则可以使用编译器扩展将变量手动放置到链接器脚本中定义的大小正确且对齐的内存区域或节中。
xTaskCreate()可用于创建用户模式和特权模式任务,但不能用于在任务创建时为其分配MPU区域。相反,特权模式任务将访问整个内存映射,而用户模式任务将访问任何未配置为仅允许特权访问的闪存和RAM内存。
与xTaskCreateRestricted()一样,将uxPriority设置为所需的任务优先级以创建用户模式任务,或使用portPRIVILEGE位按位或所需的任务优先级来创建特权模式任务。下清单演示了这一点。
int main( void )
{
/* 使用xTaskCreate()创建用户模式任务。 */
xTaskCreate
(
vOldStyleUserModeTask, /* 实现任务的函数。 */
"Task1", /* 任务的文本名称。 */
100, /* 单词的栈深度。 */
NULL, /* 任务参数 */
3, /* 优先级和模式(本例中为用户)。*/
NULL /* 句柄 */
);
/* 使用xTaskCreate()创建特权模式任务。注意使用指定任务优先级的端口特权位。 */
xTaskCreate
(
vOldStylePrivilegedModeTask, /* 实现任务的函数。 */
( signed char * ) "Task2", /* 任务的文本名称。 */
100, /* 单词的栈深度。 */
NULL, /* 任务参数 */
( 3 | portPRIVILEGE_BIT ), /* P优先级和模式(在这种情况下是特权的)。*/
NULL /* Handle. */
);
/* 开始调度器 */
vTaskStartScheduler();
/* 如果一切正常,那么main()将不会像调度程序那样到达这里
现在开始执行任务。如果main()确实到达这里,那么很可能
堆内存不足,无法创建空闲任务 */
for( ;; );
}
创建任务时,最多可以为任务分配三个MPU区域定义。然后可以使用vTaskAllocateMPURegions()API函数重新定义区域。
void vTaskAllocateMPURegions( xTaskHandle xTask, const xMemoryRegion * const pxRegions );
void vAFunction( xTaskHandle xTask )
{
/* 定义xMemoryRegion数组,该数组定义从地址0到只读,地址0x10004000的2K块只能从特权模式访问。数组仅定义三个可能的MPU区域中的两个,
但必须包含所有三个条目。未使用项的成员仅设置为零,因此没有任何效果。 */
static const xMemoryRegion xRegions[ 3 ] =
{
/* 基址长度访问参数 */
{ 0x00, 8096, portMPU_REGION_READ_ONLY },
{ 0x10004000, 2048, portMPU_REGION_PRIVILEGED_READ_WRITE },
{ 0, 0, 0 } /* 第三个条目未使用,因此设置为零。 */
}
/* 将xTask引用的任务的MPU区域更改为X地区。 */
vTaskAllocateMPURegions( xTask, xRegions );
/* 还可以将此任务使用的MPU区域更改为由xRegions定义的区域。*/
vTaskAllocateMPURegions( NULL, xRegions );
}
portSWITCH_TO_USER_MODE()API宏
特权模式任务可以调用portSWITCH_TO_USER_mode(),以降低自己对用户模式的权限。用户模式任务无法将其权限提升到特权模式。
portSWITCH_TO_USER_MODE()不接受任何参数。
FreeRTOS MPU要求链接器脚本定义表中描述的两个命名部分和表3中描述的八个链接器变量。
用于定义所需节和变量的语法取决于所使用的工具链。清单提供了一个使用GNU LD语法的示例。LD是与GCC一起分发的链接器。生成合适的链接器脚本的最简单方法是从FreeRTOS MPU演示应用程序的预配置示例开始。
/* Given the memory map…. */
MEMORY
{
FLASH (rx) : ORIGIN = 0x0, LENGTH = 0x80000
SRAM (rwx) : ORIGIN = 0x10000000, LENGTH = 0x8000
AHBRAM0 : ORIGIN = 0x2007c000, LENGTH = 0x4000
AHBRAM1 : ORIGIN = 0x20080000, LENGTH = 0x4000
}
/* ….define the variables required by FreeRTOS-MPU. First ensure the section sizes
are a binary power of two to comply with the MPU region size rules. */
_Privileged_Functions_Region_Size = 16K;
_Privileged_Data_Region_Size = 256;
/* Then define the variables themselves. */
__FLASH_segment_start__ = ORIGIN( FLASH );
__FLASH_segment_end__ = __FLASH_segment_start__ + LENGTH( FLASH );
__privileged_functions_start__ = ORIGIN( FLASH );
__privileged_functions_end__ = __privileged_functions_start__ +
_Privileged_Functions_Region_Size;
__SRAM_segment_start__ = ORIGIN( SRAM );
__SRAM_segment_end__ = __SRAM_segment_start__ + LENGTH( SRAM );
__privileged_data_start__ = ORIGIN( SRAM );
__privileged_data_end__ = ORIGIN( SRAM ) + _Privileged_Data_Region_Size;
/* Given the memory map…. */
MEMORY
{
FLASH (rx) : ORIGIN = 0x0, LENGTH = 0x80000
SRAM (rwx) : ORIGIN = 0x10000000, LENGTH = 0x8000
AHBRAM0 : ORIGIN = 0x2007c000, LENGTH = 0x4000
AHBRAM1 : ORIGIN = 0x20080000, LENGTH = 0x4000
}
/* ….define the variables required by FreeRTOS-MPU. First ensure the section sizes
are a binary power of two to comply with the MPU region size rules. */
_Privileged_Functions_Region_Size = 16K;
_Privileged_Data_Region_Size = 256;
/* Then define the variables themselves. */
__FLASH_segment_start__ = ORIGIN( FLASH );
__FLASH_segment_end__ = __FLASH_segment_start__ + LENGTH( FLASH );
__privileged_functions_start__ = ORIGIN( FLASH );
__privileged_functions_end__ = __privileged_functions_start__ +
_Privileged_Functions_Region_Size;
__SRAM_segment_start__ = ORIGIN( SRAM );
__SRAM_segment_end__ = __SRAM_segment_start__ + LENGTH( SRAM );
__privileged_data_start__ = ORIGIN( SRAM );
__privileged_data_end__ = ORIGIN( SRAM ) + _Privileged_Data_Region_Size;
从用户模式任务访问数据
用户模式任务无法访问超出其自身堆栈空间的RAM,除非该地址在任务的MPU区域定义范围内。例如,如果用户模式任务需要全局声明的队列句柄的值,则要访问该值,必须首先将该值复制到任务堆栈上的变量中。实现这一目标有几种方法,包括:
1.在特权模式下创建任务,然后将全局变量值复制到堆栈变量中,然后将任务切换到所需的用户模式。清单98演示了这个方法。
2.使用task参数将全局变量的值传递到任务中。清单99演示了这个方法。
清单98:
/* 队列的句柄存储在全局(或文件范围)变量中。 */
xQueueHandle xGlobalQueue;
void vATask( void *pvParameters )
{
xQueueHandle xStackQueue;
/* 此任务是在特权模式下创建的,因此可以访问全局变量。当任务是仍然处于特权模式。 */
xStackQueue = xGlobalQueue;
/* 现在将任务设置为用户模式。从此任务就不能再访问全局变量的值,但可以访问其本地堆栈副本。 */
portSWITCH_TO_USER_MODE();
for( ;; )
{
/* 主要任务功能在用户模式下执行。可以发送数据使用xStackQueue作为句柄在队列中往返。 */
}
}
清单99
/* 队列的句柄存储在全局(或文件范围)变量中。 */
xQueueHandle xGlobalQueue;
void vATask( void *pvParameters )
{
xQueueHandle xStackQueue;
/* 此任务是在用户模式下创建的,因此无法访问全局变量。它可以访问存储在其自身堆栈和任务参数中的变量。xGlobalQueue的值使用task参数传递到此任务中,然后复制到本地堆栈变量中,强制转换为适当的类型。 */
xStackQueue = ( xQueueHandle ) pvParameters;
for( ;; )
{
/* 主要任务功能在这里完成。数据可以发送到使用xStackQueue作为句柄的队列。. */
}
以用户模式执行的代码无法访问其自身堆栈和为其配置的MPU区域之外的RAM。这不会阻止用户模式任务使用队列或信号量与其他任务或中断通信。
队列和信号量使用的RAM由内核拥有和控制,只有在处理器以特权模式执行时才能访问。调用诸如xQueueSend()之类的API函数会导致处理器暂时切换到特权模式,从中可以将排队的数据从用户模式任务复制到内核控制的队列存储区域。类似地,调用诸如xQueueReceive()之类的API函数会导致处理器暂时切换到特权模式,从中可以将接收到的数据从内核控制的队列存储区域复制到用户模式任务中。
FreeRTOS MPU包含在FreeRTOS的主下载中。一些评论颇多的FreeRTOS MPU演示应用程序位于子目录中,其名称以FreeRTOS\demo目录中的“Cortex MPU”开头。