作者: brody.zhang@foxmail.com
转载请附本文链接:https://www.cnblogs.com/will-brody/articles/16157656.html
1 MCU 简述
MCU 即单片机,适合 IO 密集、高实时要求任务(如控制、低功耗、快启动高响应应用)。
AP (Aplication Processer) 处理器适合做高计算、低 IO 运算。针对不同系统需要设计合理的计算单元。
掌握 MCU 底层基本知识,对于软件优化可以提供很多帮助。
2 MCU 内存映射、链接与跳转
图1:STM32F40x 内存映射
上图为 STM32 内存空间映射,32位芯片最大寻址4GB,FLASH、RAM、外设都在此空间(Linux 高端映射不在本文讨论范围)。
通常工程中描述的地址都指链接地址(ld 文件或 scatter 文件),也是程序运行时所跳转的地址。大部分 ARM 架构芯片上电从 0x0 地址开始运行,例如华大的芯片,Flash地址直接从 0x0 开始,链接脚本 RESET 段也从 0x0 开始。Flash 如果是 NAND 类型(不能 XIP 执行),需要上电后将程序所有内容搬运到 RAM 中,否则无法寻址。
备注:XIP 方式要求 Flash 随机访问读写,Flash 相关知识和链接文件各段意义不在此赘述,推荐阅读《程序员的自我修养》学习。
提问:STM32 从 RAM 和 Flash 启动地址是 0x20000000 和 0x0800000,为什么不是 0x0 地址?
答:根据 memory map 可以看出,STM32 根据 boot pin 配置不同,将 0x0 - 0x000FFFFF 映射到 SRAM 或者 Flash,相当于别名,可以尝试将链接地址改为这个范围,代码执行不会受到任何影响。
3 MCU 启动文件(基于 STM32)
3.1 MDK 版本:
跳转到 Reset 后先初始化系统时钟,再执行 __main() 函数。
__main 为 armcc 库函数,先执行 __scatterload(),装载 RW,清空 ZI。再使用 __rt_entry() 初始化 C 库堆栈。堆栈建立用到了__user_initial_stackheap()
中断向量表
前 16 个为 Cotex-M 内核定义,其余中断号由厂商定义。
在链接文件中,将中断向量表定义为 RESET 段并定义在可执行文件最前面,+First / LAST 可指定段位于最前或者最后,详情可以查阅 ARMCC 手册。
3.2 GCC 版本:
流程相似,不再赘述,请同学自己分析。
提问:0x0 填栈顶指针是必须的吗?
答案:非必须,在 ARM 手册中此地址为保留地址,厂商通常实现为上电自动赋值给 MSP。FreeRTOS 便利用这个值在内核启动后重新初始化 MSP。可以测试将此值定义为0,只要在 ResetHandler 中和内核启动后将 MSP 设置为正确值即可。
4 实时操作系统-SVC 与 PendSV
Handler mode 中断中 |
Thread mode 非中断中 |
|
Privileged 可访问全部寄存器 |
中断一定是特权模式,复位后使用 MSP |
可以在特权模式配置为非特权模式 |
Unprivileged 禁止访问内核寄存器,例如:CONTROL 寄存器、NVIC 寄存器、MPU 寄存器 |
不存在 |
可以通过出发 SVC 进行系统调用,在中断中进入特权模式 |
对于堆栈指针使用:
中断中一定使用 MSP,但是在线程中根据 CONTROL 寄存器的第 1 位确定 MSP 或 PSP
4.1 SVC 软中断
SVC 指令(汇编调用或者函数声明SVC)可以直接出发 SVC 中断。
此指令可以传递一个参数(即 SVC 服务号),在 ARM 模式下 24 位,thumb 模式下 8 位。模式切换根据 BX 指令跳转地址的第 0 位是否为 1 决定。
在 FreeRTOS 中实现了三个 SVC 调用,分别是启动调度器、触发任务调度、进入特权模式。
4.2 PendSV 可挂起中断
RTOS 在 Systick 中断置位 PendSV 标志位,当当前无中断响应时进入 PendSV 进行调度。
调度基本流程:
- 入栈(线程栈)当前任务 TCB
- O(1) 位图算法选择下一个需要执行任务
- 切换 SP 出栈恢复任务现场
5 MPU 任务隔离
MPU 单元为不同的内存区域设置了访问权限,特权级访问、非特权级访问,针对有 cache 的芯片(例如Cotex-M7系列)还可以配置是否使用 cache 避免数据不一致。
STM32F4xx 支持配置 8 个区域,支持背景区域,即不设置 MPU 的区域可配置默认访问权限。
提问:MPU 如何保护任务栈?
答案:FreeRTOS 支持 MPU 接口。MPU 接口针对每个任务设置可访问区域,任务运行在非特权模式,当任务切换时,MPU 寄存器也跟随任务进行切换到该任务可访问区域。这样当任务越过自己的任务栈时便触发内存访问错误中断。使用 MPU 增强了安全性,但是编程复杂度提升,无法通过全局传参,编码限制增加。
提问:不带 MPU 接口的 FreeRTOS 堆栈检测机制是怎样的?
答案:任务切换时检测堆栈指针是否越过栈底。缺点是无法检测越过后在调度前又回来的情况。常见的栈检测做法还有栈染色法,预先给任务栈刷固定魔数,通过检查魔数是否改变统计使用率。
6 MMU、页表、进程实现与地址隔离(以32位 rt-smart 为例讲解)
ARM 架构目前有三个系列,Cotex-M、Cotex-A、Cortex-R。分别用于低成本嵌入式设备(物联网、工控等),高端消费品(手机、无人机、显示设备),高实时高安全场景。其中 Cotex-M 和 Cotex-R (除最新的Cortex-R82)不带MMU,Cotex-A 系列基本都带 MMU 组件。MMU 实质上是一种用硬件实现保证内存管理安全的方法,隔离了各个进程的运行空间。
本节以 rt-smart 在 Cotex-A 的移植分享下个人理解。
代码可以参考:https://www.100ask.net/detail/p_5fdec53ce4b0231ba88dc8d1/8
页表:页表是虚拟地址和物理地址的映射,在具体的实现上,就是一个数组。按照 1M 大小划分页表,则 32 位系统 4G 地址空间需要 4K 个页表项。数组成员则代表数组下标 * 1M的虚拟地址所对应的物理地址。
ARM 通过 CP15 协处理器进行 MMU 控制,支持二级页表。当设置页表基地址(实现上是传入页表数组指针)使能后,CPU 寻址可通过硬件级别自动转换。同时 MMU 支持对内存区域权限配置,例如是否可以使用 cache、writebuffer。
ARM 一级页表和二级页表定义:
- 一级页表按照 1M 划分,每个页表项代表 1M 物理地址的基地址。32位系统需要 4K 个一级页表项,需要 16K 的空间存储。
- 二级页表按照 4K 划分,每个页表项代表 4K 物理地址的基地址。4G 地址空间需要 1M 个二级页表项,需要 4M 空间。
CPU典型寻址过程:
6.1 以 rt-smart 为例详解 MMU 操作
需要搞明白的几个关键点:
- 物理内存地址都映射在内核空间,创建进程从内核空间管理的 page 映射为用户空间。
- 创建进程页表时候,需要将内核页表复制到进程页表内核空间位置。
- 内核没有用户空间,rt-smart 只支持内核空间直接映射 0xC0000000 - 0xF0000000,即最大支持 768M 物理内存(Linux 中无法直接映射的内存空间使用高端映射访问)
- 用户进程通过 SVC 中断进行内核服务调用
rt-smart 内存空间映射(全局视角):
用户进程空间映射:
rt-smart 内核启动流程:
1. 拷贝内核代码到 RAM 中运行,此时使用物理地址,从 0x0 地址开始运行。
2. 初始化页表,将物理地址同时映射到 0x0 和 0xC0000000,防止 CPU 使能 MMU 瞬间寻址错误(这里可以仔细思考下原因)。
3. 映射内核页表到 0xC0000000,此时 CPU 访问的都是虚拟地址,内核代码链接地址在 0xC0000000 空间。当没有进程创建时 CPU 不会访问 0~3G 地址空间。
物理角度看进程内存映射:
如上图,用户程序装载后物理上都位于页表管理区,由 MMU 硬件保证不会访问别的进程内存空间。当 CPU 需要访问硬件寄存器时则通过动态映射到绿色区域寻址。
创建进程和调度分为以下几步:
- 内核装载用户进程 elf:申请内存进行进程页表映射(0~3G)(先创建一级页表,每个页表项都是二级页表,装载用到时再创建二级页表),复制内核页表到3~4G空间。
- 传参进程入口函数,创建进程内核任务。rt-smart 创建进程及进程中线程时等效创建内核线程,进程创建几个线程则对应创建几个内核线程。同一进程创建的线程地址空间相同,不同进程地址空间不同,所有内核线程统一进行调度。
- 任务调度,若前后线程不属于同一个进程,则触发页表切换操作
- 进程中用户若想访问硬件资源,需要通过 SVC 调用陷入内核,由内核进行真正的硬件操作,用户无法直接寻址访问内核资源。
- 进程间通信采用共享内存、管道等方式
提问:为什么需要二级页表?
答:节省内存。直觉上一级页表二级页表全空间映射每个进程需要 16K+4M 内存,更加浪费内存,实际并非如此。
- 页表每项需要物理上连续,1M大小的页过大使用很不方便,当内存碎片化时很难分配。即使只使用1字节内存,也必须分配1M空间,造成内存浪费。
- 实际上根据 用户进程空间映射 一节描述,用户程序内存使用并不连续,大多数程序很多地址空间都不需要访问,如果全使用 4K 大小页表,则很多页表项都是无用的,造成很大内存开销。
- 通常进程无法用完0~3G的用户空间,当用户进程内存使用远小于0~3G,装载时先创建一级页表,用不到的空间则不映射二级页表,则既满足了使用小页表,也避免了用不到的空间页表映射开销,代价就是多一次寻址。