4. 内存区域与保护集:Region概念、保护集(Segment)的定义与划分
好,咱们接着往下聊。上一章我们把MPU的整体框架搭起来了,这一章要深入核心——Region和保护集。说白了,这就是MPU用来“圈地”和“分组管理”的两大法宝。
我个人习惯把Region想象成一块块“带围栏的地皮”,而保护集就是把这些地皮按用途归类的“文件夹”。你想想看,一个复杂的嵌入式系统里,代码、数据、外设寄存器、DMA缓冲区……乱七八糟的东西全挤在4G的地址空间里。没有Region和保护集,MPU根本不知道哪块归谁管。
4.1 Region:MPU眼中的“最小管理单元”
Region是什么?它是MPU能够识别和保护的最小连续地址块。每个Region由三个要素定义:起始地址、大小、属性。
我在项目中遇到过一个问题:有个同事把Region的起始地址设成了非对齐的,结果MPU死活不触发异常。后来查手册才发现,TC3xx的Region起始地址必须与大小对齐。比如你设一个4KB的Region,起始地址必须是4KB的整数倍。这个坑,我帮大家踩过了。
Region的核心参数:
- 起始地址(Base Address):必须与Region大小对齐
- 大小(Size):从32字节到4GB,必须是2的幂次
- 读/写/执行权限:三种基本权限控制
- 访问权限码(AP):决定特权模式和用户模式的访问规则
嗯,这里要注意:TC3xx每个CPU核有16个Region。听起来不少,但如果你把每个外设都单独设一个Region,很快就用完了。我建议你把功能相近的地址块合并成一个Region,比如把所有PWM相关的寄存器放在一个Region里。
4.2 保护集(Segment):把Region组织起来
保护集,英文叫Segment,其实就是一组Region的集合。为什么要搞这么一层?直接管理Region不行吗?
你想想看,一个任务可能要用到代码Region、数据Region、堆栈Region。如果每次切换任务都要挨个配置16个Region,那上下文切换的开销就太大了。保护集就是来解决这个问题的——把多个Region打包成一个“保护集”,切换时只需切换保护集编号。
TC3xx支持最多8个保护集(Segment 0~7)。每个保护集可以包含任意多个Region,但同一个Region可以同时属于多个保护集。这个设计很灵活,比如一个共享的数据缓冲区,可以同时出现在多个任务的保护集中。
| 保护集编号 | 典型用途 | 包含的Region示例 |
|---|---|---|
| Segment 0 | 内核代码 | Flash代码区、中断向量表、内核数据 |
| Segment 1 | 任务A | 任务A代码、任务A堆栈、任务A数据 |
| Segment 2 | 任务B | 任务B代码、任务B堆栈、任务B数据 |
| Segment 3 | 共享资源 | 共享内存、外设寄存器区 |
我曾经在一个项目中,把所有的ISR(中断服务程序)都放在Segment 0里,这样不管当前运行的是哪个任务,中断来了都能正常响应。这个做法后来成了我们团队的标配。
4.3 Region与保护集的划分策略
怎么划分Region和保护集?没有标准答案,但我可以分享一些实战经验。
第一,按功能域划分。 把代码、数据、外设分开。代码区设为只读+可执行,数据区设为读写+不可执行,外设区按需配置。这是最基础的划分方式。
第二,按安全等级划分。 如果你的系统有安全和非安全分区,那就把安全相关的代码和数据放在一个保护集里,非安全的放在另一个。两个保护集之间通过MPU隔离,互不干扰。
第三,按任务划分。 每个任务一个保护集,任务私有的代码和数据放在自己的保护集里。共享资源放在一个公共保护集里,所有任务都能访问。
我的个人建议: 刚开始做MPU配置时,不要追求完美。先粗粒度地划分,比如只分3~4个保护集。等系统跑稳定了,再逐步细化。我见过太多人一开始就搞了8个保护集,结果调试时把自己绕晕了。
4.4 实战:配置一个简单的保护集
光说不练假把式。咱们看一个实际的配置例子。假设我们要配置一个保护集,包含两个Region:一个Flash代码区(0x80000000~0x8000FFFF),一个RAM数据区(0xD0000000~0xD0003FFF)。
/* 配置Region 0:Flash代码区 */
MPU0_RG0_ADDR = 0x80000000; // 起始地址
MPU0_RG0_LEN = 0x00010000; // 64KB大小
MPU0_RG0_ACC = 0x0000000B; // 特权读写执行,用户只读执行
/* 配置Region 1:RAM数据区 */
MPU0_RG1_ADDR = 0xD0000000; // 起始地址
MPU0_RG1_LEN = 0x00004000; // 16KB大小
MPU0_RG1_ACC = 0x00000003; // 特权读写,用户读写
/* 将Region 0和Region 1加入保护集0 */
MPU0_SEG0_MSK = 0x00000003; // 位0和位1置1,表示包含Region 0和Region 1
/* 使能保护集0 */
MPU0_SEG0_CON = 0x00000001; // 使能位
这段代码看起来简单,但有几个关键点要注意:
- Region编号从0开始,MPU0_RG0对应Region 0,MPU0_RG1对应Region 1
- 保护集掩码(SEG_MSK)的每一位对应一个Region,位0对应Region 0,位1对应Region 1,以此类推
- 使能保护集后,MPU才会开始检查。别忘了最后一步
警告: 配置MPU时,建议先关中断,配置完成后再开中断。否则在配置过程中,如果发生中断,MPU可能处于不一致的状态,导致异常。我曾经因为这个原因,调试了整整两天才找到问题。
4.5 保护集切换:任务调度的关键
保护集最大的价值在于快速切换。在RTOS中,任务切换时只需要修改当前保护集编号,就能瞬间改变整个内存视图。
TC3xx提供了一个寄存器叫CPUx_PSW,其中的PRS字段就是当前保护集编号。切换任务时,把这个字段改成目标任务的保护集编号即可。
/* 切换到保护集1 */
uint32 psw = __mfcr(CPU_PSW);
psw &= ~0x000000E0; // 清除PRS字段(位5~7)
psw |= (1 << 5); // 设置PRS = 1
__mtcr(CPU_PSW, psw);
嗯,这里要注意:保护集切换是立即生效的。也就是说,切换后的下一条指令就开始使用新的保护集配置。所以切换前一定要确保目标任务的Region配置已经就绪。
我记得有一次,我在任务A里配置了保护集1,但忘了使能。结果切换到任务B时,MPU直接罢工了。从那以后,我每次配置完保护集都会检查使能位,成了强迫症。
4.6 小结
Region和保护集是TC3xx MPU的基石。Region定义了“保护什么”,保护集定义了“怎么分组”。把这两个概念搞清楚了,后面的权限控制和异常处理就水到渠成了。
下一章,咱们聊聊访问权限的具体配置,包括那个让人头疼的AP码。到时候我会分享一个我总结的“AP码速查表”,保证让你一看就懂。