第一章:初始化流程驱动开发
内存控制器的初始化,是整个驱动开发中最关键的一环。我见过太多工程师一上来就调读写时序,结果连颗粒都没认到——说白了,初始化没做好,后面全是白费功夫。
这一章,我会带你走一遍完整的初始化流程。从状态机设计开始,到PHY锁定、DLL校准,再到MRS命令序列和ECC配置。每一步我都会结合项目中的实际踩坑经验来讲。
1.1 上电初始化状态机设计
先说说状态机。为什么要用状态机?因为初始化过程是严格顺序的——你不能在PLL还没锁定时就去发MRS命令,对吧?
我个人习惯把初始化状态机分成这几个阶段:
- POWER_ON:等待电源稳定,至少200us
- PLL_LOCK:等待PLL锁定,检查lock信号
- DLL_CAL:执行DLL校准,调整延迟
- MRS_INIT:发送MRS命令序列
- ECC_CFG:配置ECC引擎
- 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校准的流程:
- 发送校准命令到PHY
- 等待校准完成中断或轮询状态寄存器
- 读取校准结果,检查是否在有效范围内
- 如果校准失败,重新尝试(我一般设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初始化的步骤:
- 配置ECC算法:选择SEC-DED(单纠错双检错)还是SECDED+
- 设置ECC区域:哪些地址范围启用ECC保护
- 初始化ECC引擎:清除错误计数,使能中断
- 执行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到PLL_LOCK之间,必须保证电源已经稳定。我见过有人把这两个状态合并了,结果PLL锁定时电源还在抖动,导致锁定失败。
好了,初始化流程的核心内容就这些。记住一句话:初始化是内存控制器的地基,地基没打好,后面再怎么优化都是白搭。