第一章:寄存器编程实战——内存控制器寄存器映射(CSR空间)

各位同学,咱们今天直接切入正题。内存控制器这东西,说白了就是CPU和DRAM之间的“翻译官”。你写的每一行代码,最终都要落到寄存器上。我做了十几年驱动,最深的体会就是:寄存器搞不定,其他都是白搭。

1.1 CSR空间:内存控制器的“神经中枢”

CSR,全称是Control and Status Register。你可以把它想象成内存控制器的操作面板。每个寄存器都有固定的地址,驱动通过读写这些地址来指挥硬件干活。

以海力士的HBM2E控制器为例,CSR空间通常映射在系统总线的某个基地址上。比如:

#define DDRC_CSR_BASE        0xFE00_0000
#define DDRC_CSR_SIZE        0x0000_1000   // 4KB空间

// 寄存器偏移定义
#define DDRC_MR0_OFFSET      0x0000
#define DDRC_MR1_OFFSET      0x0004
#define DDRC_MR2_OFFSET      0x0008
#define DDRC_MR3_OFFSET      0x000C
#define DDRC_TIMING_OFFSET   0x0100
#define DDRC_TRAIN_OFFSET    0x0200

我个人习惯把CSR空间分成三块:

  • 配置区(0x0000-0x00FF):模式寄存器、工作模式
  • 时序区(0x0100-0x01FF):tRCD、tCL、tRP等参数
  • 训练区(0x0200-0x02FF):Read/Write Leveling相关

重要提醒:CSR映射必须使用ioremap,不能直接拿物理地址当指针用。我在项目中遇到过有人直接强转物理地址,结果在ARM64上直接panic——因为MMU没开。

1.2 模式寄存器配置:MR0-MR3

DRAM芯片内部有4个模式寄存器,分别控制不同的功能。咱们一个一个说。

MR0:突发长度与CAS延迟

MR0主要管两件事:突发长度(BL)和CAS延迟(CL)。

// MR0寄存器位域定义
#define MR0_BL_SHIFT         0
#define MR0_BL_MASK          (0x3 << 0)   // 突发长度
#define MR0_CL_SHIFT         4
#define MR0_CL_MASK          (0x7 << 4)   // CAS延迟

// 配置示例:BL=8, CL=12
uint32_t mr0_val = 0;
mr0_val |= (0x2 << MR0_BL_SHIFT);   // BL=8
mr0_val |= (0x6 << MR0_CL_SHIFT);   // CL=12

writel(mr0_val, csr_base + DDRC_MR0_OFFSET);

嗯,这里要注意:CL值不是直接写数字。比如CL=12,在MR0里可能编码成6。具体要看芯片手册的编码表。我踩过这个坑,第一次调DDR4的时候,直接写了12进去,结果板子直接不亮。

MR1:驱动强度与ODT

MR1控制输出驱动强度和片上端接电阻(ODT)。这两个参数直接影响信号质量。

位域 功能 典型值
[5:4] 驱动强度 0x1 (RZQ/7)
[9:6] ODT阻抗 0x2 (60Ω)
[12] DLL使能 0x1 (使能)

实战技巧:驱动强度别设太大。我曾经为了追求信号边沿陡峭,把驱动强度调到最大,结果EMI超标,整块板子过不了认证。后来乖乖用RZQ/7,世界清净了。

MR2:CAS写延迟与刷新模式

MR2配置CAS写延迟(CWL)和刷新相关参数。CWL通常等于CL-1或CL-2。

// MR2配置示例
uint32_t mr2_val = 0;
mr2_val |= (0x5 << 3);    // CWL=8 (编码值5)
mr2_val |= (0x1 << 7);    // 自刷新温度范围使能

writel(mr2_val, csr_base + DDRC_MR2_OFFSET);

MR3:MPR与数据掩码

MR3用得少,主要控制多用途寄存器(MPR)和数据掩码功能。调试阶段偶尔会用MPR来验证读写通路。

1.3 时序参数寄存器编程

时序参数是内存控制器的灵魂。设错了,轻则性能低下,重则数据出错。我见过最离谱的案例:有人把tRCD设成1,结果跑memtest一晚上报了几万个错误。

关键时序参数包括:

  • tCL(CAS Latency):读命令到数据输出的延迟
  • tRCD(RAS to CAS Delay):行激活到列命令的延迟
  • tRP(Row Precharge Time):预充电时间
  • tRAS(Row Active Time):行激活最小时间
  • tRFC(Refresh Cycle Time):刷新周期
// 时序寄存器结构体
struct ddr_timing_reg {
    uint32_t tcl:4;      // CAS延迟
    uint32_t trcd:4;     // RAS到CAS延迟
    uint32_t trp:4;      // 预充电时间
    uint32_t tras:5;     // 行激活时间
    uint32_t trfc:8;     // 刷新周期
    uint32_t reserved:7;
};

// 配置DDR4-3200时序
struct ddr_timing_reg timing = {
    .tcl  = 12,   // CL=12
    .trcd = 12,   // tRCD=12
    .trp  = 12,   // tRP=12
    .tras = 28,   // tRAS=28
    .trfc = 350,  // tRFC=350ns
};

writel(*(uint32_t *)&timing, csr_base + DDRC_TIMING_OFFSET);

警告:时序参数的单位是时钟周期,不是纳秒!一定要根据DDR频率换算。比如DDR4-3200,周期是0.625ns,tRFC=350ns就要设560个周期。我曾经犯过这个低级错误,排查了整整两天。

1.4 训练寄存器:Read/Write Leveling

训练寄存器是DDR3/DDR4引入的“自动校准”机制。为什么需要训练?因为PCB走线长度不同,时钟和数据到达时间会有偏差。训练的目的就是消除这些偏差。

Read Leveling

读训练调整DQS和CK之间的相位关系。控制器会发送读命令,然后调整DQS的延迟,直到找到正确的采样窗口。

// 读训练控制寄存器
#define DDRC_RL_CTRL_OFFSET   0x0200
#define DDRC_RL_DELAY_OFFSET  0x0204

// 启动读训练
uint32_t rl_ctrl = readl(csr_base + DDRC_RL_CTRL_OFFSET);
rl_ctrl |= (1 << 0);   // 使能读训练
writel(rl_ctrl, csr_base + DDRC_RL_CTRL_OFFSET);

// 等待训练完成
while (!(readl(csr_base + DDRC_RL_CTRL_OFFSET) & (1 << 1)))
    cpu_relax();

// 读取训练结果
uint32_t rl_delay = readl(csr_base + DDRC_RL_DELAY_OFFSET);
pr_info("Read Leveling delay: %d taps\n", rl_delay & 0xFF);

Write Leveling

写训练调整DQS和CK的相位,确保数据能被DRAM正确采样。流程和读训练类似,但方向相反。

经验之谈:训练结果不是一成不变的。温度变化、电压波动都会影响最佳延迟值。我建议在驱动初始化时做一次完整训练,然后在系统运行时每隔几分钟做一次“微调”。这样既能保证性能,又能应对环境变化。

1.5 实战:完整的寄存器初始化流程

好了,理论说完了,咱们来点实际的。下面是一个简化的初始化流程:

  1. 复位控制器:写复位寄存器,等待自复位完成
  2. 配置MR0-MR3:设置工作模式、时序参数
  3. 配置时序寄存器:写入tCL、tRCD等参数
  4. 启动DLL:等待DLL锁定
  5. 执行ZQ校准:校准输出驱动和ODT
  6. 运行Read Leveling:调整读数据路径
  7. 运行Write Leveling:调整写数据路径
  8. 验证:读写测试,确认数据正确
// 初始化函数示例
int ddr_init(void __iomem *csr_base)
{
    // 步骤1:复位
    writel(0x1, csr_base + DDRC_RESET_OFFSET);
    mdelay(10);
    
    // 步骤2:配置MR寄存器
    configure_mr_registers(csr_base);
    
    // 步骤3:配置时序
    configure_timing(csr_base);
    
    // 步骤4:等待DLL锁定
    wait_dll_lock(csr_base);
    
    // 步骤5:ZQ校准
    zq_calibration(csr_base);
    
    // 步骤6-7:训练
    read_leveling(csr_base);
    write_leveling(csr_base);
    
    // 步骤8:验证
    if (ddr_verify(csr_base) < 0) {
        pr_err("DDR initialization failed!\n");
        return -EIO;
    }
    
    pr_info("DDR initialized successfully\n");
    return 0;
}

核心要点:初始化顺序不能乱!特别是DLL锁定和ZQ校准必须在训练之前完成。我见过有人把顺序搞反了,结果训练永远不通过,还以为是硬件坏了。

1.6 本章知识体系

下面这张图展示了本章的核心逻辑关系:

内存控制器寄存器编程知识体系 CSR空间映射 (ioremap) 配置区 (MR0-MR3) 时序区 (tCL/tRCD等) 训练区 (Leveling) MR0: BL, CL MR1: 驱动强度, ODT MR2: CWL, 刷新 MR3: MPR, 数据掩码 tCL: CAS延迟 tRCD: 行到列延迟 tRP: 预充电时间 tRFC: 刷新周期 Read Leveling Write Leveling DQS相位调整 采样窗口校准 初始化流程:复位 → 配置 → 训练 → 验证 顺序不能乱!DLL锁定 → ZQ校准 → Leveling

这张图把本章的知识点串起来了。你想想看,从CSR空间映射开始,到三大功能区的配置,最后汇聚到初始化流程。每一步都有它的道理,少一步都不行。

好了,第一章的内容就到这。记住:寄存器编程没有捷径,多读手册,多动手写代码。下次咱们聊时钟和复位管理,那又是另一番天地了。


公众号:蓝海资料掘金营,微信deep3321