跳转到内容
新建笔记

i.MX8MP M7 启动:MPU、资源域、时钟与延时

i.MX8MP 的 Cortex-M7 初始化要同时处理三类状态:本核的 MPU/cache、SoC 的资源权限,以及多个主控共享的时钟和电源。某个 SDK 例程能在评估板上启动,并不表示它可以在 Linux 已运行后原样再次执行。

本文对照 NXP MCUXpresso SDK MCUX_2.16.000 的 EVK 板级代码、MIMX8ML8 驱动与 RM Rev.3,说明原工程中的 FL_BOARD_InitMemory()、FL_BOARD_RdcInit()、FL_BOARD_BootClockRUN() 和微秒延时。FL_ 前缀属于项目包装;NXP 对应函数使用 BOARD_ 前缀,调用处必须与实际定义一致。

1. MPU 配置实际改变了什么

跳转到“1. MPU 配置实际改变了什么”

BOARD_InitMemory() 读取链接符号,关闭已启用的 I-cache/D-cache,禁用 MPU,配置区域后重新启用 MPU 和缓存。这是启动阶段的板级策略。在线重配时,还涉及其他执行上下文、已有脏数据、DMA 和对端访问,不能仅复制这几次开关调用。

ARM_MPU_RASR() 的参数依次是 XN、访问权限 AP、TEX、S、C、B、禁用子区域掩码 SRD 和 SIZE。区域大小为 2SIZE+12^{SIZE+1} 字节,最小 32 字节;基址须按区域大小对齐。相互覆盖时,编号较高的有效区域优先。

SDK 的区域表按闭区间展开如下。KiB、MiB、GiB 都按 1024 进制计算。

区域起止地址大小类型及用途
00x00000000…0x3fffffff1 GiBDevice、不可执行、不可缓存;低地址兜底
10x00000000…0x0001ffff128 KiBITCM/TCML;Normal、不可缓存、可执行
20x08000000…0x0fffffff128 MiBQSPI 映射;Normal、可缓存、可执行
30x20000000…0x2001ffff128 KiBDTCM/TCMU;Normal、不可缓存、可执行
40x40000000…0x7fffffff1 GiBDDR;Normal、不可缓存、可执行
50x80000000…0xbfffffff1 GiBDDR;Normal、不可缓存、可执行
6由链接符号决定2 的幂覆盖其中的程序/数据区,设为 Normal、可缓存、可执行

原注释中部分终点多写了 1:例如 0x20000000 开始的 128 KiB 结束于 0x2001ffff,不能把 0x20020000 包含在区域内。区域 0 的编码为 TEX=0,C=0,B=1,对应 shareable Device;虽然 SDK 注释写着 not shareable、参数 S 为 0,但这一组合的 Device 共享属性由类型编码决定,不能单看 S 位。

常见属性组合如下,x 表示该组合下这个位不决定表中属性:

TEXSCB类型与缓存策略
0x00Strongly ordered,shareable
0x01Device,shareable
00/110Normal,write-through、no write allocate;S 选择共享属性
00/111Normal,write-back、no write allocate;S 选择共享属性
10/100Normal,内外层均 non-cacheable;S 选择共享属性
10/111Normal,write-back、read/write allocate;S 选择共享属性
2x00Device,non-shareable

这些区域均使用 ARM_MPU_AP_FULL,因此这份配置主要表达内存类型与缓存策略,不能描述成已经实现任务隔离。XN=0 允许取指,也不等于整个地址范围都有实际存储器。

启用时的 PRIVDEFENA 允许特权访问在未命中区域时使用默认内存映射;HFNMIENA 让 MPU 在 HardFault、NMI 和 FAULTMASK 场景下继续工作。二者不替代区域本身的权限设计。

链接符号、Linux 保留区和 MPU 区域是三回事

跳转到“链接符号、Linux 保留区和 MPU 区域是三回事”

链接脚本把 __CACHE_REGION_START、__CACHE_REGION_SIZE 定义为绝对符号。SDK 用数组形式声明,再取符号的地址值;不是去这个地址读取一个 uint32_t 变量。实际值应从所用链接脚本和最终 map 文件确认。

此版本的 DDR 链接脚本把前 4 MiB 的程序/数据区设为可缓存,后续区域放置 NonCacheable 段;TCM 和 flash 链接脚本则把 __CACHE_REGION_SIZE 设为 0。它们是示例布局,必须与具体固件和 Linux 的 reserved-memory 一致。

SDK 注释提到 0x80000000 开始的 16 MiB Linux 保留区;区域 5 为方便对齐实际覆盖 1 GiB。设置 MPU 属性既不会让 Linux 保留这 1 GiB,也不会改变 A53 页表或使 DMA 自动一致。把变量放进 .NonCacheable 一类段,仅决定链接位置,仍需对应的 MPU 属性与对端映射。

用显式检查替代仅依赖 assert

跳转到“用显式检查替代仅依赖 assert”

原算法通过右移计算对数,再用 assert 检查。发布构建可能关闭 assert;size=1 会跳过 i!=0 分支,1 << 31 还涉及有符号左移问题。大小为 0 时也应明确清除区域 6,防止重复调用时保留上一轮配置。

下面只计算 32 字节至 2 GiB 区域的 SIZE 字段,不读写 MPU。4 GiB 无法放入本接口的 uint32_t size,不在接口范围内。调用者仍需确认该区完全位于已分配的实际 RAM,并且不会覆盖共享区。

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
bool m7_mpu_size_field(uint32_t base, uint32_t size, uint32_t *field)
{
if (field == NULL || size < 32U ||
(size & (size - 1U)) != 0U || (base & (size - 1U)) != 0U) {
return false;
}
uint32_t exponent = 0U;
for (uint32_t value = size; value > 1U; value >>= 1U) {
++exponent;
}
*field = exponent - 1U;
return true;
}

在已停用 MPU/cache 的启动配置阶段,先禁用区域 6,再处理 size==0;非零时调用检查函数,失败应终止启动并报告错误,成功才设置该区域。不要把失败变成“继续使用默认属性”。配置完成后使用 CMSIS 的 MPU/cache 接口完成必要的屏障和启用步骤。共享缓冲的所有权、clean/invalidate 顺序另见缓存一致性说明。

2. RDC 域分配与访问策略

跳转到“2. RDC 域分配与访问策略”

RDC 把总线主控分配到域,并为外设或内存区域设置各域访问策略。i.MX8MP 支持四个域;EVK 的 BOARD_DOMAIN_ID=1 是板级选择,不能据此认为所有固件启动时都已属于域 1。

SDK 的 BOARD_RdcInit() 做三件事:读取当前主控的域号,查询该域对 RDC 外设本身的权限,在具有写权限时把 M7 指派给 BOARD_DOMAIN_ID。RDC_GetPeriphAccessPolicy() 返回对应域的两位权限:

值权限
0无访问权限
1可写
2可读
3可读写

因此原来的 0x1U & policy 检查的是写权限,不是笼统地“有权限”。指派 M7 到域 1,并不会自动给域 1 开放 UART/PWM,也不会把域 0 从这些外设排除。外设 PDAP、内存区域、锁定位及其他主控仍须由整体启动方案配置。

如果 A53/U-Boot 已完成正确分配,这一步可能不需要再次写入;如果没有写权限,SDK 分支会跳过分配。随后直接宣称“现在 M7 必在域 1”并不严谨,应读取当前域并核对所需资源策略。单核启动与 A53 先启动的区别在于已经建立的系统状态,不在于函数名称。

3. 域门控不能代替时钟所有权

跳转到“3. 域门控不能代替时钟所有权”

RDC 域号也影响 CCM 门控依赖。CCGR 不是“每个域一位”,而是四个两位字段,位移为 0、4、8、12。RM 规定主控写入时仅更新自己所属域的字段,其他域字段写入被忽略。

kCLOCK_ClockNeededAll 的常量覆盖四个字段,但上述硬件规则仍生效。它表示当前域在所定义模式下需要该时钟,不等于“强制所有核永久打开”,也不保证 A53 不能修改共享 PLL、root、复位或电源域。

原 BOARD_RdcInit() 涉及的门控应按用途理解:

门控原例程用途
IOMUX、IPMUX1/2/3引脚及外设互连访问
QSPI仅在 FLASH_TARGET 构建下启用
SYSTEM PLL1/2/3为相关时钟源建立当前域需求
AUDIO PLL1/2、VIDEO PLL1为音频/视频相关时钟源建立当前域需求

共享外设由 M7 使用时,Linux 还需在设备树、驱动、时钟和电源管理中遵守同一所有权安排。仅把某个 Linux 节点禁用,也不自动完成其所有上游时钟的跨核协调。

原代码来自板级示例,主要动作如下。下表频率依赖 SYSTEM PLL1 实际为 800 MHz、示例 PLL 参数与电源/速度等级允许该配置。

步骤示例动作需要核对的条件
1AHB、M7 root 临时切到 24 MHz这是共享系统变更,不能默认对 Linux 无影响
2保留已注释的 SYSTEM PLL2/3 初始化ROM/U-Boot 已使用这些 PLL 时,重复配置可能使 SoC 失去时钟
3AUDIO PLL1/2 配置为约 393.216/361.2672 MHz音频 PLL 也可能正被 A53 或其他外设使用
4M7 root 选择 PLL1,PRE/POST 均为 1目标为 800 MHz;须满足器件工作点约束
5AHB 选择 PLL1/6,PRE/POST 均为 1约 133.333 MHz;固定除以 6 已包含在 MUX 输入中
6Audio AHB 选择 PLL1,POST=2400 MHz,不是 AUDIO PLL1 输出
7UART4 选择 PLL1/10,PRE/POST 均为 180 MHz 是 UART 输入时钟,串口波特率仍由 UART 分频决定
8打开 RDC、OCRAM、Audio 以及 SIM、Debug、DRAM 等门控服务于例程的工作与低功耗假设,不表示每个应用都需全部启用
9请求 AUDIOMIX 上电并完成 NoC 握手核对 GPC 所有权和现有电源状态,等待应有失败处理
10配置 AUDIOMIX 内的 SAI PLL此 SDK 版本先检查 CLKEN1.PLL;原笔记直接调用遗漏了条件
11SystemCoreClockUpdate()更新软件记录的内核频率,不改变外设时钟,也不会测量晶振

CLOCK_SetRootDivider() 的两个参数是实际除数,分别 1~8、1~64;对应 getter 也已返回实际除数。PLL 的 postDiv 编码则表示 2postDiv2^{postDiv},不能把两种参数含义混用。

分数 PLL 使用:

fout=(mainDiv+dsm/65536)frefpreDiv 2postDivf_{out}=\frac{(mainDiv+dsm/65536)f_{ref}}{preDiv\,2^{postDiv}}

示例 AUDIO PLL1 参数为 mainDiv=262,dsm=9437,preDiv=2,postDiv=3;AUDIO PLL2 与 SAI PLL 为 361,17511,3,3,参考均为 24 MHz。由于分数系数与晶振误差,393.216 MHz 和 361.2672 MHz 是目标/近似值。前者除以 8192 对应 48 kHz,后者对应 44.1 kHz,实际音频链仍需要 SAI 和 codec 的完整时钟配置。

原注释中的可选 QSPI“400 MHz”也只是 PLL1/2 的算术值,不能据此设置 flash 总线:应分别核对该 root、控制器、flash、板级布线及数据采样的限值。

AUDIOMIX 上电的四步含义

跳转到“AUDIOMIX 上电的四步含义”

例程先在 PGC_CPU_M7_MAPPING 建立映射,再设置 PU_PGC_SW_PUP_REQ 的 AUDIOMIX 请求并等待自清;然后设置 PU_PWRHSK 中的 NoC 电源请求信号,等待对应 ACK。GPC 负责电源序列,CCM 门控负责时钟,这两层不能互相替代。

原例程使用无期限轮询。产品代码需要限定等待并输出故障位置;超时后不能继续访问未上电模块。Linux 运行期间重新映射电源域、请求上电或改 SAI PLL,须先协调双方所有权。不能把 SYSTEM PLL 列为“不可动”,却把 Audio PLL、AHB 或 AUDIOMIX 列为“随时可改”。

启动顺序与项目函数名

跳转到“启动顺序与项目函数名”

原工程意图是先初始化内存属性与资源域,再准备引脚和时钟,最后启用调试串口。具体启动代码还需保证向量表、栈和 C 运行时已经就绪,以及访问 RDC/CCM 等模块所需的基础时钟和权限已经可用。

FL_BOARD_BootClockRUN() 与 BOARD_BootClockRUN() 是两个不同的 C 标识符。若实现采用前者,调用处也要调用前者,或明确提供包装。不能只改注释,就声称调用到了已经修改过的函数。

5. 微秒延时的计数边界

跳转到“5. 微秒延时的计数边界”

SDK_DelayAtLeastUs() 是忙等:占用当前核等待,不会让出 RTOS 任务。未启用 SDK_DELAY_USE_DWT 时,该版本使用按内核类型估算循环次数的汇编循环;指令吞吐、缓存和总线状态都会影响结果。启用 DWT 后,用 CYCCNT 计数更可解释,但仍不等于精确的引脚定时。

SDK 用 USEC_TO_COUNT(us,hz) 计算周期数,并断言结果不超过 UINT32_MAX。1 GHz 下原注释的最大整数微秒数 4294967 μs,约 4.295 s 是这个转换上限,不能写成任意长延时。关闭 assert 也不会放宽计数器范围。

此版本 DWT 分支将当前计数加到目标周期数上,跨界时减去 UINT32_MAX。32 位计数器的模数实际是 2322^{32},因此这个减法存在一个计数的边界偏差。源码还设置 DEMCR.TRCENA、检查 NOCYCCNT 并使能 CYCCNTENA;这说明计数器的可用性是前提,不能只有循环比较。

无符号差值 (uint32_t)(now-start) 可以处理单次回绕。下面把周期上限限制为 INT32_MAX,并用向上取整避免非整数周期被截短;只有轮询足够频繁、计数器持续运行且频率稳定时,才可据此判断经过了足够周期。中断、抢占和调试暂停都可能延长实际等待;超过计数器可辨别范围的停顿会丢失经过了几圈的信息。

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <limits.h>
#include "fsl_device_registers.h"
bool m7_delay_cycle_count(uint32_t us, uint32_t hz, uint32_t *cycles)
{
if (cycles == NULL || hz == 0U) {
return false;
}
uint64_t count = ((uint64_t)us * hz + 999999U) / 1000000U;
if (count > INT32_MAX) {
return false;
}
*cycles = (uint32_t)count;
return true;
}
bool m7_delay_us_checked(uint32_t us, uint32_t hz, uint32_t poll_limit)
{
uint32_t cycles;
if (!m7_delay_cycle_count(us, hz, &cycles)) {
return false;
}
if (cycles == 0U) {
return true;
}
if (poll_limit == 0U || (DWT->CTRL & DWT_CTRL_NOCYCCNT_Msk) != 0U) {
return false;
}
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
uint32_t start = DWT->CYCCNT;
while (poll_limit-- != 0U) {
if ((uint32_t)(DWT->CYCCNT - start) >= cycles) {
return true;
}
}
return false;
}

poll_limit 限制循环次数,避免 CYCCNT 停止时一直等待;它不是微秒超时。返回 false 时可能已经等待了一部分时间,调用者必须处理失败。函数没有把 CYCCNT 清零,以免破坏其他使用它的测量。已有性能统计、调试器和电源管理仍需统一管理 DWT。

在 800 MHz 下,这个接口因半范围限制最多接受 2684354 μs;实际应只用于短时等待,并确保任意两次有效采样间隔小于半个回绕周期。周期性触发 ADC、精确脉宽和长时任务等待应分别采用硬件定时器/PWM、硬件事件链或 RTOS 时间接口。