3、内存控制器寄存器映射:地址映射与位域定义详解
好,咱们直接进入正题。内存控制器说到底就是个状态机,你通过寄存器告诉它怎么干活。Exynos的内存控制器寄存器组,我把它分成四大类:配置类、状态类、时序类、性能计数类。每一类都有它的脾气。
3.1 寄存器基地址与访问规则
Exynos内存控制器的寄存器空间,通常映射在0x1A_0000到0x1A_FFFF这个范围内。具体基地址要看芯片型号,我做过Exynos 5422和9810,基地址就不一样。但有个共同点——所有寄存器都是32位宽,必须按4字节对齐访问。
访问规则其实就三条:
- 读操作:直接读,返回当前值
- 写操作:大部分寄存器支持直接写,但有些需要先解锁
- 读-改-写:对于位域操作,建议用读-改-写模式,别直接赋值
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复位 |
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 | 行激活时间 |
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 // 停顿周期数
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 寄存器访问的避坑指南
最后,我总结几个实际项目中踩过的坑:
- 不要相信复位值:很多寄存器的复位值并不是最优值。比如时序寄存器,复位值通常设得很大,你得根据实际DDR颗粒重新配。
- 写寄存器前先读:特别是配置寄存器,很多位域是只读的。你直接写会把只读位覆盖掉。
- 注意写顺序:有些寄存器有依赖关系。比如改时序前,得先把控制器停掉。我习惯的做法是:停控制器 → 改配置 → 等状态稳定 → 重新使能。
- 性能计数器会溢出:在高频场景下,32位计数器可能几毫秒就溢出了。记得及时读取。
嗯,寄存器映射这块内容比较多,但都是基本功。你把这些寄存器吃透了,内存控制器调优就成功了一半。下一章我会讲具体的调优流程,到时候咱们再细聊。