3、内存控制器寄存器映射:地址映射与位域定义详解

好,咱们直接进入正题。内存控制器说到底就是个状态机,你通过寄存器告诉它怎么干活。Exynos的内存控制器寄存器组,我把它分成四大类:配置类、状态类、时序类、性能计数类。每一类都有它的脾气。

3.1 寄存器基地址与访问规则

Exynos内存控制器的寄存器空间,通常映射在0x1A_0000到0x1A_FFFF这个范围内。具体基地址要看芯片型号,我做过Exynos 5422和9810,基地址就不一样。但有个共同点——所有寄存器都是32位宽,必须按4字节对齐访问

⚠️ 注意:千万别用8位或16位访问!我见过有同事用char指针去读,结果读回来的值全是乱的。硬件设计上就没支持非对齐访问。

访问规则其实就三条:

  • 读操作:直接读,返回当前值
  • 写操作:大部分寄存器支持直接写,但有些需要先解锁
  • 读-改-写:对于位域操作,建议用读-改-写模式,别直接赋值

3.2 配置寄存器组(CONFIG)

配置寄存器,说白了就是告诉控制器「你要怎么干活」。我个人习惯把配置寄存器分成三个子类:

3.2.1 全局配置寄存器(MCG_CON)

基地址偏移:0x0000

位域 名称 描述
[31:16] CHIP_ID 芯片版本号,只读。可以用来判断是哪个步进
[15:8] BURST_LEN 突发长度配置。0x08=8拍,0x10=16拍。我建议用16拍,效率高
[7:4] CH_SEL 通道选择。0=单通道,1=双通道交织
[3:0] ENABLE 控制器使能。写0xA5使能,写0x5A复位
💡 经验之谈:ENABLE位域的设计很有意思。它不是简单的0/1,而是需要写特定pattern。为什么?防止软件跑飞时误操作。我曾经在调试时不小心写了个0x01,结果控制器没反应,查了半天才发现要写0xA5。

3.2.2 通道配置寄存器(CH_CONF0 ~ CH_CONF3)

偏移:0x0010 ~ 0x001C,每个通道一个

每个通道有4个配置寄存器,我最常用的是CH_CONF0:

// CH_CONF0 位域定义
#define CH_PRIORITY     (1 << 0)    // 优先级:0=普通,1=高优
#define CH_QOS_EN       (1 << 1)    // QoS使能
#define CH_URGENT       (1 << 2)    // 紧急请求标志
#define CH_BW_LIMIT_EN  (1 << 3)    // 带宽限制使能
#define CH_BW_LIMIT_VAL (0xFF << 8) // 带宽限制值,单位MB/s

嗯,这里要注意。QoS使能位一旦打开,控制器就会根据优先级调度。但如果你所有通道都设成高优先级,那这个功能就废了。我建议只给显示控制器和CPU设高优先级,其他外设用普通。

3.3 状态寄存器组(STATUS)

状态寄存器,说白了就是看控制器「现在在干嘛」。调试的时候我最爱看这些寄存器。

3.3.1 主状态寄存器(MCG_STATUS)

偏移:0x0100

位域 名称 描述
[31:24] STATE 主状态机状态:0x01=空闲,0x02=自刷新,0x04=激活,0x08=读写
[23:16] PSTATE 电源状态:0=正常,1=低功耗
[15:0] PENDING 待处理请求数。这个值如果一直增长,说明有瓶颈

🔑 关键技巧:PENDING字段是我调试性能问题的第一站。如果这个值超过128还降不下来,那你的内存带宽肯定不够用了。

3.3.2 错误状态寄存器(ERR_STATUS)

偏移:0x0104

这个寄存器我建议你定期轮询。为什么?因为内存错误往往是间歇性的,你不查就不知道。

// 错误状态位定义
#define ERR_ECC_CORRECTABLE  (1 << 0)  // ECC可纠正错误
#define ERR_ECC_UNCORRECTABLE (1 << 1) // ECC不可纠正错误
#define ERR_ADDR_MISMATCH    (1 << 2)  // 地址不匹配
#define ERR_TIMEOUT          (1 << 3)  // 请求超时

我曾经在一个项目中,发现ERR_ECC_CORRECTABLE位频繁置1。一开始以为是内存条坏了,后来查了半天,发现是时序配置太紧,导致数据采样不稳定。把时序放宽一档,问题就解决了。

3.4 时序寄存器组(TIMING)

时序寄存器,这是内存控制器调优的核心。说白了就是告诉控制器「DDR颗粒的时序参数是多少」。这部分我建议你对照DDR的datasheet来配。

3.4.1 核心时序寄存器(TIMING_CORE)

偏移:0x0200

位域 参数 说明
[31:24] tCL CAS延迟,单位时钟周期。DDR3通常设7~11,DDR4设10~16
[23:16] tRCD 行地址到列地址延迟
[15:8] tRP 行预充电时间
[7:0] tRAS 行激活时间
⚠️ 警告:时序参数设得太小,系统不稳定;设得太大,性能下降。我建议你先用JEDEC标准值,然后逐步收紧。每次只改一个参数,跑完压力测试再改下一个。

3.4.2 刷新时序寄存器(TIMING_REF)

偏移:0x0204

刷新时序这块,很多人容易忽略。DDR颗粒需要定期刷新,否则数据会丢。Exynos支持两种刷新模式:

  • 自动刷新:硬件自己控制,你只需要设刷新间隔
  • 自刷新:进入低功耗模式时用,控制器自动处理
// 刷新间隔计算公式
// tREFI = 7.8us (DDR3) 或 3.9us (DDR4)
// 寄存器值 = tREFI * 时钟频率 / 1000
// 例如:DDR3 @ 800MHz,tREFI=7.8us
// 寄存器值 = 7.8 * 800 / 1000 = 6.24,取整为6

嗯,这里有个坑。刷新间隔不能设得太接近理论值,要留余量。我一般会加10%的裕量,比如算出来是6,我就设5。为什么?因为温度变化会导致刷新需求变化,留点余量更安全。

3.5 性能计数器寄存器组(PERF_CNT)

性能计数器,这是调优的利器。没有它,你就像在黑暗中开车。

3.5.1 计数器控制寄存器(PERF_CTRL)

偏移:0x0300

位域 名称 描述
[31:24] CNT_SEL0 计数器0的事件选择
[23:16] CNT_SEL1 计数器1的事件选择
[15:8] CNT_SEL2 计数器2的事件选择
[7:0] ENABLE 计数器使能,写0xFF启动

3.5.2 可计数的事件类型

Exynos支持的事件类型很多,我挑几个最常用的:

// 事件选择码
#define EVT_READ_REQ     0x01  // 读请求数
#define EVT_WRITE_REQ    0x02  // 写请求数
#define EVT_READ_BYTES   0x03  // 读字节数
#define EVT_WRITE_BYTES  0x04  // 写字节数
#define EVT_BANK_CONFLICT 0x05 // 行冲突次数
#define EVT_PAGE_MISS    0x06  // 页缺失次数
#define EVT_STALL_CYCLES 0x07  // 停顿周期数
💡 实战技巧:我最常用的组合是:计数器0统计读请求,计数器1统计写请求,计数器2统计行冲突。这样就能算出读写比例和行命中率。行命中率低于80%的话,说明你的地址映射策略有问题。

3.5.3 计数器值寄存器(PERF_VAL0 ~ PERF_VAL2)

偏移:0x0304 ~ 0x030C

每个计数器都是32位,溢出后自动归零。我建议你定时读取,比如每1ms读一次。这样就能算出瞬时带宽:

// 带宽计算示例
uint32_t val1 = readl(PERF_VAL0);  // 读字节数
uint32_t val2 = readl(PERF_VAL0);  // 1ms后再次读取
uint32_t delta = val2 - val1;      // 差值
float bandwidth = delta / 1000.0;  // 单位:MB/s

我曾经用这个方法发现一个奇怪的现象:某个场景下读带宽只有理论值的30%。查了半天,发现是DMA配置错了,每次只传4字节而不是64字节cache line。改过来之后,带宽直接翻了三倍。

3.6 寄存器访问的避坑指南

最后,我总结几个实际项目中踩过的坑:

  1. 不要相信复位值:很多寄存器的复位值并不是最优值。比如时序寄存器,复位值通常设得很大,你得根据实际DDR颗粒重新配。
  2. 写寄存器前先读:特别是配置寄存器,很多位域是只读的。你直接写会把只读位覆盖掉。
  3. 注意写顺序:有些寄存器有依赖关系。比如改时序前,得先把控制器停掉。我习惯的做法是:停控制器 → 改配置 → 等状态稳定 → 重新使能。
  4. 性能计数器会溢出:在高频场景下,32位计数器可能几毫秒就溢出了。记得及时读取。

嗯,寄存器映射这块内容比较多,但都是基本功。你把这些寄存器吃透了,内存控制器调优就成功了一半。下一章我会讲具体的调优流程,到时候咱们再细聊。