第一章:初始化流程驱动开发

内存控制器的初始化,是整个驱动开发中最关键的一环。我见过太多工程师一上来就调读写时序,结果连颗粒都没认到——说白了,初始化没做好,后面全是白费功夫。

这一章,我会带你走一遍完整的初始化流程。从状态机设计开始,到PHY锁定、DLL校准,再到MRS命令序列和ECC配置。每一步我都会结合项目中的实际踩坑经验来讲。

1.1 上电初始化状态机设计

先说说状态机。为什么要用状态机?因为初始化过程是严格顺序的——你不能在PLL还没锁定时就去发MRS命令,对吧?

我个人习惯把初始化状态机分成这几个阶段:

  1. POWER_ON:等待电源稳定,至少200us
  2. PLL_LOCK:等待PLL锁定,检查lock信号
  3. DLL_CAL:执行DLL校准,调整延迟
  4. MRS_INIT:发送MRS命令序列
  5. ECC_CFG:配置ECC引擎
  6. READY:初始化完成,进入正常工作模式

嗯,这里要注意:每个状态都要有超时处理。我曾经在一个项目里,PLL锁定的lock信号因为PCB走线问题一直没拉高,结果状态机卡死在PLL_LOCK状态,整板复位都救不回来。从那以后,我每个状态都加了超时计数器。

核心原则:状态机必须包含超时机制和错误恢复路径。不要假设硬件永远按预期工作。

下面是我常用的状态机代码框架:

enum init_state {
    STATE_POWER_ON,
    STATE_PLL_LOCK,
    STATE_DLL_CAL,
    STATE_MRS_INIT,
    STATE_ECC_CFG,
    STATE_READY,
    STATE_ERROR
};

struct mc_init_fsm {
    enum init_state current;
    uint32_t timeout_cnt;
    uint32_t retry_cnt;
    int (*handler)(struct mc_init_fsm *fsm);
};

static int power_on_handler(struct mc_init_fsm *fsm) {
    // 等待电源稳定
    mdelay(200);
    fsm->current = STATE_PLL_LOCK;
    return 0;
}

static int pll_lock_handler(struct mc_init_fsm *fsm) {
    // 检查PLL锁定状态
    if (readl(PLL_LOCK_REG) & PLL_LOCK_BIT) {
        fsm->current = STATE_DLL_CAL;
        return 0;
    }
    if (++fsm->timeout_cnt > PLL_TIMEOUT_MAX) {
        fsm->current = STATE_ERROR;
        return -ETIMEDOUT;
    }
    return -EAGAIN;
}

1.2 PHY初始化:PLL锁定与DLL校准

PHY初始化说白了就是让物理层准备好收发数据。这里面两个关键步骤:PLL锁定和DLL校准。

1.2.1 PLL锁定

PLL的作用是产生内存控制器需要的高频时钟。我记得第一次调DDR4的时候,PLL怎么都锁不住。后来发现是参考时钟的jitter太大了——你想想看,参考时钟本身都不干净,PLL怎么可能锁得稳?

PLL锁定的检查要点:

  • lock信号:硬件会提供一个lock信号,拉高表示锁定成功
  • 频率稳定度:lock信号拉高后,建议再等几十个时钟周期
  • 温度漂移:高温下PLL可能失锁,需要定期检查

我的经验:PLL锁定后不要立即进入下一步。我习惯再等100us,让时钟彻底稳定。这个习惯帮我避免过好几次莫名其妙的初始化失败。

1.2.2 DLL校准

DLL校准是为了补偿时钟到数据的延迟。说白了,就是让数据采样点落在眼图的正中间。

DLL校准的流程:

  1. 发送校准命令到PHY
  2. 等待校准完成中断或轮询状态寄存器
  3. 读取校准结果,检查是否在有效范围内
  4. 如果校准失败,重新尝试(我一般设3次重试)

为什么会校准失败?最常见的原因是电压不稳。我在一个项目里遇到过,DLL校准结果每次都不一样,从0x20到0x80乱跳。查了两天才发现是DDR供电的纹波太大,加了电容就解决了。

static int dll_calibration(struct mc_device *mc) {
    uint32_t cal_result;
    int retry = 3;
    
    while (retry--) {
        // 触发DLL校准
        writel(DLL_CAL_START, mc->base + DLL_CTRL);
        
        // 等待校准完成,超时100us
        if (wait_for_completion_timeout(&mc->dll_done, 
                                        usecs_to_jiffies(100))) {
            cal_result = readl(mc->base + DLL_RESULT);
            // 检查结果是否在合理范围
            if (cal_result >= DLL_MIN && cal_result <= DLL_MAX) {
                dev_info(mc->dev, "DLL calibrated: 0x%x\n", cal_result);
                return 0;
            }
        }
        dev_warn(mc->dev, "DLL calibration retry %d\n", 3 - retry);
    }
    return -EIO;
}

1.3 内存颗粒初始化:MRS命令序列

MRS(Mode Register Set)是配置内存颗粒工作模式的命令。每个DDR颗粒都有多个模式寄存器,需要通过MRS命令来设置。

MRS命令序列的顺序是固定的,不能乱:

步骤 命令 说明
1 RESET 复位颗粒(DDR4需要)
2 CKE高 使能时钟
3 MRS命令 配置MR0~MR6
4 ZQ校准 校准输出驱动

我建议把MRS命令封装成一个函数,参数是寄存器地址和值。这样代码清晰,也方便调试:

static int mrs_command(struct mc_device *mc, uint8_t reg_addr, uint16_t value) {
    uint32_t cmd = MRS_CMD | (reg_addr << 16) | (value & 0xFFFF);
    
    writel(cmd, mc->base + MRS_CMD_REG);
    
    // 等待命令完成
    if (wait_for_completion_timeout(&mc->mrs_done, 
                                    usecs_to_jiffies(10))) {
        return 0;
    }
    return -ETIMEDOUT;
}

// MRS初始化序列
static int mrs_init_sequence(struct mc_device *mc) {
    // MR0: 设置CAS延迟、突发长度
    mrs_command(mc, 0, MR0_BL8 | MR0_CL_11);
    // MR1: 设置DLL使能、输出驱动强度
    mrs_command(mc, 1, MR1_DLL_EN | MR1_ODT_40);
    // MR2: 设置CWL、刷新率
    mrs_command(mc, 2, MR2_CWL_6);
    // MR3: 设置MPR模式
    mrs_command(mc, 3, 0);
    // MR4: 设置刷新模式
    mrs_command(mc, 4, MR4_REF_1X);
    // MR5: 设置CA延迟
    mrs_command(mc, 5, 0);
    // MR6: 设置VrefDQ
    mrs_command(mc, 6, MR6_VREF_75);
    
    return 0;
}

注意:MRS命令之间需要满足tMRD时序要求。不同DDR规格的tMRD不一样,DDR4是8个时钟周期,DDR5是16个。别搞混了。

1.4 ECC初始化配置

ECC(Error Correction Code)是内存可靠性保障。说实话,很多嵌入式系统为了省成本会跳过ECC,但如果你做的是服务器或工业级产品,ECC是必须的。

ECC初始化的步骤:

  1. 配置ECC算法:选择SEC-DED(单纠错双检错)还是SECDED+
  2. 设置ECC区域:哪些地址范围启用ECC保护
  3. 初始化ECC引擎:清除错误计数,使能中断
  4. 执行ECC scrub:对整个内存区域进行ECC初始化

ECC scrub这一步很多人会忽略。你想想看,如果内存里全是随机数据,ECC校验位也是随机的,那第一次读的时候就会报ECC错误。所以初始化时必须把整个区域写一遍,生成正确的ECC校验位。

static int ecc_init(struct mc_device *mc) {
    // 1. 配置ECC算法
    writel(ECC_ALGO_SEC_DED, mc->base + ECC_CFG);
    
    // 2. 设置ECC保护区域
    writel(mc->ecc_start, mc->base + ECC_START_ADDR);
    writel(mc->ecc_end, mc->base + ECC_END_ADDR);
    
    // 3. 使能ECC
    writel(ECC_ENABLE | ECC_INT_EN, mc->base + ECC_CTRL);
    
    // 4. 执行ECC scrub
    writel(ECC_SCRUB_START, mc->base + ECC_SCRUB);
    
    // 等待scrub完成
    if (wait_for_completion_timeout(&mc->ecc_scrub_done, 
                                    msecs_to_jiffies(1000))) {
        dev_info(mc->dev, "ECC scrub completed\n");
        return 0;
    }
    return -ETIMEDOUT;
}

避坑指南:我曾经在一个项目里,ECC scrub完成后直接开始正常读写,结果发现偶尔会报单比特错误。查了半天,原来是scrub时用的是burst write,但ECC引擎对burst write的支持有bug。后来改成单次写操作,问题就解决了。

1.5 初始化流程总览

最后,我用一张流程图把整个初始化过程串起来。这张图是我做项目时一直贴在工位上的:

内存控制器初始化流程 上电复位 POWER_ON (等待200us) PLL_LOCK (检查lock信号) DLL_CAL (校准延迟) MRS_INIT (配置模式寄存器) ECC_CFG (配置ECC) READY (初始化完成) 超时/错误 重试/复位

这张图看起来简单,但每个箭头背后都有时序要求。比如POWER_ON到PLL_LOCK之间,必须保证电源已经稳定。我见过有人把这两个状态合并了,结果PLL锁定时电源还在抖动,导致锁定失败。

好了,初始化流程的核心内容就这些。记住一句话:初始化是内存控制器的地基,地基没打好,后面再怎么优化都是白搭。