一些嵌入式系统使用多任务操作系统或控制系统,并必须确保运行中的任务不会干扰其他任务的操作。系统资源和其他任务免受未经授权访问的保护称为保护,并是本章的主题。
控制对系统资源的访问有两种方法,即不受保护和受保护。不受保护的系统完全依赖软件来保护系统资源。受保护的系统依赖硬件和软件来保护系统资源。控制系统使用的方法选择取决于处理器的能力和控制系统的要求。
不受保护的嵌入式系统在操作过程中没有专门用于执行内存和外设设备使用规则的硬件。在这些系统中,每个任务在访问系统资源时都必须与所有其他任务合作,因为任何一个任务都可能破坏另一个任务的状态。当一个任务无视另一个任务的环境访问限制时,这种合作方案可能导致任务失败。
在不受保护的系统中可能发生任务失败的一个例子是读写串口寄存器进行通信。如果一个任务正在使用该端口,就没有办法阻止另一个任务使用同一个端口。通过系统调用进行协调以提供对端口的访问是成功使用端口的关键。绕过这些调用的任务的未经授权访问可以轻易地干扰通过端口进行的通信。对资源的不良使用可能是无意的,也可能是敌对的。
相比之下,受保护的系统具有专用的硬件来检查和限制对系统资源的访问。它可以强制执行资源所有权。任务必须按照操作环境定义的一组规则行事,并由硬件强制执行,该硬件为在硬件级别监控和控制资源的程序授予特殊权限。受保护的系统能够积极防止一个任务使用另一个任务的资源。与合作实现的软件例程相比,通过硬件主动监视系统提供更好的保护。
ARM提供了几种配备硬件保护系统资源的处理器,通过内存保护单元(MPU)或内存管理单元(MMU)来实现。本章将介绍一款配备MPU的处理器核心,它在多个软件指定的区域上提供硬件保护。下一章将介绍配备MMU的处理器核心,它提供了硬件保护并增加了虚拟内存功能。
在受保护的系统中,需要监控的两个主要资源类别是内存系统和外围设备。由于ARM外围设备通常是内存映射的,MPU使用相同的方法来保护这两种资源。
ARM MPU使用区域来管理系统保护。区域是与内存区域相关联的一组属性。处理器核心将这些属性保存在多个CP15寄存器中,并通过编号来标识每个区域,编号范围在0到7之间。
配置区域的内存边界使用两个属性:起始地址和长度,长度可以是4 KB到4 GB之间的任意二的幂。此外,操作系统为这些区域分配了额外的属性:访问权限以及缓存和写缓冲区策略。对于内存中的区域访问权限设置为读写、只读或禁止访问,并根据当前处理器模式(特权或用户)获得附加权限。区域还具有缓存写策略,用于控制缓存和写缓冲区属性。例如,一个区域可以设置为使用写穿策略访问内存,而另一个区域则作为非缓存和非缓冲区运行。
当处理器访问主存中的区域时,MPU将区域的访问权限属性与当前处理器模式进行比较,以确定采取的操作。如果请求满足区域访问条件,则允许核心读取或写入主存。然而,如果内存请求导致内存访问违规,MPU将生成中止信号。
中止信号被路由到处理器核心,在接收到中止信号后,处理器核心通过处理中止向量异常。然后,中止处理程序确定中止类型,是预取中止还是数据中止,并根据中止类型跳转到适当的服务例程。
要实现一个受保护的系统,控制系统在主存中定义了几个区域。区域可以一次创建并持续存在于嵌入式系统的整个生命周期中,也可以临时创建以满足特定操作的需求,然后移除。如何分配和创建区域是下一节的主题。
13.1 Protected Regions
目前有四个ARM核心包含MPU,它们是ARM740T、ARM940T、ARM946E-S和ARM1026EJ-S。ARM740T、ARM946E-S和ARM1026EJ-S每个核心都包含8个保护区域;ARM940T包含16个(见表13.1)。ARM740T、ARM946E-S和ARM1026EJ-S具有统一的指令和数据区域,使用相同的寄存器来定义数据区域和指令区域的大小和起始地址。在ARM946E-S和ARM1026EJ-S核心中,内存访问权限和缓存策略可以独立配置用于指令和数据访问;在ARM740T中,相同的访问权限和缓存策略被分配给了指令和数据内存。区域与核心是否具有冯·诺依曼体系结构或哈佛体系结构无关。每个区域由0到7之间的标识号引用。
由于ARM940T具有分开控制指令和数据内存的区域,核心可以为指令和数据区域维护不同的大小和起始地址。指令和数据区域的分离导致了这个具有缓存的核心中的8个额外区域。尽管ARM940T中的标识区域号仍然从0到7,但每个区域号都有一对区域,一个数据区域和一个指令区域。
有几个规则来管理区域:
1. 区域可以与其他区域重叠。
2. 区域被分配了独立于区域权限的优先级号码。
3. 当区域重叠时,具有最高优先级号码的区域属性优先于其他区域。优先级仅适用于重叠区域内的地址。
4. 区域的起始地址必须是其大小的倍数。
5. 区域的大小可以是4 KB到4 GB之间的任何二的幂次数,即以下任一值:4 KB、8 KB、16 KB、32 KB、64 KB,...,2 GB、4 GB。
6. 访问一个未定义区域范围的主存将导致中止。如果核心正在获取指令,则MPU生成预取中止;如果内存请求是针对数据的,则生成数据中止。
13.1.1 Overlapping Regions
当一个区域被分配的内存空间的一部分与另一个区域被分配的内存空间重叠时,就会出现重叠区域。重叠区域比非重叠区域在分配访问权限时提供了更大的灵活性。
例如,假设一个小型嵌入式系统具有256 KB的可用内存,起始地址为0x00000000,并且必须保护一个特权系统区域免受用户模式的读写。特权区域的代码、数据和堆栈占用32 KB的区域,从0x00000000开始,包括向量表。剩余的内存分配给用户空间。
使用重叠区域,系统使用两个区域,一个是256 KB的用户区域,另一个是32 KB的特权区域(参见图13.1)。特权区域1被赋予更高的编号,因为它的属性必须优先于用户区域0。
//创建重叠的区域
13.1.2 Background Regions
重叠区域提供的另一个有用功能是背景区域(background region)- 一个低优先级的区域,用于为一个大内存区域分配相同的属性。然后,其他优先级较高的区域被放置在这个背景区域上,以更改定义的背景区域的一个较小子集的属性。因此,较高优先级的区域正在更改背景区域的一个子集的属性。背景区域可以将几个休眠的内存区域屏蔽起来,防止未经授权的访问,同时背景区域的另一部分在受不同区域控制下活动。
例如,如果一个嵌入式系统定义了一个较大的特权背景区域,它可以在这个背景上放置一个较小的非特权区域。较小区域的位置可以在背景区域的不同区域移动,以显示不同的用户空间。当系统将较小的用户区域从一个位置移动到另一个位置时,先前被覆盖的区域将由背景区域保护。因此,用户区域充当窗口,允许访问特权背景的不同部分,但具有用户级别的属性(见图13.2)。
图13.2显示了一个简单的三任务保护方案。区域3定义了活动任务的保护属性,背景区域0在其他任务休眠时控制对它们的访问。当任务1正在运行时,背景区域保护任务2和任务3免受任务1的影响。当任务2正在运行时,任务1和任务3受到保护。最后,当任务3正在运行时,任务1和任务2受到保护。这种工作的原因是区域3的优先级高于区域0,即使区域0具有较高的特权。
在本章末尾的示例代码中,我们使用了一个背景区域来演示一个简单的多任务保护方案。
13.2 Initializing the MPU, Caches, and Write Buffer
为了初始化MPU(Memory Protection Unit,内存保护单元)、缓存和写缓冲区,控制系统必须在目标操作期间定义所需的保护区域。
至少,在启用保护单元之前,控制系统必须定义至少一个数据区域和一个指令区域。保护单元必须在启用缓存和写缓冲区之前或同时启用。
控制系统通过设置主要CP15寄存器C1、C2、C3、C5和C6来配置MPU。表13.2列出了控制MPU运行所需的主要寄存器。
寄存器C1是主要控制寄存器。
通过配置寄存器C2和C3,可以设置各个区域的缓存和写缓冲区属性。
寄存器C5控制区域访问权限。寄存器C6中有8个或16个辅助寄存器,用于定义每个区域的位置和大小。ARM740T、ARM940T、ARM946E-S和ARM1026EJ-S中还有其他配置寄存器,但它们的使用不涉及MPU的基本操作。有关协处理器15寄存器的用法,请参考第3.5.2节。
初始化MPU、缓存和写缓冲区需要以下步骤:
1. 使用CP15:C6定义指令和数据区域的大小和位置。
2. 使用CP15:C5为每个区域设置访问权限。
3. 使用CP15:C2(用于缓存)和CP15:C3(用于写缓冲区)设置每个区域的缓存和写缓冲区属性。
4. 使用CP15:C1启用缓存和MPU。
对于这些步骤,后面有一个章节描述了配置每个寄存器所需的协处理器15命令。还有示例代码显示了在初始化过程中完成该步骤的例程中使用的命令。
13.2.1 Defining Region Size and Location
为了定义每个区域的大小和地址范围,嵌入式系统需要写入其中一个八个辅助寄存器,即CP15:C6:C0:0到CP15:C6:C7:0。每个辅助寄存器编号对应于相应的区域编号标识符。
每个区域的起始地址必须对齐到它的大小的倍数的地址。例如,如果一个区域的大小为128 KB,则它可以从任何0x20000的倍数地址开始。区域的大小可以是从4 KB到4 GB的任意二的幂次。
第13.3图和第13.3表中显示了八个辅助寄存器CP15:C6:C0到CP15:C6:C7的位字段和格式。起始地址存储在顶部的位字段[31:20]中,并且必须是大小位字段[5:1]的倍数。E字段位[0]用于启用或禁用区域;也就是说,区域可以被定义和禁用,直到启用位被设置之前,其属性不会被执行。CP15:C6辅助寄存器中未使用的位应设置为零。
要定义区域的大小,可以使用公式size = 2^N+1或查找第13.4表中的值。要设置大小,在CP15:C6寄存器的大小位字段中放置指数值N。N的取值受硬件设计限制,可以是11到31之间的任何整数,表示4 KB到4 GB。二进制值提供了大小条目的精确位字段。确定了区域的大小后,区域的起始地址可以是从公式计算出的大小的任何整数值倍数,或者如果您愿意,可以从第13.4表中获取。区域的大小和起始地址由系统的内存映射和控制系统必须保护的区域决定。本章末尾的演示系统展示了如何在给定系统内存映射的情况下设置区域。
ARM740T、ARM946E-S和ARM1026EJ-S处理器每个都有八个区域。要设置区域的大小和位置,需要对CP15:C6:CX中的辅助寄存器进行写入。例如,设置区域3的起始地址为0x300000,大小为256 KB的指令语法是:
MOV r1, #0x300000;设置起始地址
ORR r1, r1, #0x11 << 1;将大小设置为256 KB
MCR p15, 0, r1, c6, c3, 0
核心寄存器r1中填充所需的位字段数据,然后使用MCR指令将其写入CP15辅助寄存器。
ARM940T有八个指令区域和八个数据区域。这些区域需要额外的opcode2修饰符来选择指令区域或数据区域。对于数据区域,opcode2为零;对于指令区域,opcode2为一。
例如,要读取数据和指令区域5的大小和位置,需要两个MRC指令,一个用于指令区域,一个用于数据区域。读取区域大小和起始位置的指令如下:
MRC p15, 0, r2, c6, c5, 0;r2 = base/size 数据区域5
MRC p15, 0, r3, c6, c5, 1;r3 = base/size 指令区域5
第一条指令将核心寄存器r2加载为数据区域5的大小和起始地址,第二条指令将核心寄存器r3加载为指令区域5的大小和起始地址。ARM940T是唯一具有单独指令和数据区域的处理器核心。
以下示例13.1代码显示了如何设置区域的起始地址、大小以及启用位。函数regionSet具有以下C原型:
void regionSet(unsigned region, unsigned address, unsigned sizeN, unsigned enable);
该函数具有四个无符号整数输入:要配置的区域,区域的起始地址,编码的区域大小sizeN,以及区域是否启用或禁用。在更改区域属性时,最好禁用该区域,并在更改完成后重新启用它。
为了使此函数适用于四个可用版本的MPU处理器,我们通过使用大小和起始地址信息来配置指令和数据区域,统一了ARM940T区域空间。为此,我们编写了一个名为SET_REGION的宏,其中包含ARM940T和其他核心的两个部分。这样可以使同一个函数支持四个MPU核心。
#if defined( __TARGET_CPU_ARM940T)
#define SET_REGION(REGION) \
/* set Data region base & size */ \
__asm{MCR p15, 0, c6f, c6, c ## REGION,0}\
/* set Instruction region base & size */ \
__asm{MCR p15, 0, c6f, c6, c ## REGION, 1 }
#endif
#if defined(__TARGET_CPU_ARM946E_S) | \
defined(__TARGET_CPU_ARM1026EJ_S)
#define SET_REGION(REGION_NUMBER) \
/* set region base & size */ \
__asm{MCR p15, 0, c6f, c6, c ## REGION_NUMBER, 0 }
#endif
void regionSet(unsigned region, unsigned address,
unsigned sizeN, unsigned enable)
{
unsigned int c6f;
c6f = enable | (sizeN << 1) | address;
switch (region)
{
case 0: { SET_REGION(0); break;}
case 1: { SET_REGION(1); break;}
case 2: { SET_REGION(2); break;}
case 3: { SET_REGION(3); break;}
case 4: { SET_REGION(4); break;}
case 5: { SET_REGION(5); break;}
case 6: { SET_REGION(6); break;}
case 7: { SET_REGION(7); break;}
default: { break; }
}
}
代码首先将起始地址、大小sizeN和启用状态合并到名为c6f的无符号整数中。然后,函数根据宏SET_REGION创建的八个regionSet例程之一进行分支,通过写入对应的CP15:c6辅助寄存器,设置区域的起始地址、大小和启用状态。
13.2.2 Access Permission
有两组访问权限方案可供选择,一个是标准集,另一个是扩展集。所有四个核心都支持标准集,它提供了四个级别的权限。更新的ARM946E-S和ARM1026EJ-S支持扩展集,新增了两个级别的权限(参见表13.5)。扩展集的AP(访问权限)位域编码支持12个额外的权限值。到目前为止,只有其中两个位被分配使用。使用未定义的编码将导致不可预测的行为。
为了给一个区域分配访问权限,需要向CP15:c5的一个辅助寄存器进行写入。CP15:c5:c0:0和CP15:c5:c0:1配置标准AP,而CP15:c5:c0:2或CP15:c5:c0:3配置扩展AP。表13.6和图13.4显示了AP寄存器的权限位分配。
支持扩展权限的处理器也可以运行为标准权限编写的软件。实际使用的权限类型取决于对CP15 AP寄存器的最后一次写入:如果最后一次写入的是标准AP寄存器,则核心使用标准权限;如果最后一次写入的是扩展AP寄存器,则核心使用扩展权限。这是因为对标准AP寄存器的写入也会更新扩展AP寄存器,意味着扩展AP区域条目的高位[2:3]将被清除。
当使用标准AP时,每个区域在寄存器CP15:c5:c0:0和CP15:c5:c0:1中都有两位。CP15:c5:c0:0设置数据区域的AP,CP15:c5:c0:1设置指令区域的AP。
要读取指令和数据内存的标准AP,需要读取两个寄存器。以下两条MRC指令序列将数据区域内存的AP信息放置在核心寄存器r1中,将指令区域的AP信息放置在寄存器r2中:
MRC p15, 0, r1, c5, c0, 0 ; 标准AP数据区域
MRC p15, 0, r2, c5, c0, 1 ; 标准AP指令区域
当使用扩展AP时,每个区域在寄存器CP15:c5:c0:2和CP15:c5:c0:3中使用四位。核心将八个区域的指令信息存储在一个寄存器中,将数据信息存储在另一个寄存器中。CP15:c5:c0:2设置数据区域的AP,CP15:c5:c0:3设置指令区域的AP。
获取扩展AP的指令和数据区域需要读取两个寄存器。以下两条指令将区域数据AP放置在核心寄存器r3中,将区域指令AP放置在寄存器r4中:
MRC p15, 0, r3, c5, c0, 2 ; 扩展AP数据区域
MRC p15, 0, r4, c5, c0, 3 ; 扩展AP指令区域
我们提供两个示例来演示如何使用访问权限,一个是标准AP,另一个是扩展AP。这些示例使用内联汇编器读写CP15寄存器。
我们提供了两个标准AP例程regionSetISAP和regionSetDSAP,用于设置区域的标准AP位。可以使用以下函数原型从C语言调用它们:
void regionSetISAP(unsigned region,unsigned ap);
void regionSetDSAP(unsigned region,unsigned ap);
第一个参数是区域编号,第二个参数是定义所控制的指令或数据内存的标准AP的两位值。
示例13.2
这两个例程完全相同,唯一的区别是它们读取和写入不同的CP15:c5辅助寄存器;一个写入指令寄存器,另一个写入数据寄存器。该例程对CP15:c5寄存器进行简单的读-修改-写操作,设置指定区域的AP,保持其他区域不变。
void regionSetISAP(unsigned region, unsigned ap)
{
unsigned c5f, shift;
shift = 2*region;
__asm{ MRC p15, 0, c5f, c5, c0, 1 } /* load standard D AP */
c5f = c5f &∼ (0x3 << shift);
/* clear old AP bits */
c5f = c5f | (ap << shift);
/* set new AP bits */
__asm{ MCR p15, 0, c5f, c5, c0, 1 } /* store standard D AP */
}
void regionSetDSAP(unsigned region, unsigned ap)
{
unsigned c5f, shift;
shift = 2*region;
/* set bit field width */
__asm { MRC p15, 0, c5f, c5, c0, 0 } /* load standard I AP */
c5f = c5f &∼ (0x3 << shift);
/* clear old AP bits */
c5f = c5f | (ap << shift);
/* set new AP bits */
__asm { MCR p15, 0, c5f, c5, c0, 0 } /* store standard I AP */
}
该例程通过使用移位掩码值来清除指定区域的AP位,并使用ap输入参数设置AP位字段,从而设置指定区域的权限。AP位字段的位置计算为区域大小乘以权限位字段中的位数,即移位变量。通过将ap值进行位移并使用OR操作来修改c5f核心寄存器来设置位字段的值。
我们提供了两个扩展AP例程regionSetIEAP和regionSetDEAP,用于设置区域的扩展AP位。可以使用以下函数原型从C语言调用它们:
void regionSetIEAP(unsigned region, unsigned ap);
void regionSetDEAP(unsigned region, unsigned ap);
第一个参数是区域编号,第二个参数是表示区域控制的指令或数据内存的四位值扩展AP。
示例13.3
这两个例程与标准AP例程完全相同,唯一的区别是它们读取和写入不同的CP15:c5辅助寄存器,并且它们具有四位宽的AP位字段。
void regionSetIEAP(unsigned region, unsigned ap)
{
unsigned c5f, shift;
shift = 4*region;
/* set bit field width */
__asm{ MRC p15, 0, c5f, c5, c0, 3 } /* load extended D AP */
c5f = c5f &∼ (0xf shift);
/* clear old AP bits */
c5f = c5f | (ap
shift);
/* set new AP bits */
__asm{ MCR p15, 0, c5f, c5, c0, 3 } /* store extended D AP */
}
void regionSetDEAP(unsigned region, unsigned ap)
{
unsigned c5f, shift;
shift = 4*region;
/* set bit field width */
__asm{ MRC p15, 0, c5f, c5, c0, 2 } /* load extended I AP */
c5f = c5f &∼ (0xf << shift); /* clear old AP bits */
c5f = c5f | (ap << shift);
/* set new AP bits */
__asm{ MCR p15, 0, c5f, c5, c0, 2 } /* store extended I AP */
}
每个例程通过使用移位掩码值来清除指定区域的AP位,并使用ap输入参数设置AP位字段,从而设置指定区域的权限。AP位字段的位置计算为区域大小乘以权限位字段中的位数;这就是移位变量。通过将ap值进行位移并使用OR操作来修改c5f核心寄存器来设置位字段的值。
13.2.3 Setting Region Cache and Write Buffer Attributes
有三个CP15寄存器控制每个核心的缓存和写缓冲区属性。两个寄存器,CP15:c2:c0:0和CP15:c2:c0:1,保存D-cache和I-cache区域属性。第三个寄存器,CP15:c3:c0:0,保存区域写缓冲区属性,并适用于内存数据区域(详见图13.5和表13.7)。
寄存器CP15:c2:c0:1包含所有八个指令区域的缓存配置数据,寄存器CP15:c2:c0:0包含所有八个数据区域的配置数据。这两个寄存器使用相同的位字段编码。
缓存位确定在给定区域内的特定地址是否启用了缓存。在ARM740T和ARM940T中,无论缓存位的状态如何,缓存都会被搜索。如果控制器找到一个有效的缓存条目,它将使用缓存数据而不是外部存储器中的数据。
由于这种缓存行为,当缓存策略从缓存更改为非缓存时,您需要刷新并可能清除区域中的缓存。因此,MPU控制系统在将缓存策略从写通模式更改为非缓存模式时必须始终刷新缓存。在将缓存策略从写回模式更改为非缓存模式时,它必须始终清除并刷新缓存。在将缓存策略从写回模式更改为写通模式时,它还必须清除缓存。有关清除和/或刷新缓存的例程,请参阅第12章。
在ARM946E-S中,如果缓存位为0,则不会从缓存中返回数据,而是执行外部内存访问。这种设计减轻了在禁用缓存时刷新缓存的要求。然而,对于旧区域的清除规则仍然适用。
寄存器CP15:c3:c0:0中的八个区域写缓冲区位用于启用或禁用每个区域的写缓冲区(详见图13.5)。
在配置数据区域时,区域缓存和写缓冲区位共同确定区域的策略。写缓冲区位有两种用途:启用或禁用区域的写缓冲区,并设置区域的缓存写策略。区域缓存位控制写缓冲区位的用途。当缓存位为零时,缓冲区位在值为1时启用写缓冲区,在值为零时禁用写缓冲区。当缓存位设置为1时,缓存和写缓冲区都被启用,缓冲区位确定缓存的写策略。如果缓冲区位为零,区域使用写通策略;如果缓冲区位设置为1,则使用写回策略。表13.8给出了缓存和写缓冲区位的各种状态及其含义的表格视图。有关写回和写通策略的更多详细信息,请参阅第12.3.1节。
我们提供了两个例程来演示如何启用和禁用缓存和写缓冲区。这两个例程使用内联汇编器来读写CP15寄存器。我们将缓存和写缓冲区的控制合并为一个单独的例程调用,以简化系统配置。我们通过控制写策略来引用数据缓存和写缓冲区位,指令缓存位则独立存在。从系统角度来看,将缓存和写缓冲区的状态合并为每个区域的单个值,可以更容易地将区域信息分组到区域控制块中(在第13.3.3节中讨论)。
设置缓存和缓冲区的例程称为regionSetCB,示例13.4显示了其C函数原型如下:
void regionSetCB(unsigned region, unsigned CB);
该例程有两个输入参数。第一个参数region是区域编号,第二个参数CB将区域的指令缓存属性和数据缓存、写缓冲区属性组合起来。第二个参数的格式使用无符号整数的低三位:指令缓存位在位[3],数据缓存位在位[1],数据缓冲位在位[0]。
例子13.4中的例程依次设置数据写缓冲区位、数据缓存位和指令缓存位。为此,对于每个位,它读取CP15寄存器,清除旧的位值,设置新的位值,并将该值写回CP15寄存器。
void regionSetCB(unsigned region, unsigned CB)
{
unsigned c3f, tempCB;
tempCB = CB;
__asm{MRC p15, 0, c3f, c3, c0, 0 } /* load buffer register */
c3f = c3f &∼ (0x1 << region); /* clear old buffer bit */
c3f = c3f | ((tempCB & 0x1) << region); /* set new buffer bit */
__asm{MCR p15, 0, c3f, c3, c0, 0 } /* store buffer info */
tempCB = CB >> 0x1;
/* shift to D-cache bit */
__asm{MRC p15, 0, c3f, c2, c0, 0 } /* load D-cache register */
c3f = c3f &∼ (0x1 << region); /* clear old D-cache bit */
c3f = c3f | ((tempCB & 0x1) << region); /* set new D-cache bit */
__asm{MCR p15, 0, c3f, c2, c0, 0 } /* store D-cache info */
tempCB = CB >> 0x2;
/* shift to I-cache bit */
__asm{MRC p15, 0, c3f, c2, c0, 1 } /* load I-cache register */
c3f = c3f &∼ (0x1 << region); /* clear old I-cache bit */
c3f = c3f | ((tempCB & 0x1) << region); /* set new I-cache bit */
__asm{MCR p15, 0, c3f, c2, c0, 1 } /* store I-cache info */
}
13.2.4 Enabling Regions and the MPU
初始化过程中还剩下两个步骤。第一个是启用活动区域,第二个是通过启用MPU、缓存和写缓冲区来启用保护单元硬件。
要启用区域,控制系统可以重用在第13.2.1节介绍的regionSet例程。本章末尾的示例13.6展示了regionSet的多次使用。
要启用MPU、缓存和写缓冲区,需要修改CP15:c1:c0:0寄存器(系统控制寄存器)中的位值。在ARM940T、ARM946E-S和ARM1026EJ-S处理器中,MPU、缓存和写缓冲区位在CP15:c1:c0中的位置相同,这使得配置MPU的启用对于这三个核心而言是相同的。图13.6和表13.9显示了启用位的位置。CP15:c1:c0寄存器中有一些在图13.6中未显示的配置位;这些位的目的和位置是特定于处理器的,不是保护系统的一部分。
我们使用在示例13.5中显示的changeControl例程来启用MPU和缓存。然而,changeControl例程可以更改CP15:c1:c0:0寄存器中任意一组值。它具有以下C函数原型:
void controlSet(unsigned value, unsigned mask);
传递的第一个参数是一个无符号整数,包含要更改的位值。第二个参数用于选择要更改的位:1表示更改控制寄存器中的该位,0表示保持该位值不变,不考虑第一个参数中位的状态。
例如,要启用MPU和I-cache,并禁用D-cache,将位[12]设置为1,位[2]设置为0,位[0]设置为1。第一个参数的值应为0x00001001;其余不变的位应为零。要将仅位[12]、位[2]和位[0]选为要更改的值,将掩码值设置为0x00001005。
例程13.5会读取控制寄存器并将值放入一个暂存寄存器中。然后,使用掩码输入清除所有要更改的位,并使用值输入指定的状态赋予它们。最后,该例程将新的控制值写入CP15:c1:c0寄存器。
void controlSet(unsigned value, unsigned mask)
{
unsigned int c1f;
__asm{ MRC p15, 0, c1f, c1, c0, 0 } /* read control register */
c1f = c1f &∼ mask;
/* mask off bit that change */
c1f = c1f | value;
/* set bits that change */
__asm{ MCR p15, 0, c1f, c1, c0, 0 } /* write control register */
}
13.3 Demonstration of an MPU system
我们提供了一组例程作为初始化和控制保护系统的构建块。本节使用所描述的例程来初始化和控制一个使用固定内存映射的简单保护系统。
以下是一个演示,使用本章前面部分所介绍的示例来创建一个功能性的保护系统。它提供了一个基础架构,使得三个任务在简单的受保护的多任务系统中运行。我们相信它提供了一个适当的演示ARM MPU硬件背后的概念。它是用C语言编写的,并使用标准访问权限。
13.3.1 System Requirements
该演示系统具有以下硬件特性:
■ 带有MPU的ARM核心
■ 物理内存大小为256 KB,起始地址为0x0,终止地址为0x40000
■ 多个内存映射外设,分布在从0x10000000到0x12000000之间的几兆字节空间上
在这个演示中,所有的内存映射外设被视为需要保护的单一内存区域(参见表13.10)。
该演示系统具有以下软件组件:
■ 系统软件大小小于64 KB。它包括向量表、异常处理程序和用于支持异常的数据栈。系统软件必须对用户模式不可访问;也就是说,用户模式任务必须通过系统调用来运行代码或访问此区域中的数据。
■ 有一个共享软件,大小小于64 KB。它包含常用的库和用于用户任务之间消息传递的数据空间。
■ 系统中有三个控制独立功能的用户任务。这些任务的大小小于32 KB。当这些任务运行时,必须保护它们不被其他两个任务访问。
软件被链接以将软件组件放置在分配给它们的区域中。表13.10显示了示例的软件内存映射。系统软件具有系统级访问权限。共享软件区域可由整个系统访问。任务软件区域包含用户级任务。
13.3.2 Assigning Regions Using a Memory Map
表13.10的最后一列显示了我们分配给内存区域的四个区域。这些区域是使用表中列出的起始地址和代码块和数据块的大小来定义的。图13.7中提供了显示区域布局的内存映射。
区域1是一个背景区域,覆盖整个可寻址内存空间。它是一个特权区域(即不允许用户模式访问)。指令缓存被启用,并且数据缓存采用写透方式操作。该区域具有最低的区域优先级,因为它是分配的最低编号区域。
区域1的主要功能是限制对0x0至0x10000之间的64 KB空间(受保护的系统区域)的访问。区域1还有两个次要功能:它充当背景区域和休眠用户任务的保护区域。作为背景区域,它默认将整个内存空间分配为系统级访问;这样做是为了防止用户任务访问备用或未使用的内存位置。作为用户任务保护区域,它保护休眠任务免受运行任务的不当操作(见图13.7)。
区域2控制对共享系统资源的访问。它的起始地址为0x10000,长度为64 KB。它直接映射到共享系统代码的共享内存空间上。区域2位于受保护区域1的一部分之上,并且会优先于受保护区域1,因为它具有较高的区域编号。区域2允许用户和系统级内存访问。
区域3控制运行任务的内存区域和属性。当控制从一个任务转移到另一个任务,比如在上下文切换期间,操作系统会重新定义区域3,使其覆盖运行任务的内存区域。当区域3被重新定位到新任务时,它将向前一个任务公开区域1的属性。前一个任务成为区域1的一部分,而运行任务成为新的区域3。运行任务无法访问前一个任务,因为它受到区域1属性的保护。
区域4是内存映射的外围系统空间。该区域的主要目的是将该区域设置为不使用缓存和缓冲区。我们不希望输入、输出或控制寄存器受到缓存引起的旧数据问题或使用缓冲写入时涉及的时间或顺序问题的影响(有关在缓存和写缓冲区中使用I/O设备的详细信息,请参见第12章)。
13.3.3 Initializing the MPU
为了组织初始化过程,我们创建了一种称为“Region”的数据类型;它是一个结构体,其成员包含系统操作中使用的区域的属性。当使用MPU时,并不需要这个Region结构体;它只是为了支持演示软件而创建的设计方便。在此演示中,我们称这些数据结构集合为区域控制块(RCB)。
初始化软件使用存储在RCB中的信息来配置MPU中的区域。请注意,RCB中可以定义比物理区域更多的Region结构。例如,区域3是唯一用于任务的区域,但有三个Region结构使用区域3,一个用于每个用户任务。该结构的typedef定义如下:
typedef struct {
unsigned int number;
unsigned int type;
unsigned int baseaddress;
unsigned int size;
unsigned int IAP;
unsigned int DAP;
unsigned int CB;
} Region;
Region结构体中有八个值。前两个值描述了该区域本身的特性:它们是分配给该区域的MPU区域号和使用的访问权限类型,即STANDARD或EXTENDED。结构体的其余四个成员是指定区域的属性:区域起始地址baseaddress,区域大小SIZE,访问权限IAP和DAP,以及缓存和缓冲区配置CB。
RCB中的六个Region结构体如下:
/*
REGION NUMBER, APTYPE */
/*
START ADDRESS, SIZE, IAP, DAP, CB */
Region peripheralRegion = {PERIPH, STANDARD,
0x10000000, SIZE_1M, RONA, RWNA, ccb};
Region kernelRegion = {KERNEL, STANDARD,
0x00000000, SIZE_4G, RONA, RWNA, CWT};
Region sharedRegion = {SHARED, STANDARD,
0x00010000, SIZE_64K, RORO, RWRW, CWT};
Region task1Region = {TASK, STANDARD,
0x00020000, SIZE_32K, RORO, RWRW, CWT};
Region task2Region = {TASK, STANDARD,
0x00028000, SIZE_32K, RORO, RWRW, CWT};
Region task3Region = {TASK, STANDARD,
0x00030000, SIZE_32K, RORO, RWRW, CWT};
我们创建了一组宏来使RCB中的条目更加易读,如图13.8所示。特别要注意的是,我们使用四个简单的字母组合来输入对数据和指令内存的访问权限。前两个字母表示系统访问权限,后两个字母表示用户访问权限。系统和用户访问权限的两个字母可以是读/写(RW)、只读(RO)或无访问(NA)。
我们还将缓存和缓冲区信息映射到指令缓存和数据缓存策略属性上。第一个字母是C或c,用于启用或禁用该区域的指令缓存。最后两个字母确定数据缓存策略和写缓冲控制。值可以是WT表示写直通(writethrough),或者WB表示写回(writeback)。字母c和b也被支持,并且用于手动配置缓存和缓冲区位。Cb是WT的别名,CB是WB的别名。cB表示不缓存和缓冲,最后cb表示既不缓存又不缓冲。
13.3.4 Initializing and Configuring a Region
接下来,我们提供configRegion例程,它接受RCB中的单个Region结构体条目,并将CP15寄存器填充为描述该区域的数据。
该例程遵循第13.3.3节中列出的初始化步骤。该例程的输入是指向区域的RCB的指针。在例程内部,Region的成员被用作初始化过程中的数据输入。该例程具有以下C函数原型:
void configRegion(Region *region);
示例13.6
此示例初始化保护系统的MPU、缓存和写缓冲区。本章前面介绍的例程在初始化过程中被使用。我们首先实现了第13.2节中列出的初始化MPU、缓存和写缓冲区的步骤。在示例代码中,这些步骤被标记为注释。执行此示例将初始化MPU。
void configRegion(Region *region)
{
/* Step 1 - Define the size and location of the instruction */
/* and data regions using CP15:c6 */
regionSet(region->number, region->baseaddress,
region->size, R_DISABLE);
/* Step 2 - Set access permission for each region using CP15:c5 */
if (region->type == STANDARD)
{
regionSetISAP(region->number, region->IAP);
regionSetDSAP(region->number, region->DAP);
}
else if (region->type == EXTENDED)
{
regionSetIEAP(region->number, region->IAP);
regionSetDEAP(region->number, region->DAP);
}
/* Step 3 - Set the cache and write buffer attributes */
/*
for each region using CP15:c2 for cache */
/*
and CP15:c3 for the write buffer. */
regionSetCB(region->number, region->CB);
/* Step 4 - Enable the caches, write buffer and the MPU */
/* using CP15:c6 and CP15:c1 */
regionSet(region->number, region->baseaddress,
region->size, region->enable);
}
13.3.5 Putting It All Together, Initializing the MPU
为了进行演示,我们使用RCB来存储描述所有区域的数据。为了初始化MPU,我们使用一个名为initActiveRegions的顶层例程。该例程在系统启动时针对每个活动区域调用一次。为了完成初始化,该例程还会启用MPU。该例程具有以下C函数原型:
void initActiveRegions();
该例程没有输入参数。
示例13.7
该例程首先针对系统启动时的每个活动区域(kernelRegion、sharedRegion、peripheralRegion和task1Region)分别调用configRegion一次。
在本演示中,任务1是第一个进入的任务。最后调用的例程是controlSet,它启用了缓存和MPU。
#define ENABLEMPU (0x1)
#define ENABLEDCACHE (0x1 << 2)
#define ENABLEICACHE (0x1 << 12)
#define MASKMPU (0x1)
#define MASKDCACHE (0x1 << 2)
#define MASKICACHE (0x1 << 12)
void initActiveRegions()
{
unsigned value,mask;
configRegion(&kernelRegion);
configRegion(&sharedRegion);
configRegion(&peripheralRegion);
configRegion(&task1Region);
value = ENABLEMPU | ENABLEDCACHE | ENABLEICACHE;
mask = MASKMPU | MASKDCACHE | MASKICACHE;
controlSet(value, mask);
}
13.3.6 A Protected Context Switch
演示系统现在已经初始化,并且控制系统已经启动了第一个任务。在某个时刻,系统将进行上下文切换以运行另一个任务。RCB包含了当前任务的区域上下文信息,因此在上下文切换过程中不需要保存CP15寄存器中的区域数据。
为了切换到下一个任务,比如任务2,操作系统会将区域3移动到任务2的内存区域上(参见图13.7)。我们重用configRegion例程,在执行当前任务和下一个任务之间的上下文切换代码之前,作为设置的一部分来执行这个功能。传递给configRegion的输入是指向task2Region的指针。以下是相应的汇编代码示例:
STMFD sp!, {r0-r3,r12,lr}
BL configRegion
LDMFD sp!, {r0-r3,r12,pc} ; 返回
在C语言中,相同的调用是
configRegion(&task2Region);
13.3.7 mpuSLOS
许多概念和代码示例已经被整合到一个名为mpuSLOS的功能控制系统中。
mpuSLOS是SLOS的内存保护单元变体,其在第11章中进行了描述。它可以在出版商的网站上找到,并实现了与基本SLOS相同的功能,但有一些重要的区别。
■ mpuSLOS充分利用了MPU。
■ 应用程序与内核分别编译和构建,然后合并为单个二进制文件。每个应用程序链接到执行不同的内存区域。
■ 三个应用程序中的每一个都由一个称为静态应用程序加载器的例程加载到大小为32 KB的独立固定区域。该地址是应用程序的执行地址。由于每个区域的大小为32 KB,堆栈指针设置在32 KB的顶部。
■ 应用程序只能通过设备驱动程序调用来访问硬件。如果应用程序尝试直接访问硬件,则会引发数据异常。这与基本的SLOS变体不同,因为当应用程序直接从设备访问时,不会引发数据异常。
■ 跳转到应用程序涉及设置spsr,然后使用MOVS指令将pc更改为指向任务1的入口点。
■ 每次调度程序被调用时,活动区域2会更改以反映新的正在执行的应用程序。
13.4 Summary
处理内存保护有两种方法。第一种方法称为无保护,使用自愿执行的软件控制例程来管理任务交互规则。第二种方法称为受保护,使用硬件和软件来强制执行任务交互规则。在受保护的系统中,硬件通过在访问权限违规时生成异常来保护内存区域,并且软件根据异常例程来处理异常并管理内存资源的控制。
ARM MPU 使用区域作为系统保护的主要构造。一个区域是与一块内存区域相关联的一组属性。区域可以重叠,允许使用背景区域来保护休眠任务的内存区域,以免被当前正在运行的任务非法访问。
初始化 MPU 需要执行几个步骤,其中包括设置各个区域属性的例程。第一步使用 CP15:c6 设置指令和数据区域的大小和位置。第二步使用 CP15:c5 设置每个区域的访问权限。第三步使用 CP15:c2(用于缓存)和 CP15:c3(用于写缓冲区)设置每个区域的缓存和写缓冲区属性。最后一步使用 CP15:c6 启用活动区域,并使用 CP15:c1 启用缓存、写缓冲区和 MPU。
最后,一个演示系统展示了在简单的多任务环境中相互保护的三个任务。该演示系统定义了一个受保护的系统,并展示了如何进行初始化。初始化之后,运行受保护系统的最后一步是在任务切换期间将区域分配更改为下一个任务。这个演示系统被整合到 mpuSLOS 中,提供了一个受保护操作系统的功能性示例。