3. UFS 设备初始化:上电复位、设备枚举、UFS 初始化序列
设备初始化,说白了就是让 UFS 芯片从死寂状态变得活蹦乱跳。整个过程分为三步:上电复位 → 设备枚举 → UFS 初始化序列。每一步出问题,后面的链路都起不来。我早年调第一个 UFS 项目时,总是在第二步卡住——设备枚举超时,后来发现是上电时序里 REF_CLK 启动太慢。嗯,咱们一步一步拆开看。
3.1 上电复位
这一步的目标:给 UFS 设备提供干净、稳定的初始状态。具体要做三件事:
- 上电:VCC(主电源)、VCCQ(I/O 电源)、VCCQ2(可选辅助电源)按照 UFS Spec 规定的时序依次上升。注意,两个电源之间通常有延迟要求(
tVCC_ENA等)。 - 提供 REF_CLK:参考时钟,频率通常为 19.2 MHz、26 MHz 或 38.4 MHz。时钟必须在复位释放之前稳定。
- 释放复位信号(RST_n):复位引脚从低拉高,设备开始内部初始化。
我习惯用示波器同时抓电源和复位波形。有一次我发现 VCC 刚落到 2.95V 以下就释放了 RST_n,结果设备根本无法完成内部校准。后来查 Spec,发现 VCC 必须保持在 3.0V 以上持续至少 1ms。
| 参数 | 典型值 | 说明 |
|---|---|---|
| tVCC_ENA | ≥ 0.5 ms | VCC 有效到 VCCQ 使能之间的延时 |
| tRST_HOLD | ≥ 100 μs | RST_n 低电平保持时间,确保内部复位完成 |
| tREF_CLK_STABLE | ≥ 1 ms | 时钟稳定后到复位释放之间的等待时间 |
3.2 设备枚举:链路启动
复位释放后,UFS 设备会通过低速模式(LS-Mode)在数据线上发送一个特殊的训练模式(TRAINING PATTERN),直接告诉你“我在这儿”。主机(Host)侧会监听并回应,完成链路初始化。具体流程如下:
- Local Reset:主机驱动 M-PHY 进入复位状态,然后启动参考时钟。
- Link Startup:主机向设备方向发送连续的低速码流,等待设备回应。
- 设备回复 TRAINING PATTERN:设备收到主机码流后,开始发送自己的训练序列。
- 速率协商:双方通过 DME(Device Management Entity)命令(例如
DME_GET、DME_SET)交换各自支持的速率和 Gear。 - 进入 Active 状态:协商成功后,链路进入 Active 状态,数据可以开始传输。
我曾经在 Link Startup 阶段踩过坑——主机侧发送的训练模式持续了 200μs 还没收到设备回包,导致超时。最后发现是设备的上电时序里 REF_CLK 不稳定。所以建议代码里加上重试机制,至少重试 3 次。
链路启动成功后,设备会进入DME 模式。这时候主机可以通过 UIC_CMD(UniPro Interface Command)配置 UniPro 链路属性,比如把数据线从 1-lane 扩展到 2-lane 或 4-lane。
3.3 UFS 初始化序列
链路起来了,但设备还没准备好。接下来主机需要通过Query Request(查询请求)向设备索取基本信息,并按照需求配置设备。我习惯把这一步叫做“设备信息拉取与配置”。
主要步骤:
- 发送 UPIU 查询请求:主机发送
QUERY REQUEST(Opcode: 0x01),请求读取 Device Descriptor(描述符),获取设备类型、版本号、支持的命令集等。 - 读取 Geometry Descriptor:获取物理存储参数,比如总容量、Erase Block Size、Max Number of Blocks 等。
- 读取 Unit Descriptor:获取两个默认逻辑单元(LU 0 和 LU 1)的参数,比如可写状态、逻辑块大小。
- 配置任务优先级:可选步骤,通过
SET_FLAG或CLEAR_FLAG修改内部标志。 - 发送 NOP OUT UPIU:用来检测设备是否应答正常,确认链路侧已就绪。
- 进入 Ready 状态:一切就绪后,主机发送
START_STOP_UNIT命令(或直接切换设备状态),设备进入 Ready 状态,可以接收 IO 请求了。
有些设备在 UFS 3.1 规范中,需要先通过查询请求确认它是否支持 Command Duration Limits(CDL)。如果不支持却下发 CDL 命令,设备会返回
CHECK CONDITION,导致初始化失败。我就在量产测试里遇到过这种问题——还好 log 里有恶心的 ILLEGAL REQUEST,让我快速定位到了。
为了让整个过程更直观,我画了一张流程图:
3.4 初始化序列的代码示例
下面是一个简化版的初始化序列伪代码,我用它来解构整个流程:
// 伪代码:UFS 设备初始化
int ufs_init(void) {
// 1. 上电时序
set_power(VCC, ENABLE);
delay_ms(1);
set_ref_clk(START);
delay_ms(2);
set_reset(RST_n, HIGH);
delay_ms(100); // 等待设备内部初始化
// 2. 链路启动
while (link_startup() != SUCCESS) {
if (retry_count++ > 3) return ERROR_TIMEOUT;
delay_ms(10);
}
// 3. 设备枚举:读取设备描述符
uic_cmd(UIC_DME_GET, ATTRIBUTE_PA_ACTIVETXDATALANES);
uic_cmd(UIC_DME_SET, ATTRIBUTE_PA_ACTIVETXDATALANES, 2); // 设置 2-lane
// 4. 发送 Query Request
upiu_send(UPIU_QUERY_REQ, QUERY_OPCODE_READ_DESC, DESC_TYPE_DEVICE, 0);
wait_response(); // 解析描述符
// 5. 配置设备
upiu_send(UPIU_QUERY_REQ, QUERY_OPCODE_SET_FLAG, FLAG_POWER_ON_WP_EN, 0);
wait_response();
// 6. 发送 NOP OUT 以验证链路
upiu_send(UPIU_NOP_OUT, 0, 0, 0);
wait_response();
// 7. 准备就绪
scsi_send(START_STOP_UNIT, 0x02); // 启动设备
return SUCCESS;
}
“为什么上电后要等 100 毫秒才发 Query Request?”
因为设备内部还有一个内部初始化完成标志(通过设备描述符的第 0 字节可以读),但我更建议读取设备描述符后再继续,不要硬等。如果设备支持
ASYNC EVENT,也可以用中断来通知。
好了,至此你已经了解了从上电到 Ready 的完整链路。实际项目中可能还会涉及端到端 CRC 校验、Boot LUN 切换等特殊场景,但基础流程就是上面这些。我把它们做成了一张知识结构表,方便你复习:
| 阶段 | 核心动作 | 常见失败原因 |
|---|---|---|
| 上电复位 | VCC/VCCQ 上电,REF_CLK 稳定,释放 RST_n | 电源上升斜率过慢,时钟不稳 |
| 链路启动 | LS-Mode 训练,DME 速率协商 | 主机/设备速率不匹配,信号完整性差 |
| 设备枚举 | 读取 Device/Geometry/Unit 描述符 | 查询超时,描述符内容非法 |
| 配置阶段 | 设置标志,发送 NOP OUT | 设备不支持某些 Flag,NOP OUT 无应答 |
| 进入 Ready | 下发 START STOP UNIT | 设备处于故障状态,电源不稳定 |
我在调试过程中曾遇到过最奇葩的一个问题:设备明明处于 Ready 状态,但一发送 READ 命令就返回 BUSY。后来发现是因为几何描述符里的 Max Number of Blocks 被我读错了——一个 32 位值的高 16 位和低 16 位我解析反了。嗯,这些小坑,你们自己绕一下就好。