在本系列关于将 RTOS 与 MPU 结合使用的最后一部分中,我们将了解如何按进程对 RAM 进行分组,并以使用 Cortex-M MPU 时的建议列表作为结尾。
到目前为止,我们已经了解了 MPU 是什么以及它如何帮助将任务和流程相互隔离。我们还研究了如何设置 Cortex-M MPU,发现它非常易于使用。使用 MPU 的复杂性更多地与组织应用程序的内存有关,而不是更新这个非常有用的设备的机制。
在本系列关于将 RTOS 与 MPU 结合使用的最后一部分中,我们将了解如何按进程对 RAM 进行分组,并以使用 Cortex-M MPU 时的建议列表作为结尾。
创建 MPU 进程表
使用 MPU 时最大的困难可能是按进程对内存进行分组并创建 MPU 进程表。这部分是因为您需要更深入地了解您的工具链:编译器、汇编器和链接器/定位器。
假设我使用的是 IAR 工具链(即 EWARM),但概念非常相似,您可以根据自己使用的工具调整这些概念。除非另有说明,否则链接器会将数据(即 RAM)放置在图 2 所示的三个部分之一中。
未初始化的数据
零初始化数据
初始化数据
顾名思义,未初始化的数据对应于在编译时未赋予初始值或未声明为静态的变量。
零初始化数据对应于声明为静态并在启动时初始化为零的数据。链接器将其分组为一个连续的块,以便启动代码可以执行块集(为 0)。
初始化数据对应于具有初始值的数据(例如 int x = 10;)。同样,链接器将这些数据分组到一个连续的块中,但在 ROM 中创建一个并行块,其中包含 RAM 中每个相应变量的初始值。启动时,整个块从 ROM 复制到 RAM。
【图2 | RAM 部分。]
如前所述,进程的 RAM 必须连续分组,如图 3 所示。为此,我们需要绕过编译器/链接器标准部分并创建将按进程分组的新部分。工具链通常能够创建多个零块和初始化部分,如图 3 所示。
【图3 | 基于 MPU 的应用程序的 RAM 部分。]
创建命名的 RAM 部分
要按进程对数据进行分组,我们需要使用 EWARM #pragma 指令 default_variable_attributes 并将所有要在一个进程中分组的变量包装在一起。
#pragma default_variable_attributes = @”.Process1”
// All variables that we want to be part of the section named “.Process1”。
#pragma default_variable_attributes =
如果您的应用程序包含在汇编语言文件中声明的变量,那么您还需要确保汇编语言文件包含适当的汇编程序指令。
按块对 RAM 进行分组
您的应用程序肯定会包含不一定与任何特定进程相关联的代码。在这种情况下,最好为这些模块创建命名部分,然后将这些部分组合成一个公共代码块。然后,您将使用上述#pragma 指令创建不同的命名段,每个模块一个,并使用链接器的 块 指令(如下所示)对这些段进行分组。
define block COMMON_RAM_BLOCK with alignment = 4K, size = 4K
{
section .DRIVER_RAM,
section .COMMOM_RAM,
section .MATH_RAM,
section .STRING_RAM,
}
define block PROCESS_AI_RAM_BLOCK with alignment = 16K, size = 16K
{
section .AI_DRIVER_RAM, // Analog input driver
section .RTD_LIN_RAM, // RTD linearization
section .THERMOCOUPLE_LIN_RAM, // Thermocouple linearization
section .UNIT_CONVERSION_RAM, // Shared RAM with AO module
}
define block PROCESS_AO_RAM_BLOCK with alignment = 8K, size = 8K
{
section .AO_DRIVER_RAM, // Analog output driver
section .4_20MA_LIN_RAM, // 4-20 mA linearization
section .ACTUATOR_LIN_RAM, // Actuator linearization
section .UNIT_CONVERSION_RAM, // Shared RAM with AI module
}
define block SHARED_RAM_BLOCK with alignment = 2K, size = 2K
{
}
您会注意到 block 指令允许您指定内存块的大小和对齐方式。为了将块的起始地址放置在 MPU 进程表中,两个值必须相同,这一点很重要。此外,每个块所需的 RAM 量取决于应用程序。为了便于说明,我决定使用 16K、8K、4K 和 2K 字节。
定位 RAM 块
我们现在可以使用两个链接器指令将所有块放置在 MCU 的可寻址空间中:区域和位置:
define region RAM = Mem:[from 0x20000000 size 64K];
place in RAM
{
block RAM_ALL with fixed order
{
block PROCESS_AI_RAM_BLOCK,
block PROCESS_AO_RAM_BLOCK,
block COMMON_RAM_BLOCK,
block SHARED_RAM_BLOCK
}
}
region 指令指定 MCU 的可寻址存储器。如果您的 RAM 并非全部连续,则可能有不同的区域指令。
RAM 指令中的位置指定在 RAM 区域中定位块。您会注意到我们需要将块放入块中以指定块放置的顺序。事实上,为了减少浪费的空间量,应该先使用较大的块。
为每个任务创建 MPU 进程表
现在 RAM 按进程分组,您可以返回并编辑每个任务/进程的 MPU 表。但是,要做到这一点,编译器必须知道块的名称,因此,您需要使用 #pragma section 指令,如下所示:
#pragma section = “COMMON_RAM_BLOCK”
#pragma section = “PROCESS_AI_RAM_BLOCK”
#pragma section = “PROCESS_AO_RAM_BLOCK”
#pragma section = “SHARED_RAM_BLOCK”
这两个进程表现在可以如下所示(假设您没有使用包含上一节中描述的每个任务回调的版本):
建议
以下是使用 Armv7-M MPU 时的一些建议。
在非特权模式下运行用户代码:
可以使用 MPU,但仍以特权模式运行所有应用程序代码。当然,这意味着应用程序代码将能够更改 MPU 设置,因此会破坏拥有 MPU 的目的之一。最初以特权模式运行应用程序可能会更容易迁移应用程序代码。但是,在某些时候,您的大部分应用程序代码都需要在非特权模式下运行,因此您需要添加 SVC 处理程序。
将 PRIVDEFENA 设置为 1:
这允许特权代码访问完整的内存映射。理想情况下,您的大多数应用程序将在非特权模式下运行,只有 ISR 和 RTOS 将在特权模式下运行。此建议可避免为每个任务使用三个 MPU 区域,以授予特权代码访问任何 RAM 位置、任何代码和任何外围设备。将 PRIVDEFENA 设置为 1 的决定可能已经由 RTOS 供应商做出,您无法更改。
ISR 具有完全访问权限:
每当识别到中断并启动 ISR 时,处理器就会切换到特权模式。由于 PRIVDEFENA 将设置为 1,因此 ISR 无论如何都可以访问 I/O 位置的任何内存。您根本不想在进入 ISR 时重新配置 MPU,并在退出时重新配置它。因此,ISR 应该被视为系统级代码,因此确实应该被允许具有完全访问权限。
此外,ISR 应始终尽可能短,并简单地向任务发出信号以执行中断设备所需的大部分工作。当然,这假设 ISR 是内核感知的,并且任务有相当多的工作来处理中断设备。例如,处理以太网数据包不应该在 ISR 级别完成。但是,可以直接在 ISR 中切换 LED 或更新脉冲宽度调制 (PWM) 定时器的占空比。
将 XN 位设置为 1:
如果您的应用程序代码不希望在 RAM 外执行代码,则应为所有 RAM 或外围区域设置 RASR 寄存器的 eXecute Never 位。为外围设备设置 XN 位可能看起来很奇怪,但它不会伤害并防止黑客试图进入您的系统。
限制外围设备对其进程的访问:
您应该留出一个或多个 MPU 区域来限制进程只能访问其自己的外围设备。换句话说,如果一个进程管理 USB 端口,那么它应该只能访问 USB 外围设备或与 USB 控制器需求相关的外围设备,例如 DMA。
限制 RTOS API:
系统设计人员需要确定哪些 RTOS API 应可用于应用程序代码。具体来说,您想防止应用程序代码在系统初始化后创建和删除任务或其他 RTOS 对象(如信号量、队列等)吗?换句话说,RTOS 对象是否应该只在系统启动时创建,而不是在运行时创建?如果是这样,那么 SVC 处理程序查找表应该只包含您想要向应用程序公开的 API。然而,即使 ISR 在特权模式下运行并因此可以访问任何 RTOS API,一个好的 RTOS 仍会阻止从 ISR 创建和删除 RTOS 对象。
在 RTOS 空间中分配 RTOS 对象:
任务堆栈位于进程的内存空间内。然而,RTOS 对象(信号量、队列、任务控制块等)最好分配在内核空间中并通过引用进行访问。换句话说,您不想在进程的内存空间中分配 RTOS 对象,因为这意味着应用程序代码可以有意或无意地修改这些对象,而无需通过 RTOS API。
没有全局堆:
将 MPU 设置为使用全局堆(即所有进程使用的堆)几乎是不可能的,因此您应该尽可能避免使用这些堆。相反,如前所述,如果进程需要动态分配的内存(例如以太网帧缓冲区),则应允许进程特定的堆。
不要禁用中断:
如果您的应用程序在非特权模式下运行,任何禁用中断的尝试都将被忽略。这样做的问题是,您不会从 CPU 获得中断 未被 禁用的指示。
如果您的应用程序在非特权模式下运行并且您尝试通过 NVIC 禁用中断,则会触发总线故障。
保护对代码的访问:
尽管 MPU 区域通常用于提供或限制对 RAM 和外围设备的访问,但如果您有空闲区域并且能够按进程组织代码(通过链接器命令),那么限制代码对代码的访问可能很有用。这可以防止某些类型的安全攻击,例如 Return-to-libc [2]。
减少进程间通信:
就像任务应该设计得尽可能独立一样,流程也应该遵循同样的规则。因此,要么进程不相互通信,要么将进程间通信保持在最低限度。
如果您必须与其他进程通信,只需留出一个包含输出和输入缓冲区的共享区域。发送方将其数据放入输出缓冲区,然后触发中断以唤醒接收进程。一旦数据被处理,响应(如果需要)可以放在发送者的缓冲区中,并且可以使用中断来通知发送者。
确定遇到 MPU 故障时该怎么做:
理想情况下,在开发过程中检测并纠正所有 MPU 故障。您应该计划由于意外故障或错误或您的系统受到安全攻击而在现场发生故障。在大多数情况下,建议对每个任务或每个进程都有一个受控的关闭顺序。是否重新启动有问题的任务、进程内的所有任务或整个系统取决于故障的严重程度。
有办法记录和报告故障:
理想情况下,您有办法记录(可能记录到文件系统)并显示故障原因,以允许开发人员解决问题。
结论
内存保护单元 (MPU) 是将对内存和外围设备的访问限制为仅需要访问这些资源的代码的硬件。如果任务试图访问其分配空间之外的内存位置或外围设备,则会触发 CPU 异常,并且根据应用程序,必须采取纠正措施。
Cortex-M MCU 中的 MPU 是一个相当简单的设备,并且相对容易配置。然而,使用 MPU 的复杂性更倾向于按进程分配存储(主要是 RAM)以及创建将在上下文切换期间加载到 MPU 中的 MPU 进程表。
最后,我提供了一个建议列表,这些建议可以更好地在您的应用程序中使用 MPU。
单独的软件无法阻止对未分配给 RTOS 环境中任务的内存或外围设备的访问。您需要硬件来实现这一点,而 MPU 是目前 Cortex-M (Armv7-M) 上唯一可以做到这一点的机制。
迁移应用程序以使用 MPU 是一个相当简单但乏味的过程。添加 MPU 也会给您的应用程序带来开销:在上下文切换期间您需要加载额外的寄存器,并且用户代码应该在非特权模式下运行以避免此类代码更改 MPU 设置。
审核编辑:郭婷
全部0条评论
快来发表一下你的评论吧 !